From fa0721b160ff2c34cc5202281696114cd0942e69 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 6 Jun 2022 12:38:07 +0300 Subject: [PATCH] Rageshake Service (#70) --- ElementX.xcodeproj/project.pbxproj | 248 ++++++++++- .../xcshareddata/swiftpm/Package.resolved | 18 + .../close_circle.imageset/Contents.json | 15 + .../url_preview_close_dark.pdf | 116 ++++++ .../Localizations/en.lproj/InfoPlist.strings | 1 + .../en.lproj/Untranslated.strings | 2 + ElementX/Sources/AppCoordinator.swift | 118 +++++- .../Sources/AppCoordinatorStateMachine.swift | 8 + ElementX/Sources/BuildSettings.swift | 20 + ElementX/Sources/Generated/Assets.swift | 1 + ElementX/Sources/Generated/InfoPlist.swift | 67 +++ .../Generated/Strings+Untranslated.swift | 4 + ElementX/Sources/Other/ImageAnonymizer.swift | 111 +++++ .../Sources/Other/{ => Logging}/MXLog.swift | 16 +- ElementX/Sources/Other/Logging/MXLogger.h | 114 +++++ ElementX/Sources/Other/Logging/MXLogger.m | 392 ++++++++++++++++++ .../SwiftUI/Views/ElementToggleStyle.swift | 23 + .../UserIndicators/RoundedToastView.swift | 3 + .../Other/UserIndicators/ToastViewState.swift | 1 + .../UserIndicatorPresenter.swift | 17 + .../BugReport/BugReportCoordinator.swift | 109 +++++ .../Screens/BugReport/BugReportModels.swift | 48 +++ .../BugReport/BugReportViewModel.swift | 87 ++++ .../BugReportViewModelProtocol.swift | 25 ++ .../Screens/BugReport/View/BugReport.swift | 140 +++++++ .../HomeScreen/HomeScreenCoordinator.swift | 7 +- .../Screens/HomeScreen/HomeScreenModels.swift | 2 + .../HomeScreen/HomeScreenViewModel.swift | 2 + .../Screens/HomeScreen/View/HomeScreen.swift | 21 +- .../Settings/SettingsCoordinator.swift | 109 +++++ .../Screens/Settings/SettingsModels.swift | 42 ++ .../Screens/Settings/SettingsViewModel.swift | 50 +++ .../Settings/SettingsViewModelProtocol.swift | 25 ++ .../Screens/Settings/View/Settings.swift | 61 +++ .../Services/BugReport/BugReportService.swift | 223 ++++++++++ .../BugReport/BugReportServiceProtocol.swift | 27 ++ .../BugReport/MockBugReportService.swift | 28 ++ .../BugReport/ScreenshotDetector.swift | 110 +++++ ElementX/Sources/UITestsAppCoordinator.swift | 11 +- .../ElementX-Bridging-Header.h | 1 + ElementX/SupportingFiles/target.yml | 2 + .../ElementX/View/TemplateSimpleScreen.swift | 2 +- Tools/SwiftGen/swiftgen-config.yml | 7 + UITests/Sources/BugReportUITests.swift | 93 +++++ UITests/Sources/SettingsUITests.swift | 31 ++ UITests/SupportingFiles/target.yml | 4 + UnitTests/Resources/sample_screenshot.png | Bin 0 -> 195222 bytes UnitTests/Sources/BugReportServiceTests.swift | 90 ++++ .../Sources/BugReportViewModelTests.swift | 50 +++ .../Sources/HomeScreenViewModelTests.swift | 58 ++- UnitTests/Sources/ImageAnonymizerTests.swift | 57 +++ UnitTests/Sources/LoggingTests.swift | 96 +++++ .../Sources/ScreenshotDetectorTests.swift | 63 +++ .../Sources/SettingsViewModelTests.swift | 58 +++ UnitTests/SupportingFiles/target.yml | 1 + changelog.d/23.feature | 1 + project.yml | 6 + 57 files changed, 3010 insertions(+), 32 deletions(-) create mode 100644 ElementX/Resources/Assets.xcassets/Images/close_circle.imageset/Contents.json create mode 100644 ElementX/Resources/Assets.xcassets/Images/close_circle.imageset/url_preview_close_dark.pdf create mode 100644 ElementX/Resources/Localizations/en.lproj/InfoPlist.strings create mode 100644 ElementX/Sources/BuildSettings.swift create mode 100644 ElementX/Sources/Generated/InfoPlist.swift create mode 100644 ElementX/Sources/Other/ImageAnonymizer.swift rename ElementX/Sources/Other/{ => Logging}/MXLog.swift (94%) create mode 100644 ElementX/Sources/Other/Logging/MXLogger.h create mode 100644 ElementX/Sources/Other/Logging/MXLogger.m create mode 100644 ElementX/Sources/Other/SwiftUI/Views/ElementToggleStyle.swift create mode 100644 ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift create mode 100644 ElementX/Sources/Screens/BugReport/BugReportModels.swift create mode 100644 ElementX/Sources/Screens/BugReport/BugReportViewModel.swift create mode 100644 ElementX/Sources/Screens/BugReport/BugReportViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/BugReport/View/BugReport.swift create mode 100644 ElementX/Sources/Screens/Settings/SettingsCoordinator.swift create mode 100644 ElementX/Sources/Screens/Settings/SettingsModels.swift create mode 100644 ElementX/Sources/Screens/Settings/SettingsViewModel.swift create mode 100644 ElementX/Sources/Screens/Settings/SettingsViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/Settings/View/Settings.swift create mode 100644 ElementX/Sources/Services/BugReport/BugReportService.swift create mode 100644 ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift create mode 100644 ElementX/Sources/Services/BugReport/MockBugReportService.swift create mode 100644 ElementX/Sources/Services/BugReport/ScreenshotDetector.swift create mode 100644 UITests/Sources/BugReportUITests.swift create mode 100644 UITests/Sources/SettingsUITests.swift create mode 100644 UnitTests/Resources/sample_screenshot.png create mode 100644 UnitTests/Sources/BugReportServiceTests.swift create mode 100644 UnitTests/Sources/BugReportViewModelTests.swift create mode 100644 UnitTests/Sources/ImageAnonymizerTests.swift create mode 100644 UnitTests/Sources/LoggingTests.swift create mode 100644 UnitTests/Sources/ScreenshotDetectorTests.swift create mode 100644 UnitTests/Sources/SettingsViewModelTests.swift create mode 100644 changelog.d/23.feature 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 0000000000000000000000000000000000000000..30c02a83d72182d2ad3fe5292ba91c27a6ea498e GIT binary patch literal 195222 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�!lt!-El0R_Yg0#XzK>C&Zxh%_nE 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

<_ZkgWebdIv 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 literal 0 HcmV?d00001 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