diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 883a752a5..f8646883e 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -8,15 +8,19 @@ /* Begin PBXBuildFile section */ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.swift */; }; + 00AC53151BA23A90FAAE9FBF /* BugReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D607F47FDEF16CC63684BE0 /* BugReport.swift */; }; + 00F3059B1E0CFCA019710C3E /* BugReportModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B516212D9FE785DDD5E490D1 /* BugReportModels.swift */; }; 01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */; }; 01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109C0201D8CB3F947340DC80 /* WeakDictionary.swift */; }; 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */; }; 03B8FEA668A5B76A93113BB1 /* MemberDetailProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2ABC1A9B62BDB3D216E7FD /* MemberDetailProviderManager.swift */; }; 03CB204C52F18E24A5C3D219 /* UITestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967873B9E11828B67F64C89A /* UITestsAppCoordinator.swift */; }; + 05776B005C57E92582F0CF08 /* BuildSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F87116470221880017CF522 /* BuildSettings.swift */; }; 059173B3C77056C406906B6D /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = D4DA544B2520BFA65D6DB4BB /* target.yml */; }; 0602FA07557F580086782A9E /* UserIndicatorPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA072E995316CD18BC29313 /* UserIndicatorPresentationContext.swift */; }; 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */; }; + 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */; }; 0E8C480700870BB34A2A360F /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; }; 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; @@ -25,21 +29,27 @@ 1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */; }; 12F70C493FB69F4D7E9A37EA /* NavigationRouterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29EBCBFEC6FD0941749404D /* NavigationRouterStore.swift */; }; 13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; }; + 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */; }; 15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B73D5E21F524A9BE44448D /* UserIndicatorRequest.swift */; }; + 172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */; }; 17CC4FB64F3A670F43ECBE5F /* UITestsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCA431E6EDD71F7067B5F9E7 /* UITestsRootView.swift */; }; + 187E18F21EF4DA244E436E58 /* BugReportViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; 1999ECC6777752A2616775CF /* MemberDetailsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */; }; 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; }; 1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; }; 1F3232BD368DF430AB433907 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; }; + 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; 224A55EEAEECF5336B14A4A5 /* EmoteRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2DF459F1737A594667CC46 /* EmoteRoomMessage.swift */; }; 22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */; }; 24906A1E82D0046655958536 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CF12478983A5EB390FB26 /* MessageComposer.swift */; }; 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */; }; 277D2531C70F207A2F9F5906 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956BDA4AE16429AD015661A8 /* KeychainControllerProtocol.swift */; }; 2797C9D9BA642370F1C85D78 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; }; + 28410F3DE89C2C44E4F75C92 /* MockBugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */; }; 297CD0A27C87B0C50FF192EE /* RoomTimelineViewFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */; }; 29AEE68A604940180AB9EBFF /* MockRoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BDAC8895AB2B77B47703AE /* MockRoomSummary.swift */; }; + 29EE1791E0AFA1ABB7F23D2F /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 67E7A6F388D3BF85767609D9 /* Sentry */; }; 2BA59D0AEFB4B82A2EC2A326 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = A981A4CA233FB5C13B9CA690 /* SwiftyBeaver */; }; 2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */; }; 2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90733775209F4D4D366A268F /* RootRouterType.swift */; }; @@ -53,19 +63,26 @@ 33912D1B9264D897033E0681 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0779B2CC9A687CBB82A5B920 /* LoginScreenViewModelProtocol.swift */; }; 33B4E59D408AE6E02323EE41 /* NoticeRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */; }; 344AF4CBB6D8786214878642 /* NavigationRouterStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */; }; + 34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; }; 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; }; 35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; }; 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; }; 3772354754450F2B54107E17 /* TemplateSimpleScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4EDB32B97910AAAFE632B2 /* TemplateSimpleScreenViewModelProtocol.swift */; }; 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; }; 39AE84C8E5F2FE9D2DC7775C /* EventBasedTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56008790A9C4479A6B31FDF4 /* EventBasedTimelineView.swift */; }; + 3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */; }; + 3C549A0BF39F8A854D45D9FD /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; 3D325A1147F6281C57BFCDF6 /* EventBrief.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4411C0DA0087A1CB143E96FA /* EventBrief.swift */; }; + 3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; }; 3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; }; 418B4AEFD03DC7A6D2C9D5C8 /* EventBriefFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36322DD0D4E29D31B0945ADC /* EventBriefFactory.swift */; }; + 438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */; }; 462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */; }; 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; }; 4669804D0369FBED4E8625D1 /* ToastViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4470B8CB654B097D807AA713 /* ToastViewPresenter.swift */; }; + 490E606044B18985055FF690 /* SettingsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */; }; 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; }; + 4A2E0DBB63919AC8309B6D40 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */; }; 4B8A2C45FF906ADBB1F5C3B4 /* UserIndicatorQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E1273CC3BC3E471AF87BE5 /* UserIndicatorQueueTests.swift */; }; 4D23C56053013437C35E511E /* ActivityIndicatorPresenterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C2318DF4C0E601EEE31F84 /* ActivityIndicatorPresenterType.swift */; }; 4D970CB606276717B43E2332 /* TimelineItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804F9B0FABE093C7284CD09B /* TimelineItemList.swift */; }; @@ -75,6 +92,7 @@ 500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874A1842477895F199567BD7 /* TimelineView.swift */; }; 50391038BC50C8ED9A4D88A0 /* WeakDictionaryReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B23371BC8BF6164D9F6A05 /* WeakDictionaryReference.swift */; }; 51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */; }; + 524C9C31EF8D58C2249F8A10 /* sample_screenshot.png in Resources */ = {isa = PBXBuildFile; fileRef = 9414DCADBDF9D6C4B806F61E /* sample_screenshot.png */; }; 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; @@ -91,10 +109,13 @@ 6832733838C57A7D3FE8FEB5 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 04C28663564E008DB32B5972 /* Introspect */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 6A367F3D7A437A79B7D9A31C /* FullscreenLoadingViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4112D04077F6709C5CA0A13E /* FullscreenLoadingViewPresenter.swift */; }; + 6C72F66DA26A0956E9A9077A /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BEB3259B2208E5AE5BB3F65 /* Settings.swift */; }; + 6EA61FCA55D950BDE326A1A7 /* ImageAnonymizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */; }; 6F2AB43A1EFAD8A97AF41A15 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5986E300FC849DEAB2EE7AEB /* Introspect */; }; 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */; }; + 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; }; 77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; }; 77E192BA943B90F9F310CA23 /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */; }; 78B71D53C1FC55FB7A9B75F0 /* RoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */; }; @@ -108,8 +129,10 @@ 7DE5EB4CB2401C672257283C /* WeakKeyDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B12969CEC0051BC750DA5068 /* WeakKeyDictionary.swift */; }; 7E1EDBA3934E6C29E5BD045B /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77DD2DA5DC8654F2A80FF1D /* Bundle.swift */; }; 7F19E97E7985F518C9018B83 /* RootRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF47564C584F614B7287F3EB /* RootRouter.swift */; }; + 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */; }; 7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */; }; 80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */; }; + 86C2E93920FD15AD17E193A9 /* BugReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E532D95330139D118A9BF88 /* BugReportViewModel.swift */; }; 8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */; }; 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */; }; 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; }; @@ -132,6 +155,9 @@ 9D2E03DB175A6AB14589076D /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; }; A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; }; + A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */; }; + A4E885358D7DD5A072A06824 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; + A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A901D95158B02CA96C79C7F /* InfoPlist.swift */; }; A5EC21A071F58FC1229C20D0 /* MemberDetailsProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09747989908EC5E4AA29F844 /* MemberDetailsProviderProtocol.swift */; }; A636D4900E0D98ED91536482 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF3EDF23226895776553F04A /* AppCoordinator.swift */; }; A69B7B421C28C6CDEBBD0613 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD575D36B9F6D1D543305D1 /* AuthenticationCoordinator.swift */; }; @@ -140,13 +166,15 @@ A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; A941EAD7F407F2ED6DA54A31 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA97D630B74B0616C1468CBD /* LoginScreen.swift */; }; AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */; }; - B0887A7B5AFEC88981626389 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64839516BD56D1C81D84C5E0 /* MXLog.swift */; }; B0EDAF55877DE19B67837C22 /* TemplateSimpleScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C29670CEC77346F31EE94C /* TemplateSimpleScreenModels.swift */; }; B245583C63F8F90357B87FAE /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; B3FDB1D9CF40777695DBBD1D /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9AB74614131D6706894E0C /* AppCoordinatorStateMachine.swift */; }; B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; + B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; + B94368839BDB69172E28E245 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; + BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = EF188681D6B6068CFAEAFC3F /* MXLogger.m */; }; BE3237142FA6E1A13C0E7D11 /* RoomSummaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21ECC295F4DE8DAA86D62AC /* RoomSummaryProtocol.swift */; }; BEEC06EFD30BFCA02F0FD559 /* UserIndicatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8EA85D4F10D7445BB6368A /* UserIndicatorTests.swift */; }; BF35062D06888FA80BD139FF /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB7F9D6FC121204D59E18DF /* Presentable.swift */; }; @@ -154,14 +182,18 @@ C1156BBE4F977AEEE1E80C48 /* TemplateSimpleScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2869CFFF6CD2A642AB4B743 /* TemplateSimpleScreenCoordinator.swift */; }; C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; }; + C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */; }; + CB137BFB3E083C33E398A6CB /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; }; CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */; }; CB498F4E27AA0545DCEF0F6F /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 50009897F60FAE7D63EF5E5B /* Kingfisher */; }; + CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */; }; CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */; }; D013E70C8E28E43497820444 /* TextRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4110685D9CA159F3FD2D6BA1 /* TextRoomMessage.swift */; }; D0619D2E6B9C511190FBEB95 /* RoomMessageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */; }; D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; }; D826154612415D2A3BB6EBF3 /* ListTableViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E854E7CF531DAC5CBEBDC75 /* ListTableViewAdapter.swift */; }; + D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; }; D94F664677C380A3CAB8D7F6 /* ActivityIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */; }; DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CC95CD75B688E946438165 /* Coordinator.swift */; }; DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */; }; @@ -170,6 +202,7 @@ E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61ADFB893DEF81E58DF3FAB9 /* MockRoomTimelineController.swift */; }; E9CEAF2C38E4E00459B811D9 /* LoginScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2082B5226B2A3A4D0798B6 /* LoginScreenModels.swift */; }; EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885D8C42DD17625B5261BEFF /* MediaProvider.swift */; }; + EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; }; ED4F663C783E9A8C0E80B983 /* TemplateSimpleScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47543EB19F3DCF308751F53C /* TemplateSimpleScreenViewModel.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; F03E16ED043C62FED5A07AE0 /* MatrixEntitityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */; }; @@ -211,7 +244,9 @@ 08F64963396A6A23538EFCEC /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = is; path = is.lproj/Localizable.stringsdict; sourceTree = ""; }; 0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = ""; }; 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderProtocol.swift; sourceTree = ""; }; + 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToggleStyle.swift; sourceTree = ""; }; 09747989908EC5E4AA29F844 /* MemberDetailsProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailsProviderProtocol.swift; sourceTree = ""; }; + 0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 0AB7A0C06CB527A1095DEB33 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = da; path = da.lproj/Localizable.stringsdict; sourceTree = ""; }; 0AD575D36B9F6D1D543305D1 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = ""; }; 0C13A92C1E9C79F055B8133D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -222,9 +257,12 @@ 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactory.swift; sourceTree = ""; }; 105D16E7DB0CCE9526612BDD /* bn-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bn-IN"; path = "bn-IN.lproj/Localizable.strings"; sourceTree = ""; }; 109C0201D8CB3F947340DC80 /* WeakDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionary.swift; sourceTree = ""; }; + 111B698739E3410E2CDB7144 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = ""; }; 113356152C099951A6D17D85 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; + 1215A4FC53D2319E81AE8970 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 124D85E85505B6B81845235F /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fy; path = fy.lproj/Localizable.stringsdict; sourceTree = ""; }; + 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizer.swift; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; @@ -245,10 +283,12 @@ 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; 2583416C8974272ADBADDBE1 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = ""; }; 26C4D226FCD20BAC53F1E092 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ml; path = ml.lproj/Localizable.strings; sourceTree = ""; }; + 28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelProtocol.swift; sourceTree = ""; }; 28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = ""; }; 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = ""; }; 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = ""; }; 2AE83A3DD63BCFBB956FE5CB /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = ""; }; + 2BEB3259B2208E5AE5BB3F65 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; 2CF9FE7E0CF9F40D1509E63A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Localizable.stringsdict; sourceTree = ""; }; 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = ""; }; 33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; @@ -261,8 +301,12 @@ 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomProxy.swift; sourceTree = ""; }; 3B5B535DA49C54523FF7A412 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nn; path = nn.lproj/Localizable.strings; sourceTree = ""; }; 3CDF9E55650D6035D6536538 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "nb-NO"; path = "nb-NO.lproj/Localizable.stringsdict"; sourceTree = ""; }; + 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = ""; }; 3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = ""; }; + 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = ""; }; + 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = ""; }; 3DD6E7C1D8B53F47789778CD /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = ""; }; + 3F87116470221880017CF522 /* BuildSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildSettings.swift; sourceTree = ""; }; 3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorStore.swift; sourceTree = ""; }; 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = ""; }; 4110685D9CA159F3FD2D6BA1 /* TextRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomMessage.swift; sourceTree = ""; }; @@ -281,6 +325,7 @@ 48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SplashViewController.xib; sourceTree = ""; }; 48CE6BF18E542B32FA52CE06 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = ""; }; 49193CB0C248D621A96FB2AA /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; + 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = ""; }; 49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benchmark.swift; sourceTree = ""; }; 4B40B7F6FCCE2D8C242492D9 /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/Localizable.strings; sourceTree = ""; }; @@ -303,8 +348,10 @@ 56008790A9C4479A6B31FDF4 /* EventBasedTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineView.swift; sourceTree = ""; }; 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelledActivityIndicatorView.swift; sourceTree = ""; }; 5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = ""; }; + 5872785B9C7934940146BFBA /* MXLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXLogger.h; sourceTree = ""; }; 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = ""; }; 5A9AB74614131D6706894E0C /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = ""; }; + 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelProtocol.swift; sourceTree = ""; }; 5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStoreProtocol.swift; sourceTree = ""; }; 5CB7F9D6FC121204D59E18DF /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = ""; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; @@ -314,6 +361,7 @@ 5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = ""; }; 5FF214969B25BFCBF87B908B /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-BD"; path = "bn-BD.lproj/Localizable.stringsdict"; sourceTree = ""; }; 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = ""; }; + 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizerTests.swift; sourceTree = ""; }; 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageProtocol.swift; sourceTree = ""; }; 616197D81103330BF2ADD559 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/Localizable.strings; sourceTree = ""; }; 61ADFB893DEF81E58DF3FAB9 /* MockRoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineController.swift; sourceTree = ""; }; @@ -322,7 +370,6 @@ 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectangleToastView.swift; sourceTree = ""; }; 624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; - 64839516BD56D1C81D84C5E0 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = ""; }; 64B23371BC8BF6164D9F6A05 /* WeakDictionaryReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionaryReference.swift; sourceTree = ""; }; 653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = ""; }; 6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -331,6 +378,8 @@ 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorPresenter.swift; sourceTree = ""; }; 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailsProvider.swift; sourceTree = ""; }; + 6A901D95158B02CA96C79C7F /* InfoPlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlist.swift; sourceTree = ""; }; + 6D607F47FDEF16CC63684BE0 /* BugReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReport.swift; sourceTree = ""; }; 6DB53055CB130F0651C70763 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = ""; }; @@ -343,6 +392,7 @@ 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = ""; }; 752DEC02D93AFF46BC13313A /* NavigationRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterType.swift; sourceTree = ""; }; 799A3A11C434296ED28F87C8 /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = iw; path = iw.lproj/Localizable.strings; sourceTree = ""; }; + 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelTests.swift; sourceTree = ""; }; 7B04BD3874D736127A8156B8 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 7BDF6A69C2BB99535193E554 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; 7D0CBC76C80E04345E11F2DB /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; @@ -350,6 +400,7 @@ 7DA80FADE73CDF01E96F5B8E /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sq; path = sq.lproj/Localizable.strings; sourceTree = ""; }; 7DDBF99755A9008CF8C8499E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 7E154FEA1E6FE964D3DF7859 /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fy; path = fy.lproj/Localizable.strings; sourceTree = ""; }; + 7E532D95330139D118A9BF88 /* BugReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModel.swift; sourceTree = ""; }; 7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionaryKeyReference.swift; sourceTree = ""; }; 804F9B0FABE093C7284CD09B /* TimelineItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemList.swift; sourceTree = ""; }; 8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; @@ -375,6 +426,7 @@ 938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = ""; }; 93B21E72926FACB13A186689 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ml; path = ml.lproj/Localizable.stringsdict; sourceTree = ""; }; 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelTests.swift; sourceTree = ""; }; + 9414DCADBDF9D6C4B806F61E /* sample_screenshot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = sample_screenshot.png; sourceTree = ""; }; 956BDA4AE16429AD015661A8 /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; 95CC95CD75B688E946438165 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 967873B9E11828B67F64C89A /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = ""; }; @@ -383,6 +435,7 @@ 97F893DBB5F88D746C6DCDE5 /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/Localizable.strings; sourceTree = ""; }; 997783054A2E95F9E624217E /* kaa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kaa; path = kaa.lproj/Localizable.strings; sourceTree = ""; }; 99DE232F24EAD72A3DF7EF1A /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = kab; path = kab.lproj/Localizable.stringsdict; sourceTree = ""; }; + 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = ""; }; 9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = ""; }; 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = ""; }; A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -401,6 +454,7 @@ ACA11F7F50A4A3887A18CA5A /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewProvider.swift; sourceTree = ""; }; AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; + AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportCoordinator.swift; sourceTree = ""; }; ADCB8A232D3A8FB3E16A7303 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; AE225C66978648AA4AF37B45 /* te */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = te; path = te.lproj/Localizable.strings; sourceTree = ""; }; AE5DDBEBBA17973ED4638823 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -414,6 +468,7 @@ B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorDismissal.swift; sourceTree = ""; }; B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemContextMenu.swift; sourceTree = ""; }; + B516212D9FE785DDD5E490D1 /* BugReportModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportModels.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ActivityIndicatorView.xib; sourceTree = ""; }; B695D0D12086158BAD1D9859 /* UserIndicatorPresenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresenterSpy.swift; sourceTree = ""; }; @@ -434,6 +489,7 @@ C21ECC295F4DE8DAA86D62AC /* RoomSummaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProtocol.swift; sourceTree = ""; }; C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; C485C186CEC78443DA96BDC8 /* TemplateSimpleScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateSimpleScreenViewModelTests.swift; sourceTree = ""; }; + C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = ""; }; C88508B6F7974CFABEC4B261 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; C91A6BC1A54CDB598EE2A81B /* UserIndicatorQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueue.swift; sourceTree = ""; }; @@ -457,6 +513,7 @@ D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = ""; }; D67CBAFA48ED0B6FCE74F88F /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = ""; }; D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; + D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = ""; }; D77DD2DA5DC8654F2A80FF1D /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = ""; }; DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicator.swift; sourceTree = ""; }; @@ -465,6 +522,7 @@ E157152B11E347F735C3FD6E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = ""; }; E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; E2869CFFF6CD2A642AB4B743 /* TemplateSimpleScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateSimpleScreenCoordinator.swift; sourceTree = ""; }; + E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITests.swift; sourceTree = ""; }; E45C57120F28F8D619150219 /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/Localizable.strings; sourceTree = ""; }; E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; E579A0DA01F488C97B771EF6 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lv; path = lv.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -477,11 +535,16 @@ ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactoryProtocol.swift; sourceTree = ""; }; + EF188681D6B6068CFAEAFC3F /* MXLogger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MXLogger.m; sourceTree = ""; }; EFFA5FD06AAAC4AF544B594E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceTests.swift; sourceTree = ""; }; F012CB5EE3F2B67359F6CC52 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; + F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetectorTests.swift; sourceTree = ""; }; + F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = ""; }; F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = ""; }; F3BC93D4555571E8B4BC47F9 /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = ""; }; F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetector.swift; sourceTree = ""; }; F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = ""; }; F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = ""; }; @@ -505,6 +568,8 @@ 6832733838C57A7D3FE8FEB5 /* Introspect in Frameworks */, 2BA59D0AEFB4B82A2EC2A326 /* SwiftyBeaver in Frameworks */, B245583C63F8F90357B87FAE /* SwiftState in Frameworks */, + A4E885358D7DD5A072A06824 /* GZIP in Frameworks */, + 29EE1791E0AFA1ABB7F23D2F /* Sentry in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -519,6 +584,8 @@ 6F2AB43A1EFAD8A97AF41A15 /* Introspect in Frameworks */, 93BA4A81B6D893271101F9F0 /* SwiftyBeaver in Frameworks */, 9AC5F8142413862A9E3A2D98 /* SwiftState in Frameworks */, + CB137BFB3E083C33E398A6CB /* GZIP in Frameworks */, + 3C549A0BF39F8A854D45D9FD /* Sentry in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -538,14 +605,26 @@ isa = PBXGroup; children = ( 10578D9852BA78D309A1CBDF /* ViewModel */, + 328DD5DA1281F758B72006C7 /* Views */, ); path = SwiftUI; sourceTree = ""; }; + 06501F0E978B2D5C92771DC7 /* Logging */ = { + isa = PBXGroup; + children = ( + 111B698739E3410E2CDB7144 /* MXLog.swift */, + 5872785B9C7934940146BFBA /* MXLogger.h */, + EF188681D6B6068CFAEAFC3F /* MXLogger.m */, + ); + path = Logging; + sourceTree = ""; + }; 0787F81684E503024BD0C051 /* Services */ = { isa = PBXGroup; children = ( AAFDD509929A0CCF8BCE51EB /* Authentication */, + 0ED3F5C21537519389C07644 /* BugReport */, 8039515BAA53B7C3275AC64A /* Client */, 79E560F5113ED25D172E550C /* Media */, 40E6246F03D1FE377BC5D963 /* Room */, @@ -567,6 +646,17 @@ path = SupportingFiles; sourceTree = ""; }; + 0ED3F5C21537519389C07644 /* BugReport */ = { + isa = PBXGroup; + children = ( + D6DC38E64A5ED3FDB201029A /* BugReportService.swift */, + 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */, + F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */, + F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */, + ); + path = BugReport; + sourceTree = ""; + }; 10578D9852BA78D309A1CBDF /* ViewModel */ = { isa = PBXGroup; children = ( @@ -595,10 +685,19 @@ path = Resources; sourceTree = ""; }; + 328DD5DA1281F758B72006C7 /* Views */ = { + isa = PBXGroup; + children = ( + 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */, + ); + path = Views; + sourceTree = ""; + }; 337015ADFBA3AB96660DB3A6 /* Generated */ = { isa = PBXGroup; children = ( 71D52BAA5BADB06E5E8C295D /* Assets.swift */, + 6A901D95158B02CA96C79C7F /* InfoPlist.swift */, 47EBB5D698CE9A25BB553A2D /* Strings.swift */, 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */, ); @@ -623,6 +722,18 @@ path = View; sourceTree = ""; }; + 4009BE2E791C16AC6EE39A7E /* BugReport */ = { + isa = PBXGroup; + children = ( + AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */, + B516212D9FE785DDD5E490D1 /* BugReportModels.swift */, + 7E532D95330139D118A9BF88 /* BugReportViewModel.swift */, + 28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */, + 58F951CB7BD7F96C37BE5CAD /* View */, + ); + path = BugReport; + sourceTree = ""; + }; 405B00F139AEE3994601B36A = { isa = PBXGroup; children = ( @@ -650,6 +761,14 @@ path = Room; sourceTree = ""; }; + 4541090DFE1A5499BD67BD14 /* View */ = { + isa = PBXGroup; + children = ( + 2BEB3259B2208E5AE5BB3F65 /* Settings.swift */, + ); + path = View; + sourceTree = ""; + }; 4658A940E89BC42EE3346A97 /* Messages */ = { isa = PBXGroup; children = ( @@ -704,6 +823,14 @@ path = Scripts; sourceTree = ""; }; + 58F951CB7BD7F96C37BE5CAD /* View */ = { + isa = PBXGroup; + children = ( + 6D607F47FDEF16CC63684BE0 /* BugReport.swift */, + ); + path = View; + sourceTree = ""; + }; 5958CAF6E56422496E0063AF /* LoginScreen */ = { isa = PBXGroup; children = ( @@ -738,6 +865,18 @@ name = Products; sourceTree = ""; }; + 70B74A432C241E56A7ACE610 /* Settings */ = { + isa = PBXGroup; + children = ( + 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */, + 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */, + 0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */, + 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */, + 4541090DFE1A5499BD67BD14 /* View */, + ); + path = Settings; + sourceTree = ""; + }; 70DABA39C844CA931B829395 /* RoomSummary */ = { isa = PBXGroup; children = ( @@ -763,11 +902,17 @@ isa = PBXGroup; children = ( AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */, + EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */, + 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */, DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */, 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */, + 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */, C070FD43DC6BF4E50217965A /* LocalizationTests.swift */, + 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */, 5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, + F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */, + 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */, AF552BB969DC98A4BB8CF8D5 /* UserIndicators */, ); path = Sources; @@ -875,9 +1020,11 @@ isa = PBXGroup; children = ( 7D0CBC76C80E04345E11F2DB /* Application.swift */, + C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */, 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */, 086B997409328F091EBA43CE /* RoomScreenUITests.swift */, + E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */, ); path = Sources; sourceTree = ""; @@ -930,6 +1077,7 @@ A0C06C0F6A8621B22BFAEB56 /* Localizations */ = { isa = PBXGroup; children = ( + 91DE43B8815918E590912DDA /* InfoPlist.strings */, 7109E709A7738E6BCC4553E6 /* Localizable.strings */, 187853A7E643995EE49FAD43 /* Localizable.stringsdict */, D2F7194F440375338F8E2487 /* Untranslated.strings */, @@ -941,6 +1089,7 @@ A4852B57D55D71EEBFCD931D /* UnitTests */ = { isa = PBXGroup; children = ( + E600AACDF87CDBCE32683236 /* Resources */, 73CD9796729EB702B4DFA88C /* Sources */, 24FD174C31912A5FACFEAFB5 /* SupportingFiles */, ); @@ -1009,9 +1158,10 @@ E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */, D77DD2DA5DC8654F2A80FF1D /* Bundle.swift */, 95CC95CD75B688E946438165 /* Coordinator.swift */, + 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */, F7B81C8227BBEA95CCE86037 /* MatrixEntitityRegex.swift */, - 64839516BD56D1C81D84C5E0 /* MXLog.swift */, 8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */, + 06501F0E978B2D5C92771DC7 /* Logging */, FE50232944F9E67ADD7A2D21 /* Routers */, 052CC920F473C10B509F9FC1 /* SwiftUI */, F8474EB69289112888B65518 /* UserIndicators */, @@ -1032,20 +1182,31 @@ E59565F441830B19DBAE567C /* Screens */ = { isa = PBXGroup; children = ( + 4009BE2E791C16AC6EE39A7E /* BugReport */, B53CA9BECD3F97805E1432D0 /* HomeScreen */, 5958CAF6E56422496E0063AF /* LoginScreen */, 679E9837ECA8D6776079D16E /* RoomScreen */, + 70B74A432C241E56A7ACE610 /* Settings */, 02175C9269C4632DB6D12C25 /* Splash */, ); path = Screens; sourceTree = ""; }; + E600AACDF87CDBCE32683236 /* Resources */ = { + isa = PBXGroup; + children = ( + 9414DCADBDF9D6C4B806F61E /* sample_screenshot.png */, + ); + path = Resources; + sourceTree = ""; + }; E68740F873AB18A5C26844EA /* Sources */ = { isa = PBXGroup; children = ( CF3EDF23226895776553F04A /* AppCoordinator.swift */, 5A9AB74614131D6706894E0C /* AppCoordinatorStateMachine.swift */, EFFA5FD06AAAC4AF544B594E /* AppDelegate.swift */, + 3F87116470221880017CF522 /* BuildSettings.swift */, 967873B9E11828B67F64C89A /* UITestsAppCoordinator.swift */, CCA431E6EDD71F7067B5F9E7 /* UITestsRootView.swift */, 0787F81684E503024BD0C051 /* Services */, @@ -1134,6 +1295,8 @@ 04C28663564E008DB32B5972 /* Introspect */, A981A4CA233FB5C13B9CA690 /* SwiftyBeaver */, 3853B78FB8531B83936C5DA6 /* SwiftState */, + 1BCD21310B997A6837B854D6 /* GZIP */, + 67E7A6F388D3BF85767609D9 /* Sentry */, ); productName = UITests; productReference = F506C6ADB1E1DA6638078E11 /* UITests.xctest */; @@ -1179,6 +1342,8 @@ 5986E300FC849DEAB2EE7AEB /* Introspect */, FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */, 9573B94B1C86C6DF751AF3FD /* SwiftState */, + 997C7385E1A07E061D7E2100 /* GZIP */, + 7731767AE437BA3BD2CC14A8 /* Sentry */, ); productName = ElementX; productReference = 4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */; @@ -1283,10 +1448,12 @@ mainGroup = 405B00F139AEE3994601B36A; packageReferences = ( C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */, + 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */, A24ABD6F9CEE4D0749A6173E /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, 61916C63E3F5BD900F08DA0C /* XCRemoteSwiftPackageReference "KeychainAccess" */, D283517192CAC3E2E6920765 /* XCRemoteSwiftPackageReference "Kingfisher" */, 4FCDA8D25C7415C8FB33490D /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */, + D94BF33F492E2443005F809A /* XCRemoteSwiftPackageReference "sentry-cocoa" */, 6582B5AF3F104B0F7E031E7D /* XCRemoteSwiftPackageReference "SwiftState" */, 25B4484A6A20B9F1705DEEDA /* XCRemoteSwiftPackageReference "SwiftyBeaver" */, ); @@ -1308,6 +1475,7 @@ 30122AB3484AC6C3A7F6A717 /* ActivityIndicatorView.xib in Resources */, B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */, 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */, + B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */, AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */, 5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */, 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */, @@ -1330,6 +1498,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 524C9C31EF8D58C2249F8A10 /* sample_screenshot.png in Resources */, 35E975CFDA60E05362A7CF79 /* target.yml in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1381,11 +1550,17 @@ buildActionMask = 2147483647; files = ( 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */, + 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */, + C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */, 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */, F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */, + 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */, 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, + 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, 7434A7F02D587A920B376A9A /* LoginScreenViewModelTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, + EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */, + 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */, 7B3D3AFD511D496DED18910B /* TemplateSimpleScreenViewModelTests.swift in Sources */, 1151DCC5EC2C6585826545EC /* UserIndicatorPresenterSpy.swift in Sources */, 4B8A2C45FF906ADBB1F5C3B4 /* UserIndicatorQueueTests.swift in Sources */, @@ -1410,12 +1585,21 @@ CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */, 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */, B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */, + 00AC53151BA23A90FAAE9FBF /* BugReport.swift in Sources */, + A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */, + 00F3059B1E0CFCA019710C3E /* BugReportModels.swift in Sources */, + 3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */, + 172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */, + 86C2E93920FD15AD17E193A9 /* BugReportViewModel.swift in Sources */, + 187E18F21EF4DA244E436E58 /* BugReportViewModelProtocol.swift in Sources */, + 05776B005C57E92582F0CF08 /* BuildSettings.swift in Sources */, 7E1EDBA3934E6C29E5BD045B /* Bundle.swift in Sources */, 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */, 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */, DCB781BD227CA958809AFADF /* Coordinator.swift in Sources */, C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */, EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */, + D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */, 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */, 224A55EEAEECF5336B14A4A5 /* EmoteRoomMessage.swift in Sources */, 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */, @@ -1432,9 +1616,11 @@ 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */, DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */, 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */, + 6EA61FCA55D950BDE326A1A7 /* ImageAnonymizer.swift in Sources */, 2E59008365E01F0AFB3A6B24 /* ImageRoomMessage.swift in Sources */, DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */, D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */, + A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */, 2D8A687149E46B8C8B989561 /* KeychainController.swift in Sources */, 277D2531C70F207A2F9F5906 /* KeychainControllerProtocol.swift in Sources */, 9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */, @@ -1444,7 +1630,8 @@ E9CEAF2C38E4E00459B811D9 /* LoginScreenModels.swift in Sources */, 7C9121245B11CA48307CB462 /* LoginScreenViewModel.swift in Sources */, 33912D1B9264D897033E0681 /* LoginScreenViewModelProtocol.swift in Sources */, - B0887A7B5AFEC88981626389 /* MXLog.swift in Sources */, + B94368839BDB69172E28E245 /* MXLog.swift in Sources */, + BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */, F03E16ED043C62FED5A07AE0 /* MatrixEntitityRegex.swift in Sources */, EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */, 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */, @@ -1454,6 +1641,7 @@ A5EC21A071F58FC1229C20D0 /* MemberDetailsProviderProtocol.swift in Sources */, 24906A1E82D0046655958536 /* MessageComposer.swift in Sources */, 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */, + 28410F3DE89C2C44E4F75C92 /* MockBugReportService.swift in Sources */, 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */, 51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */, 29AEE68A604940180AB9EBFF /* MockRoomSummary.swift in Sources */, @@ -1494,8 +1682,14 @@ 7F19E97E7985F518C9018B83 /* RootRouter.swift in Sources */, 2C0CE61E5DC177938618E0B1 /* RootRouterType.swift in Sources */, 462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */, + CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */, 1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */, 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */, + 6C72F66DA26A0956E9A9077A /* Settings.swift in Sources */, + 34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */, + 3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */, + 4A2E0DBB63919AC8309B6D40 /* SettingsViewModel.swift in Sources */, + 438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */, FCB640C576292BEAF7FA3B2E /* SplashViewController.swift in Sources */, B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */, A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */, @@ -1538,9 +1732,11 @@ buildActionMask = 2147483647; files = ( 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */, + 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */, 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */, 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */, + 490E606044B18985055FF690 /* SettingsUITests.swift in Sources */, 2E68C57E7D644E94778743D5 /* TemplateSimpleScreenUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1697,6 +1893,14 @@ name = Localizable.strings; sourceTree = ""; }; + 91DE43B8815918E590912DDA /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 1215A4FC53D2319E81AE8970 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2043,6 +2247,14 @@ minimumVersion = 6.0.0; }; }; + 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/nicklockwood/GZIP"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.3.0; + }; + }; A24ABD6F9CEE4D0749A6173E /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/siteline/SwiftUI-Introspect.git"; @@ -2067,6 +2279,14 @@ minimumVersion = 7.2.0; }; }; + D94BF33F492E2443005F809A /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/getsentry/sentry-cocoa.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.15.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -2085,6 +2305,11 @@ package = D283517192CAC3E2E6920765 /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; }; + 1BCD21310B997A6837B854D6 /* GZIP */ = { + isa = XCSwiftPackageProductDependency; + package = 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */; + productName = GZIP; + }; 36B7FC232711031AA2B0D188 /* DTCoreText */ = { isa = XCSwiftPackageProductDependency; package = C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */; @@ -2110,6 +2335,16 @@ package = A24ABD6F9CEE4D0749A6173E /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = Introspect; }; + 67E7A6F388D3BF85767609D9 /* Sentry */ = { + isa = XCSwiftPackageProductDependency; + package = D94BF33F492E2443005F809A /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = Sentry; + }; + 7731767AE437BA3BD2CC14A8 /* Sentry */ = { + isa = XCSwiftPackageProductDependency; + package = D94BF33F492E2443005F809A /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = Sentry; + }; 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; package = 61916C63E3F5BD900F08DA0C /* XCRemoteSwiftPackageReference "KeychainAccess" */; @@ -2120,6 +2355,11 @@ package = 6582B5AF3F104B0F7E031E7D /* XCRemoteSwiftPackageReference "SwiftState" */; productName = SwiftState; }; + 997C7385E1A07E061D7E2100 /* GZIP */ = { + isa = XCSwiftPackageProductDependency; + package = 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */; + productName = GZIP; + }; A678E40E917620059695F067 /* MatrixRustSDK */ = { isa = XCSwiftPackageProductDependency; package = 4FCDA8D25C7415C8FB33490D /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b13152db0..e71ec1a55 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -18,6 +18,15 @@ "version" : "1.7.18" } }, + { + "identity" : "gzip", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/GZIP", + "state" : { + "revision" : "c45c8526dad61240a79aaff5b1cf6e082d2b90b2", + "version" : "1.3.0" + } + }, { "identity" : "keychainaccess", "kind" : "remoteSourceControl", @@ -45,6 +54,15 @@ "revision" : "43c88a4b0912a1589c2a28cc9bb2df45c70cdcad" } }, + { + "identity" : "sentry-cocoa", + "kind" : "remoteSourceControl", + "location" : "https://github.com/getsentry/sentry-cocoa.git", + "state" : { + "revision" : "afa4cd596e5cf97a797fb9b5c3afeea4add2c7a3", + "version" : "7.15.0" + } + }, { "identity" : "swiftstate", "kind" : "remoteSourceControl", diff --git a/ElementX/Resources/Assets.xcassets/Images/close_circle.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/Images/close_circle.imageset/Contents.json new file mode 100644 index 000000000..9aa8a10fc --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/Images/close_circle.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "url_preview_close_dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/ElementX/Resources/Assets.xcassets/Images/close_circle.imageset/url_preview_close_dark.pdf b/ElementX/Resources/Assets.xcassets/Images/close_circle.imageset/url_preview_close_dark.pdf new file mode 100644 index 000000000..c2b155fab --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/Images/close_circle.imageset/url_preview_close_dark.pdf @@ -0,0 +1,116 @@ +%PDF-1.7 + +1 0 obj + << /ExtGState << /E1 << /ca 0.800000 >> >> >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.223529 0.250980 0.286275 scn +24.000000 12.000000 m +24.000000 5.372583 18.627417 0.000000 12.000000 0.000000 c +5.372583 0.000000 0.000000 5.372583 0.000000 12.000000 c +0.000000 18.627417 5.372583 24.000000 12.000000 24.000000 c +18.627417 24.000000 24.000000 18.627417 24.000000 12.000000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 7.999756 5.805014 cm +0.662745 0.698039 0.737255 scn +0.707107 10.902368 m +0.316583 11.292892 -0.316582 11.292892 -0.707107 10.902368 c +-1.097631 10.511844 -1.097631 9.878678 -0.707107 9.488154 c +0.707107 10.902368 l +h +7.292891 1.488155 m +7.683415 1.097631 8.316580 1.097631 8.707105 1.488155 c +9.097629 1.878679 9.097629 2.511844 8.707105 2.902369 c +7.292891 1.488155 l +h +-0.707107 9.488154 m +7.292891 1.488155 l +8.707105 2.902369 l +0.707107 10.902368 l +-0.707107 9.488154 l +h +f +n +Q +q +-1.000000 -0.000000 -0.000000 1.000000 16.000488 5.805014 cm +0.662745 0.698039 0.737255 scn +0.707107 10.902368 m +0.316582 11.292892 -0.316583 11.292892 -0.707107 10.902368 c +-1.097631 10.511844 -1.097631 9.878678 -0.707107 9.488154 c +0.707107 10.902368 l +h +7.292893 1.488155 m +7.683417 1.097631 8.316583 1.097631 8.707107 1.488155 c +9.097631 1.878679 9.097631 2.511845 8.707107 2.902369 c +7.292893 1.488155 l +h +-0.707107 9.488154 m +7.292893 1.488155 l +8.707107 2.902369 l +0.707107 10.902368 l +-0.707107 9.488154 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1439 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 24.000000 24.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000074 00000 n +0000001569 00000 n +0000001592 00000 n +0000001765 00000 n +0000001839 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1898 +%%EOF \ No newline at end of file diff --git a/ElementX/Resources/Localizations/en.lproj/InfoPlist.strings b/ElementX/Resources/Localizations/en.lproj/InfoPlist.strings new file mode 100644 index 000000000..4352f822e --- /dev/null +++ b/ElementX/Resources/Localizations/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"NSPhotoLibraryUsageDescription" = "The photo library is used to send photos and videos."; diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index d380ece65..29aa4c928 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -1 +1,3 @@ "untranslated" = "Untranslated"; +"screenshot_detected_title" = "You took a screenshot"; +"screenshot_detected_message" = "Would you like to submit a bug report?"; diff --git a/ElementX/Sources/AppCoordinator.swift b/ElementX/Sources/AppCoordinator.swift index ca51ee4b4..3c5483074 100644 --- a/ElementX/Sources/AppCoordinator.swift +++ b/ElementX/Sources/AppCoordinator.swift @@ -25,7 +25,10 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { private var userSession: UserSession! private let memberDetailProviderManager: MemberDetailProviderManager - + + private let bugReportService: BugReportServiceProtocol + private let screenshotDetector: ScreenshotDetector + private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var loadingIndicator: UserIndicator? private var errorIndicator: UserIndicator? @@ -34,7 +37,14 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { init() { stateMachine = AppCoordinatorStateMachine() - + + do { + bugReportService = try BugReportService(withBaseUrlString: BuildSettings.bugReportServiceBaseUrlString, + sentryEndpoint: BuildSettings.bugReportSentryEndpoint) + } catch { + fatalError(error.localizedDescription) + } + splashViewController = SplashViewController() mainNavigationController = UINavigationController(rootViewController: splashViewController) window = UIWindow(frame: UIScreen.main.bounds) @@ -53,12 +63,20 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { keychainController = KeychainController(identifier: bundleIdentifier) authenticationCoordinator = AuthenticationCoordinator(keychainController: keychainController, navigationRouter: navigationRouter) + + screenshotDetector = ScreenshotDetector() + screenshotDetector.callback = processScreenshotDetection + authenticationCoordinator.delegate = self setupStateMachine() let loggerConfiguration = MXLogConfiguration() loggerConfiguration.logLevel = .verbose + // Redirect NSLogs to files only if we are not debugging + if isatty(STDERR_FILENO) == 0 { + loggerConfiguration.redirectLogsToFiles = true + } MXLog.configure(loggerConfiguration) // Benchmark.trackingEnabled = true @@ -117,6 +135,10 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { self.tearDownUserSession() case (.signingOut, .failedSigningOut, _): self.showLogoutErrorToast() + case (.homeScreen, .showSettingsScreen, .settingsScreen): + self.presentSettingsScreen() + case (.settingsScreen, .dismissedSettingsScreen, .homeScreen): + self.tearDownDismissedSettingsScreen() default: fatalError("Unknown transition: \(context)") } @@ -162,13 +184,33 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { switch action { case .logout: self.stateMachine.processEvent(.attemptSignOut) - case .selectRoom(let roomIdentifier): + case .presentRoom(let roomIdentifier): self.stateMachine.processEvent(.showRoomScreen(roomId: roomIdentifier)) + case .presentSettings: + self.stateMachine.processEvent(.showSettingsScreen) } } add(childCoordinator: coordinator) navigationRouter.setRootModule(coordinator) + + if bugReportService.crashedLastRun { + showCrashPopup() + } + } + + private func presentSettingsScreen() { + let parameters = SettingsCoordinatorParameters(navigationRouter: navigationRouter, + bugReportService: bugReportService) + let coordinator = SettingsCoordinator(parameters: parameters) + + add(childCoordinator: coordinator) + coordinator.start() + navigationRouter.push(coordinator) { [weak self] in + guard let self = self else { return } + + self.stateMachine.processEvent(.dismissedSettingsScreen) + } } private func presentRoomWithIdentifier(_ roomIdentifier: String) { @@ -206,6 +248,14 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { remove(childCoordinator: coordinator) } + + private func tearDownDismissedSettingsScreen() { + guard let coordinator = childCoordinators.last as? SettingsCoordinator else { + fatalError("Invalid coordinator hierarchy: \(childCoordinators)") + } + + remove(childCoordinator: coordinator) + } private func showLoadingIndicator() { loadingIndicator = indicatorPresenter.present(.loading(label: "Loading", isInteractionBlocking: true)) @@ -216,10 +266,70 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator { } private func showLoginErrorToast() { - errorIndicator = indicatorPresenter.present(.success(label: "Failed logging in")) + errorIndicator = indicatorPresenter.present(.error(label: "Failed logging in")) } private func showLogoutErrorToast() { errorIndicator = indicatorPresenter.present(.success(label: "Failed logging out")) } + + private func showCrashPopup() { + let alert = UIAlertController(title: nil, + message: ElementL10n.sendBugReportAppCrashed, + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: ElementL10n.no, style: .cancel)) + alert.addAction(UIAlertAction(title: ElementL10n.yes, style: .default) { [weak self] _ in + self?.presentBugReportScreen() + }) + + navigationRouter.present(alert, animated: true) + } + + private func processScreenshotDetection(image: UIImage?, error: Error?) { + MXLog.debug("[AppCoordinator] processScreenshotDetection: \(String(describing: image)), error: \(String(describing: error))") + + let alert = UIAlertController(title: ElementL10n.screenshotDetectedTitle, + message: ElementL10n.screenshotDetectedMessage, + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: ElementL10n.no, style: .cancel)) + alert.addAction(UIAlertAction(title: ElementL10n.yes, style: .default) { [weak self] _ in + self?.presentBugReportScreen(for: image) + }) + + navigationRouter.present(alert, animated: true) + } + + private func presentBugReportScreen(for image: UIImage? = nil) { + let parameters = BugReportCoordinatorParameters(bugReportService: bugReportService, + screenshot: image) + let coordinator = BugReportCoordinator(parameters: parameters) + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { return } + self.navigationRouter.dismissModule(animated: true) + self.remove(childCoordinator: coordinator) + } + + add(childCoordinator: coordinator) + coordinator.start() + let navController = UINavigationController(rootViewController: coordinator.toPresentable()) + navController.navigationBar.topItem?.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, + target: self, + action: #selector(dismissBugReportScreen)) + navController.isModalInPresentation = true + navigationRouter.present(navController, animated: true) + } + + @objc + private func dismissBugReportScreen() { + MXLog.debug("[AppCoorrdinator] dismissBugReportScreen") + + guard let bugReportCoordinator = childCoordinators.first(where: { $0 is BugReportCoordinator }) else { + return + } + + navigationRouter.dismissModule() + remove(childCoordinator: bugReportCoordinator) + } } diff --git a/ElementX/Sources/AppCoordinatorStateMachine.swift b/ElementX/Sources/AppCoordinatorStateMachine.swift index 1e2050d46..1add9b16c 100644 --- a/ElementX/Sources/AppCoordinatorStateMachine.swift +++ b/ElementX/Sources/AppCoordinatorStateMachine.swift @@ -22,6 +22,8 @@ class AppCoordinatorStateMachine { case signedIn /// Showing the home screen case homeScreen + /// Showing the settings screen + case settingsScreen /// Showing a particular room's timeline /// - Parameter roomId: that room's identifier case roomScreen(roomId: String) @@ -52,6 +54,10 @@ class AppCoordinatorStateMachine { case showRoomScreen(roomId: String) /// The room screen has been dismissed case dismissedRoomScreen + /// The settings screen has been dismissed + case dismissedSettingsScreen + /// Request settings screen presentation + case showSettingsScreen } private let stateMachine: StateMachine @@ -71,6 +77,8 @@ class AppCoordinatorStateMachine { machine.addRoutes(event: .succeededSigningOut, transitions: [ .signingOut => .signedOut ]) machine.addRoutes(event: .failedSigningOut, transitions: [ .signingOut => .homeScreen ]) + machine.addRoutes(event: .showSettingsScreen, transitions: [ .homeScreen => .settingsScreen ]) + machine.addRoutes(event: .dismissedSettingsScreen, transitions: [ .settingsScreen => .homeScreen ]) // Transitions with associated values need to be handled through `addRouteMapping` machine.addRouteMapping { event, fromState, _ in diff --git a/ElementX/Sources/BuildSettings.swift b/ElementX/Sources/BuildSettings.swift new file mode 100644 index 000000000..55c614ec5 --- /dev/null +++ b/ElementX/Sources/BuildSettings.swift @@ -0,0 +1,20 @@ +// +// BuildSettings.swift +// ElementX +// +// Created by Ismail on 2.06.2022. +// Copyright © 2022 Element. All rights reserved. +// + +import Foundation + +final class BuildSettings { + + // MARK: - Bug report + static let bugReportServiceBaseUrlString = "https://riot.im/bugreports" + static let bugReportSentryEndpoint = "https://f39ac49e97714316965b777d9f3d6cd8@sentry.tools.element.io/44" + // Use the name allocated by the bug report server + static let bugReportApplicationId = "riot-ios" + static let bugReportUISIId = "element-auto-uisi" + +} diff --git a/ElementX/Sources/Generated/Assets.swift b/ElementX/Sources/Generated/Assets.swift index 0d2ecf8f8..de517a621 100644 --- a/ElementX/Sources/Generated/Assets.swift +++ b/ElementX/Sources/Generated/Assets.swift @@ -27,6 +27,7 @@ internal enum Asset { } internal enum Images { internal static let appLogo = ImageAsset(name: "Images/app-logo") + internal static let closeCircle = ImageAsset(name: "Images/close_circle") internal static let timelineComposerSendMessage = ImageAsset(name: "Images/timelineComposerSendMessage") internal static let timelineScrollToBottom = ImageAsset(name: "Images/timelineScrollToBottom") } diff --git a/ElementX/Sources/Generated/InfoPlist.swift b/ElementX/Sources/Generated/InfoPlist.swift new file mode 100644 index 000000000..71a316a80 --- /dev/null +++ b/ElementX/Sources/Generated/InfoPlist.swift @@ -0,0 +1,67 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +import Foundation + +// swiftlint:disable superfluous_disable_command +// swiftlint:disable file_length + +// MARK: - Plist Files + +// swiftlint:disable identifier_name line_length type_body_length +internal enum ElementInfoPlist { + private static let _document = PlistDocument(path: "Info.plist") + + internal static let cfBundleDevelopmentRegion: String = _document["CFBundleDevelopmentRegion"] + internal static let cfBundleExecutable: String = _document["CFBundleExecutable"] + internal static let cfBundleIdentifier: String = _document["CFBundleIdentifier"] + internal static let cfBundleInfoDictionaryVersion: String = _document["CFBundleInfoDictionaryVersion"] + internal static let cfBundleName: String = _document["CFBundleName"] + internal static let cfBundlePackageType: String = _document["CFBundlePackageType"] + internal static let cfBundleShortVersionString: String = _document["CFBundleShortVersionString"] + internal static let cfBundleVersion: String = _document["CFBundleVersion"] + internal static let uiLaunchStoryboardName: String = _document["UILaunchStoryboardName"] + internal static let uiSupportedInterfaceOrientations: [String] = _document["UISupportedInterfaceOrientations"] +} +// swiftlint:enable identifier_name line_length type_body_length + +// MARK: - Implementation Details + +private func arrayFromPlist(at path: String) -> [T] { + guard let url = BundleToken.bundle.url(forResource: path, withExtension: nil), + let data = NSArray(contentsOf: url) as? [T] else { + fatalError("Unable to load PLIST at path: \(path)") + } + return data +} + +private struct PlistDocument { + let data: [String: Any] + + init(path: String) { + guard let url = BundleToken.bundle.url(forResource: path, withExtension: nil), + let data = NSDictionary(contentsOf: url) as? [String: Any] else { + fatalError("Unable to load PLIST at path: \(path)") + } + self.data = data + } + + subscript(key: String) -> T { + guard let result = data[key] as? T else { + fatalError("Property '\(key)' is not of type \(T.self)") + } + return result + } +} + +// swiftlint:disable convenience_type +private final class BundleToken { + static let bundle: Bundle = { + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: BundleToken.self) + #endif + }() +} +// swiftlint:enable convenience_type diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index 3d2e45bef..419a25ab5 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -10,6 +10,10 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces extension ElementL10n { + /// Would you like to submit a bug report? + public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message") + /// You took a screenshot + public static let screenshotDetectedTitle = ElementL10n.tr("Untranslated", "screenshot_detected_title") /// Untranslated public static let untranslated = ElementL10n.tr("Untranslated", "untranslated") /// Plural format key: "%#@VARIABLE@" diff --git a/ElementX/Sources/Other/ImageAnonymizer.swift b/ElementX/Sources/Other/ImageAnonymizer.swift new file mode 100644 index 000000000..3d26011d4 --- /dev/null +++ b/ElementX/Sources/Other/ImageAnonymizer.swift @@ -0,0 +1,111 @@ +// +// UIImage+.swift +// ElementX +// +// Created by Ismail on 20.05.2022. +// Copyright © 2022 element.io. All rights reserved. +// + +import Foundation +import Vision +import UIKit + +enum ImageAnonymizerError: Error { + case noCgImageBased +} + +struct ImageAnonymizer { + + private static var allowedTextItems: [String] = [ + "#", + "@", + "%", + "&", + "+", + "-", + "_", + "\"", + "?", + "*" + ] + + static func anonymizedImage(from image: UIImage, + confidenceLevel: Float = 0.5, + fillColor: UIColor = .red) async throws -> UIImage { + guard let cgImage = image.cgImage else { + throw ImageAnonymizerError.noCgImageBased + } + + // create a handler with cgImage + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + var observations: [VNDetectedObjectObservation] = [] + + // create a text request + let textRequest = VNRecognizeTextRequest { request, error in + guard let results = request.results as? [VNRecognizedTextObservation], + error == nil else { + return + } + observations.append(contentsOf: results) + } + textRequest.recognitionLevel = .accurate + textRequest.revision = VNRecognizeTextRequestRevision2 + + // create a face request + let faceRequest = VNDetectFaceRectanglesRequest { request, error in + guard let results = request.results as? [VNFaceObservation], + error == nil else { + return + } + observations.append(contentsOf: results) + } + // revision3 doesn't work! + faceRequest.revision = VNDetectFaceRectanglesRequestRevision2 + + // perform requests + try handler.perform([ + textRequest, + faceRequest + ]) + + return render(image: image, + confidenceLevel: confidenceLevel, + fillColor: fillColor, + observations: observations) + } + + private static func render(image: UIImage, + confidenceLevel: Float, + fillColor: UIColor, + observations: [VNDetectedObjectObservation]) -> UIImage { + let size = image.size + let result = UIGraphicsImageRenderer(size: size).image { rendererContext in + // first draw self + image.draw(in: CGRect(origin: .zero, size: size)) + // set fill color + fillColor.setFill() + for observation in observations { + guard observation.confidence >= confidenceLevel else { + // ensure observation's confidence level + continue + } + if let textObservation = observation as? VNRecognizedTextObservation, + let text = textObservation.topCandidates(1).first?.string { + if Double(text) != nil || Self.allowedTextItems.contains(text) { + continue + } + } + let box = observation.boundingBox + // boc is normalized (and in starts from the lower left corner) + // convert it to a rect in the image + let rect = CGRect(x: box.minX * size.width, + y: size.height - box.maxY * size.height, + width: box.width * size.width, + height: box.height * size.height) + rendererContext.fill(rect) + } + } + return result + } + +} diff --git a/ElementX/Sources/Other/MXLog.swift b/ElementX/Sources/Other/Logging/MXLog.swift similarity index 94% rename from ElementX/Sources/Other/MXLog.swift rename to ElementX/Sources/Other/Logging/MXLog.swift index f813762c1..d55380d00 100644 --- a/ElementX/Sources/Other/MXLog.swift +++ b/ElementX/Sources/Other/Logging/MXLog.swift @@ -136,14 +136,14 @@ private var logger: SwiftyBeaver.Type = { // MARK: - Private fileprivate static func configureLogger(_ logger: SwiftyBeaver.Type, withConfiguration configuration: MXLogConfiguration) { -// if let subLogName = configuration.subLogName { -// MXLogger.setSubLogName(subLogName) -// } -// -// MXLogger.redirectNSLog(toFiles: configuration.redirectLogsToFiles, -// numberOfFiles: configuration.maxLogFilesCount, -// sizeLimit: configuration.logFilesSizeLimit) -// + if let subLogName = configuration.subLogName { + MXLogger.setSubLogName(subLogName) + } + + MXLogger.redirectNSLog(toFiles: configuration.redirectLogsToFiles, + numberOfFiles: configuration.maxLogFilesCount, + sizeLimit: configuration.logFilesSizeLimit) + guard configuration.logLevel != .none else { return } diff --git a/ElementX/Sources/Other/Logging/MXLogger.h b/ElementX/Sources/Other/Logging/MXLogger.h new file mode 100644 index 000000000..56eebc26d --- /dev/null +++ b/ElementX/Sources/Other/Logging/MXLogger.h @@ -0,0 +1,114 @@ +/* + Copyright 2015 OpenMarket Ltd + Copyright 2020 The Matrix.org Foundation C.I.C + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +/** + The `MXLogger` tool redirects NSLog output into a fixed pool of files. + Another log file is used every time [MXLogger redirectNSLogToFiles:YES] + is called. The pool contains 3 files. + + `MXLogger` can track and log uncatched exceptions or crashes. + */ +@interface MXLogger : NSObject + +/** + Redirect NSLog output to MXLogger files. + + It is advised to condition this redirection in '#if (!isatty(STDERR_FILENO))' block to enable + it only when the device is not attached to the debugger. + + @param redirectNSLogToFiles YES to enable the redirection. + */ ++ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles; + +/** + Redirect NSLog output to MXLogger files. + + It is advised to condition this redirection in '#if (!isatty(STDERR_FILENO))' block to enable + it only when the device is not attached to the debugger. + + @param redirectNSLogToFiles YES to enable the redirection. + @param numberOfFiles number of files to keep (default is 10). + */ ++ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles numberOfFiles:(NSUInteger)numberOfFiles; + +/** + Redirect NSLog output to MXLogger files. + + @param redirectNSLogToFiles YES to enable the redirection. + @param numberOfFiles number of files to keep (default is 10). + @param sizeLimit size limit of log files in bytes. 0 means no limitation, the default value for other methods + */ ++ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles numberOfFiles:(NSUInteger)numberOfFiles sizeLimit:(NSUInteger)sizeLimit; + +/** + Delete all log files. + */ ++ (void)deleteLogFiles; + +/** + Get the list of all log files. + + @return files of + */ ++ (NSArray*)logFiles; + +/** + Make `MXLogger` catch and log unmanaged exceptions or application crashes. + + When such error happens, `MXLogger` stores the application stack trace into a file + just before the application leaves. The path of this file is provided by [MXLogger crashLog]. + + @param logCrashes YES to enable the catch. + */ ++ (void)logCrashes:(BOOL)logCrashes; + +/** + Set the app build version. + It will be reported in crash report. + */ ++ (void)setBuildVersion:(NSString*)buildVersion; + +/** + Set a sub name for namespacing log files. + + A sub name must be set when running from an app extension because extensions can + run in parallel to the app. + It must be called before `redirectNSLogToFiles`. + + @param subLogName the subname for log files. Files will be named as 'console-[subLogName].log' + Default is nil. + */ ++ (void)setSubLogName:(NSString*)subLogName; + +/** + If any, get the file containing the last application crash log. + + Only one crash log is stored at a time. The best moment for the app to handle it is the + at its next startup. + + @return the crash log file. nil if there is none. + */ ++ (NSString*)crashLog; + +/** + Delete the crash log file. + */ ++ (void)deleteCrashLog; + +@end diff --git a/ElementX/Sources/Other/Logging/MXLogger.m b/ElementX/Sources/Other/Logging/MXLogger.m new file mode 100644 index 000000000..7591734f5 --- /dev/null +++ b/ElementX/Sources/Other/Logging/MXLogger.m @@ -0,0 +1,392 @@ +/* + Copyright 2015 OpenMarket Ltd + Copyright 2020 The Matrix.org Foundation C.I.C + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "MXLogger.h" +#import + +// stderr so it can be restored +int stderrSave = 0; + +static NSString *buildVersion; +static NSString *subLogName; + +#define MXLOGGER_CRASH_LOG @"crash.log" + +@implementation MXLogger + +#pragma mark - NSLog redirection ++ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles +{ + [self redirectNSLogToFiles:redirectNSLogToFiles numberOfFiles:10]; +} + ++ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles numberOfFiles:(NSUInteger)numberOfFiles +{ + [self redirectNSLogToFiles:redirectNSLogToFiles numberOfFiles:numberOfFiles sizeLimit:0]; +} + ++ (void)redirectNSLogToFiles:(BOOL)redirectNSLogToFiles numberOfFiles:(NSUInteger)numberOfFiles sizeLimit:(NSUInteger)sizeLimit +{ + if (redirectNSLogToFiles) + { + NSMutableString *log = [NSMutableString string]; + + // Default subname + if (!subLogName) + { + subLogName = @""; + } + + // Set log location + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *logsFolderPath = [MXLogger logsFolderPath]; + + // Do a circular buffer based on X files + for (NSInteger index = numberOfFiles - 2; index >= 0; index--) + { + NSString *nsLogPathOlder; + NSString *nsLogPathCurrent; + + if (index == 0) + { + nsLogPathOlder = [NSString stringWithFormat:@"console%@.1.log", subLogName]; + nsLogPathCurrent = [NSString stringWithFormat:@"console%@.log", subLogName]; + } + else + { + nsLogPathOlder = [NSString stringWithFormat:@"console%@.%tu.log", subLogName, index + 1]; + nsLogPathCurrent = [NSString stringWithFormat:@"console%@.%tu.log", subLogName, index]; + } + + nsLogPathOlder = [logsFolderPath stringByAppendingPathComponent:nsLogPathOlder]; + nsLogPathCurrent = [logsFolderPath stringByAppendingPathComponent:nsLogPathCurrent]; + + if ([fileManager fileExistsAtPath:nsLogPathCurrent]) + { + if ([fileManager fileExistsAtPath:nsLogPathOlder]) + { + // Temp log + [log appendFormat:@"[MXLogger] redirectNSLogToFiles: removeItemAtPath: %@\n", nsLogPathOlder]; + + NSError *error; + [fileManager removeItemAtPath:nsLogPathOlder error:&error]; + if (error) + { + [log appendFormat:@"[MXLogger] ERROR: removeItemAtPath: %@. Error: %@\n", nsLogPathOlder, error]; + } + } + + // Temp log + [log appendFormat:@"[MXLogger] redirectNSLogToFiles: moveItemAtPath: %@ toPath: %@\n", nsLogPathCurrent, nsLogPathOlder]; + + NSError *error; + [fileManager moveItemAtPath:nsLogPathCurrent toPath:nsLogPathOlder error:&error]; + if (error) + { + [log appendFormat:@"[MXLogger] ERROR: moveItemAtPath: %@ toPath: %@. Error: %@\n", nsLogPathCurrent, nsLogPathOlder, error]; + } + } + } + + // Save stderr so it can be restored. + stderrSave = dup(STDERR_FILENO); + + NSString *nsLogPath = [logsFolderPath stringByAppendingPathComponent:[NSString stringWithFormat:@"console%@.log", subLogName]]; + freopen([nsLogPath fileSystemRepresentation], "w+", stderr); + +// MXLogDebug(@"[MXLogger] redirectNSLogToFiles: YES"); + if (log.length) + { + // We can now log into files +// MXLogDebug(@"%@", log); + } + + [self removeExtraFilesFromCount:numberOfFiles]; + + if (sizeLimit > 0) + { + [self removeFilesAfterSizeLimit:sizeLimit]; + } + } + else if (stderrSave) + { + // Flush before restoring stderr + fflush(stderr); + + // Now restore stderr, so new output goes to console. + dup2(stderrSave, STDERR_FILENO); + close(stderrSave); + } +} + ++ (void)deleteLogFiles +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + for (NSString *logFile in [self logFiles]) + { + [fileManager removeItemAtPath:logFile error:nil]; + } +} + ++ (NSArray*)logFiles +{ + NSMutableArray *logFiles = [NSMutableArray array]; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *logsFolderPath = [MXLogger logsFolderPath]; + + NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:logsFolderPath]; + + // Find all *.log files + NSString *file = nil; + while ((file = [dirEnum nextObject])) + { + if ([[file lastPathComponent] hasPrefix:@"console"]) + { + NSString *logPath = [logsFolderPath stringByAppendingPathComponent:file]; + [logFiles addObject:logPath]; + } + } + +// MXLogDebug(@"[MXLogger] logFiles: %@", logFiles); + + return logFiles; +} + + +#pragma mark - Exceptions and crashes +// Exceptions uncaught by try catch block are handled here +static void handleUncaughtException(NSException *exception) +{ + [MXLogger logCrashes:NO]; + + // Extract running app information + NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary]; + NSString* appVersion; + NSString* app, *appId; + + app = infoDict[@"CFBundleExecutable"]; + appId = infoDict[@"CFBundleIdentifier"]; + + if ([infoDict objectForKey:@"CFBundleVersion"]) + { + appVersion = [NSString stringWithFormat:@"%@ (r%@)", [infoDict objectForKey:@"CFBundleShortVersionString"], [infoDict objectForKey:@"CFBundleVersion"]]; + } + else + { + appVersion = [infoDict objectForKey:@"CFBundleShortVersionString"]; + } + + // Build the crash log +#if TARGET_OS_IPHONE + NSString *model = [[UIDevice currentDevice] model]; + NSString *version = [[UIDevice currentDevice] systemVersion]; +#elif TARGET_OS_OSX + NSString *model = @"Mac"; + NSString *version = [[NSProcessInfo processInfo] operatingSystemVersionString]; +#endif + NSArray *backtrace = [exception callStackSymbols]; + NSString *description = [NSString stringWithFormat:@"%.0f - %@\n%@\nApplication: %@ (%@)\nApplication version: %@\nMatrix SDK version: %@\nBuild: %@\n%@ %@\n\nMain thread: %@\n%@\n", + [[NSDate date] timeIntervalSince1970], + [NSDate date], + exception.description, + app, appId, + appVersion, + @"", + buildVersion, + model, version, + [NSThread isMainThread] ? @"YES" : @"NO", + backtrace]; + + // Write to the crash log file + [MXLogger deleteCrashLog]; + NSString *crashLog = crashLogPath(); + [description writeToFile:crashLog + atomically:NO + encoding:NSStringEncodingConversionAllowLossy + error:nil]; + + NSLog(@"[MXLogger] handleUncaughtException:\n%@", description); +// MXLogError(@"[MXLogger] handleUncaughtException:\n%@", description); +} + +// Signals emitted by the app are handled here +static void handleSignal(int signalValue) +{ + // Throw a custom Objective-C exception + // The Objective-C runtime will then be able to build a readable call stack in handleUncaughtException + [NSException raise:@"Signal detected" format:@"Signal detected: %d", signalValue]; +} + ++ (void)logCrashes:(BOOL)logCrashes +{ + if (logCrashes) + { + // Handle not managed exceptions by ourselves + NSSetUncaughtExceptionHandler(&handleUncaughtException); + + // Register signal event (seg fault & cie) + signal(SIGABRT, handleSignal); + signal(SIGILL, handleSignal); + signal(SIGSEGV, handleSignal); + signal(SIGFPE, handleSignal); + signal(SIGBUS, handleSignal); + } + else + { + // Disable crash handling + NSSetUncaughtExceptionHandler(NULL); + signal(SIGABRT, SIG_DFL); + signal(SIGILL, SIG_DFL); + signal(SIGSEGV, SIG_DFL); + signal(SIGFPE, SIG_DFL); + signal(SIGBUS, SIG_DFL); + } +} + ++ (void)setBuildVersion:(NSString *)theBuildVersion +{ + buildVersion = theBuildVersion; +} + ++ (void)setSubLogName:(NSString *)theSubLogName +{ + subLogName = [NSString stringWithFormat:@"-%@", theSubLogName]; +} + +// Return the path of the crash log file +static NSString* crashLogPath(void) +{ + return [[MXLogger logsFolderPath] stringByAppendingPathComponent:MXLOGGER_CRASH_LOG]; +} + ++ (NSString*)crashLog +{ + NSString *exceptionLog; + + NSString *crashLog = crashLogPath(); + NSFileManager *fileManager = [NSFileManager defaultManager]; + if([fileManager fileExistsAtPath:crashLog]) + { + exceptionLog = crashLog; + } + return exceptionLog; +} + ++ (void)deleteCrashLog +{ + NSString *crashLog = crashLogPath(); + NSFileManager *fileManager = [NSFileManager defaultManager]; + if([fileManager fileExistsAtPath:crashLog]) + { + [fileManager removeItemAtPath:crashLog error:nil]; + } +} + +// The folder where logs are stored ++ (NSString*)logsFolderPath +{ + NSString *logsFolderPath = nil; + +// NSURL *sharedContainerURL = [[NSFileManager defaultManager] applicationGroupContainerURL]; +// if (sharedContainerURL) +// { +// logsFolderPath = [sharedContainerURL path]; +// } +// else +// { + NSArray *paths = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; + logsFolderPath = paths[0].path; +// } + + return logsFolderPath; +} + + +// If [self redirectNSLogToFiles: numberOfFiles:] is called with a lower numberOfFiles we need to do some cleanup ++ (void)removeExtraFilesFromCount:(NSUInteger)count +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *logsFolderPath = [MXLogger logsFolderPath]; + + NSUInteger index = count; + do + { + NSString *fileName = [NSString stringWithFormat:@"console%@.%tu.log", subLogName, index]; + NSString *logFile = [logsFolderPath stringByAppendingPathComponent:fileName]; + + if ([fileManager fileExistsAtPath:logFile]) + { + [fileManager removeItemAtPath:logFile error:nil]; +// MXLogDebug(@"[MXLogger] removeExtraFilesFromCount: %@. removeItemAtPath: %@\n", @(count), logFile); + } + else + { + break; + } + } + while (index++); +} + ++ (void)removeFilesAfterSizeLimit:(NSUInteger)sizeLimit +{ + NSUInteger logSize = 0; + BOOL removeFiles = NO; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *logsFolderPath = [MXLogger logsFolderPath]; + + // Start from console.1.log. Do not consider console.log. It should be almost empty + NSUInteger index = 0; + while (++index) + { + NSString *fileName = [NSString stringWithFormat:@"console%@.%tu.log", subLogName, index]; + NSString *logFile = [logsFolderPath stringByAppendingPathComponent:fileName]; + + if ([fileManager fileExistsAtPath:logFile]) + { + logSize += [fileManager attributesOfItemAtPath:logFile error:nil].fileSize; + + if (logSize >= sizeLimit) + { + removeFiles = YES; + break; + } + } + else + { + break; + } + } + + if (removeFiles) + { +// MXLogDebug(@"[MXLogger] removeFilesAfterSizeLimit: Remove files from index %@ because logs are too large (%@ for a limit of %@)\n", +// @(index), +// [NSByteCountFormatter stringFromByteCount:logSize countStyle:NSByteCountFormatterCountStyleBinary], +// [NSByteCountFormatter stringFromByteCount:sizeLimit countStyle:NSByteCountFormatterCountStyleBinary]); + [self removeExtraFilesFromCount:index]; + } + else + { +// MXLogDebug(@"[MXLogger] removeFilesAfterSizeLimit: No need: %@ for a limit of %@\n", +// [NSByteCountFormatter stringFromByteCount:logSize countStyle:NSByteCountFormatterCountStyleBinary], +// [NSByteCountFormatter stringFromByteCount:sizeLimit countStyle:NSByteCountFormatterCountStyleBinary]); + } + +} +@end + diff --git a/ElementX/Sources/Other/SwiftUI/Views/ElementToggleStyle.swift b/ElementX/Sources/Other/SwiftUI/Views/ElementToggleStyle.swift new file mode 100644 index 000000000..ce2acd1ed --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/Views/ElementToggleStyle.swift @@ -0,0 +1,23 @@ +// +// ElementToggleStyle.swift +// ElementX +// +// Created by Ismail on 2.06.2022. +// Copyright © 2022 Element. All rights reserved. +// + +import SwiftUI + +/// A toggle style that uses a button, with a checked/unchecked image like a checkbox. +struct ElementToggleStyle: ToggleStyle { + + func makeBody(configuration: Configuration) -> some View { + Button { configuration.isOn.toggle() } label: { + Image(systemName: configuration.isOn ? "checkmark.square.fill" : "square") + .font(.title3.weight(.regular)) + .imageScale(.large) + .foregroundColor(Color(uiColor: Asset.Colors.elementGreen.color)) + } + .buttonStyle(.plain) + } +} diff --git a/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift b/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift index 922b5ea2e..3a0306e19 100644 --- a/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift +++ b/ElementX/Sources/Other/UserIndicators/RoundedToastView.swift @@ -103,6 +103,9 @@ class RoundedToastView: UIView { case .success: imageView.image = UIImage(systemName: "checkmark.circle") return imageView + case .error: + imageView.image = UIImage(systemName: "x.circle") + return imageView } } } diff --git a/ElementX/Sources/Other/UserIndicators/ToastViewState.swift b/ElementX/Sources/Other/UserIndicators/ToastViewState.swift index e241d4645..341dce195 100644 --- a/ElementX/Sources/Other/UserIndicators/ToastViewState.swift +++ b/ElementX/Sources/Other/UserIndicators/ToastViewState.swift @@ -20,6 +20,7 @@ struct ToastViewState { enum Style { case loading case success + case error } let style: Style diff --git a/ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift b/ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift index d7631b9a7..848a00595 100644 --- a/ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift +++ b/ElementX/Sources/Other/UserIndicators/UserIndicatorPresenter.swift @@ -20,6 +20,7 @@ import UIKit enum UserIndicatorType { case loading(label: String, isInteractionBlocking: Bool) case success(label: String) + case error(label: String) } /// A presenter which can handle `UserIndicatorType` by creating the underlying `UserIndicator` @@ -72,6 +73,8 @@ class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol { } case .success(let label): return successRequest(label: label) + case .error(let label): + return errorRequest(label: label) } } @@ -113,4 +116,18 @@ class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol { dismissal: .timeout(1.5) ) } + + private func errorRequest(label: String) -> UserIndicatorRequest { + let presenter = ToastViewPresenter( + viewState: .init( + style: .error, + label: label + ), + presentationContext: presentationContext + ) + return UserIndicatorRequest( + presenter: presenter, + dismissal: .timeout(1.5) + ) + } } diff --git a/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift b/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift new file mode 100644 index 000000000..2d3b0b224 --- /dev/null +++ b/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift @@ -0,0 +1,109 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct BugReportCoordinatorParameters { + let bugReportService: BugReportServiceProtocol + let screenshot: UIImage? +} + +final class BugReportCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: BugReportCoordinatorParameters + private let bugReportHostingController: UIViewController + private var bugReportViewModel: BugReportViewModelProtocol + + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + private var errorIndicator: UserIndicator? + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + + // MARK: - Setup + + init(parameters: BugReportCoordinatorParameters) { + self.parameters = parameters + + let viewModel = BugReportViewModel(bugReportService: parameters.bugReportService, + screenshot: parameters.screenshot) + let view = BugReport(context: viewModel.context) + bugReportViewModel = viewModel + bugReportHostingController = UIHostingController(rootView: view) + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: bugReportHostingController) + } + + // MARK: - Public + + func start() { + MXLog.debug("[BugReportCoordinator] did start.") + bugReportViewModel.callback = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[BugReportCoordinator] BugReportViewModel did complete with result: \(result).") + switch result { + case .submitStarted: + self.startLoading() + case .submitFinished: + self.stopLoading() + self.showSuccess(label: ElementL10n.done) + case .submitFailed(let error): + self.stopLoading() + self.showError(label: error.localizedDescription) + case .cancel: + self.completion?() + } + } + } + + func toPresentable() -> UIViewController { + bugReportHostingController + } + + // MARK: - Private + + /// Show an activity indicator whilst loading. + /// - Parameters: + /// - label: The label to show on the indicator. + /// - isInteractionBlocking: Whether the indicator should block any user interaction. + private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) { + loadingIndicator = indicatorPresenter.present(.loading(label: label, + isInteractionBlocking: isInteractionBlocking)) + } + + /// Hide the currently displayed activity indicator. + private func stopLoading() { + loadingIndicator = nil + } + + /// Show success indicator + private func showSuccess(label: String) { + errorIndicator = indicatorPresenter.present(.success(label: label)) + } + + /// Show error indicator + private func showError(label: String) { + errorIndicator = indicatorPresenter.present(.error(label: label)) + } +} diff --git a/ElementX/Sources/Screens/BugReport/BugReportModels.swift b/ElementX/Sources/Screens/BugReport/BugReportModels.swift new file mode 100644 index 000000000..2403df0e1 --- /dev/null +++ b/ElementX/Sources/Screens/BugReport/BugReportModels.swift @@ -0,0 +1,48 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import UIKit + +// MARK: - Coordinator + +// MARK: View model + +enum BugReportViewModelAction { + case submitStarted + case submitFinished + case submitFailed(error: Error) + case cancel +} + +// MARK: View + +struct BugReportViewState: BindableState { + var screenshot: UIImage? + var bindings: BugReportViewStateBindings +} + +struct BugReportViewStateBindings { + var reportText: String + var sendingLogsEnabled: Bool +} + +enum BugReportViewAction { + case submit + case cancel + case toggleSendLogs + case removeScreenshot +} diff --git a/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift b/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift new file mode 100644 index 000000000..1bf8725f6 --- /dev/null +++ b/ElementX/Sources/Screens/BugReport/BugReportViewModel.swift @@ -0,0 +1,87 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14, *) +typealias BugReportViewModelType = StateStoreViewModel +@available(iOS 14, *) +class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol { + + // MARK: - Properties + + let bugReportService: BugReportServiceProtocol + + // MARK: Private + + func submitBugReport() async { + callback?(.submitStarted) + do { + var files: [URL] = [] + if let screenshot = state.screenshot { + let anonymized = try await ImageAnonymizer.anonymizedImage(from: screenshot) + let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("screenshot").appendingPathExtension("png") + // remove old screenshot if exists + if FileManager.default.fileExists(atPath: tmpUrl.path) { + try FileManager.default.removeItem(at: tmpUrl) + } + try anonymized.dataForPNGRepresentation().write(to: tmpUrl) + files.append(tmpUrl) + } + + let result = try await bugReportService.submitBugReport(text: context.reportText, + includeLogs: context.sendingLogsEnabled, + includeCrashLog: true, + githubLabels: [], + files: files) + MXLog.info("[BugReportViewModel] submitBugReport succeeded, result: \(result.reportUrl)") + callback?(.submitFinished) + } catch let error { + MXLog.error("[BugReportViewModel] submitBugReport failed: \(error)") + callback?(.submitFailed(error: error)) + } + } + + // MARK: Public + + var callback: ((BugReportViewModelAction) -> Void)? + + // MARK: - Setup + + init(bugReportService: BugReportServiceProtocol, + screenshot: UIImage?) { + self.bugReportService = bugReportService + let bindings = BugReportViewStateBindings(reportText: "", sendingLogsEnabled: true) + super.init(initialViewState: BugReportViewState(screenshot: screenshot, + bindings: bindings)) + } + + // MARK: - Public + + override func process(viewAction: BugReportViewAction) async { + switch viewAction { + case .submit: + await submitBugReport() + case .cancel: + callback?(.cancel) + case .toggleSendLogs: + context.sendingLogsEnabled.toggle() + case .removeScreenshot: + state.screenshot = nil + } + } +} diff --git a/ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift b/ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift new file mode 100644 index 000000000..906444ae9 --- /dev/null +++ b/ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@MainActor +protocol BugReportViewModelProtocol { + + var callback: ((BugReportViewModelAction) -> Void)? { get set } + @available(iOS 14, *) + var context: BugReportViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/BugReport/View/BugReport.swift b/ElementX/Sources/Screens/BugReport/View/BugReport.swift new file mode 100644 index 000000000..4c793c0d8 --- /dev/null +++ b/ElementX/Sources/Screens/BugReport/View/BugReport.swift @@ -0,0 +1,140 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct BugReport: View { + + // MARK: Private + + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + private var horizontalPadding: CGFloat { + horizontalSizeClass == .regular ? 50 : 16 + } + + // MARK: Public + + @ObservedObject var context: BugReportViewModel.Context + + // MARK: Views + + var body: some View { + GeometryReader { geometry in + VStack { + ScrollView { + mainContent + .padding(.top, 50) + .padding(.horizontal, horizontalPadding) + } + .introspectScrollView { scrollView in + scrollView.keyboardDismissMode = .onDrag + } + + buttons + .padding(.horizontal, horizontalPadding) + .padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16) + } + .navigationTitle(ElementL10n.titleActivityBugReport) + } + } + + /// The main content of the view to be shown in a scroll view. + var mainContent: some View { + VStack(alignment: .leading, spacing: 12) { + Text(ElementL10n.sendBugReportDescription) + .accessibilityIdentifier("reportBugDescription") + ZStack(alignment: .topLeading) { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color(UIColor.secondarySystemBackground)) + + if context.reportText.isEmpty { + Text(ElementL10n.sendBugReportPlaceholder) + .foregroundColor(Color(UIColor.placeholderText)) + .padding(.horizontal, 8) + .padding(.vertical, 12) + } + TextEditor(text: $context.reportText) + .padding(4) + .background(Color.clear) + .cornerRadius(8) + .accessibilityIdentifier("reportTextView") + .introspectTextView { textView in + textView.backgroundColor = .clear + } + } + .frame(maxWidth: .infinity) + .frame(height: 300) + .font(.body) + Text(ElementL10n.sendBugReportLogsDescription) + .accessibilityIdentifier("sendLogsDescription") + HStack(spacing: 12) { + Toggle(ElementL10n.sendBugReportIncludeLogs, isOn: $context.sendingLogsEnabled) + .toggleStyle(ElementToggleStyle()) + .accessibilityIdentifier("sendLogsToggle") + Text(ElementL10n.sendBugReportIncludeLogs).accessibilityIdentifier("sendLogsText") + } + .onTapGesture { + context.send(viewAction: .toggleSendLogs) + } + screenshot + } + } + + /// The action buttons shown at the bottom of the view. + var buttons: some View { + VStack { + Button { context.send(viewAction: .submit) } label: { + Text(ElementL10n.actionSend) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .disabled(context.reportText.count < 5) + .accessibilityIdentifier("sendButton") + } + } + + @ViewBuilder + var screenshot: some View { + if let screenshot = context.viewState.screenshot { + ZStack(alignment: .topTrailing) { + Image(uiImage: screenshot) + .resizable() + .scaledToFit() + .frame(width: 100) + .accessibilityIdentifier("screenshotImage") + Button { context.send(viewAction: .removeScreenshot) } label: { + Image(uiImage: Asset.Images.closeCircle.image) + } + .offset(x: 10, y: -10) + .accessibilityIdentifier("removeScreenshotButton") + } + .padding(.vertical, 10) + } + } +} + +// MARK: - Previews + +struct BugReport_Previews: PreviewProvider { + static var previews: some View { + Group { + let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image) + BugReport(context: viewModel.context) + .previewInterfaceOrientation(.portrait) + } + } +} diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index e92880cd7..d9dd6b640 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -25,7 +25,8 @@ struct HomeScreenCoordinatorParameters { enum HomeScreenCoordinatorAction { case logout - case selectRoom(roomIdentifier: String) + case presentRoom(roomIdentifier: String) + case presentSettings } final class HomeScreenCoordinator: Coordinator, Presentable { @@ -65,7 +66,9 @@ final class HomeScreenCoordinator: Coordinator, Presentable { case .logout: self.callback?(.logout) case .selectRoom(let roomIdentifier): - self.callback?(.selectRoom(roomIdentifier: roomIdentifier)) + self.callback?(.presentRoom(roomIdentifier: roomIdentifier)) + case .tapUserAvatar: + self.callback?(.presentSettings) } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index ba7864b81..021ae06e8 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -20,12 +20,14 @@ import UIKit enum HomeScreenViewModelAction { case logout case selectRoom(roomIdentifier: String) + case tapUserAvatar } enum HomeScreenViewAction { case logout case loadRoomData(roomIdentifier: String) case selectRoom(roomIdentifier: String) + case tapUserAvatar } struct HomeScreenViewState: BindableState { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 1e438f8af..4bd010fac 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -51,6 +51,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol loadRoomDataForIdentifier(roomIdentifier) case .selectRoom(let roomIdentifier): callback?(.selectRoom(roomIdentifier: roomIdentifier)) + case .tapUserAvatar: + callback?(.tapUserAvatar) } } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index b738e4953..518dd22b1 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -75,11 +75,13 @@ struct HomeScreen: View { HStack { ZStack { if let avatar = context.viewState.userAvatar { - Image(uiImage: avatar) - .resizable() - .scaledToFill() - .frame(width: 40, height: 40, alignment: .center) - .mask(Circle()) + Button { context.send(viewAction: .tapUserAvatar) } label: { + Image(uiImage: avatar) + .resizable() + .scaledToFill() + .frame(width: 40, height: 40, alignment: .center) + .mask(Circle()) + } } else { EmptyView() } @@ -89,9 +91,12 @@ struct HomeScreen: View { ZStack { if let displayName = context.viewState.userDisplayName { - Text("Hello, \(displayName)!") - .font(.subheadline) - .fontWeight(.bold) + Button { context.send(viewAction: .tapUserAvatar) } label: { + Text("Hello, \(displayName)!") + .font(.subheadline) + .fontWeight(.bold) + .foregroundColor(.black) + } } else { EmptyView() } diff --git a/ElementX/Sources/Screens/Settings/SettingsCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsCoordinator.swift new file mode 100644 index 000000000..3e278fe79 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/SettingsCoordinator.swift @@ -0,0 +1,109 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct SettingsCoordinatorParameters { + let navigationRouter: NavigationRouterType + let bugReportService: BugReportServiceProtocol +} + +final class SettingsCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: SettingsCoordinatorParameters + private let settingsHostingController: UIViewController + private var settingsViewModel: SettingsViewModelProtocol + + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + // MARK: - Setup + + init(parameters: SettingsCoordinatorParameters) { + self.parameters = parameters + + let viewModel = SettingsViewModel() + let view = Settings(context: viewModel.context) + settingsViewModel = viewModel + settingsHostingController = UIHostingController(rootView: view) + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: settingsHostingController) + + settingsViewModel.callback = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[SettingsCoordinator] SettingsViewModel did complete with result: \(result).") + switch result { + case .reportBug: + self.presentBugReportScreen() + case .crash: + self.parameters.bugReportService.crash() + } + } + } + + // MARK: - Public + + func start() { + // no-op + } + + func toPresentable() -> UIViewController { + settingsHostingController + } + + // MARK: - Private + + /// Show an activity indicator whilst loading. + /// - Parameters: + /// - label: The label to show on the indicator. + /// - isInteractionBlocking: Whether the indicator should block any user interaction. + private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) { + loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) + } + + /// Hide the currently displayed activity indicator. + private func stopLoading() { + loadingIndicator = nil + } + + private func presentBugReportScreen() { + let params = BugReportCoordinatorParameters(bugReportService: parameters.bugReportService, + screenshot: nil) + let coordinator = BugReportCoordinator(parameters: params) + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { return } + self.parameters.navigationRouter.popModule(animated: true) + self.remove(childCoordinator: coordinator) + } + + add(childCoordinator: coordinator) + coordinator.start() + self.parameters.navigationRouter.push(coordinator, animated: true) { [weak self] in + guard let self = self else { return } + + self.remove(childCoordinator: coordinator) + } + } +} diff --git a/ElementX/Sources/Screens/Settings/SettingsModels.swift b/ElementX/Sources/Screens/Settings/SettingsModels.swift new file mode 100644 index 000000000..03f4e4dc5 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/SettingsModels.swift @@ -0,0 +1,42 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +// MARK: - Coordinator + +// MARK: View model + +enum SettingsViewModelAction { + case reportBug + case crash +} + +// MARK: View + +struct SettingsViewState: BindableState { + var crashButtonVisible: Bool + var bindings: SettingsViewStateBindings +} + +struct SettingsViewStateBindings { + +} + +enum SettingsViewAction { + case reportBug + case crash +} diff --git a/ElementX/Sources/Screens/Settings/SettingsViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsViewModel.swift new file mode 100644 index 000000000..03ad7d9d6 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/SettingsViewModel.swift @@ -0,0 +1,50 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +@available(iOS 14, *) +typealias SettingsViewModelType = StateStoreViewModel +@available(iOS 14, *) +class SettingsViewModel: SettingsViewModelType, SettingsViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + // MARK: Public + + var callback: ((SettingsViewModelAction) -> Void)? + + // MARK: - Setup + + init() { + let bindings = SettingsViewStateBindings() + super.init(initialViewState: .init(crashButtonVisible: true, bindings: bindings)) + } + + // MARK: - Public + + override func process(viewAction: SettingsViewAction) async { + switch viewAction { + case .reportBug: + callback?(.reportBug) + case .crash: + callback?(.crash) + } + } +} diff --git a/ElementX/Sources/Screens/Settings/SettingsViewModelProtocol.swift b/ElementX/Sources/Screens/Settings/SettingsViewModelProtocol.swift new file mode 100644 index 000000000..ef1de01bb --- /dev/null +++ b/ElementX/Sources/Screens/Settings/SettingsViewModelProtocol.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@MainActor +protocol SettingsViewModelProtocol { + + var callback: ((SettingsViewModelAction) -> Void)? { get set } + @available(iOS 14, *) + var context: SettingsViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/Settings/View/Settings.swift b/ElementX/Sources/Screens/Settings/View/Settings.swift new file mode 100644 index 000000000..d01e3bd2e --- /dev/null +++ b/ElementX/Sources/Screens/Settings/View/Settings.swift @@ -0,0 +1,61 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct Settings: View { + + // MARK: Private + + // MARK: Public + + @ObservedObject var context: SettingsViewModel.Context + + // MARK: Views + + var body: some View { + Form { + Button { context.send(viewAction: .reportBug) } label: { + Text(ElementL10n.sendBugReport) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .accessibilityIdentifier("reportBugButton") + + if context.viewState.crashButtonVisible { + Button { context.send(viewAction: .crash) } label: { + Text("Crash the app") + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .accessibilityIdentifier("crashButton") + } + } + .navigationTitle(ElementL10n.settings) + } +} + +// MARK: - Previews + +struct Settings_Previews: PreviewProvider { + static var previews: some View { + Group { + let viewModel = SettingsViewModel() + Settings(context: viewModel.context) + .previewInterfaceOrientation(.portrait) + } + } +} diff --git a/ElementX/Sources/Services/BugReport/BugReportService.swift b/ElementX/Sources/Services/BugReport/BugReportService.swift new file mode 100644 index 000000000..890e5f003 --- /dev/null +++ b/ElementX/Sources/Services/BugReport/BugReportService.swift @@ -0,0 +1,223 @@ +// +// BugReportService.swift +// ElementX +// +// Created by Ismail on 16.05.2022. +// Copyright © 2022 element.io. All rights reserved. +// + +import Foundation +import MatrixRustSDK +import UIKit +import GZIP +import Sentry + +enum BugReportServiceError: Error { + case invalidBaseUrlString + case invalidSentryEndpoint +} + +class BugReportService: BugReportServiceProtocol { + private let baseURL: URL + private let sentryEndpoint: String + private let applicationId: String + private let session: URLSession + + init(withBaseUrlString baseUrlString: String, + sentryEndpoint: String, + applicationId: String = BuildSettings.bugReportApplicationId, + session: URLSession = .shared) throws { + guard let url = URL(string: baseUrlString) else { + throw BugReportServiceError.invalidBaseUrlString + } + guard !sentryEndpoint.isEmpty else { + throw BugReportServiceError.invalidSentryEndpoint + } + self.baseURL = url + self.sentryEndpoint = sentryEndpoint + self.applicationId = applicationId + self.session = session + + // enable SentrySDK + SentrySDK.start { options in + options.dsn = sentryEndpoint + #if DEBUG + options.debug = true + #endif + + // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring. + // We recommend adjusting this value in production. + options.tracesSampleRate = 1.0 + + options.beforeSend = { event in + MXLog.error("Sentry detected crash: \(event)") + return event + } + + options.onCrashedLastRun = { event in + MXLog.debug("Sentry detected application was crashed: \(event)") + } + } + + // also enable logging crashes, to send them with bug reports + MXLogger.logCrashes(true) + // set build version for logger + MXLogger.setBuildVersion(ElementInfoPlist.cfBundleShortVersionString) + } + + // MARK: - BugReportServiceProtocol + + var crashedLastRun: Bool { + return SentrySDK.crashedLastRun + } + + func crash() { + SentrySDK.crash() + } + + func submitBugReport(text: String, + includeLogs: Bool, + includeCrashLog: Bool, + githubLabels: [String], + files: [URL]) async throws -> SubmitBugReportResponse { + MXLog.debug("[BugReportService] submitBugReport") + + var params = [ + MultipartFormData(key: "text", type: .text(value: text)) + ] + params.append(contentsOf: defaultParams) + for label in githubLabels { + params.append(MultipartFormData(key: "label", type: .text(value: label))) + } + let zippedFiles = try await zipFiles(includeLogs: includeLogs, + includeCrashLog: includeCrashLog) + // log or compressed-log + if !zippedFiles.isEmpty { + for url in zippedFiles { + params.append(MultipartFormData(key: "compressed-log", type: .file(url: url))) + } + } + for url in files { + params.append(MultipartFormData(key: "file", type: .file(url: url))) + } + + let boundary = "Boundary-\(UUID().uuidString)" + var body = Data() + for param in params { + body.appendString(string: "--\(boundary)\r\n") + body.appendString(string: "Content-Disposition:form-data; name=\"\(param.key)\"") + switch param.type { + case .text(let value): + body.appendString(string: "\r\n\r\n\(value)\r\n") + case .file(let url): + body.appendString(string: "; filename=\"\(url.lastPathComponent)\"\r\n") + body.appendString(string: "Content-Type: \"content-type header\"\r\n\r\n") + body.append(try Data(contentsOf: url)) + body.appendString(string: "\r\n") + } + } + body.appendString(string: "--\(boundary)--\r\n") + + var request = URLRequest(url: baseURL.appendingPathComponent("submit")) + request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") + + request.httpMethod = "POST" + request.httpBody = body as Data + + let (data, _) = try await session.data(for: request) + + // Parse the JSON data + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let result = try decoder.decode(SubmitBugReportResponse.self, from: data) + + if !result.reportUrl.isEmpty { + MXLogger.deleteCrashLog() + } + return result + } + + // MARK: - Private + + private var defaultParams: [MultipartFormData] { + [ + MultipartFormData(key: "user_agent", type: .text(value: "iOS")), + MultipartFormData(key: "app", type: .text(value: applicationId)), + MultipartFormData(key: "version", type: .text(value: version)), + MultipartFormData(key: "os", type: .text(value: os)), + MultipartFormData(key: "client", type: .text(value: "Element-X")) + ] + } + + private var os: String { + "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)" + } + + private var version: String { + ElementInfoPlist.cfBundleShortVersionString + } + + private func zipFiles(includeLogs: Bool, + includeCrashLog: Bool) async throws -> [URL] { + MXLog.debug("[BugReportService] zipFiles: includeLogs: \(includeLogs), includeCrashLog: \(includeCrashLog)") + + var filesToCompress: [URL] = [] + if includeLogs, let logFiles = MXLogger.logFiles() { + let urls = logFiles.compactMap { URL(fileURLWithPath: $0) } + filesToCompress.append(contentsOf: urls) + } + if includeCrashLog, let crashLogFile = MXLogger.crashLog() { + filesToCompress.append(URL(fileURLWithPath: crashLogFile)) + } + + var totalSize: Int = 0 + var totalZippedSize: Int = 0 + var zippedFiles: [URL] = [] + + for url in filesToCompress { + let zippedFileURL = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent(url.lastPathComponent) + + // remove old zipped file if exists + try? FileManager.default.removeItem(at: zippedFileURL) + + let rawData = try Data(contentsOf: url) + if rawData.isEmpty { + continue + } + guard let zippedData = (rawData as NSData).gzipped() else { + continue + } + + totalSize += rawData.count + totalZippedSize += zippedData.count + + try zippedData.write(to: zippedFileURL) + + zippedFiles.append(zippedFileURL) + } + + MXLog.debug("[BugReportService] zipFiles: totalSize: \(totalSize), totalZippedSize: \(totalZippedSize)") + + return zippedFiles + } + +} + +private extension Data { + mutating func appendString(string: String, encoding: String.Encoding = .utf8) { + if let data = string.data(using: encoding) { + append(data) + } + } +} + +private struct MultipartFormData { + let key: String + let type: MultipartFormDataType +} + +private enum MultipartFormDataType { + case text(value: String) + case file(url: URL) +} diff --git a/ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift b/ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift new file mode 100644 index 000000000..8f1e25e02 --- /dev/null +++ b/ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift @@ -0,0 +1,27 @@ +// +// BugReportServiceProtocol.swift +// ElementX +// +// Created by Ismail on 16.05.2022. +// Copyright © 2022 element.io. All rights reserved. +// + +import Foundation +import UIKit + +struct SubmitBugReportResponse: Decodable { + var reportUrl: String +} + +protocol BugReportServiceProtocol { + + var crashedLastRun: Bool { get } + + func crash() + + func submitBugReport(text: String, + includeLogs: Bool, + includeCrashLog: Bool, + githubLabels: [String], + files: [URL]) async throws -> SubmitBugReportResponse +} diff --git a/ElementX/Sources/Services/BugReport/MockBugReportService.swift b/ElementX/Sources/Services/BugReport/MockBugReportService.swift new file mode 100644 index 000000000..4cc5beddc --- /dev/null +++ b/ElementX/Sources/Services/BugReport/MockBugReportService.swift @@ -0,0 +1,28 @@ +// +// MockBugReportService.swift +// ElementX +// +// Created by Ismail on 16.05.2022. +// Copyright © 2022 element.io. All rights reserved. +// + +import Foundation +import UIKit + +class MockBugReportService: BugReportServiceProtocol { + + func submitBugReport(text: String, + includeLogs: Bool, + includeCrashLog: Bool, + githubLabels: [String], + files: [URL]) async throws -> SubmitBugReportResponse { + return SubmitBugReportResponse(reportUrl: "https://www.example/com/123") + } + + var crashedLastRun: Bool = false + + func crash() { + // no-op + } + +} diff --git a/ElementX/Sources/Services/BugReport/ScreenshotDetector.swift b/ElementX/Sources/Services/BugReport/ScreenshotDetector.swift new file mode 100644 index 000000000..0c2143549 --- /dev/null +++ b/ElementX/Sources/Services/BugReport/ScreenshotDetector.swift @@ -0,0 +1,110 @@ +// +// ScreenshotObserver.swift +// ElementX +// +// Created by Ismail on 31.05.2022. +// Copyright © 2022 element.io. All rights reserved. +// + +import Foundation +import UIKit +import Photos + +enum ScreenshotDetectorError: String, Error { + case loadFailed + case notAuthorized +} + +@MainActor +class ScreenshotDetector { + + var callback: (@MainActor (UIImage?, Error?) -> Void)? + + /// Flag to whether ask for photos authorization by default if needed. + var autoRequestPHAuthorization = true + + init() { + startObservingScreenshots() + } + + private func startObservingScreenshots() { + NotificationCenter.default.addObserver(self, + selector: #selector(userDidTakeScreenshot), + name: UIApplication.userDidTakeScreenshotNotification, + object: nil) + } + + @objc private func userDidTakeScreenshot() { + let authStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite) + if authStatus == .authorized { + findScreenshot() + } else if authStatus == .notDetermined && autoRequestPHAuthorization { + Task { + self.handleAuthStatus(await PHPhotoLibrary.requestAuthorization(for: .readWrite)) + } + } else { + fail(withError: ScreenshotDetectorError.notAuthorized) + } + } + + private func handleAuthStatus(_ status: PHAuthorizationStatus) { + if status == .authorized { + findScreenshot() + } else { + fail(withError: ScreenshotDetectorError.notAuthorized) + } + } + + private func findScreenshot() { + if let asset = PHAsset.fetchLastScreenshot() { + let imageManager = PHImageManager() + imageManager.requestImage(for: asset, + targetSize: PHImageManagerMaximumSize, + contentMode: .default, + options: PHImageRequestOptions.highQualitySyncLocal) { [weak self] image, _ in + guard let image = image else { + self?.fail(withError: ScreenshotDetectorError.loadFailed) + return + } + self?.succeed(withImage: image) + } + } else { + fail(withError: ScreenshotDetectorError.loadFailed) + } + } + + func succeed(withImage image: UIImage) { + callback?(image, nil) + } + + func fail(withError error: Error) { + callback?(nil, error) + } + +} + +private extension PHAsset { + + static func fetchLastScreenshot() -> PHAsset? { + let options = PHFetchOptions() + + options.fetchLimit = 1 + options.includeAssetSourceTypes = [.typeUserLibrary] + options.wantsIncrementalChangeDetails = false + options.predicate = NSPredicate(format: "(mediaSubtype & %d) != 0", PHAssetMediaSubtype.photoScreenshot.rawValue) + options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] + + return PHAsset.fetchAssets(with: .image, options: options).firstObject + } +} + +private extension PHImageRequestOptions { + + static var highQualitySyncLocal: PHImageRequestOptions { + let options = PHImageRequestOptions() + options.deliveryMode = .highQualityFormat + options.isNetworkAccessAllowed = false + options.isSynchronous = true + return options + } +} diff --git a/ElementX/Sources/UITestsAppCoordinator.swift b/ElementX/Sources/UITestsAppCoordinator.swift index 41a66f811..3599b9f4a 100644 --- a/ElementX/Sources/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITestsAppCoordinator.swift @@ -37,9 +37,14 @@ class UITestsAppCoordinator: Coordinator { } private func mockScreens() -> [MockScreen] { - [MockScreen(id: "Login screen", coordinator: LoginScreenCoordinator(parameters: .init())), - MockScreen(id: "Simple Screen - Regular", coordinator: TemplateSimpleScreenCoordinator(parameters: .init(promptType: .regular))), - MockScreen(id: "Simple Screen - Upgrade", coordinator: TemplateSimpleScreenCoordinator(parameters: .init(promptType: .upgrade)))] + [ + MockScreen(id: "Login screen", coordinator: LoginScreenCoordinator(parameters: .init())), + MockScreen(id: "Simple Screen - Regular", coordinator: TemplateSimpleScreenCoordinator(parameters: .init(promptType: .regular))), + MockScreen(id: "Simple Screen - Upgrade", coordinator: TemplateSimpleScreenCoordinator(parameters: .init(promptType: .upgrade))), + MockScreen(id: "Settings screen", coordinator: SettingsCoordinator(parameters: .init(navigationRouter: NavigationRouter(navigationController: UINavigationController()), bugReportService: MockBugReportService()))), + MockScreen(id: "Bug report screen", coordinator: BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(), screenshot: nil))), + MockScreen(id: "Bug report screen with screenshot", coordinator: BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(), screenshot: Asset.Images.appLogo.image))) + ] } } diff --git a/ElementX/SupportingFiles/ElementX-Bridging-Header.h b/ElementX/SupportingFiles/ElementX-Bridging-Header.h index e11d920b1..572883ea7 100644 --- a/ElementX/SupportingFiles/ElementX-Bridging-Header.h +++ b/ElementX/SupportingFiles/ElementX-Bridging-Header.h @@ -1,3 +1,4 @@ // // Use this file to import your target's public headers that you would like to expose to Swift. // +#import "MXLogger.h" diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 387a5d2e3..c8ca7ac25 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -88,6 +88,8 @@ targets: - package: Introspect - package: SwiftyBeaver - package: SwiftState + - package: GZIP + - package: Sentry sources: - path: ../Sources diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/View/TemplateSimpleScreen.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/View/TemplateSimpleScreen.swift index 217c50b1f..f8efa92ca 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/View/TemplateSimpleScreen.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/View/TemplateSimpleScreen.swift @@ -35,7 +35,7 @@ struct TemplateSimpleScreen: View { var body: some View { GeometryReader { geometry in VStack { - ScrollView(showsIndicators: false) { + ScrollView { mainContent .padding(.top, 50) .padding(.horizontal, horizontalPadding) diff --git a/Tools/SwiftGen/swiftgen-config.yml b/Tools/SwiftGen/swiftgen-config.yml index 43144c5d3..2cf933ff8 100755 --- a/Tools/SwiftGen/swiftgen-config.yml +++ b/Tools/SwiftGen/swiftgen-config.yml @@ -24,3 +24,10 @@ strings: params: enumName: ElementL10n publicAccess: true +plist: + inputs: SupportingFiles/Info.plist + outputs: + templateName: runtime-swift5 + output: InfoPlist.swift + params: + enumName: ElementInfoPlist diff --git a/UITests/Sources/BugReportUITests.swift b/UITests/Sources/BugReportUITests.swift new file mode 100644 index 000000000..c8868e29e --- /dev/null +++ b/UITests/Sources/BugReportUITests.swift @@ -0,0 +1,93 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import ElementX + +class BugReportUITests: XCTestCase { + + func testInitialStateComponents() { + let app = Application.launch() + app.goToScreenWithIdentifier("Bug report screen") + + XCTAssert(app.navigationBars["Bug report"].exists) + XCTAssert(app.staticTexts["reportBugDescription"].exists) + XCTAssert(app.staticTexts["sendLogsDescription"].exists) + XCTAssert(app.textViews["reportTextView"].exists) + let sendingLogsToggle = app.switches["sendLogsToggle"] + XCTAssert(sendingLogsToggle.exists) + XCTAssert(sendingLogsToggle.isOn) + XCTAssert(app.staticTexts["sendLogsText"].exists) + let sendButton = app.buttons["sendButton"] + XCTAssert(sendButton.exists) + XCTAssertFalse(sendButton.isEnabled) + XCTAssertFalse(app.images["screenshotImage"].exists) + XCTAssertFalse(app.buttons["removeScreenshotButton"].exists) + } + + func testToggleSendingLogs() { + let app = Application.launch() + app.goToScreenWithIdentifier("Bug report screen") + + app.switches["sendLogsToggle"].tap() + + let sendingLogsToggle = app.switches["sendLogsToggle"] + XCTAssert(sendingLogsToggle.exists) + XCTAssertFalse(sendingLogsToggle.isOn) + } + + func testReportText() { + let app = Application.launch() + app.goToScreenWithIdentifier("Bug report screen") + + // type 4 chars + app.textViews["reportTextView"].tap() + app.textViews["reportTextView"].typeText("Test") + XCTAssertFalse(app.buttons["sendButton"].isEnabled) + + // type one more char and see the button enabled + app.textViews["reportTextView"].tap() + app.textViews["reportTextView"].typeText("-") + XCTAssert(app.buttons["sendButton"].isEnabled) + } + + func testInitialStateComponentsWithScreenshot() { + let app = Application.launch() + app.goToScreenWithIdentifier("Bug report screen with screenshot") + + XCTAssert(app.navigationBars["Bug report"].exists) + XCTAssert(app.staticTexts["reportBugDescription"].exists) + XCTAssert(app.staticTexts["sendLogsDescription"].exists) + + XCTAssert(app.textViews["reportTextView"].exists) + let sendingLogsToggle = app.switches["sendLogsToggle"] + XCTAssert(sendingLogsToggle.exists) + XCTAssert(sendingLogsToggle.isOn) + XCTAssert(app.staticTexts["sendLogsText"].exists) + let sendButton = app.buttons["sendButton"] + XCTAssert(sendButton.exists) + XCTAssertFalse(sendButton.isEnabled) + XCTAssert(app.images["screenshotImage"].exists) + XCTAssert(app.buttons["removeScreenshotButton"].exists) + } + +} + +extension XCUIElement { + var isOn: Bool { + (value as? String) == "1" + } +} diff --git a/UITests/Sources/SettingsUITests.swift b/UITests/Sources/SettingsUITests.swift new file mode 100644 index 000000000..337b871a0 --- /dev/null +++ b/UITests/Sources/SettingsUITests.swift @@ -0,0 +1,31 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +import ElementX + +class SettingsUITests: XCTestCase { + + func testInitialStateComponents() { + let app = Application.launch() + app.goToScreenWithIdentifier("Settings screen") + + XCTAssert(app.navigationBars["Settings"].exists) + XCTAssert(app.buttons["reportBugButton"].exists) + XCTAssert(app.buttons["crashButton"].exists) + } + +} diff --git a/UITests/SupportingFiles/target.yml b/UITests/SupportingFiles/target.yml index e08327c74..821716722 100644 --- a/UITests/SupportingFiles/target.yml +++ b/UITests/SupportingFiles/target.yml @@ -21,6 +21,10 @@ targets: linkType: static - package: SwiftState linkType: static + - package: GZIP + linkType: static + - package: Sentry + linkType: static info: path: ../SupportingFiles/Info.plist diff --git a/UnitTests/Resources/sample_screenshot.png b/UnitTests/Resources/sample_screenshot.png new file mode 100644 index 000000000..30c02a83d Binary files /dev/null and b/UnitTests/Resources/sample_screenshot.png differ diff --git a/UnitTests/Sources/BugReportServiceTests.swift b/UnitTests/Sources/BugReportServiceTests.swift new file mode 100644 index 000000000..4476f0028 --- /dev/null +++ b/UnitTests/Sources/BugReportServiceTests.swift @@ -0,0 +1,90 @@ +// +// BugReportServiceTests.swift +// UnitTests +// +// Created by Ismail on 31.05.2022. +// Copyright © 2022 element.io. All rights reserved. +// + +import Foundation +@testable import ElementX +import XCTest + +class BugReportServiceTests: XCTestCase { + + let bugReportService = MockBugReportService() + + func testInitialStateWithMockService() { + XCTAssertFalse(bugReportService.crashedLastRun) + } + + func testSubmitBugReportWithMockService() async throws { + let result = try await bugReportService.submitBugReport(text: "i cannot send message", + includeLogs: true, + includeCrashLog: true, + githubLabels: [], + files: []) + XCTAssertFalse(result.reportUrl.isEmpty) + } + + func testInitialStateWithRealService() throws { + let service = try BugReportService(withBaseUrlString: "https://www.example.com", + sentryEndpoint: "mock_sentry_dsn", + applicationId: "mock_app_id", + session: .mock) + XCTAssertFalse(service.crashedLastRun) + } + + @MainActor func testSubmitBugReportWithRealService() async throws { + let service = try BugReportService(withBaseUrlString: "https://www.example.com", + sentryEndpoint: "mock_sentry_dsn", + applicationId: "mock_app_id", + session: .mock) + + let result = try await service.submitBugReport(text: "i cannot send message", + includeLogs: true, + includeCrashLog: true, + githubLabels: [], + files: []) + + XCTAssertEqual(result.reportUrl, "https://example.com/123") + } + +} + +private class MockURLProtocol: URLProtocol { + + override func startLoading() { + let response = "{\"report_url\":\"https://example.com/123\"}" + if let data = response.data(using: .utf8) { + let urlResponse = URLResponse() + client?.urlProtocol(self, didReceive: urlResponse, cacheStoragePolicy: .allowedInMemoryOnly) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } + } + + override func stopLoading() { + // no-op + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + +} + +private extension URLSession { + + static var mock: URLSession { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [MockURLProtocol.self] + (configuration.protocolClasses ?? []) + let result = URLSession(configuration: configuration) + return result + } + +} diff --git a/UnitTests/Sources/BugReportViewModelTests.swift b/UnitTests/Sources/BugReportViewModelTests.swift new file mode 100644 index 000000000..73b517157 --- /dev/null +++ b/UnitTests/Sources/BugReportViewModelTests.swift @@ -0,0 +1,50 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +@MainActor +class BugReportViewModelTests: XCTestCase { + + func testInitialState() { + let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: nil) + let context = viewModel.context + + XCTAssertEqual(context.reportText, "") + XCTAssertNil(context.viewState.screenshot) + XCTAssertTrue(context.sendingLogsEnabled) + } + + func testToggleSendingLogs() async throws { + let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: nil) + let context = viewModel.context + + context.send(viewAction: .toggleSendLogs) + await Task.yield() + XCTAssertFalse(context.sendingLogsEnabled) + } + + func testClearScreenshot() async throws { + let viewModel = BugReportViewModel(bugReportService: MockBugReportService(), screenshot: UIImage.actions) + let context = viewModel.context + + context.send(viewAction: .removeScreenshot) + await Task.yield() + XCTAssertNil(context.viewState.screenshot) + } +} diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index 2b5a7a956..7de397fb1 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -19,11 +19,65 @@ import XCTest @testable import ElementX class HomeScreenViewModelTests: XCTestCase { - override func setUpWithError() throws { + var viewModel: HomeScreenViewModelProtocol! + var context: HomeScreenViewModelType.Context! + + @MainActor override func setUpWithError() throws { + viewModel = HomeScreenViewModel(attributedStringBuilder: AttributedStringBuilder()) + context = viewModel.context } - func testInitialState() { + @MainActor func testLogout() async throws { + var correctResult = false + viewModel.callback = { result in + switch result { + case .logout: + correctResult = true + default: + break + } + } + context.send(viewAction: .logout) + await Task.yield() + XCTAssert(correctResult) } + + @MainActor func testSelectRoom() async throws { + let mockRoomId = "mock_room_id" + var correctResult = false + var selectedRoomId = "" + viewModel.callback = { result in + switch result { + case .selectRoom(let roomId): + correctResult = true + selectedRoomId = roomId + default: + break + } + } + + context.send(viewAction: .selectRoom(roomIdentifier: mockRoomId)) + await Task.yield() + XCTAssert(correctResult) + XCTAssertEqual(mockRoomId, selectedRoomId) + } + + @MainActor func testTapUserAvatar() async throws { + var correctResult = false + viewModel.callback = { result in + switch result { + case .tapUserAvatar: + correctResult = true + default: + break + } + } + + context.send(viewAction: .tapUserAvatar) + await Task.yield() + XCTAssert(correctResult) + } + } diff --git a/UnitTests/Sources/ImageAnonymizerTests.swift b/UnitTests/Sources/ImageAnonymizerTests.swift new file mode 100644 index 000000000..c66c1669e --- /dev/null +++ b/UnitTests/Sources/ImageAnonymizerTests.swift @@ -0,0 +1,57 @@ +// +// ImageExtensionTests.swift +// UnitTests +// +// Created by Ismail on 31.05.2022. +// Copyright © 2022 element.io. All rights reserved. +// + +import XCTest +@testable import ElementX + +enum ImageAnonymizerTestsError: String, Error { + case screenshotNotFound +} + +class ImageAnonymizerTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func sampleScreenshot() throws -> UIImage { + let bundle = Bundle(for: self.classForCoder) + guard let path = bundle.path(forResource: "sample_screenshot", ofType: "png"), + let image = UIImage(contentsOfFile: path) else { + throw ImageAnonymizerTestsError.screenshotNotFound + } + return image + } + + func testImageAnonymizationConfidenceLevel() async throws { + let image = try sampleScreenshot() + + let anonymized5 = try await ImageAnonymizer.anonymizedImage(from: image) + let anonymized1 = try await ImageAnonymizer.anonymizedImage(from: image, confidenceLevel: 0.1) + + // comparing colors is a complicated process, just compare images for now + XCTAssertNotEqual(image, anonymized5) + XCTAssertNotEqual(anonymized1, anonymized5) + } + + func testImageAnonymizationFillColor() async throws { + let image = try sampleScreenshot() + + let anonymizedRed = try await ImageAnonymizer.anonymizedImage(from: image) + let anonymizedBlue = try await ImageAnonymizer.anonymizedImage(from: image, fillColor: .blue) + + // comparing colors is a complicated process, just compare images for now + XCTAssertNotEqual(image, anonymizedRed) + XCTAssertNotEqual(anonymizedBlue, anonymizedRed) + } + +} diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift new file mode 100644 index 000000000..4d12ca3df --- /dev/null +++ b/UnitTests/Sources/LoggingTests.swift @@ -0,0 +1,96 @@ +// +// LoggingTests.swift +// UnitTests +// +// Created by Ismail on 31.05.2022. +// Copyright © 2022 element.io. All rights reserved. +// + +import XCTest +@testable import ElementX + +class LoggingTests: XCTestCase { + + private enum Constants { + static let genericFailure = "Test failed" + } + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testFileLogging() throws { + MXLogger.deleteLogFiles() + guard let logFiles = MXLogger.logFiles() else { + XCTFail(Constants.genericFailure) + return + } + XCTAssertTrue(logFiles.isEmpty) + + let log = UUID().uuidString + + let configuration = MXLogConfiguration() + configuration.redirectLogsToFiles = true + MXLog.configure(configuration) + MXLog.debug(log) + guard let logFile = MXLogger.logFiles().first else { + XCTFail(Constants.genericFailure) + return + } + + let content = try String(contentsOfFile: logFile) + XCTAssert(content.contains(log)) + } + + func testLogLevels() throws { + MXLogger.deleteLogFiles() + guard let logFiles = MXLogger.logFiles() else { + XCTFail(Constants.genericFailure) + return + } + XCTAssert(logFiles.isEmpty) + + let log = UUID().uuidString + + let configuration = MXLogConfiguration() + configuration.logLevel = .error + configuration.redirectLogsToFiles = true + MXLog.configure(configuration) + MXLog.debug(log) + guard let logFile = MXLogger.logFiles().first else { + XCTFail(Constants.genericFailure) + return + } + + let content = try String(contentsOfFile: logFile) + XCTAssertFalse(content.contains(log)) + } + + func testSubLogName() { + MXLogger.deleteLogFiles() + guard let logFiles = MXLogger.logFiles() else { + XCTFail(Constants.genericFailure) + return + } + XCTAssert(logFiles.isEmpty) + + let subLogName = "nse" + + let configuration = MXLogConfiguration() + configuration.subLogName = subLogName + configuration.redirectLogsToFiles = true + MXLog.configure(configuration) + MXLog.debug(UUID().uuidString) + guard let logFile = MXLogger.logFiles().first else { + XCTFail(Constants.genericFailure) + return + } + + XCTAssertTrue(logFile.contains(subLogName)) + } + +} diff --git a/UnitTests/Sources/ScreenshotDetectorTests.swift b/UnitTests/Sources/ScreenshotDetectorTests.swift new file mode 100644 index 000000000..2f94c4736 --- /dev/null +++ b/UnitTests/Sources/ScreenshotDetectorTests.swift @@ -0,0 +1,63 @@ +// +// ScreenshotDetectorTests.swift +// UnitTests +// +// Created by Ismail on 31.05.2022. +// Copyright © 2022 element.io. All rights reserved. +// + +import Foundation +@testable import ElementX +import XCTest +import Photos + +class ScreenshotDetectorTests: XCTestCase { + + @MainActor func testDetection() async { + async { expectation in + let detector = ScreenshotDetector() + // disable auto request authorization + detector.autoRequestPHAuthorization = false + detector.callback = { image, error in + + if PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized { + // if Photos already authorized on the simulator + + // we should get an image + XCTAssertNotNil(image) + + // we should not get an error + XCTAssertNil(error) + } else { + // otherwise we should not get an image + XCTAssertNil(image) + + // and get an error + guard let error = error else { + XCTFail("Should get an error") + return + } + + switch error { + case ScreenshotDetectorError.notAuthorized: + break + default: + XCTFail("Unknown error") + } + } + + expectation.fulfill() + } + + NotificationCenter.default.post(name: UIApplication.userDidTakeScreenshotNotification, object: nil) + } + } + + private func async(_ timeout: TimeInterval = 0.5, _ block: @escaping (XCTestExpectation) -> Void) { + let waiter = XCTWaiter() + let expectation = XCTestExpectation(description: "Async operation expectation") + block(expectation) + waiter.wait(for: [expectation], timeout: timeout) + } + +} diff --git a/UnitTests/Sources/SettingsViewModelTests.swift b/UnitTests/Sources/SettingsViewModelTests.swift new file mode 100644 index 000000000..981d85710 --- /dev/null +++ b/UnitTests/Sources/SettingsViewModelTests.swift @@ -0,0 +1,58 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +@MainActor +class SettingsViewModelTests: XCTestCase { + + var viewModel: SettingsViewModelProtocol! + var context: SettingsViewModelType.Context! + + @MainActor override func setUpWithError() throws { + viewModel = SettingsViewModel() + context = viewModel.context + } + + func testInitialState() { + XCTAssert(context.viewState.crashButtonVisible) + } + + func testReportBug() async throws { + var correctResult = false + viewModel.callback = { result in + correctResult = result == .reportBug + } + + context.send(viewAction: .reportBug) + await Task.yield() + XCTAssert(correctResult) + } + + func testCrash() async throws { + var correctResult = false + viewModel.callback = { result in + correctResult = result == .crash + } + + context.send(viewAction: .crash) + await Task.yield() + XCTAssert(correctResult) + } + +} diff --git a/UnitTests/SupportingFiles/target.yml b/UnitTests/SupportingFiles/target.yml index 060b15cad..446f1df82 100644 --- a/UnitTests/SupportingFiles/target.yml +++ b/UnitTests/SupportingFiles/target.yml @@ -22,3 +22,4 @@ targets: - path: ../Sources - path: ../SupportingFiles - path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit + - path: ../Resources diff --git a/changelog.d/23.feature b/changelog.d/23.feature new file mode 100644 index 000000000..c958c5acd --- /dev/null +++ b/changelog.d/23.feature @@ -0,0 +1 @@ +Implement rageshake service. diff --git a/project.yml b/project.yml index 990b90c7b..fe6b68a1a 100644 --- a/project.yml +++ b/project.yml @@ -51,3 +51,9 @@ packages: SwiftState: url: https://github.com/ReactKit/SwiftState majorVersion: 6.0.0 + GZIP: + url: https://github.com/nicklockwood/GZIP + majorVersion: 1.3.0 + Sentry: + url: https://github.com/getsentry/sentry-cocoa.git + majorVersion: 7.15.0