Rageshake Service (#70)

This commit is contained in:
ismailgulek 2022-06-06 12:38:07 +03:00 committed by GitHub
parent 1e7243fcf9
commit fa0721b160
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 3010 additions and 32 deletions

View File

@ -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 = "<group>"; };
0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = "<group>"; };
095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderProtocol.swift; sourceTree = "<group>"; };
0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToggleStyle.swift; sourceTree = "<group>"; };
09747989908EC5E4AA29F844 /* MemberDetailsProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailsProviderProtocol.swift; sourceTree = "<group>"; };
0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
0AB7A0C06CB527A1095DEB33 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = da; path = da.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
0AD575D36B9F6D1D543305D1 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = "<group>"; };
0C13A92C1E9C79F055B8133D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@ -222,9 +257,12 @@
105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactory.swift; sourceTree = "<group>"; };
105D16E7DB0CCE9526612BDD /* bn-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "bn-IN"; path = "bn-IN.lproj/Localizable.strings"; sourceTree = "<group>"; };
109C0201D8CB3F947340DC80 /* WeakDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionary.swift; sourceTree = "<group>"; };
111B698739E3410E2CDB7144 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = "<group>"; };
113356152C099951A6D17D85 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
1215A4FC53D2319E81AE8970 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
124D85E85505B6B81845235F /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fy; path = fy.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizer.swift; sourceTree = "<group>"; };
13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = "<group>"; };
184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = "<group>"; };
@ -245,10 +283,12 @@
24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = "<group>"; };
2583416C8974272ADBADDBE1 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
26C4D226FCD20BAC53F1E092 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ml; path = ml.lproj/Localizable.strings; sourceTree = "<group>"; };
28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelProtocol.swift; sourceTree = "<group>"; };
28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = "<group>"; };
2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = "<group>"; };
2AE83A3DD63BCFBB956FE5CB /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
2BEB3259B2208E5AE5BB3F65 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
2CF9FE7E0CF9F40D1509E63A /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = "<group>"; };
33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -261,8 +301,12 @@
3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomProxy.swift; sourceTree = "<group>"; };
3B5B535DA49C54523FF7A412 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nn; path = nn.lproj/Localizable.strings; sourceTree = "<group>"; };
3CDF9E55650D6035D6536538 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "nb-NO"; path = "nb-NO.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = "<group>"; };
3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = "<group>"; };
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = "<group>"; };
3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; };
3DD6E7C1D8B53F47789778CD /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; };
3F87116470221880017CF522 /* BuildSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildSettings.swift; sourceTree = "<group>"; };
3FAA6438B00FDB130F404E31 /* UserIndicatorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorStore.swift; sourceTree = "<group>"; };
3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = "<group>"; };
4110685D9CA159F3FD2D6BA1 /* TextRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomMessage.swift; sourceTree = "<group>"; };
@ -281,6 +325,7 @@
48971F1FFD7FC5C466889FC7 /* SplashViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SplashViewController.xib; sourceTree = "<group>"; };
48CE6BF18E542B32FA52CE06 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
49193CB0C248D621A96FB2AA /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
4990FDBDA96B88E214F92F48 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = "<group>"; };
49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benchmark.swift; sourceTree = "<group>"; };
4B40B7F6FCCE2D8C242492D9 /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -303,8 +348,10 @@
56008790A9C4479A6B31FDF4 /* EventBasedTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineView.swift; sourceTree = "<group>"; };
56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelledActivityIndicatorView.swift; sourceTree = "<group>"; };
5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = "<group>"; };
5872785B9C7934940146BFBA /* MXLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXLogger.h; sourceTree = "<group>"; };
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = "<group>"; };
5A9AB74614131D6706894E0C /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = "<group>"; };
5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelProtocol.swift; sourceTree = "<group>"; };
5B9D5F812E5AD6DC786DBC9B /* NavigationRouterStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterStoreProtocol.swift; sourceTree = "<group>"; };
5CB7F9D6FC121204D59E18DF /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = "<group>"; };
5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = "<group>"; };
@ -314,6 +361,7 @@
5F77E8010D41AA3F5F9A1FCA /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = "<group>"; };
5FF214969B25BFCBF87B908B /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-BD"; path = "bn-BD.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
6033779EB37259F27F938937 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = "<group>"; };
6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizerTests.swift; sourceTree = "<group>"; };
607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageProtocol.swift; sourceTree = "<group>"; };
616197D81103330BF2ADD559 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/Localizable.strings; sourceTree = "<group>"; };
61ADFB893DEF81E58DF3FAB9 /* MockRoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineController.swift; sourceTree = "<group>"; };
@ -322,7 +370,6 @@
6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RectangleToastView.swift; sourceTree = "<group>"; };
624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = "<group>"; };
64839516BD56D1C81D84C5E0 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = "<group>"; };
64B23371BC8BF6164D9F6A05 /* WeakDictionaryReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionaryReference.swift; sourceTree = "<group>"; };
653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@ -331,6 +378,8 @@
68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorPresenter.swift; sourceTree = "<group>"; };
6920A4869821BF72FFC58842 /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = "<group>"; };
6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailsProvider.swift; sourceTree = "<group>"; };
6A901D95158B02CA96C79C7F /* InfoPlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlist.swift; sourceTree = "<group>"; };
6D607F47FDEF16CC63684BE0 /* BugReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReport.swift; sourceTree = "<group>"; };
6DB53055CB130F0651C70763 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; };
6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; };
6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = "<group>"; };
@ -343,6 +392,7 @@
72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = "<group>"; };
752DEC02D93AFF46BC13313A /* NavigationRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterType.swift; sourceTree = "<group>"; };
799A3A11C434296ED28F87C8 /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = iw; path = iw.lproj/Localizable.strings; sourceTree = "<group>"; };
7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelTests.swift; sourceTree = "<group>"; };
7B04BD3874D736127A8156B8 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
7BDF6A69C2BB99535193E554 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
7D0CBC76C80E04345E11F2DB /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
@ -350,6 +400,7 @@
7DA80FADE73CDF01E96F5B8E /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sq; path = sq.lproj/Localizable.strings; sourceTree = "<group>"; };
7DDBF99755A9008CF8C8499E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
7E154FEA1E6FE964D3DF7859 /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fy; path = fy.lproj/Localizable.strings; sourceTree = "<group>"; };
7E532D95330139D118A9BF88 /* BugReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModel.swift; sourceTree = "<group>"; };
7FFCC48E7F701B6C24484593 /* WeakDictionaryKeyReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionaryKeyReference.swift; sourceTree = "<group>"; };
804F9B0FABE093C7284CD09B /* TimelineItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemList.swift; sourceTree = "<group>"; };
8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -375,6 +426,7 @@
938BD1FCD9E6FF3FCFA7AB4C /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
93B21E72926FACB13A186689 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ml; path = ml.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelTests.swift; sourceTree = "<group>"; };
9414DCADBDF9D6C4B806F61E /* sample_screenshot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = sample_screenshot.png; sourceTree = "<group>"; };
956BDA4AE16429AD015661A8 /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = "<group>"; };
95CC95CD75B688E946438165 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = "<group>"; };
967873B9E11828B67F64C89A /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = "<group>"; };
@ -383,6 +435,7 @@
97F893DBB5F88D746C6DCDE5 /* ku */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ku; path = ku.lproj/Localizable.strings; sourceTree = "<group>"; };
997783054A2E95F9E624217E /* kaa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kaa; path = kaa.lproj/Localizable.strings; sourceTree = "<group>"; };
99DE232F24EAD72A3DF7EF1A /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = kab; path = kab.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = "<group>"; };
9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = "<group>"; };
9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = "<group>"; };
A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@ -401,6 +454,7 @@
ACA11F7F50A4A3887A18CA5A /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewProvider.swift; sourceTree = "<group>"; };
AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportCoordinator.swift; sourceTree = "<group>"; };
ADCB8A232D3A8FB3E16A7303 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
AE225C66978648AA4AF37B45 /* te */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = te; path = te.lproj/Localizable.strings; sourceTree = "<group>"; };
AE5DDBEBBA17973ED4638823 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@ -414,6 +468,7 @@
B1D1532B5D9FB0C8461A1453 /* UserIndicatorDismissal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorDismissal.swift; sourceTree = "<group>"; };
B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = "<group>"; };
B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemContextMenu.swift; sourceTree = "<group>"; };
B516212D9FE785DDD5E490D1 /* BugReportModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportModels.swift; sourceTree = "<group>"; };
B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; };
B64F3A3D0DF86ED5A241AB05 /* ActivityIndicatorView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ActivityIndicatorView.xib; sourceTree = "<group>"; };
B695D0D12086158BAD1D9859 /* UserIndicatorPresenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresenterSpy.swift; sourceTree = "<group>"; };
@ -434,6 +489,7 @@
C21ECC295F4DE8DAA86D62AC /* RoomSummaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProtocol.swift; sourceTree = "<group>"; };
C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = "<group>"; };
C485C186CEC78443DA96BDC8 /* TemplateSimpleScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateSimpleScreenViewModelTests.swift; sourceTree = "<group>"; };
C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = "<group>"; };
C88508B6F7974CFABEC4B261 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = "<group>"; };
C91A6BC1A54CDB598EE2A81B /* UserIndicatorQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorQueue.swift; sourceTree = "<group>"; };
@ -457,6 +513,7 @@
D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = "<group>"; };
D67CBAFA48ED0B6FCE74F88F /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = "<group>"; };
D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = "<group>"; };
D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = "<group>"; };
D77DD2DA5DC8654F2A80FF1D /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = "<group>"; };
DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicator.swift; sourceTree = "<group>"; };
@ -465,6 +522,7 @@
E157152B11E347F735C3FD6E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = "<group>"; };
E2869CFFF6CD2A642AB4B743 /* TemplateSimpleScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateSimpleScreenCoordinator.swift; sourceTree = "<group>"; };
E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITests.swift; sourceTree = "<group>"; };
E45C57120F28F8D619150219 /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/Localizable.strings; sourceTree = "<group>"; };
E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
E579A0DA01F488C97B771EF6 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lv; path = lv.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@ -477,11 +535,16 @@
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactoryProtocol.swift; sourceTree = "<group>"; };
EF188681D6B6068CFAEAFC3F /* MXLogger.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MXLogger.m; sourceTree = "<group>"; };
EFFA5FD06AAAC4AF544B594E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceTests.swift; sourceTree = "<group>"; };
F012CB5EE3F2B67359F6CC52 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetectorTests.swift; sourceTree = "<group>"; };
F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = "<group>"; };
F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
F3BC93D4555571E8B4BC47F9 /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; };
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 = "<group>"; };
F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; };
F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = "<group>"; };
F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
06501F0E978B2D5C92771DC7 /* Logging */ = {
isa = PBXGroup;
children = (
111B698739E3410E2CDB7144 /* MXLog.swift */,
5872785B9C7934940146BFBA /* MXLogger.h */,
EF188681D6B6068CFAEAFC3F /* MXLogger.m */,
);
path = Logging;
sourceTree = "<group>";
};
0787F81684E503024BD0C051 /* Services */ = {
isa = PBXGroup;
children = (
AAFDD509929A0CCF8BCE51EB /* Authentication */,
0ED3F5C21537519389C07644 /* BugReport */,
8039515BAA53B7C3275AC64A /* Client */,
79E560F5113ED25D172E550C /* Media */,
40E6246F03D1FE377BC5D963 /* Room */,
@ -567,6 +646,17 @@
path = SupportingFiles;
sourceTree = "<group>";
};
0ED3F5C21537519389C07644 /* BugReport */ = {
isa = PBXGroup;
children = (
D6DC38E64A5ED3FDB201029A /* BugReportService.swift */,
9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */,
F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */,
F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */,
);
path = BugReport;
sourceTree = "<group>";
};
10578D9852BA78D309A1CBDF /* ViewModel */ = {
isa = PBXGroup;
children = (
@ -595,10 +685,19 @@
path = Resources;
sourceTree = "<group>";
};
328DD5DA1281F758B72006C7 /* Views */ = {
isa = PBXGroup;
children = (
0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */,
);
path = Views;
sourceTree = "<group>";
};
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 = "<group>";
};
4009BE2E791C16AC6EE39A7E /* BugReport */ = {
isa = PBXGroup;
children = (
AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */,
B516212D9FE785DDD5E490D1 /* BugReportModels.swift */,
7E532D95330139D118A9BF88 /* BugReportViewModel.swift */,
28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */,
58F951CB7BD7F96C37BE5CAD /* View */,
);
path = BugReport;
sourceTree = "<group>";
};
405B00F139AEE3994601B36A = {
isa = PBXGroup;
children = (
@ -650,6 +761,14 @@
path = Room;
sourceTree = "<group>";
};
4541090DFE1A5499BD67BD14 /* View */ = {
isa = PBXGroup;
children = (
2BEB3259B2208E5AE5BB3F65 /* Settings.swift */,
);
path = View;
sourceTree = "<group>";
};
4658A940E89BC42EE3346A97 /* Messages */ = {
isa = PBXGroup;
children = (
@ -704,6 +823,14 @@
path = Scripts;
sourceTree = "<group>";
};
58F951CB7BD7F96C37BE5CAD /* View */ = {
isa = PBXGroup;
children = (
6D607F47FDEF16CC63684BE0 /* BugReport.swift */,
);
path = View;
sourceTree = "<group>";
};
5958CAF6E56422496E0063AF /* LoginScreen */ = {
isa = PBXGroup;
children = (
@ -738,6 +865,18 @@
name = Products;
sourceTree = "<group>";
};
70B74A432C241E56A7ACE610 /* Settings */ = {
isa = PBXGroup;
children = (
3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */,
4990FDBDA96B88E214F92F48 /* SettingsModels.swift */,
0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */,
5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */,
4541090DFE1A5499BD67BD14 /* View */,
);
path = Settings;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 = "<group>";
};
E600AACDF87CDBCE32683236 /* Resources */ = {
isa = PBXGroup;
children = (
9414DCADBDF9D6C4B806F61E /* sample_screenshot.png */,
);
path = Resources;
sourceTree = "<group>";
};
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 = "<group>";
};
91DE43B8815918E590912DDA /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
1215A4FC53D2319E81AE8970 /* en */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
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" */;

View File

@ -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",

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "url_preview_close_dark.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -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

View File

@ -0,0 +1 @@
"NSPhotoLibraryUsageDescription" = "The photo library is used to send photos and videos.";

View File

@ -1 +1,3 @@
"untranslated" = "Untranslated";
"screenshot_detected_title" = "You took a screenshot";
"screenshot_detected_message" = "Would you like to submit a bug report?";

View File

@ -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)
}
}

View File

@ -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<State, Event>
@ -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

View File

@ -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"
}

View File

@ -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")
}

View File

@ -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<T>(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<T>(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

View File

@ -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@"

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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 <Foundation/Foundation.h>
/**
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<NSString*>*)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

View File

@ -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 <UIKit/UIKit.h>
// 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<NSString*>*)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<NSURL *> *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

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -20,6 +20,7 @@ struct ToastViewState {
enum Style {
case loading
case success
case error
}
let style: Style

View File

@ -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)
)
}
}

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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<BugReportViewState,
BugReportViewAction>
@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
}
}
}

View File

@ -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 }
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -51,6 +51,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
loadRoomDataForIdentifier(roomIdentifier)
case .selectRoom(let roomIdentifier):
callback?(.selectRoom(roomIdentifier: roomIdentifier))
case .tapUserAvatar:
callback?(.tapUserAvatar)
}
}

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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<SettingsViewState,
SettingsViewAction>
@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)
}
}
}

View File

@ -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 }
}

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)))
]
}
}

View File

@ -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"

View File

@ -88,6 +88,8 @@ targets:
- package: Introspect
- package: SwiftyBeaver
- package: SwiftState
- package: GZIP
- package: Sentry
sources:
- path: ../Sources

View File

@ -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)

View File

@ -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

View File

@ -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"
}
}

View File

@ -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)
}
}

View File

@ -21,6 +21,10 @@ targets:
linkType: static
- package: SwiftState
linkType: static
- package: GZIP
linkType: static
- package: Sentry
linkType: static
info:
path: ../SupportingFiles/Info.plist

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -22,3 +22,4 @@ targets:
- path: ../Sources
- path: ../SupportingFiles
- path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit
- path: ../Resources

1
changelog.d/23.feature Normal file
View File

@ -0,0 +1 @@
Implement rageshake service.

View File

@ -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