diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 6dd7c8c58..e3a694ba0 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -17,21 +17,24 @@ 059173B3C77056C406906B6D /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = D4DA544B2520BFA65D6DB4BB /* target.yml */; }; 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */; }; 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; + 06B55882911B4BF5B14E9851 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; + 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; }; 071A017E415AD378F2961B11 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 07240B7159A3990C4C2E8FFC /* LoginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D256FEE2F1AF1E51D39B622 /* LoginTests.swift */; }; 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */; }; 086C2FA7750378EB2BFD0BEE /* UITestsRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D751BB69BB7C38FD247517B4 /* UITestsRootCoordinator.swift */; }; 095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */; }; + 09713669577CDA8D012EE380 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */; }; 09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */; }; 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; }; 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */; }; 0BEFE400B4802FE8C9DB39B3 /* FilePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62BDF0FF4F59AF6EA858B70B /* FilePreviewViewModel.swift */; }; 0C38C3E771B472E27295339D /* SessionVerificationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BB9A17AC512A7EF4B106E5 /* SessionVerificationModels.swift */; }; - 0C601923A872A87C775B889A /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3689E6F87850DD65DAA45428 /* KeychainControllerProtocol.swift */; }; 0E8C480700870BB34A2A360F /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 4346F63D53A346271577FD9C /* AppAuth */; }; 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; 0ED951768EC443A8728DE1D7 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; + 0F3F2FDD4021A25A0D57F801 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885D8C42DD17625B5261BEFF /* MediaProvider.swift */; }; 0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */; }; 1281625B25371BE53D36CB3A /* SeparatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1ED7E89865201EE7D53E6DA /* SeparatorRoomTimelineItem.swift */; }; 132D241B09F9044711FD70A5 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; @@ -54,13 +57,16 @@ 19ED6CF7FDBB1158692D101C /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D783758EAE6A88C93564EB /* VideoPlayerViewModel.swift */; }; 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; }; 1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; }; + 1B4B3E847BF944DB2C1C217F /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; 1CF18DE71D5D23C61BD88852 /* DebugScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9238D3A3A00F45E841FE4EFF /* DebugScreen.swift */; }; 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; }; 1F04C63D4FA95948E3F52147 /* FileRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */; }; 1F3232BD368DF430AB433907 /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5A56C4F47C368EBE5C5E870 /* DesignKit */; }; + 1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; }; 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; }; 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; 214C6B416609E58CCBF6DCEE /* SoftLogoutModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC77FC5C4F2000133047AA27 /* SoftLogoutModels.swift */; }; + 214CDBF0C783155242FFE4A0 /* NotificationItemProxy+NSE.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1FBF8CA40199B8058B1F08 /* NotificationItemProxy+NSE.swift */; }; 2276870A19F34B3FFFDA690F /* SoftLogoutCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEA20A6B4883E60469ACF8F /* SoftLogoutCoordinator.swift */; }; 2352C541AF857241489756FF /* MockRoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */; }; 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; }; @@ -77,13 +83,16 @@ 2B9AEEC12B1BBE5BD61D0F5E /* UserSessionFlowCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3429142FE11930422E7CC1A0 /* UserSessionFlowCoordinatorStateMachine.swift */; }; 2BA59D0AEFB4B82A2EC2A326 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; }; 2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */; }; + 2CA8AD07773A38BA4662098B /* MediaProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */; }; 2CB6787E25B11711518E9588 /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6281B199D8A8F0892490C2E /* OnboardingCoordinator.swift */; }; + 2D794361CFE790C8FB3C9C0F /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = ED482057AE39D5C6D9C5F3D8 /* message.caf */; }; 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; }; 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; }; 2F30EFEB7BD39242D1AD96F3 /* LoginViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E1FB768A24FDD2A5CA16E3C /* LoginViewModelProtocol.swift */; }; 2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; }; 308BD9343B95657FAA583FB7 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = AD2AC190E55B2BD4D0F1D4A7 /* SwiftyBeaver */; }; 3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; }; + 323F36D880363C473B81A9EA /* MediaProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */; }; 3274219F7F26A5C6C2C55630 /* FilePreviewViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */; }; 32BA37B01B05261FCF2D4B45 /* WeakDictionaryKeyReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090CA61A835C151CEDF8F372 /* WeakDictionaryKeyReference.swift */; }; 33CAC1226DFB8B5D8447D286 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; @@ -107,10 +116,13 @@ 3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; }; 3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; }; 3F2148F11164C7C5609984EB /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 19CD5B074D7DD44AF4C58BB6 /* SwiftState */; }; + 3F70E237CE4C3FAB02FC227F /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; }; 407DCE030E0F9B7C9861D38A /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; }; + 414F50CFCFEEE2611127DCFB /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; 41DFDD212D1BE57CA50D783B /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */; }; 41E16904B30C529373B4E1A4 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495D3EC4972639C1A87DDF8E /* NavigationController.swift */; }; 438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */; }; + 43BD17BC8794BB9B04F2A26B /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179423E34EE846E048E49CBF /* MediaSourceProxy.swift */; }; 43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */; }; 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */; }; 447E8580A0A2569E32529E17 /* MockRoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */; }; @@ -125,39 +137,51 @@ 49E9B99CB6A275C7744351F0 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D58333B377888012740101 /* LoginViewModel.swift */; }; 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; }; 4A2E0DBB63919AC8309B6D40 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */; }; + 4C3365818DE1CEAEDF590FD3 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; }; 4D970CB606276717B43E2332 /* TimelineItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804F9B0FABE093C7284CD09B /* TimelineItemList.swift */; }; 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; }; 4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */; }; 4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */; }; 500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874A1842477895F199567BD7 /* TimelineView.swift */; }; + 518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.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 */; }; 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; + 53DEF39F0C4DE02E3FC56D91 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = AC5D19D7A65EB05A9704FB44 /* SwiftyBeaver */; }; 541374590CA7E8318BD480FD /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; + 5455147CAC63F71E48F7D699 /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D455BC2423D911A62ACFB2 /* NSELogger.swift */; }; 54C774874BED4A8FAD1F22FE /* AnalyticsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */; }; 563A05B43207D00A6B698211 /* OIDCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9010EE0CC913D095887EF36E /* OIDCService.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; + 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; }; + 5B8B51CEC4717AF487794685 /* NotificationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B490675B8E31423AF116BDA /* NotificationServiceProxy.swift */; }; + 5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; }; 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; }; 5D2AF8C0DF872E7985F8FE54 /* TimelineDeliveryStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */; }; 5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */; }; + 5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; 5D7960B32C350FA93F48D02B /* OnboardingModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */; }; 5D9F0695DC6C0057F85C12B6 /* UserNotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113CA0A67B4AA227AAFB63B /* UserNotificationController.swift */; }; 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; }; + 5E25568E1CDAD983517E58B5 /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179423E34EE846E048E49CBF /* MediaSourceProxy.swift */; }; 5E540CAEF764D7FBD8D80776 /* VideoPlayerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3FC45B7643298BF361CEB1 /* VideoPlayerModels.swift */; }; + 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; 5F1FDE49DFD0C680386E48F9 /* TemplateViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B80895CE021B49847BD7D74 /* TemplateViewModelProtocol.swift */; }; 5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; }; 60ED66E63A169E47489348A8 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 2B788C81F6369D164ADEB917 /* GZIP */; }; + 6126CC51654E159804999E6A /* UNMutableNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5741CD0691019B32FE74CE9E /* UNMutableNotificationContent.swift */; }; 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */; }; 6298AB0906DDD3525CD78C6B /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 9573B94B1C86C6DF751AF3FD /* SwiftState */; }; - 62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8210612D17A39369480FC183 /* MediaSource.swift */; }; 630E89EBB0F791208EEE6D11 /* FileRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00A7110B937C6AE2EF5D7D6 /* FileRoomTimelineItem.swift */; }; 63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10E673916D2B8D21FD197 /* TemplateModels.swift */; }; 64F43D7390DA2A0AFD6BA911 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */; }; 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */; }; + 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1423AB065857FA546444DB15 /* NotificationManager.swift */; }; 656427D3C59554E03ECD898E /* MediaPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C2348F84A80F682E3A68D0 /* MediaPlayerCoordinator.swift */; }; 663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; }; 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */; }; 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; + 67D6E0700A9C1E676F6231F8 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 940C605265DD82DA0C655E23 /* Kingfisher */; }; 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */; }; 6832733838C57A7D3FE8FEB5 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; @@ -177,6 +201,7 @@ 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; }; 706F79A39BDB32F592B8C2C7 /* UIKitBackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */; }; 7096FA3AC218D914E88BFB70 /* AggregratedReaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */; }; + 719E7AAD1F8E68F68F30FECD /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; 744C029EB6C43429926A0499 /* AnalyticsPromptViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A86C95340248A8B7BA9A43 /* AnalyticsPromptViewModelProtocol.swift */; }; 74604ACFDBE7F54260E7B617 /* ApplicationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */; }; @@ -195,6 +220,7 @@ 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */; }; 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; }; 7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */; }; + 7E3B1F8D72573ED2FCB2D94B /* NotificationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD5FEE195446A9E458DDDAF /* NotificationServiceProxyProtocol.swift */; }; 7E7DF1867F98B0D10A6C0A63 /* FileCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3648F2FADEF2672D6A0D489 /* FileCacheTests.swift */; }; 7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */; }; 7F08F4BC1312075E2B5EAEFA /* AuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */; }; @@ -210,6 +236,7 @@ 841172E1576A863F4450132D /* WeakKeyDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ADFDC712027931F2216668 /* WeakKeyDictionary.swift */; }; 85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */; }; 86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */; }; + 8691186F9B99BCDDB7CACDD8 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; }; 86C2E93920FD15AD17E193A9 /* BugReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E532D95330139D118A9BF88 /* BugReportViewModel.swift */; }; 872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */; }; 87756CA950ED55870A1AAE8F /* ServerSelectionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7D706FFF438CAF16F44D8C /* ServerSelectionCoordinator.swift */; }; @@ -219,6 +246,7 @@ 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; }; 8B807DC963D1D4155A241BCC /* UserSessionFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F9E67AAB66638C69626866C /* UserSessionFlowCoordinator.swift */; }; 8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F49CDE349C490D617332770 /* NoticeRoomTimelineItem.swift */; }; + 8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; }; 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; }; 8D332A24CD23B4216E33EC5C /* MediaPlayerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 447A6399BC5EDE7AF7713267 /* MediaPlayerScreen.swift */; }; 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */; }; @@ -230,9 +258,11 @@ 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; }; 93BA4A81B6D893271101F9F0 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; }; + 9462C62798F47E39DCC182D2 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA89A2DD51B6BBE1DA55E263 /* Application.swift */; }; 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */; }; 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; + 968A5B890004526AB58A217C /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; 97189E495F0E47805D1868DB /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 527578916BD388A09F5A8036 /* DTCoreText */; }; 9738F894DB1BD383BE05767A /* ElementSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1027BB9A852F445B7623897F /* ElementSettings.swift */; }; 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; }; @@ -242,6 +272,7 @@ 992477AB8E3F3C36D627D32E /* OnboardingViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC4437C107D52ED19357DFC /* OnboardingViewModelProtocol.swift */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; 99ED42B8F8D6BFB1DBCF4C45 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = D661CAB418C075A94306A792 /* AnalyticsEvents */; }; + 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 9A47B7EFE3793760EEF68FFE /* UITestScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6FE34A0A47D010BBB4D4D4 /* UITestScreenIdentifier.swift */; }; 9AC5F8142413862A9E3A2D98 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */; }; 9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */; }; @@ -255,18 +286,23 @@ 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; }; 9E8AE387FD03E4F1C1B8815A /* SessionVerificationStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */; }; 9F41FF9C53F7A6EAEA6259C9 /* InviteFriendsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7AB0A148FCCAC28681C190 /* InviteFriendsCoordinator.swift */; }; + A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; }; A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; }; + A23B8B27A1436A1049EEF68E /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */; }; + A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; A371629728E597C5FCA3C2B2 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73FC861755C6388F62B9280A /* Analytics.swift */; }; A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287FC98AF2664EAD79C0D902 /* UIDevice.swift */; }; + A440D4BC02088482EC633A88 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; }; A494741843F087881299ACF0 /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; }; A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */; }; - A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A901D95158B02CA96C79C7F /* InfoPlist.swift */; }; A663FE6704CB500EBE782AE1 /* AnalyticsPromptCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DE1CF8F5EFD353B1A5E36F /* AnalyticsPromptCoordinator.swift */; }; + A69A54FF11A3F9EA0660E6BF /* NSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0D8F620C8B314840D8602E3F /* NSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; }; + A823A4E8CB71D7D9743E7E95 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; A8EC7C9D886244DAE9433E37 /* SessionVerificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */; }; A9D23B78F42BCDD896531436 /* UserNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649759084B0C9FE1F8DF8D17 /* UserNotificationPresenter.swift */; }; @@ -278,26 +314,31 @@ AC5CC8250CEAE57B73900C57 /* UserNotificationModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD80F22830C2360F3F39DDCE /* UserNotificationModalView.swift */; }; AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */; }; ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */; }; + AD2A81B65A9F6163012086F1 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; + AEE3981A0F090208E4445808 /* MockNotificationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B37DCC9025452F46F91340E /* MockNotificationServiceProxy.swift */; }; B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */; }; B064D42BA087649ACAE462E8 /* SoftLogoutUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */; }; B09514A0A3EB3C19A4FD0B71 /* SoftLogoutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCBDE671A613B3EB70794C4 /* SoftLogoutScreen.swift */; }; + B14BC354E56616B6B7D9A3D7 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */; }; B245583C63F8F90357B87FAE /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 50009897F60FAE7D63EF5E5B /* Kingfisher */; }; B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */; }; B3357B00F1AA930E54F76609 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; B5111BAF5F601C139EBBD8BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */; }; + B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; B66757D0254843162595B25D /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; }; B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; B6F92EBE04D4AABF30B9E73A /* AnalyticsPromptModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8BA82CF99D843FEF680E91 /* AnalyticsPromptModels.swift */; }; B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; B94368839BDB69172E28E245 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; + BA074E9812F96FFA3200ED1D /* TimelineItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */; }; BA31448FBD9697F8CB9A83CD /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; }; BB01CC19C3D3322308D1B2CF /* ServerSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167521635A1CC27624FCEB7F /* ServerSelectionViewModel.swift */; }; - BB4C6F362F75933DDDE30F3E /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A901D95158B02CA96C79C7F /* InfoPlist.swift */; }; BB6B0B91CE11E06330017000 /* SessionVerificationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */; }; BCEC41FB1F2BB663183863E4 /* LoginServerInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D379E13DD9D987470A3C70C /* LoginServerInfoSection.swift */; }; + BFB534E338A3D949944FB2F5 /* NotificationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B490675B8E31423AF116BDA /* NotificationServiceProxy.swift */; }; BFD1AC03B6F8C5F5897D5B55 /* ReversedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DE30233B57761F8AFEB415 /* ReversedScrollView.swift */; }; C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */; }; C35CF4DAB1467FE1BBDC204B /* MessageTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAF1C75771D9DC75877F4B4 /* MessageTimelineItem.swift */; }; @@ -315,16 +356,19 @@ CB137BFB3E083C33E398A6CB /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */; }; CB498F4E27AA0545DCEF0F6F /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4003BC24B24C9E63D3304177 /* DeviceKit */; }; + CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; }; CBF64DE774298D773DBD5354 /* VideoPlayerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB634B42CFE667112369D57 /* VideoPlayerScreen.swift */; }; CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */; }; CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */; }; CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; CE7148E80F09B7305E026AC6 /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */; }; + CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41FABA2B0AEF4389986495 /* LoginMode.swift */; }; CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACB6C5E4950B6C9842F35A38 /* RoomTimelineViewProvider.swift */; }; D034A195A3494E38BF060485 /* MockSessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */; }; D05A193AE63030F2CFCE2E9C /* UITestScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6FE34A0A47D010BBB4D4D4 /* UITestScreenIdentifier.swift */; }; D0619D2E6B9C511190FBEB95 /* RoomMessageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */; }; + D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; }; D3E603A5E9D529CF293E1BF9 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1651A532305027D3F605E2B /* VideoPlayerCoordinator.swift */; }; D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; }; D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */; }; @@ -338,39 +382,48 @@ DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */; }; DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */; }; DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */; }; - DF790EF2E4D41D1091AEB263 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F02B15921BF5CC8486990 /* KeychainController.swift */; }; + DFCA89C4EC2A5332ED6B441F /* DataProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4959CECEC984B3995616F427 /* DataProtectionManager.swift */; }; DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = F012CB5EE3F2B67359F6CC52 /* target.yml */; }; E01373F2043E76393A0CE073 /* AnalyticsPromptViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11B74ACE8D71747E1044A9C /* AnalyticsPromptViewModel.swift */; }; E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; }; E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; E290C78E7F09F47FD2662986 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; }; + E3C328EF20C4B3326D263BCD /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */; }; E47CD939D8480657D4B706C6 /* AnalyticsPromptCheckmarkItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7B2E9CC5DC3B76ADC35A43 /* AnalyticsPromptCheckmarkItem.swift */; }; E481C8FDCB6C089963C95344 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = BC01130651CB23340B899032 /* DeviceKit */; }; E5895C74615CBE8462FB840F /* SessionVerificationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF86010A0A719A9A50EEC59 /* SessionVerificationCoordinator.swift */; }; + E67418DACEDBC29E988E6ACD /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = ED482057AE39D5C6D9C5F3D8 /* message.caf */; }; E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61ADFB893DEF81E58DF3FAB9 /* MockRoomTimelineController.swift */; }; + E8AB8D16E6D8E8E501F29BD9 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B0B1226DA8DB55918B34CD /* FileCache.swift */; }; E96005321849DBD7C72A28F2 /* UITestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */; }; EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885D8C42DD17625B5261BEFF /* MediaProvider.swift */; }; EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; }; EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; }; + EBE13FAB4E29738AC41BD3E5 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; EC280623A42904341363EAAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; }; EC4C31963E755EEC77BD778C /* AnalyticsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */; }; EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F40F48279322E504153AB0D /* MockClientProxy.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; + EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; }; F040ABFEB0A2B142D948BA12 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; }; + F06CE9132855E81EBB6DDC32 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 800631D7250B7F93195035F1 /* KeychainAccess */; }; F0F82C3C848C865C3098AA52 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 67E7A6F388D3BF85767609D9 /* Sentry */; }; F257F964493A9CD02A6F720C /* OnboardingPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2717AB91060260E5F4781 /* OnboardingPageView.swift */; }; + F425C3F85BFF28C9AC593F52 /* MockNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96561CC53F7C1E24D4C292E4 /* MockNotificationManager.swift */; }; F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */; }; + F61AFA8BF2E739FBC30472F5 /* NotificationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD5FEE195446A9E458DDDAF /* NotificationServiceProxyProtocol.swift */; }; F656F92A63D3DC1978D79427 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; }; F6E860FF7B18B81DF43B30B8 /* EncryptedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FA7C8D4EF2B1873C180ED7 /* EncryptedRoomTimelineItem.swift */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; }; + F9981191DC408AED537C1749 /* MediaProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C9E0B61A77C7F0EE7918C /* MediaProxy.swift */; }; F99FB21EFC6D99D247FE7CBE /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DE8DC9B3FBA402117DC4C49F /* Kingfisher */; }; F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */; }; FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; + FBCD77D557AACBE9B445133A /* MediaProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C9E0B61A77C7F0EE7918C /* MediaProxy.swift */; }; FCD3F2B82CAB29A07887A127 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; }; - FD4706DC752744A0C91ED6FE /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B275C686F8253E655E42BA3 /* FileManager.swift */; }; FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; }; FE79E2BCCF69E8BF4D21E15A /* RoomMessageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA154570F693D93513E584C1 /* RoomMessageFactory.swift */; }; FE8D76708280968F7A670852 /* MockUserNotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9080CDD3881D0D1B2F280A7C /* MockUserNotificationController.swift */; }; @@ -399,13 +452,36 @@ remoteGlobalIDString = C0FAEB81CFD9776CD78CE489; remoteInfo = ElementX; }; + AE0C21E7E01A23610E54DF9D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AC22997D58D612146053154D /* Project object */; + proxyType = 1; + remoteGlobalIDString = FEB53A5BC378C913769656D8; + remoteInfo = NSE; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + 32FD0140DF485A66F1B788D1 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + A69A54FF11A3F9EA0660E6BF /* NSE.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelTests.swift; sourceTree = ""; }; 01C4C7DB37597D7D8379511A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 02A07FF019724B6ACEA73076 /* szl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = szl; path = szl.lproj/Localizable.strings; sourceTree = ""; }; + 033DB41C51865A2E83174E87 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 04BBC9E08250EF92ADE89CFD /* sr-Latn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sr-Latn"; path = "sr-Latn.lproj/Localizable.strings"; sourceTree = ""; }; + 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionUITests.swift; sourceTree = ""; }; 057B747CF045D3C6C30EAB2C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = ""; }; 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = ""; }; @@ -418,11 +494,13 @@ 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToggleStyle.swift; sourceTree = ""; }; 0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 0AB7A0C06CB527A1095DEB33 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = da; path = da.lproj/Localizable.stringsdict; sourceTree = ""; }; + 0B490675B8E31423AF116BDA /* NotificationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceProxy.swift; sourceTree = ""; }; 0B869438A1B52836F912A702 /* MockSoftLogoutScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSoftLogoutScreenState.swift; sourceTree = ""; }; 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = ""; }; 0C13A92C1E9C79F055B8133D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = ""; }; 0C88046D6A070D9827181C4D /* OnboardingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingUITests.swift; sourceTree = ""; }; 0CB569EAA5017B5B23970655 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; + 0D8F620C8B314840D8602E3F /* NSE.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = NSE.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 0DB634B42CFE667112369D57 /* VideoPlayerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerScreen.swift; sourceTree = ""; }; 0DD16CE9A66C9040B066AD60 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = ""; }; 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerTests.swift; sourceTree = ""; }; @@ -444,10 +522,12 @@ 124D85E85505B6B81845235F /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fy; path = fy.lproj/Localizable.stringsdict; sourceTree = ""; }; 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnonymizer.swift; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; + 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryDetails.swift; sourceTree = ""; }; 167521635A1CC27624FCEB7F /* ServerSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModel.swift; sourceTree = ""; }; 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = ""; }; 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; + 179423E34EE846E048E49CBF /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; @@ -474,8 +554,10 @@ 24B0C97D2F560BCB72BE73B1 /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = ""; }; 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; 2583416C8974272ADBADDBE1 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = ""; }; + 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = ""; }; 263B3B811C2B900F12C6F695 /* BuildSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildSettings.swift; sourceTree = ""; }; 26C4D226FCD20BAC53F1E092 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ml; path = ml.lproj/Localizable.strings; sourceTree = ""; }; + 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 287FC98AF2664EAD79C0D902 /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = ""; }; 28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelProtocol.swift; sourceTree = ""; }; 289FA233E896FBC5956C67E0 /* RoomTimelineItemProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProperties.swift; sourceTree = ""; }; @@ -484,6 +566,7 @@ 2AE83A3DD63BCFBB956FE5CB /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = ""; }; 2AEA20A6B4883E60469ACF8F /* SoftLogoutCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutCoordinator.swift; sourceTree = ""; }; 2AFEF3AC64B1358083F76B8B /* List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = List.swift; sourceTree = ""; }; + 2B37DCC9025452F46F91340E /* MockNotificationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNotificationServiceProxy.swift; sourceTree = ""; }; 2B80895CE021B49847BD7D74 /* TemplateViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModelProtocol.swift; sourceTree = ""; }; 2B9BCACD0CC4CB8E37F17732 /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lt; path = lt.lproj/Localizable.stringsdict; sourceTree = ""; }; 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskProtocol.swift; sourceTree = ""; }; @@ -495,7 +578,6 @@ 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertInfo.swift; sourceTree = ""; }; 2F1B28C596DE541DA0AFD16C /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lo; path = lo.lproj/Localizable.stringsdict; sourceTree = ""; }; 304FFD608DB6E612075AB1B4 /* WeakDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionary.swift; sourceTree = ""; }; - 317F02B15921BF5CC8486990 /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = ""; }; 31B01468022EC826CB2FD2C0 /* LoginModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginModels.swift; sourceTree = ""; }; 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = ""; }; 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = ""; }; @@ -505,7 +587,6 @@ 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = ""; }; 3558A15CFB934F9229301527 /* RestorationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = ""; }; 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; - 3689E6F87850DD65DAA45428 /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; 3747C96188856006F784BF49 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = ""; }; 3782C506F4FF1AADF61B6212 /* tlh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tlh; path = tlh.lproj/Localizable.strings; sourceTree = ""; }; 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = ""; }; @@ -542,9 +623,11 @@ 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 48CE6BF18E542B32FA52CE06 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = ""; }; 49193CB0C248D621A96FB2AA /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; + 4959CECEC984B3995616F427 /* DataProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = ""; }; 495D3EC4972639C1A87DDF8E /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = ""; }; 49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = ""; }; 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benchmark.swift; sourceTree = ""; }; 4A57A4AFA6A068668AFBD070 /* UIActivityViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIActivityViewControllerWrapper.swift; sourceTree = ""; }; 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettings.swift; sourceTree = ""; }; @@ -573,6 +656,7 @@ 55EA4B03F92F31EAA83B3F7B /* FilePreviewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewModels.swift; sourceTree = ""; }; 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutUITests.swift; sourceTree = ""; }; 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = ""; }; + 5741CD0691019B32FE74CE9E /* UNMutableNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNMutableNotificationContent.swift; sourceTree = ""; }; 5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = ""; }; 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelProtocol.swift; sourceTree = ""; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; @@ -598,10 +682,9 @@ 68232D336E2B546AD95B78B5 /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = ""; }; 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; 6A1AAC8EB2992918D01874AC /* rue */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = rue; path = rue.lproj/Localizable.strings; sourceTree = ""; }; + 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlistReader.swift; sourceTree = ""; }; 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProperties+Element.swift"; sourceTree = ""; }; - 6A901D95158B02CA96C79C7F /* InfoPlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlist.swift; sourceTree = ""; }; 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegex.swift; sourceTree = ""; }; - 6B275C686F8253E655E42BA3 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; 6BC38904A9663F7FAFD47457 /* SoftLogoutViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelProtocol.swift; sourceTree = ""; }; 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationUITests.swift; sourceTree = ""; }; 6DB53055CB130F0651C70763 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = ""; }; @@ -616,6 +699,7 @@ 72D03D36422177EF01905D20 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = ""; }; 73FC861755C6388F62B9280A /* Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = ""; }; + 748AE77AC3B0A01223033B87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 799A3A11C434296ED28F87C8 /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = iw; path = iw.lproj/Localizable.strings; sourceTree = ""; }; 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelTests.swift; sourceTree = ""; }; 7B04BD3874D736127A8156B8 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; @@ -632,7 +716,6 @@ 8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; 8166F121C79C7B62BF01D508 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pt; path = pt.lproj/Localizable.stringsdict; sourceTree = ""; }; 81B17DB1BC3B0C62AF84D230 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - 8210612D17A39369480FC183 /* MediaSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSource.swift; sourceTree = ""; }; 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = ""; }; 84E92FF38EBC12EC2452C79C /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = ""; }; 854BCEAF2A832176FAACD2CB /* SplashScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenCoordinator.swift; sourceTree = ""; }; @@ -647,12 +730,12 @@ 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = ""; }; 8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; 8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = ""; }; - 8B9A55AC2FB0FE0AEAA3DF1F /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + 8B9A55AC2FB0FE0AEAA3DF1F /* LICENSE */ = {isa = PBXFileReference; path = LICENSE; sourceTree = ""; }; 8C0AA893D6F8A2F563E01BB9 /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = in; path = in.lproj/Localizable.stringsdict; sourceTree = ""; }; 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; 8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerProtocol.swift; sourceTree = ""; }; 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = ""; }; 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; @@ -667,6 +750,7 @@ 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelTests.swift; sourceTree = ""; }; 9414DCADBDF9D6C4B806F61E /* sample_screenshot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = sample_screenshot.png; sourceTree = ""; }; 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemPlainStylerView.swift; sourceTree = ""; }; + 96561CC53F7C1E24D4C292E4 /* MockNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNotificationManager.swift; sourceTree = ""; }; 96F37AB24AF5A006521D38D1 /* RoomMessageFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactoryProtocol.swift; sourceTree = ""; }; 9772C1D2223108EB3131AEE4 /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = ""; }; 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationIconImage.swift; sourceTree = ""; }; @@ -677,6 +761,7 @@ 997783054A2E95F9E624217E /* kaa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kaa; path = kaa.lproj/Localizable.strings; sourceTree = ""; }; 99DE232F24EAD72A3DF7EF1A /* kab */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = kab; path = kab.lproj/Localizable.stringsdict; sourceTree = ""; }; 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = ""; }; + 9B1FBF8CA40199B8058B1F08 /* NotificationItemProxy+NSE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationItemProxy+NSE.swift"; sourceTree = ""; }; 9B577F829C693B8DFB7014FD /* RedactedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineItem.swift; sourceTree = ""; }; 9C4048041C1A6B20CB97FD18 /* TestMeasurementParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMeasurementParser.swift; sourceTree = ""; }; 9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = ""; }; @@ -686,6 +771,7 @@ 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; }; A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = ""; }; A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = ""; }; + A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = ""; }; A0A20AE75FF4FF35B1FF6CA7 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = ""; }; A11B74ACE8D71747E1044A9C /* AnalyticsPromptViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptViewModel.swift; sourceTree = ""; }; A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = ""; }; @@ -767,11 +853,13 @@ C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = ""; }; C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C7955B20E2E6DA68E5BC0AB9 /* WeakDictionaryReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDictionaryReference.swift; sourceTree = ""; }; + C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = ""; }; C88508B6F7974CFABEC4B261 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = ""; }; C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = ""; }; C9A86C95340248A8B7BA9A43 /* AnalyticsPromptViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptViewModelProtocol.swift; sourceTree = ""; }; + CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; CAAE4A709C0A2144C103AA0F /* ang */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ang; path = ang.lproj/Localizable.strings; sourceTree = ""; }; CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CBA95E52C4C6EE8769A63E57 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eo; path = eo.lproj/Localizable.strings; sourceTree = ""; }; @@ -792,9 +880,11 @@ D0ADFDC712027931F2216668 /* WeakKeyDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakKeyDictionary.swift; sourceTree = ""; }; D1651A532305027D3F605E2B /* VideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerCoordinator.swift; sourceTree = ""; }; D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionVerificationControllerProxy.swift; sourceTree = ""; }; + D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = ""; }; D2D783758EAE6A88C93564EB /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; D31DC8105C6233E5FFD9B84C /* element-x-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "element-x-ios"; path = .; sourceTree = SOURCE_ROOT; }; D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + D3D455BC2423D911A62ACFB2 /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = ""; }; D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = ""; }; D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = ""; }; @@ -808,15 +898,18 @@ DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateCoordinator.swift; sourceTree = ""; }; DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = ""; }; DC77FC5C4F2000133047AA27 /* SoftLogoutModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutModels.swift; sourceTree = ""; }; + DCD5FEE195446A9E458DDDAF /* NotificationServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceProxyProtocol.swift; sourceTree = ""; }; DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = ""; }; DED59F9EFF273BFA2055FFDF /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelTests.swift; sourceTree = ""; }; DF38B69D2C331A499276F400 /* FilePreviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModelTests.swift; sourceTree = ""; }; E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = ""; }; + E12C9E0B61A77C7F0EE7918C /* MediaProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProxy.swift; sourceTree = ""; }; E157152B11E347F735C3FD6E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = ""; }; E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; E26747B3154A5DBC3A7E24A5 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; + E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = ""; }; E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = ""; }; E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITests.swift; sourceTree = ""; }; E45C57120F28F8D619150219 /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/Localizable.strings; sourceTree = ""; }; @@ -825,6 +918,7 @@ E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; E579A0DA01F488C97B771EF6 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lv; path = lv.lproj/Localizable.stringsdict; sourceTree = ""; }; E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = ""; }; + E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; E6281B199D8A8F0892490C2E /* OnboardingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCoordinator.swift; sourceTree = ""; }; E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; @@ -834,6 +928,7 @@ EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EDB6E40BAD4504D899FAAC9A /* TemplateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModel.swift; sourceTree = ""; }; EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; @@ -860,6 +955,8 @@ F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = ""; }; FA154570F693D93513E584C1 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = ""; }; FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = ""; }; + FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = ""; }; + FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProxyProtocol.swift; sourceTree = ""; }; FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -880,6 +977,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BF59B36A7B2DB184B62826F6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 09713669577CDA8D012EE380 /* MatrixRustSDK in Frameworks */, + 53DEF39F0C4DE02E3FC56D91 /* SwiftyBeaver in Frameworks */, + F06CE9132855E81EBB6DDC32 /* KeychainAccess in Frameworks */, + 67D6E0700A9C1E676F6231F8 /* Kingfisher in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; CD30252A70288BD4BF476ED7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -953,7 +1061,9 @@ 0ED3F5C21537519389C07644 /* BugReport */, 2D6DC9871FD7173E51D67C73 /* Cache */, 8039515BAA53B7C3275AC64A /* Client */, + CA555F7C7CA382ACACF0D82B /* Keychain */, 79E560F5113ED25D172E550C /* Media */, + 6DE13A7AE6587B079F4049D7 /* Notification */, 40E6246F03D1FE377BC5D963 /* Room */, 82D5AD3EAE3A5C1068A44A88 /* Session */, 5329E48968EB951235E83DAE /* SessionVerification */, @@ -1040,6 +1150,7 @@ 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */, 01C4C7DB37597D7D8379511A /* Assets.xcassets */, A0C06C0F6A8621B22BFAEB56 /* Localizations */, + 8AEA6A91159FA0D3EAFCCB0D /* Sounds */, ); path = Resources; sourceTree = ""; @@ -1087,7 +1198,6 @@ isa = PBXGroup; children = ( 71D52BAA5BADB06E5E8C295D /* Assets.swift */, - 6A901D95158B02CA96C79C7F /* InfoPlist.swift */, 47EBB5D698CE9A25BB553A2D /* Strings.swift */, 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */, ); @@ -1128,6 +1238,16 @@ path = OnboardingScreen; sourceTree = ""; }; + 3FDB9ADD4A6456674E748166 /* SupportingFiles */ = { + isa = PBXGroup; + children = ( + 748AE77AC3B0A01223033B87 /* Info.plist */, + D263254AFE5B7993FFBBF324 /* NSE.entitlements */, + 033DB41C51865A2E83174E87 /* target.yml */, + ); + path = SupportingFiles; + sourceTree = ""; + }; 4009BE2E791C16AC6EE39A7E /* BugReport */ = { isa = PBXGroup; children = ( @@ -1149,6 +1269,7 @@ C0FAC17D4DD7D3A502822550 /* UITests */, 8A9C09B6A392465E03B8D1B1 /* IntegrationTests */, 823ED0EC3F1B6CF47D284011 /* Tools */, + B04B538A859CD012755DC19C /* NSE */, 9413F680ECDFB2B0DDB0DEF2 /* Packages */, 681566846AF307E9BA4C72C6 /* Products */, ); @@ -1173,6 +1294,7 @@ children = ( B6E89E530A8E92EC44301CA1 /* Bundle.swift */, A9FAFE1C2149E6AC8156ED2B /* Collection.swift */, + 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */, E26747B3154A5DBC3A7E24A5 /* Image.swift */, 4E2245243369B99216C7D84E /* ImageCache.swift */, 2AFEF3AC64B1358083F76B8B /* List.swift */, @@ -1281,6 +1403,18 @@ path = SessionVerification; sourceTree = ""; }; + 566F2B84465726112B830CF6 /* Other */ = { + isa = PBXGroup; + children = ( + 4959CECEC984B3995616F427 /* DataProtectionManager.swift */, + 9B1FBF8CA40199B8058B1F08 /* NotificationItemProxy+NSE.swift */, + D3D455BC2423D911A62ACFB2 /* NSELogger.swift */, + 5741CD0691019B32FE74CE9E /* UNMutableNotificationContent.swift */, + 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */, + ); + path = Other; + sourceTree = ""; + }; 58F951CB7BD7F96C37BE5CAD /* View */ = { isa = PBXGroup; children = ( @@ -1349,12 +1483,33 @@ children = ( 4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */, 9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */, + 0D8F620C8B314840D8602E3F /* NSE.appex */, F506C6ADB1E1DA6638078E11 /* UITests.xctest */, AAC9344689121887B74877AF /* UnitTests.xctest */, ); name = Products; sourceTree = ""; }; + 6DE13A7AE6587B079F4049D7 /* Notification */ = { + isa = PBXGroup; + children = ( + C830A64609CBD152F06E0457 /* NotificationConstants.swift */, + 6EE5E2BBFBC7947CFE789B4D /* Manager */, + 832FC81F760220239E285294 /* Proxy */, + ); + path = Notification; + sourceTree = ""; + }; + 6EE5E2BBFBC7947CFE789B4D /* Manager */ = { + isa = PBXGroup; + children = ( + 96561CC53F7C1E24D4C292E4 /* MockNotificationManager.swift */, + 1423AB065857FA546444DB15 /* NotificationManager.swift */, + A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */, + ); + path = Manager; + sourceTree = ""; + }; 70B74A432C241E56A7ACE610 /* Settings */ = { isa = PBXGroup; children = ( @@ -1486,7 +1641,9 @@ children = ( 885D8C42DD17625B5261BEFF /* MediaProvider.swift */, C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */, - 8210612D17A39369480FC183 /* MediaSource.swift */, + E12C9E0B61A77C7F0EE7918C /* MediaProxy.swift */, + FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */, + 179423E34EE846E048E49CBF /* MediaSourceProxy.swift */, 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */, ); path = Media; @@ -1531,6 +1688,26 @@ path = Session; sourceTree = ""; }; + 832FC81F760220239E285294 /* Proxy */ = { + isa = PBXGroup; + children = ( + 2B37DCC9025452F46F91340E /* MockNotificationServiceProxy.swift */, + 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */, + 0B490675B8E31423AF116BDA /* NotificationServiceProxy.swift */, + DCD5FEE195446A9E458DDDAF /* NotificationServiceProxyProtocol.swift */, + ); + path = Proxy; + sourceTree = ""; + }; + 864330656491EBAADA4901D3 /* Sources */ = { + isa = PBXGroup; + children = ( + 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */, + 566F2B84465726112B830CF6 /* Other */, + ); + path = Sources; + sourceTree = ""; + }; 8A9C09B6A392465E03B8D1B1 /* IntegrationTests */ = { isa = PBXGroup; children = ( @@ -1540,6 +1717,14 @@ path = IntegrationTests; sourceTree = ""; }; + 8AEA6A91159FA0D3EAFCCB0D /* Sounds */ = { + isa = PBXGroup; + children = ( + ED482057AE39D5C6D9C5F3D8 /* message.caf */, + ); + path = Sounds; + sourceTree = ""; + }; 8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */ = { isa = PBXGroup; children = ( @@ -1691,8 +1876,10 @@ isa = PBXGroup; children = ( 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */, + FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */, 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */, C75EF87651B00A176AB08E97 /* AppDelegate.swift */, + CA89A2DD51B6BBE1DA55E263 /* Application.swift */, 263B3B811C2B900F12C6F695 /* BuildSettings.swift */, B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */, 495D3EC4972639C1A87DDF8E /* NavigationController.swift */, @@ -1719,6 +1906,23 @@ path = UI; sourceTree = ""; }; + B04B538A859CD012755DC19C /* NSE */ = { + isa = PBXGroup; + children = ( + 864330656491EBAADA4901D3 /* Sources */, + 3FDB9ADD4A6456674E748166 /* SupportingFiles */, + ); + path = NSE; + sourceTree = ""; + }; + B3E78735F63FA93FAAAF700A /* MockUserNotificationController.swift~refs */ = { + isa = PBXGroup; + children = ( + F798CDE87F83A94B8BC2E18A /* remotes */, + ); + path = "MockUserNotificationController.swift~refs"; + sourceTree = ""; + }; B442FCF47E0A6F28D7D50A4D /* FilePreview */ = { isa = PBXGroup; children = ( @@ -1782,6 +1986,7 @@ E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */, 1027BB9A852F445B7623897F /* ElementSettings.swift */, 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */, + 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */, 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */, C2DE30233B57761F8AFEB415 /* ReversedScrollView.swift */, @@ -1805,12 +2010,25 @@ path = UITests; sourceTree = ""; }; + C5A8A8B1C16BBFEA4B9D5988 /* origin */ = { + isa = PBXGroup; + children = ( + ); + path = origin; + sourceTree = ""; + }; + CA555F7C7CA382ACACF0D82B /* Keychain */ = { + isa = PBXGroup; + children = ( + E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */, + E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */, + ); + path = Keychain; + sourceTree = ""; + }; CBBF6127C313A5412E438BC6 /* UserSession */ = { isa = PBXGroup; children = ( - 6B275C686F8253E655E42BA3 /* FileManager.swift */, - 317F02B15921BF5CC8486990 /* KeychainController.swift */, - 3689E6F87850DD65DAA45428 /* KeychainControllerProtocol.swift */, 3558A15CFB934F9229301527 /* RestorationToken.swift */, 3F9E67AAB66638C69626866C /* UserSessionFlowCoordinator.swift */, 3429142FE11930422E7CC1A0 /* UserSessionFlowCoordinatorStateMachine.swift */, @@ -1891,6 +2109,7 @@ CD80F22830C2360F3F39DDCE /* UserNotificationModalView.swift */, 649759084B0C9FE1F8DF8D17 /* UserNotificationPresenter.swift */, F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */, + B3E78735F63FA93FAAAF700A /* MockUserNotificationController.swift~refs */, ); path = UserNotifications; sourceTree = ""; @@ -1989,6 +2208,14 @@ path = Background; sourceTree = ""; }; + F798CDE87F83A94B8BC2E18A /* remotes */ = { + isa = PBXGroup; + children = ( + C5A8A8B1C16BBFEA4B9D5988 /* origin */, + ); + path = remotes; + sourceTree = ""; + }; FCDF06BDB123505F0334B4F9 /* Timeline */ = { isa = PBXGroup; children = ( @@ -2067,12 +2294,14 @@ 9797D588420FCBBC228A63C9 /* Sources */, 215E1D91B98672C856F559D0 /* Resources */, EE878EAA342710DB973E0A87 /* Frameworks */, + 32FD0140DF485A66F1B788D1 /* Embed App Extensions */, 98CA896D84BFD53B2554E891 /* ⚠️ SwiftLint */, B35AB66424BB30087EEE408C /* 🧹 SwiftFormat */, ); buildRules = ( ); dependencies = ( + 2C29670603B37E38705D5FF1 /* PBXTargetDependency */, ); name = ElementX; packageProductDependencies = ( @@ -2123,6 +2352,29 @@ productReference = 9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + FEB53A5BC378C913769656D8 /* NSE */ = { + isa = PBXNativeTarget; + buildConfigurationList = CBD61DB8FBC472BAA66A0CBD /* Build configuration list for PBXNativeTarget "NSE" */; + buildPhases = ( + 064584F7D1F4A58D753BDD96 /* Sources */, + 804B8DA568046249B1261739 /* Resources */, + BF59B36A7B2DB184B62826F6 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NSE; + packageProductDependencies = ( + 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */, + AC5D19D7A65EB05A9704FB44 /* SwiftyBeaver */, + 800631D7250B7F93195035F1 /* KeychainAccess */, + 940C605265DD82DA0C655E23 /* Kingfisher */, + ); + productName = NSE; + productReference = 0D8F620C8B314840D8602E3F /* NSE.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -2133,14 +2385,22 @@ ORGANIZATIONNAME = Element; TargetAttributes = { 0E28CD62691FDBC63147D5E3 = { + DevelopmentTeam = 7J4U792NQT; TestTargetID = C0FAEB81CFD9776CD78CE489; }; - C0FAEB81CFD9776CD78CE489 = { + 32C23C8D224D46EFE62AFAD0 = { DevelopmentTeam = 7J4U792NQT; }; + C0FAEB81CFD9776CD78CE489 = { + DevelopmentTeam = "$(DEVELOPMENT_TEAM)"; + }; D3DB351B7FBE0F49649171FC = { + DevelopmentTeam = 7J4U792NQT; TestTargetID = C0FAEB81CFD9776CD78CE489; }; + FEB53A5BC378C913769656D8 = { + DevelopmentTeam = "$(DEVELOPMENT_TEAM)"; + }; }; }; buildConfigurationList = 7AE41FCCF9D1352E2770D1F9 /* Build configuration list for PBXProject "ElementX" */; @@ -2248,6 +2508,7 @@ targets = ( C0FAEB81CFD9776CD78CE489 /* ElementX */, D3DB351B7FBE0F49649171FC /* IntegrationTests */, + FEB53A5BC378C913769656D8 /* NSE */, 0E28CD62691FDBC63147D5E3 /* UITests */, 32C23C8D224D46EFE62AFAD0 /* UnitTests */, ); @@ -2268,10 +2529,19 @@ 690ED5315B401238A3249DCB /* README.md in Resources */, CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */, 2797C9D9BA642370F1C85D78 /* Untranslated.stringsdict in Resources */, + E67418DACEDBC29E988E6ACD /* message.caf in Resources */, DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 804B8DA568046249B1261739 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 86982BD498105258F3778110 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2283,6 +2553,7 @@ 541374590CA7E8318BD480FD /* Localizable.stringsdict in Resources */, 191161FE9E0DA89704301F37 /* Untranslated.strings in Resources */, F040ABFEB0A2B142D948BA12 /* Untranslated.stringsdict in Resources */, + 2D794361CFE790C8FB3C9C0F /* message.caf in Resources */, 059173B3C77056C406906B6D /* target.yml in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2324,7 +2595,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "python3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPhone 13 Pro Max' --version 'iOS.16.0'\npython3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPad (9th generation)' --version 'iOS.16.0'\n"; + shellScript = "python3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPhone 13 Pro Max' --version 'iOS.16.1'\npython3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPad (9th generation)' --version 'iOS.16.1'\n"; }; 98CA896D84BFD53B2554E891 /* ⚠️ SwiftLint */ = { isa = PBXShellScriptBuildPhase; @@ -2386,6 +2657,44 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 064584F7D1F4A58D753BDD96 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 968A5B890004526AB58A217C /* AvatarSize.swift in Sources */, + EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */, + 1B4B3E847BF944DB2C1C217F /* BackgroundTaskServiceProtocol.swift in Sources */, + 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */, + DFCA89C4EC2A5332ED6B441F /* DataProtectionManager.swift in Sources */, + E8AB8D16E6D8E8E501F29BD9 /* FileCache.swift in Sources */, + A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */, + 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */, + EBE13FAB4E29738AC41BD3E5 /* InfoPlistReader.swift in Sources */, + 8691186F9B99BCDDB7CACDD8 /* KeychainController.swift in Sources */, + A440D4BC02088482EC633A88 /* KeychainControllerProtocol.swift in Sources */, + AD2A81B65A9F6163012086F1 /* MXLog.swift in Sources */, + 8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */, + 0F3F2FDD4021A25A0D57F801 /* MediaProvider.swift in Sources */, + 4C3365818DE1CEAEDF590FD3 /* MediaProviderProtocol.swift in Sources */, + F9981191DC408AED537C1749 /* MediaProxy.swift in Sources */, + 2CA8AD07773A38BA4662098B /* MediaProxyProtocol.swift in Sources */, + 5E25568E1CDAD983517E58B5 /* MediaSourceProxy.swift in Sources */, + 5455147CAC63F71E48F7D699 /* NSELogger.swift in Sources */, + 5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */, + 214CDBF0C783155242FFE4A0 /* NotificationItemProxy+NSE.swift in Sources */, + 5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */, + B14BC354E56616B6B7D9A3D7 /* NotificationServiceExtension.swift in Sources */, + BFB534E338A3D949944FB2F5 /* NotificationServiceProxy.swift in Sources */, + 7E3B1F8D72573ED2FCB2D94B /* NotificationServiceProxyProtocol.swift in Sources */, + 414F50CFCFEEE2611127DCFB /* RestorationToken.swift in Sources */, + 719E7AAD1F8E68F68F30FECD /* Task.swift in Sources */, + BA074E9812F96FFA3200ED1D /* TimelineItemProxy.swift in Sources */, + 6126CC51654E159804999E6A /* UNMutableNotificationContent.swift in Sources */, + 518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */, + 06B55882911B4BF5B14E9851 /* URL.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 11F93544B4FC60F78F47D89C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2400,6 +2709,7 @@ CA45758F08DF42D41D8A4B29 /* FilePreviewViewModelTests.swift in Sources */, F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */, 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */, + A23B8B27A1436A1049EEF68E /* InfoPlistReader.swift in Sources */, EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */, 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, @@ -2443,8 +2753,10 @@ 3C73442084BF8A6939F0F80B /* AnalyticsService.swift in Sources */, EC4C31963E755EEC77BD778C /* AnalyticsSettings.swift in Sources */, 095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */, + A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */, 4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */, 9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */, + 9462C62798F47E39DCC182D2 /* Application.swift in Sources */, 74604ACFDBE7F54260E7B617 /* ApplicationProtocol.swift in Sources */, 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */, 3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */, @@ -2486,7 +2798,7 @@ B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */, 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, 33D630461FC4562CC767EE9F /* FileCache.swift in Sources */, - FD4706DC752744A0C91ED6FE /* FileManager.swift in Sources */, + 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */, 6C67774E8387D44426718BD9 /* FilePreviewCoordinator.swift in Sources */, 6C9F6C7F2B35288C4230EF3F /* FilePreviewModels.swift in Sources */, 91DFCB641FBA03EE2DA0189E /* FilePreviewScreen.swift in Sources */, @@ -2507,11 +2819,11 @@ BA31448FBD9697F8CB9A83CD /* ImageCache.swift in Sources */, DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */, D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */, - A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */, + B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */, 9F41FF9C53F7A6EAEA6259C9 /* InviteFriendsCoordinator.swift in Sources */, E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */, - DF790EF2E4D41D1091AEB263 /* KeychainController.swift in Sources */, - 0C601923A872A87C775B889A /* KeychainControllerProtocol.swift in Sources */, + 1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */, + CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */, 15D867E638BFD0E5E71DB1EF /* List.swift in Sources */, 83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */, 872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */, @@ -2531,7 +2843,9 @@ 80997E933A5B2C0868D80B45 /* MediaPlayerViewModelProtocol.swift in Sources */, EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */, 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */, - 62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */, + FBCD77D557AACBE9B445133A /* MediaProxy.swift in Sources */, + 323F36D880363C473B81A9EA /* MediaProxyProtocol.swift in Sources */, + 43BD17BC8794BB9B04F2A26B /* MediaSourceProxy.swift in Sources */, 24906A1E82D0046655958536 /* MessageComposer.swift in Sources */, 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */, C35CF4DAB1467FE1BBDC204B /* MessageTimelineItem.swift in Sources */, @@ -2539,6 +2853,8 @@ 28410F3DE89C2C44E4F75C92 /* MockBugReportService.swift in Sources */, EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */, 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */, + F425C3F85BFF28C9AC593F52 /* MockNotificationManager.swift in Sources */, + AEE3981A0F090208E4445808 /* MockNotificationServiceProxy.swift in Sources */, 51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */, 2352C541AF857241489756FF /* MockRoomSummaryProvider.swift in Sources */, E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */, @@ -2551,6 +2867,12 @@ 41E16904B30C529373B4E1A4 /* NavigationController.swift in Sources */, 8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */, 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */, + 3F70E237CE4C3FAB02FC227F /* NotificationConstants.swift in Sources */, + CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */, + 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */, + 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */, + 5B8B51CEC4717AF487794685 /* NotificationServiceProxy.swift in Sources */, + F61AFA8BF2E739FBC30472F5 /* NotificationServiceProxyProtocol.swift in Sources */, 563A05B43207D00A6B698211 /* OIDCService.swift in Sources */, 2CB6787E25B11711518E9588 /* OnboardingCoordinator.swift in Sources */, 5D7960B32C350FA93F48D02B /* OnboardingModels.swift in Sources */, @@ -2696,8 +3018,9 @@ 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, 29E20505F321071E8375F99B /* BuildSettings.swift in Sources */, 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */, + E3C328EF20C4B3326D263BCD /* FileManager.swift in Sources */, 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */, - BB4C6F362F75933DDDE30F3E /* InfoPlist.swift in Sources */, + A823A4E8CB71D7D9743E7E95 /* InfoPlistReader.swift in Sources */, 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */, 6B15FF984906AAFCF9DC4F58 /* OnboardingUITests.swift in Sources */, 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */, @@ -2734,6 +3057,11 @@ target = C0FAEB81CFD9776CD78CE489 /* ElementX */; targetProxy = 4D8DD8FE84794CA168A8499A /* PBXContainerItemProxy */; }; + 2C29670603B37E38705D5FF1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FEB53A5BC378C913769656D8 /* NSE */; + targetProxy = AE0C21E7E01A23610E54DF9D /* PBXContainerItemProxy */; + }; 421359F1BC0A1816DD34A2BB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C0FAEB81CFD9776CD78CE489 /* ElementX */; @@ -2925,20 +3253,41 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 4CB921EC997F763064B8E436 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = NSE/SupportingFiles/NSE.entitlements; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + INFOPLIST_FILE = NSE/SupportingFiles/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = "$(MARKETING_VERSION)"; + PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; + PRODUCT_NAME = NSE; + SDKROOT = iphoneos; + SWIFT_OBJC_INTERFACE_HEADER_NAME = "GeneratedInterface-Swift.h"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 62E1B7866DF0ED442C39A83B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ElementX/SupportingFiles/ElementX.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 7J4U792NQT; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; INFOPLIST_FILE = ElementX/SupportingFiles/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.9; + MARKETING_VERSION = "$(MARKETING_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = ElementX; SDKROOT = iphoneos; @@ -2954,14 +3303,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ElementX/SupportingFiles/ElementX.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 7J4U792NQT; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; INFOPLIST_FILE = ElementX/SupportingFiles/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.9; + MARKETING_VERSION = "$(MARKETING_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = ElementX; SDKROOT = iphoneos; @@ -3027,7 +3376,9 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 7J4U792NQT; ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3041,6 +3392,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.0; MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0.9; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3089,7 +3441,9 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 7J4U792NQT; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -3109,6 +3463,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.0; MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0.9; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -3174,6 +3529,27 @@ }; name = Release; }; + A46EFA0820A3F5A3A0C74CFE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = NSE/SupportingFiles/NSE.entitlements; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + INFOPLIST_FILE = NSE/SupportingFiles/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = "$(MARKETING_VERSION)"; + PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; + PRODUCT_NAME = NSE; + SDKROOT = iphoneos; + SWIFT_OBJC_INTERFACE_HEADER_NAME = "GeneratedInterface-Swift.h"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; AAE81BF8DCDB30B237B10C3E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3249,6 +3625,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + CBD61DB8FBC472BAA66A0CBD /* Build configuration list for PBXNativeTarget "NSE" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A46EFA0820A3F5A3A0C74CFE /* Debug */, + 4CB921EC997F763064B8E436 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; F1B67CF63C1231AEB14D70E6 /* Build configuration list for PBXNativeTarget "UITests" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -3456,6 +3841,11 @@ package = C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */; productName = DTCoreText; }; + 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 80B898A3AD2AC63F3ABFC218 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; + productName = MatrixRustSDK; + }; 67E7A6F388D3BF85767609D9 /* Sentry */ = { isa = XCSwiftPackageProductDependency; package = A08925A9D5E3770DEB9D8509 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; @@ -3471,11 +3861,21 @@ package = 61916C63E3F5BD900F08DA0C /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; + 800631D7250B7F93195035F1 /* KeychainAccess */ = { + isa = XCSwiftPackageProductDependency; + package = 61916C63E3F5BD900F08DA0C /* XCRemoteSwiftPackageReference "KeychainAccess" */; + productName = KeychainAccess; + }; 886A0A498FA01E8EDD451D05 /* Sentry */ = { isa = XCSwiftPackageProductDependency; package = A08925A9D5E3770DEB9D8509 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; productName = Sentry; }; + 940C605265DD82DA0C655E23 /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = D283517192CAC3E2E6920765 /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; 9573B94B1C86C6DF751AF3FD /* SwiftState */ = { isa = XCSwiftPackageProductDependency; package = 6582B5AF3F104B0F7E031E7D /* XCRemoteSwiftPackageReference "SwiftState" */; @@ -3510,6 +3910,11 @@ package = 4CE94127E27181B8B72188F0 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */; productName = AppAuth; }; + AC5D19D7A65EB05A9704FB44 /* SwiftyBeaver */ = { + isa = XCSwiftPackageProductDependency; + package = 25B4484A6A20B9F1705DEEDA /* XCRemoteSwiftPackageReference "SwiftyBeaver" */; + productName = SwiftyBeaver; + }; AD2AC190E55B2BD4D0F1D4A7 /* SwiftyBeaver */ = { isa = XCSwiftPackageProductDependency; package = 25B4484A6A20B9F1705DEEDA /* XCRemoteSwiftPackageReference "SwiftyBeaver" */; diff --git a/ElementX.xcodeproj/xcshareddata/xcschemes/NSE.xcscheme b/ElementX.xcodeproj/xcshareddata/xcschemes/NSE.xcscheme new file mode 100644 index 000000000..df0566dd6 --- /dev/null +++ b/ElementX.xcodeproj/xcshareddata/xcschemes/NSE.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index b50a1473f..2ac82259d 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -31,3 +31,5 @@ // Parameter is the application display name (e.g. "ElementX") "default_session_display_name" = "%@ iOS"; + +"Notification" = "Notification"; diff --git a/ElementX/Resources/Sounds/message.caf b/ElementX/Resources/Sounds/message.caf new file mode 100644 index 000000000..4c241b586 Binary files /dev/null and b/ElementX/Resources/Sounds/message.caf differ diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index a43659848..42bd7a8b0 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -31,7 +31,7 @@ struct ServiceLocator { let userNotificationController: UserNotificationControllerProtocol } -class AppCoordinator: CoordinatorProtocol { +class AppCoordinator: AppCoordinatorProtocol { private let stateMachine: AppCoordinatorStateMachine private let navigationController: NavigationController private let userSessionStore: UserSessionStoreProtocol @@ -40,6 +40,7 @@ class AppCoordinator: CoordinatorProtocol { didSet { deobserveUserSessionChanges() if let userSession, !userSession.isSoftLogout { + configureNotificationManager() observeUserSessionChanges() } } @@ -52,7 +53,8 @@ class AppCoordinator: CoordinatorProtocol { private let backgroundTaskService: BackgroundTaskServiceProtocol private var cancellables = Set() - + private(set) var notificationManager: NotificationManagerProtocol? + init() { navigationController = NavigationController() stateMachine = AppCoordinatorStateMachine() @@ -60,17 +62,12 @@ class AppCoordinator: CoordinatorProtocol { bugReportService = BugReportService(withBaseURL: BuildSettings.bugReportServiceBaseURL, sentryURL: BuildSettings.bugReportSentryURL) navigationController.setRootCoordinator(SplashScreenCoordinator()) - + ServiceLocator.serviceLocator = ServiceLocator(userNotificationController: UserNotificationController(rootCoordinator: navigationController)) - - guard let bundleIdentifier = Bundle.main.bundleIdentifier else { - fatalError("Should have a valid bundle identifier at this point") - } backgroundTaskService = UIKitBackgroundTaskService(withApplication: UIApplication.shared) - - userSessionStore = UserSessionStore(bundleIdentifier: bundleIdentifier, - backgroundTaskService: backgroundTaskService) + + userSessionStore = UserSessionStore(backgroundTaskService: backgroundTaskService) setupStateMachine() @@ -97,6 +94,7 @@ class AppCoordinator: CoordinatorProtocol { private func setupLogging() { let loggerConfiguration = MXLogConfiguration() + loggerConfiguration.maxLogFilesCount = 10 #if DEBUG // This exposes the full Rust side tracing subscriber filter for more flexibility. @@ -252,6 +250,8 @@ class AppCoordinator: CoordinatorProtocol { // regardless of the result, clear user data userSessionStore.logout(userSession: userSession) userSession = nil + notificationManager?.delegate = nil + notificationManager = nil } } @@ -268,6 +268,38 @@ class AppCoordinator: CoordinatorProtocol { startAuthentication() } } + + private func configureNotificationManager() { + guard BuildSettings.enableNotifications else { + return + } + guard notificationManager == nil else { + return + } + + let manager = NotificationManager(clientProxy: userSession.clientProxy) + if manager.isAvailable { + manager.delegate = self + notificationManager = manager + manager.start() + + if let appDelegate = AppDelegate.shared { + appDelegate.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + switch callback { + case .registeredNotifications(let deviceToken): + self?.notificationManager?.register(with: deviceToken) + case .failedToRegisteredNotifications(let error): + self?.notificationManager?.registrationFailed(with: error) + } + } + .store(in: &cancellables) + } else { + MXLog.debug("Couldn't register to AppDelegate callbacks") + } + } + } private func observeUserSessionChanges() { userSession.callbacks @@ -321,3 +353,51 @@ extension AppCoordinator: AuthenticationCoordinatorDelegate { stateMachine.processEvent(.succeededSigningIn) } } + +// MARK: - NotificationManagerDelegate + +extension AppCoordinator: NotificationManagerDelegate { + func authorizationStatusUpdated(_ service: NotificationManagerProtocol, granted: Bool) { + if granted { + UIApplication.shared.registerForRemoteNotifications() + } + } + + func shouldDisplayInAppNotification(_ service: NotificationManagerProtocol, content: UNNotificationContent) -> Bool { + guard let roomId = content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String else { + return true + } + guard let userSessionFlowCoordinator else { + // there is not a user session yet + return false + } + return !userSessionFlowCoordinator.isDisplayingRoomScreen(withRoomId: roomId) + } + + func notificationTapped(_ service: NotificationManagerProtocol, content: UNNotificationContent) async { + MXLog.debug("[AppCoordinator] tappedNotification") + + guard let roomId = content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String else { + return + } + + userSessionFlowCoordinator?.tryDisplayingRoomScreen(roomId: roomId) + } + + func handleInlineReply(_ service: NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async { + MXLog.debug("[AppCoordinator] handle notification reply") + + guard let roomId = content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String else { + return + } + let roomProxy = await userSession.clientProxy.roomForIdentifier(roomId) + switch await roomProxy?.sendMessage(replyText) { + case .success: + break + default: + // error or no room proxy + service.showLocalNotification(with: "⚠️ " + ElementL10n.dialogTitleError, + subtitle: ElementL10n.a11yErrorSomeMessageNotSent) + } + } +} diff --git a/ElementX/Sources/Application/AppCoordinatorProtocol.swift b/ElementX/Sources/Application/AppCoordinatorProtocol.swift new file mode 100644 index 000000000..e76e4384b --- /dev/null +++ b/ElementX/Sources/Application/AppCoordinatorProtocol.swift @@ -0,0 +1,21 @@ +// +// Copyright 2022 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 + +protocol AppCoordinatorProtocol: CoordinatorProtocol { + var notificationManager: NotificationManagerProtocol? { get } +} diff --git a/ElementX/Sources/Application/AppDelegate.swift b/ElementX/Sources/Application/AppDelegate.swift index b5dd71a4a..864073747 100644 --- a/ElementX/Sources/Application/AppDelegate.swift +++ b/ElementX/Sources/Application/AppDelegate.swift @@ -14,38 +14,30 @@ // limitations under the License. // -import SwiftUI +import Combine +import Foundation +import UIKit -@main -struct Application: App { - @UIApplicationDelegateAdaptor(AppDelegate.self) private var applicationDelegate - private let applicationCoordinator: CoordinatorProtocol - - init() { - if Tests.isRunningUITests { - applicationCoordinator = UITestsAppCoordinator() - } else { - applicationCoordinator = AppCoordinator() - } - } - - var body: some Scene { - WindowGroup { - if Tests.isRunningUnitTests { - EmptyView() - } else { - applicationCoordinator.toPresentable() - .tint(.element.accent) - .task { - applicationCoordinator.start() - } - } - } - } +enum AppDelegateCallback { + case registeredNotifications(deviceToken: Data) + case failedToRegisteredNotifications(error: Error) } class AppDelegate: NSObject, UIApplicationDelegate { - func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - true + private(set) static var shared: AppDelegate! + let callbacks = PassthroughSubject() + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + // worst singleton ever + Self.shared = self + return true + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + callbacks.send(.registeredNotifications(deviceToken: deviceToken)) + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + callbacks.send(.failedToRegisteredNotifications(error: error)) } } diff --git a/ElementX/Sources/Application/Application.swift b/ElementX/Sources/Application/Application.swift new file mode 100644 index 000000000..b8ba696b5 --- /dev/null +++ b/ElementX/Sources/Application/Application.swift @@ -0,0 +1,45 @@ +// +// Copyright 2022 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 + +@main +struct Application: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) private var applicationDelegate + private let applicationCoordinator: AppCoordinatorProtocol + + init() { + if Tests.isRunningUITests { + applicationCoordinator = UITestsAppCoordinator() + } else { + applicationCoordinator = AppCoordinator() + } + } + + var body: some Scene { + WindowGroup { + if Tests.isRunningUnitTests { + EmptyView() + } else { + applicationCoordinator.toPresentable() + .tint(.element.accent) + .task { + applicationCoordinator.start() + } + } + } + } +} diff --git a/ElementX/Sources/Application/BuildSettings.swift b/ElementX/Sources/Application/BuildSettings.swift index 63640e215..c2d483586 100644 --- a/ElementX/Sources/Application/BuildSettings.swift +++ b/ElementX/Sources/Application/BuildSettings.swift @@ -17,12 +17,22 @@ import Foundation final class BuildSettings { + // MARK: - Bundle Settings + + static var pusherAppId: String { + #if DEBUG + InfoPlistReader.target.baseBundleIdentifier + ".ios.dev" + #else + InfoPlistReader.target.baseBundleIdentifier + ".ios.prod" + #endif + } + // MARK: - Servers static let defaultHomeserverAddress = "matrix.org" - static let defaultSlidingSyncProxyBaseURLString = "https://slidingsync.lab.element.dev" - + static let pushGatewayBaseURL = URL(staticString: "https://matrix.org/_matrix/push/v1/notify") + // MARK: - Bug report static let bugReportServiceBaseURL = URL(staticString: "https://riot.im/bugreports") @@ -38,14 +48,14 @@ final class BuildSettings { #if DEBUG /// The configuration to use for analytics during development. Set `isEnabled` to false to disable analytics in debug builds. /// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork, set custom configurations. - static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: ElementInfoPlist.cfBundleIdentifier.starts(with: "io.element.elementx"), + static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: InfoPlistReader.target.bundleIdentifier.starts(with: "io.element.elementx"), host: "https://posthog.element.dev", apiKey: "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN", termsURL: URL(staticString: "https://element.io/cookie-policy")) #else /// The configuration to use for analytics. Set `isEnabled` to false to disable analytics. /// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork, set custom configurations. - static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: ElementInfoPlist.cfBundleIdentifier.starts(with: "io.element.elementx"), + static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: InfoPlistReader.target.bundleIdentifier.starts(with: "io.element.elementx"), host: "https://posthog.hss.element.io", apiKey: "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", termsURL: URL(staticString: "https://element.io/cookie-policy")) @@ -63,4 +73,8 @@ final class BuildSettings { // MARK: - Other static var permalinkBaseURL = URL(staticString: "https://matrix.to") + + // MARK: - Notifications + + static let enableNotifications = false } diff --git a/ElementX/Sources/Generated/InfoPlist.swift b/ElementX/Sources/Generated/InfoPlist.swift deleted file mode 100644 index 6d76458aa..000000000 --- a/ElementX/Sources/Generated/InfoPlist.swift +++ /dev/null @@ -1,66 +0,0 @@ -// 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 cfBundleDisplayName: String = _document["CFBundleDisplayName"] - 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 itsAppUsesNonExemptEncryption: Bool = _document["ITSAppUsesNonExemptEncryption"] - internal static let uiLaunchStoryboardName: String = _document["UILaunchStoryboardName"] - internal static let uiSupportedInterfaceOrientations: [String] = _document["UISupportedInterfaceOrientations"] - internal static let appGroupIdentifier: String = _document["appGroupIdentifier"] -} -// swiftlint:enable identifier_name line_length type_body_length - -// MARK: - Implementation Details - -private func arrayFromPlist(at path: String) -> [T] { - guard let url = BundleToken.bundle.url(forResource: path, withExtension: nil), - let data = NSArray(contentsOf: url) as? [T] else { - fatalError("Unable to load PLIST at path: \(path)") - } - return data -} - -private struct PlistDocument { - let data: [String: Any] - - init(path: String) { - self.data = BundleToken.bundle.infoDictionary ?? [:] - } - - subscript(key: String) -> T { - guard let result = data[key] as? T else { - fatalError("Property '\(key)' is not of type \(T.self)") - } - return result - } -} - -// swiftlint:disable convenience_type -private final class BundleToken { - static let bundle: Bundle = { - #if SWIFT_PACKAGE - return Bundle.module - #else - return Bundle(for: BundleToken.self) - #endif - }() -} -// swiftlint:enable convenience_type diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index 55fbd90b6..8b413cb15 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -24,6 +24,8 @@ extension ElementL10n { public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device") /// Tablet public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device") + /// Notification + public static let notification = ElementL10n.tr("Untranslated", "Notification") /// Editing public static let roomTimelineEditing = ElementL10n.tr("Untranslated", "room_timeline_editing") /// Failed creating the permalink diff --git a/ElementX/Sources/Other/ElementSettings.swift b/ElementX/Sources/Other/ElementSettings.swift index 153f67b83..802303cf1 100644 --- a/ElementX/Sources/Other/ElementSettings.swift +++ b/ElementX/Sources/Other/ElementSettings.swift @@ -26,13 +26,15 @@ final class ElementSettings: ObservableObject { case enableAnalytics case isIdentifiedForAnalytics case slidingSyncProxyBaseURLString + case enableInAppNotifications + case pusherProfileTag } static let shared = ElementSettings() /// UserDefaults to be used on reads and writes. static var store: UserDefaults { - guard let userDefaults = UserDefaults(suiteName: ElementInfoPlist.appGroupIdentifier) else { + guard let userDefaults = UserDefaults(suiteName: InfoPlistReader.target.appGroupIdentifier) else { fatalError("Fail to load shared UserDefaults") } return userDefaults @@ -63,9 +65,18 @@ final class ElementSettings: ObservableObject { @AppStorage(UserDefaultsKeys.timelineStyle.rawValue, store: store) var timelineStyle = BuildSettings.defaultRoomTimelineStyle - + // MARK: - Client - + @AppStorage(UserDefaultsKeys.slidingSyncProxyBaseURLString.rawValue, store: store) var slidingSyncProxyBaseURLString = BuildSettings.defaultSlidingSyncProxyBaseURLString + + // MARK: - Notifications + + @AppStorage(UserDefaultsKeys.enableInAppNotifications.rawValue, store: store) + var enableInAppNotifications = true + + @AppStorage(UserDefaultsKeys.pusherProfileTag.rawValue, store: store) + /// Tag describing which set of device specific rules a pusher executes. + var pusherProfileTag: String? } diff --git a/ElementX/Sources/Other/Extensions/FileManager.swift b/ElementX/Sources/Other/Extensions/FileManager.swift new file mode 100644 index 000000000..eac66442d --- /dev/null +++ b/ElementX/Sources/Other/Extensions/FileManager.swift @@ -0,0 +1,34 @@ +// +// Copyright 2022 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 + +extension FileManager { + func directoryExists(at url: URL) -> Bool { + var isDirectory: ObjCBool = false + guard fileExists(atPath: url.path(), isDirectory: &isDirectory) else { + return false + } + return isDirectory.boolValue + } + + func createDirectoryIfNeeded(at url: URL, withIntermediateDirectories: Bool = true) throws { + guard !directoryExists(at: url) else { + return + } + try createDirectory(at: url, withIntermediateDirectories: withIntermediateDirectories) + } +} diff --git a/ElementX/Sources/Other/Extensions/ImageCache.swift b/ElementX/Sources/Other/Extensions/ImageCache.swift index 503b917ce..7423826ef 100644 --- a/ElementX/Sources/Other/Extensions/ImageCache.swift +++ b/ElementX/Sources/Other/Extensions/ImageCache.swift @@ -23,4 +23,10 @@ extension ImageCache { result.diskStorage.config.sizeLimit = 1 return result } + + static var onlyOnDisk: ImageCache { + let result = ImageCache.default + result.memoryStorage.config.totalCostLimit = 1 + return result + } } diff --git a/ElementX/Sources/Other/Extensions/UIDevice.swift b/ElementX/Sources/Other/Extensions/UIDevice.swift index 14a036417..1620423da 100644 --- a/ElementX/Sources/Other/Extensions/UIDevice.swift +++ b/ElementX/Sources/Other/Extensions/UIDevice.swift @@ -23,6 +23,6 @@ extension UIDevice { } var initialDisplayName: String { - ElementL10n.defaultSessionDisplayName(ElementInfoPlist.cfBundleDisplayName) + ElementL10n.defaultSessionDisplayName(InfoPlistReader.target.bundleDisplayName) } } diff --git a/ElementX/Sources/Other/Extensions/URL.swift b/ElementX/Sources/Other/Extensions/URL.swift index 7632c8f44..c7a5c1a03 100644 --- a/ElementX/Sources/Other/Extensions/URL.swift +++ b/ElementX/Sources/Other/Extensions/URL.swift @@ -24,4 +24,33 @@ extension URL { self = url } + + /// The URL of the primary app group container. + static var appGroupContainerDirectory: URL { + guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: InfoPlistReader.target.appGroupIdentifier) else { + fatalError("Should always be able to retrieve the container directory") + } + return url + } + + /// The base directory where all session data is stored. + static var sessionsBaseDirectory: URL { + let url = cacheBaseDirectory + .appendingPathComponent("Sessions", isDirectory: true) + + try? FileManager.default.createDirectoryIfNeeded(at: url) + + return url + } + + /// The base directory where all cache is stored. + static var cacheBaseDirectory: URL { + let url = appGroupContainerDirectory + .appendingPathComponent("Library", isDirectory: true) + .appendingPathComponent("Caches", isDirectory: true) + + try? FileManager.default.createDirectoryIfNeeded(at: url) + + return url + } } diff --git a/ElementX/Sources/Other/InfoPlistReader.swift b/ElementX/Sources/Other/InfoPlistReader.swift new file mode 100644 index 000000000..616bf42e7 --- /dev/null +++ b/ElementX/Sources/Other/InfoPlistReader.swift @@ -0,0 +1,79 @@ +// +// Copyright 2022 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 + +struct InfoPlistReader { + private enum Keys { + static let appGroupIdentifier = "appGroupIdentifier" + static let baseBundleIdentifier = "baseBundleIdentifier" + static let bundleShortVersion = "CFBundleShortVersionString" + static let bundleDisplayName = "CFBundleDisplayName" + } + + /// Info.plist reader on the current target + static let target = InfoPlistReader(bundle: .main) + + private let bundle: Bundle + + /// Initializer + /// - Parameter bundle: bundle to read values from + init(bundle: Bundle) { + self.bundle = bundle + } + + /// App group identifier set in Info.plist of the target + var appGroupIdentifier: String { + infoPlistStringValue(forKey: Keys.appGroupIdentifier) + } + + /// Base bundle identifier set in Info.plist of the target + var baseBundleIdentifier: String { + infoPlistStringValue(forKey: Keys.baseBundleIdentifier) + } + + /// Bundle executable of the target + var bundleExecutable: String { + infoPlistStringValue(forKey: kCFBundleExecutableKey as String) + } + + /// Bundle identifier of the target + var bundleIdentifier: String { + infoPlistStringValue(forKey: kCFBundleIdentifierKey as String) + } + + /// Bundle short version string of the target + var bundleShortVersionString: String { + infoPlistStringValue(forKey: Keys.bundleShortVersion) + } + + /// Bundle version of the target + var bundleVersion: String { + infoPlistStringValue(forKey: kCFBundleVersionKey as String) + } + + /// Bundle display name of the target + var bundleDisplayName: String { + infoPlistStringValue(forKey: Keys.bundleDisplayName) + } + + private func infoPlistStringValue(forKey key: String) -> String { + guard let result = bundle.object(forInfoDictionaryKey: key) as? String else { + fatalError("Add \(key) into your target's Info.plst") + } + return result + } +} diff --git a/ElementX/Sources/Other/Logging/MXLogger.swift b/ElementX/Sources/Other/Logging/MXLogger.swift index 9d732f0cf..07d244253 100644 --- a/ElementX/Sources/Other/Logging/MXLogger.swift +++ b/ElementX/Sources/Other/Logging/MXLogger.swift @@ -150,9 +150,9 @@ class MXLogger { MXLogger.logCrashes(false) // Extract running app information - let app = ElementInfoPlist.cfBundleExecutable - let appId = ElementInfoPlist.cfBundleIdentifier - let appVersion = "\(ElementInfoPlist.cfBundleShortVersionString) (r\(ElementInfoPlist.cfBundleVersion))" + let app = InfoPlistReader.target.bundleExecutable + let appId = InfoPlistReader.target.bundleIdentifier + let appVersion = "\(InfoPlistReader.target.bundleShortVersionString) (r\(InfoPlistReader.target.bundleVersion))" // Build the crash log let model = UIDevice.current.model @@ -269,7 +269,7 @@ class MXLogger { /// The folder where logs are stored private static var logsFolderURL: URL { - FileManager.default.appGroupContainerURL ?? URL.documentsDirectory + .appGroupContainerDirectory } /// If `self.redirectNSLog(toFiles:numberOfFiles:)` is called with a lower numberOfFiles we need to do some cleanup. diff --git a/ElementX/Sources/Other/UserAgentBuilder.swift b/ElementX/Sources/Other/UserAgentBuilder.swift index 4ad3d9d28..adfc284f2 100644 --- a/ElementX/Sources/Other/UserAgentBuilder.swift +++ b/ElementX/Sources/Other/UserAgentBuilder.swift @@ -27,8 +27,8 @@ final class UserAgentBuilder { } private class func makeUserAgent() -> String? { - let clientName = ElementInfoPlist.cfBundleDisplayName - let clientVersion = ElementInfoPlist.cfBundleShortVersionString + let clientName = InfoPlistReader.target.bundleDisplayName + let clientVersion = InfoPlistReader.target.bundleShortVersionString #if os(iOS) return String( diff --git a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptModels.swift b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptModels.swift index 40d0bcb96..17c5aad10 100644 --- a/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptModels.swift +++ b/ElementX/Sources/Screens/AnalyticsPrompt/AnalyticsPromptModels.swift @@ -44,10 +44,10 @@ struct AnalyticsPromptStrings { init() { // Create the opt in content with a placeholder. let linkPlaceholder = "{link}" - var optInContent = AttributedString(ElementL10n.analyticsOptInContent(ElementInfoPlist.cfBundleDisplayName, linkPlaceholder)) + var optInContent = AttributedString(ElementL10n.analyticsOptInContent(InfoPlistReader.target.bundleDisplayName, linkPlaceholder)) guard let range = optInContent.range(of: linkPlaceholder) else { - self.optInContent = AttributedString(ElementL10n.analyticsOptInContent(ElementInfoPlist.cfBundleDisplayName, + self.optInContent = AttributedString(ElementL10n.analyticsOptInContent(InfoPlistReader.target.bundleDisplayName, ElementL10n.analyticsOptInContentLink)) MXLog.failure("Failed to add a link attribute to the opt in content.") return diff --git a/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPrompt.swift b/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPrompt.swift index 29f18c1e5..1fe079102 100644 --- a/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPrompt.swift +++ b/ElementX/Sources/Screens/AnalyticsPrompt/View/AnalyticsPrompt.swift @@ -67,7 +67,7 @@ struct AnalyticsPrompt: View { Image(uiImage: Asset.Images.analyticsLogo.image) .padding(.bottom, 25) - Text(ElementL10n.analyticsOptInTitle(ElementInfoPlist.cfBundleDisplayName)) + Text(ElementL10n.analyticsOptInTitle(InfoPlistReader.target.bundleDisplayName)) .font(.element.title2Bold) .multilineTextAlignment(.center) .foregroundColor(.element.primaryContent) diff --git a/ElementX/Sources/Screens/OnboardingScreen/OnboardingModels.swift b/ElementX/Sources/Screens/OnboardingScreen/OnboardingModels.swift index 65c4ef8a3..2e6497222 100644 --- a/ElementX/Sources/Screens/OnboardingScreen/OnboardingModels.swift +++ b/ElementX/Sources/Screens/OnboardingScreen/OnboardingModels.swift @@ -79,7 +79,7 @@ struct OnboardingViewState: BindableState { message: ElementL10n.ftueAuthCarouselEncryptedBody, image: Asset.Images.onboardingScreenPage3), OnboardingPageContent(title: page4Title.tinting("."), - message: ElementL10n.ftueAuthCarouselWorkplaceBody(ElementInfoPlist.cfBundleDisplayName), + message: ElementL10n.ftueAuthCarouselWorkplaceBody(InfoPlistReader.target.bundleDisplayName), image: Asset.Images.onboardingScreenPage4) ] bindings = OnboardingBindings() diff --git a/ElementX/Sources/Screens/Other/InviteFriendsCoordinator.swift b/ElementX/Sources/Screens/Other/InviteFriendsCoordinator.swift index 5ddefc23c..ad15d18b2 100644 --- a/ElementX/Sources/Screens/Other/InviteFriendsCoordinator.swift +++ b/ElementX/Sources/Screens/Other/InviteFriendsCoordinator.swift @@ -23,7 +23,7 @@ struct InviteFriendsCoordinator: CoordinatorProtocol { guard let permalink = try? PermalinkBuilder.permalinkTo(userIdentifier: userId).absoluteString else { return AnyView(EmptyView()) } - let shareText = ElementL10n.inviteFriendsText(ElementInfoPlist.cfBundleDisplayName, permalink) + let shareText = ElementL10n.inviteFriendsText(InfoPlistReader.target.bundleDisplayName, permalink) return AnyView(UIActivityViewControllerWrapper(activityItems: [shareText]) .presentationDetents([.medium]) diff --git a/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift b/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift index 3875fbae7..4d87d1b09 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift @@ -36,8 +36,8 @@ struct MessageComposer: View { focused: $focused, maxHeight: 300, onEnterKeyHandler: { - sendAction() - }) + sendAction() + }) Button { sendAction() diff --git a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift index 5bbba4d52..0fdb92ea2 100644 --- a/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/View/SettingsScreen.swift @@ -64,7 +64,7 @@ struct SettingsScreen: View { } private var versionText: some View { - Text(ElementL10n.settingsVersion + ": " + ElementInfoPlist.cfBundleShortVersionString + " (" + ElementInfoPlist.cfBundleVersion + ")") + Text(ElementL10n.settingsVersion + ": " + InfoPlistReader.target.bundleShortVersionString + " (" + InfoPlistReader.target.bundleVersion + ")") } private var backgroundColor: Color { diff --git a/ElementX/Sources/Services/Analytics/Analytics.swift b/ElementX/Sources/Services/Analytics/Analytics.swift index ca045c6a3..65af5d45e 100644 --- a/ElementX/Sources/Services/Analytics/Analytics.swift +++ b/ElementX/Sources/Services/Analytics/Analytics.swift @@ -92,7 +92,7 @@ class Analytics { // Catch and log crashes // MXLogger.logCrashes(true) -// MXLogger.setBuildVersion(ElementInfoPlist.cfBundleShortVersionString) +// MXLogger.setBuildVersion(Bundle.bundleShortVersionString) } /// Use the analytics settings from the supplied user session to configure analytics. diff --git a/ElementX/Sources/Services/BugReport/BugReportService.swift b/ElementX/Sources/Services/BugReport/BugReportService.swift index 19bff588f..2c098c4f3 100644 --- a/ElementX/Sources/Services/BugReport/BugReportService.swift +++ b/ElementX/Sources/Services/BugReport/BugReportService.swift @@ -61,7 +61,7 @@ class BugReportService: BugReportServiceProtocol { // also enable logging crashes, to send them with bug reports MXLogger.logCrashes(true) // set build version for logger - MXLogger.buildVersion = ElementInfoPlist.cfBundleShortVersionString + MXLogger.buildVersion = InfoPlistReader.target.bundleShortVersionString } // MARK: - BugReportServiceProtocol @@ -151,8 +151,8 @@ class BugReportService: BugReportServiceProtocol { return [ MultipartFormData(key: "user_agent", type: .text(value: "iOS")), MultipartFormData(key: "app", type: .text(value: applicationId)), - MultipartFormData(key: "version", type: .text(value: ElementInfoPlist.cfBundleShortVersionString)), - MultipartFormData(key: "build", type: .text(value: ElementInfoPlist.cfBundleVersion)), + MultipartFormData(key: "version", type: .text(value: InfoPlistReader.target.bundleShortVersionString)), + MultipartFormData(key: "build", type: .text(value: InfoPlistReader.target.bundleVersion)), MultipartFormData(key: "os", type: .text(value: os)), MultipartFormData(key: "resolved_language", type: .text(value: Bundle.preferredLanguages[0])), MultipartFormData(key: "user_language", type: .text(value: Bundle.elementLanguage ?? "null")), diff --git a/ElementX/Sources/Services/Cache/FileCache.swift b/ElementX/Sources/Services/Cache/FileCache.swift index e7be4d5d6..c76543457 100644 --- a/ElementX/Sources/Services/Cache/FileCache.swift +++ b/ElementX/Sources/Services/Cache/FileCache.swift @@ -32,11 +32,11 @@ class FileCache { private let fileManager = FileManager.default private let folder: URL - /// Default instance. Uses `FileCache` as the folder name. - static let `default` = FileCache(folderName: "FileCache") + /// Default instance. Uses `Files` as the folder name. + static let `default` = FileCache(folderName: "Files") init(folderName: String) { - folder = fileManager.temporaryDirectory.appending(path: folderName, directoryHint: .isDirectory) + folder = URL.cacheBaseDirectory.appending(path: folderName, directoryHint: .isDirectory) } // MARK: Private @@ -44,21 +44,6 @@ class FileCache { private func filePath(forKey key: String, fileExtension: String) -> URL { folder.appending(path: key, directoryHint: .notDirectory).appendingPathExtension(fileExtension) } - - private func folderExists() -> Bool { - var isDirectory: ObjCBool = false - guard fileManager.fileExists(atPath: folder.path(), isDirectory: &isDirectory) else { - return false - } - return isDirectory.boolValue - } - - private func createFolderIfNeeded() throws { - guard !folderExists() else { - return - } - try fileManager.createDirectory(at: folder, withIntermediateDirectories: true) - } } // MARK: - FileCacheProtocol @@ -70,7 +55,7 @@ extension FileCache: FileCacheProtocol { } func store(_ data: Data, with fileExtension: String, forKey key: String) throws -> URL { - try createFolderIfNeeded() + try fileManager.createDirectoryIfNeeded(at: folder) let url = filePath(forKey: key, fileExtension: fileExtension) try data.write(to: url) return url @@ -81,7 +66,7 @@ extension FileCache: FileCacheProtocol { } func removeAll() throws { - guard folderExists() else { + guard fileManager.directoryExists(at: folder) else { return } try fileManager.removeItem(at: folder) diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index b2b7aeab0..9128829fe 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -52,6 +52,7 @@ class ClientProxy: ClientProxyProtocol { private let client: ClientProtocol private let backgroundTaskService: BackgroundTaskServiceProtocol private var sessionVerificationControllerProxy: SessionVerificationControllerProxy? + private let mediaProxy: MediaProxyProtocol private let clientQueue: DispatchQueue private var slidingSyncObserverToken: StoppableSpawn? @@ -75,6 +76,8 @@ class ClientProxy: ClientProxyProtocol { self.backgroundTaskService = backgroundTaskService clientQueue = .init(label: "ClientProxyQueue", attributes: .concurrent) + mediaProxy = MediaProxy(client: client, + clientQueue: clientQueue) await Task.dispatch(on: clientQueue) { do { @@ -204,25 +207,7 @@ class ClientProxy: ClientProxyProtocol { .failure(.failedSettingAccountData) } } - - func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource { - MatrixRustSDK.mediaSourceFromUrl(url: urlString) - } - - func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data { - try await Task.dispatch(on: clientQueue) { - let bytes = try self.client.getMediaContent(source: source) - return Data(bytes: bytes, count: bytes.count) - } - } - - func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data { - try await Task.dispatch(on: clientQueue) { - let bytes = try self.client.getMediaThumbnail(source: source, width: UInt64(width), height: UInt64(height)) - return Data(bytes: bytes, count: bytes.count) - } - } - + func sessionVerificationControllerProxy() async -> Result { await Task.dispatch(on: clientQueue) { do { @@ -243,6 +228,32 @@ class ClientProxy: ClientProxyProtocol { } } } + + // swiftlint:disable:next function_parameter_count + func setPusher(pushkey: String, + kind: PusherKind?, + appId: String, + appDisplayName: String, + deviceDisplayName: String, + profileTag: String?, + lang: String, + url: String?, + format: PushFormat?, + defaultPayload: [AnyHashable: Any]?) async throws { +// let defaultPayloadString = jsonString(from: defaultPayload) +// try await Task.dispatch(on: .global()) { +// try self.client.setPusher(pushkey: pushkey, +// kind: kind?.rustValue, +// appId: appId, +// appDisplayName: appDisplayName, +// deviceDisplayName: deviceDisplayName, +// profileTag: profileTag, +// lang: lang, +// url: url, +// format: format?.rustValue, +// defaultPayload: defaultPayloadString) +// } + } // MARK: Private @@ -271,4 +282,29 @@ class ClientProxy: ClientProxyProtocol { callbacks.send(.receivedSyncUpdate) } + + /// Convenience method to get the json string of an Encodable + private func jsonString(from dictionary: [AnyHashable: Any]?) -> String? { + guard let dictionary, + let data = try? JSONSerialization.data(withJSONObject: dictionary, + options: [.fragmentsAllowed]) else { + return nil + } + + return String(data: data, encoding: .utf8) + } +} + +extension ClientProxy: MediaProxyProtocol { + func mediaSourceForURLString(_ urlString: String) -> MediaSourceProxy { + mediaProxy.mediaSourceForURLString(urlString) + } + + func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data { + try await mediaProxy.loadMediaContentForSource(source) + } + + func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data { + try await mediaProxy.loadMediaThumbnailForSource(source, width: width, height: height) + } } diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 9ddbc8336..a765d7965 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -33,7 +33,32 @@ enum ClientProxyError: Error { case failedLoadingMedia } -protocol ClientProxyProtocol { +enum PusherKind { + case http + case email + +// var rustValue: MatrixRustSDK.PusherKind { +// switch self { +// case .http: +// return .http +// case .email: +// return .email +// } +// } +} + +enum PushFormat { + case eventIdOnly + +// var rustValue: MatrixRustSDK.PushFormat { +// switch self { +// case .eventIdOnly: +// return .eventIdOnly +// } +// } +} + +protocol ClientProxyProtocol: MediaProxyProtocol { var callbacks: PassthroughSubject { get } var userIdentifier: String { get } @@ -62,13 +87,19 @@ protocol ClientProxyProtocol { func setAccountData(content: Content, type: String) async -> Result - func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource - - func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data - - func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data - func sessionVerificationControllerProxy() async -> Result func logout() async + + // swiftlint:disable:next function_parameter_count + func setPusher(pushkey: String, + kind: PusherKind?, + appId: String, + appDisplayName: String, + deviceDisplayName: String, + profileTag: String?, + lang: String, + url: String?, + format: PushFormat?, + defaultPayload: [AnyHashable: Any]?) async throws } diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index 3ca2f909a..b961d9deb 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -53,15 +53,15 @@ struct MockClientProxy: ClientProxyProtocol { .failure(.failedSettingAccountData) } - func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource { - MatrixRustSDK.mediaSourceFromUrl(url: urlString) + func mediaSourceForURLString(_ urlString: String) -> MediaSourceProxy { + .init(urlString: urlString) } - func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data { + func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data { throw ClientProxyError.failedLoadingMedia } - func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data { + func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data { throw ClientProxyError.failedLoadingMedia } @@ -72,4 +72,18 @@ struct MockClientProxy: ClientProxyProtocol { func logout() async { // no-op } + + // swiftlint:disable:next function_parameter_count + func setPusher(pushkey: String, + kind: PusherKind?, + appId: String, + appDisplayName: String, + deviceDisplayName: String, + profileTag: String?, + lang: String, + url: String?, + format: PushFormat?, + defaultPayload: [AnyHashable: Any]?) async throws { + // no-op + } } diff --git a/ElementX/Sources/Services/UserSession/KeychainController.swift b/ElementX/Sources/Services/Keychain/KeychainController.swift similarity index 89% rename from ElementX/Sources/Services/UserSession/KeychainController.swift rename to ElementX/Sources/Services/Keychain/KeychainController.swift index e6e3d72e1..56aa3c713 100644 --- a/ElementX/Sources/Services/UserSession/KeychainController.swift +++ b/ElementX/Sources/Services/Keychain/KeychainController.swift @@ -17,13 +17,24 @@ import Foundation import KeychainAccess +enum KeychainControllerService: String { + case sessions + case tests + + var identifier: String { + InfoPlistReader.target.baseBundleIdentifier + "." + rawValue + } +} + class KeychainController: KeychainControllerProtocol { private let keychain: Keychain - - init(identifier: String) { - keychain = Keychain(service: identifier) + + init(service: KeychainControllerService, + accessGroup: String) { + keychain = Keychain(service: service.identifier, + accessGroup: accessGroup) } - + func setRestorationToken(_ restorationToken: RestorationToken, forUsername username: String) { do { let tokenData = try JSONEncoder().encode(restorationToken) @@ -32,7 +43,7 @@ class KeychainController: KeychainControllerProtocol { MXLog.error("Failed storing user restore token with error: \(error)") } } - + func restorationTokenForUsername(_ username: String) -> RestorationToken? { do { guard let tokenData = try keychain.getData(username) else { @@ -49,24 +60,24 @@ class KeychainController: KeychainControllerProtocol { homeserverUrl: legacyRestorationToken.homeURL, isSoftLogout: legacyRestorationToken.isSoftLogout ?? false)) } - + return try JSONDecoder().decode(RestorationToken.self, from: tokenData) } catch { MXLog.error("Failed retrieving user restore token") return nil } } - + func restorationTokens() -> [KeychainCredentials] { keychain.allKeys().compactMap { username in guard let restorationToken = restorationTokenForUsername(username) else { return nil } - + return KeychainCredentials(userID: username, restorationToken: restorationToken) } } - + func removeRestorationTokenForUsername(_ username: String) { do { try keychain.remove(username) @@ -74,7 +85,7 @@ class KeychainController: KeychainControllerProtocol { MXLog.error("Failed removing restore token with error: \(error)") } } - + func removeAllRestorationTokens() { do { try keychain.removeAll() diff --git a/ElementX/Sources/Services/UserSession/KeychainControllerProtocol.swift b/ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift similarity index 100% rename from ElementX/Sources/Services/UserSession/KeychainControllerProtocol.swift rename to ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift diff --git a/ElementX/Sources/Services/Media/MediaProvider.swift b/ElementX/Sources/Services/Media/MediaProvider.swift index d9c4577ff..7e69c6068 100644 --- a/ElementX/Sources/Services/Media/MediaProvider.swift +++ b/ElementX/Sources/Services/Media/MediaProvider.swift @@ -18,26 +18,26 @@ import Kingfisher import UIKit struct MediaProvider: MediaProviderProtocol { - private let clientProxy: ClientProxyProtocol + private let mediaProxy: MediaProxyProtocol private let imageCache: Kingfisher.ImageCache private let fileCache: FileCache - private let backgroundTaskService: BackgroundTaskServiceProtocol + private let backgroundTaskService: BackgroundTaskServiceProtocol? - init(clientProxy: ClientProxyProtocol, + init(mediaProxy: MediaProxyProtocol, imageCache: Kingfisher.ImageCache, fileCache: FileCache, - backgroundTaskService: BackgroundTaskServiceProtocol) { - self.clientProxy = clientProxy + backgroundTaskService: BackgroundTaskServiceProtocol?) { + self.mediaProxy = mediaProxy self.imageCache = imageCache self.fileCache = fileCache self.backgroundTaskService = backgroundTaskService } - func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage? { + func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? { guard let source else { return nil } - let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), avatarSize: avatarSize) + let cacheKey = cacheKeyForURLString(source.url, avatarSize: avatarSize) return imageCache.retrieveImageInMemoryCache(forKey: cacheKey, options: nil) } @@ -46,19 +46,19 @@ struct MediaProvider: MediaProviderProtocol { return nil } - return imageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), avatarSize: avatarSize) + return imageFromSource(.init(urlString: urlString), avatarSize: avatarSize) } func loadImageFromURLString(_ urlString: String, avatarSize: AvatarSize?) async -> Result { - await loadImageFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), avatarSize: avatarSize) + await loadImageFromSource(.init(urlString: urlString), avatarSize: avatarSize) } - func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result { + func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result { if let image = imageFromSource(source, avatarSize: avatarSize) { return .success(image) } - let loadImageBgTask = await backgroundTaskService.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)") + let loadImageBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)") defer { loadImageBgTask?.stop() } @@ -73,9 +73,9 @@ struct MediaProvider: MediaProviderProtocol { do { let imageData: Data if let avatarSize { - imageData = try await clientProxy.loadMediaThumbnailForSource(source.underlyingSource, width: UInt(avatarSize.scaledValue), height: UInt(avatarSize.scaledValue)) + imageData = try await mediaProxy.loadMediaThumbnailForSource(source, width: UInt(avatarSize.scaledValue), height: UInt(avatarSize.scaledValue)) } else { - imageData = try await clientProxy.loadMediaContentForSource(source.underlyingSource) + imageData = try await mediaProxy.loadMediaContentForSource(source) } guard let image = UIImage(data: imageData) else { @@ -92,20 +92,20 @@ struct MediaProvider: MediaProviderProtocol { } } - func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL? { + func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL? { guard let source else { return nil } - let cacheKey = fileCacheKeyForURLString(source.underlyingSource.url()) + let cacheKey = fileCacheKeyForURLString(source.url) return fileCache.file(forKey: cacheKey, fileExtension: fileExtension) } - @discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result { + @discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result { if let url = fileFromSource(source, fileExtension: fileExtension) { return .success(url) } - let loadFileBgTask = await backgroundTaskService.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)") + let loadFileBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadFile: \(source.url.hashValue)") defer { loadFileBgTask?.stop() } @@ -113,7 +113,7 @@ struct MediaProvider: MediaProviderProtocol { let cacheKey = fileCacheKeyForURLString(source.url) do { - let data = try await clientProxy.loadMediaContentForSource(source.underlyingSource) + let data = try await mediaProxy.loadMediaContentForSource(source) let url = try fileCache.store(data, with: fileExtension, forKey: cacheKey) return .success(url) @@ -128,12 +128,12 @@ struct MediaProvider: MediaProviderProtocol { return nil } - return fileFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), + return fileFromSource(MediaSourceProxy(urlString: urlString), fileExtension: fileExtension) } func loadFileFromURLString(_ urlString: String, fileExtension: String) async -> Result { - await loadFileFromSource(MediaSource(source: clientProxy.mediaSourceForURLString(urlString)), + await loadFileFromSource(MediaSourceProxy(urlString: urlString), fileExtension: fileExtension) } diff --git a/ElementX/Sources/Services/Media/MediaProviderProtocol.swift b/ElementX/Sources/Services/Media/MediaProviderProtocol.swift index 52768365e..1acd3db71 100644 --- a/ElementX/Sources/Services/Media/MediaProviderProtocol.swift +++ b/ElementX/Sources/Services/Media/MediaProviderProtocol.swift @@ -24,17 +24,17 @@ enum MediaProviderError: Error { } protocol MediaProviderProtocol { - func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage? + func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? - @discardableResult func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result + @discardableResult func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result func imageFromURLString(_ urlString: String?, avatarSize: AvatarSize?) -> UIImage? @discardableResult func loadImageFromURLString(_ urlString: String, avatarSize: AvatarSize?) async -> Result - func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL? + func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL? - @discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result + @discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result func fileFromURLString(_ urlString: String?, fileExtension: String) -> URL? @@ -42,11 +42,11 @@ protocol MediaProviderProtocol { } extension MediaProviderProtocol { - func imageFromSource(_ source: MediaSource?) -> UIImage? { + func imageFromSource(_ source: MediaSourceProxy?) -> UIImage? { imageFromSource(source, avatarSize: nil) } - @discardableResult func loadImageFromSource(_ source: MediaSource) async -> Result { + @discardableResult func loadImageFromSource(_ source: MediaSourceProxy) async -> Result { await loadImageFromSource(source, avatarSize: nil) } diff --git a/ElementX/Sources/Services/Media/MediaProxy.swift b/ElementX/Sources/Services/Media/MediaProxy.swift new file mode 100644 index 000000000..a5e6caf49 --- /dev/null +++ b/ElementX/Sources/Services/Media/MediaProxy.swift @@ -0,0 +1,49 @@ +// +// Copyright 2022 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 Combine +import Foundation +import MatrixRustSDK +import UIKit + +class MediaProxy: MediaProxyProtocol { + private let client: ClientProtocol + private let clientQueue: DispatchQueue + + init(client: ClientProtocol, + clientQueue: DispatchQueue = .global()) { + self.client = client + self.clientQueue = clientQueue + } + + func mediaSourceForURLString(_ urlString: String) -> MediaSourceProxy { + .init(urlString: urlString) + } + + func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data { + try await Task.dispatch(on: clientQueue) { + let bytes = try self.client.getMediaContent(source: source.underlyingSource) + return Data(bytes: bytes, count: bytes.count) + } + } + + func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data { + try await Task.dispatch(on: clientQueue) { + let bytes = try self.client.getMediaThumbnail(source: source.underlyingSource, width: UInt64(width), height: UInt64(height)) + return Data(bytes: bytes, count: bytes.count) + } + } +} diff --git a/ElementX/Sources/Services/Media/MediaProxyProtocol.swift b/ElementX/Sources/Services/Media/MediaProxyProtocol.swift new file mode 100644 index 000000000..69e22ce05 --- /dev/null +++ b/ElementX/Sources/Services/Media/MediaProxyProtocol.swift @@ -0,0 +1,25 @@ +// +// Copyright 2022 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 + +protocol MediaProxyProtocol { + func mediaSourceForURLString(_ urlString: String) -> MediaSourceProxy + + func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data + + func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data +} diff --git a/ElementX/Sources/Services/Media/MediaSource.swift b/ElementX/Sources/Services/Media/MediaSourceProxy.swift similarity index 70% rename from ElementX/Sources/Services/Media/MediaSource.swift rename to ElementX/Sources/Services/Media/MediaSourceProxy.swift index 7220433e7..2f4c60981 100644 --- a/ElementX/Sources/Services/Media/MediaSource.swift +++ b/ElementX/Sources/Services/Media/MediaSourceProxy.swift @@ -17,24 +17,24 @@ import Foundation import MatrixRustSDK -struct MediaSource: Equatable { - let underlyingSource: MatrixRustSDK.MediaSource +struct MediaSourceProxy: Equatable { + let underlyingSource: MediaSource + + init(source: MediaSource) { + underlyingSource = source + } + + init(urlString: String) { + underlyingSource = mediaSourceFromUrl(url: urlString) + } var url: String { underlyingSource.url() } - init(source: MatrixRustSDK.MediaSource) { - underlyingSource = source - } - - init(urlString: String) { - underlyingSource = MatrixRustSDK.mediaSourceFromUrl(url: urlString) - } - // MARK: - Equatable - static func == (lhs: MediaSource, rhs: MediaSource) -> Bool { - lhs.underlyingSource.url() == rhs.underlyingSource.url() + static func == (lhs: MediaSourceProxy, rhs: MediaSourceProxy) -> Bool { + lhs.url == rhs.url } } diff --git a/ElementX/Sources/Services/Media/MockMediaProvider.swift b/ElementX/Sources/Services/Media/MockMediaProvider.swift index c92f6d85c..e6eaf2614 100644 --- a/ElementX/Sources/Services/Media/MockMediaProvider.swift +++ b/ElementX/Sources/Services/Media/MockMediaProvider.swift @@ -18,11 +18,11 @@ import Foundation import UIKit struct MockMediaProvider: MediaProviderProtocol { - func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage? { + func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? { nil } - func loadImageFromSource(_ source: MediaSource, avatarSize: AvatarSize?) async -> Result { + func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result { .failure(.failedRetrievingImage) } @@ -50,11 +50,11 @@ struct MockMediaProvider: MediaProviderProtocol { return .success(image) } - func fileFromSource(_ source: MediaSource?, fileExtension: String) -> URL? { + func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL? { nil } - @discardableResult func loadFileFromSource(_ source: MediaSource, fileExtension: String) async -> Result { + @discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result { .failure(.failedRetrievingFile) } diff --git a/ElementX/Sources/Services/Notification/Manager/MockNotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/MockNotificationManager.swift new file mode 100644 index 000000000..fc35c1f7f --- /dev/null +++ b/ElementX/Sources/Services/Notification/Manager/MockNotificationManager.swift @@ -0,0 +1,37 @@ +// +// Copyright 2022 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 + +class MockNotificationManager: NotificationManagerProtocol { + // MARK: NotificationManagerProtocol + + var isAvailable: Bool { + false + } + + weak var delegate: NotificationManagerDelegate? + + func start() { + delegate?.authorizationStatusUpdated(self, granted: false) + } + + func register(with deviceToken: Data) { } + + func registrationFailed(with error: Error) { } + + func showLocalNotification(with title: String, subtitle: String?) { } +} diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift new file mode 100644 index 000000000..668117b08 --- /dev/null +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -0,0 +1,166 @@ +// +// Copyright 2022 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 +import UserNotifications + +class NotificationManager: NSObject, NotificationManagerProtocol { + private let notificationCenter = UNUserNotificationCenter.current() + private let clientProxy: ClientProxyProtocol + + init(clientProxy: ClientProxyProtocol) { + self.clientProxy = clientProxy + super.init() + } + + // MARK: NotificationManagerProtocol + + weak var delegate: NotificationManagerDelegate? + + var isAvailable: Bool { + true + } + + func start() { + let replyAction = UNTextInputNotificationAction(identifier: NotificationConstants.Action.inlineReply, + title: ElementL10n.actionQuickReply, + options: []) + let replyCategory = UNNotificationCategory(identifier: NotificationConstants.Category.reply, + actions: [replyAction], + intentIdentifiers: [], + options: []) + notificationCenter.setNotificationCategories([replyCategory]) + notificationCenter.delegate = self + Task { + do { + let granted = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) + MXLog.debug("[NotificationManager] permission granted: \(granted)") + + await MainActor.run { + delegate?.authorizationStatusUpdated(self, granted: granted) + } + } catch { + MXLog.debug("[NotificationManager] request authorization failed: \(error)") + } + } + } + + func register(with deviceToken: Data) { + setPusher(with: deviceToken, clientProxy: clientProxy) + } + + func registrationFailed(with error: Error) { } + + func showLocalNotification(with title: String, subtitle: String?) { + let content = UNMutableNotificationContent() + content.title = title + if let subtitle { + content.subtitle = subtitle + } + let request = UNNotificationRequest(identifier: ProcessInfo.processInfo.globallyUniqueString, + content: content, + trigger: nil) + Task { + do { + try await notificationCenter.add(request) + MXLog.debug("[NotificationManager] show local notification succeeded") + } catch { + MXLog.debug("[NotificationManager] show local notification failed: \(error)") + } + } + } + + private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) { + Task { + do { + try await clientProxy.setPusher(pushkey: deviceToken.base64EncodedString(), + kind: .http, + appId: BuildSettings.pusherAppId, + appDisplayName: "\(InfoPlistReader.target.bundleDisplayName) (iOS)", + deviceDisplayName: UIDevice.current.name, + profileTag: pusherProfileTag(), + lang: Bundle.preferredLanguages.first ?? "en", + url: BuildSettings.pushGatewayBaseURL.absoluteString, + format: .eventIdOnly, + defaultPayload: [ + "aps": [ + "mutable-content": 1, + "alert": [ + "loc-key": "Notification", + "loc-args": [] + ] + ] + ]) + MXLog.debug("[NotificationManager] set pusher succeeded") + } catch { + MXLog.debug("[NotificationManager] set pusher failed: \(error)") + } + } + } + + private func pusherProfileTag() -> String { + if let currentTag = ElementSettings.shared.pusherProfileTag { + return currentTag + } + let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + let newTag = (0..<16).map { _ in + let offset = Int.random(in: 0.. UNNotificationPresentationOptions { + guard ElementSettings.shared.enableInAppNotifications else { + return [] + } + guard let delegate else { + return [.badge, .sound, .list, .banner] + } + + guard delegate.shouldDisplayInAppNotification(self, content: notification.request.content) else { + return [] + } + + return [.badge, .sound, .list, .banner] + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse) async { + switch response.actionIdentifier { + case NotificationConstants.Action.inlineReply: + guard let response = response as? UNTextInputNotificationResponse else { + return + } + await delegate?.handleInlineReply(self, + content: response.notification.request.content, + replyText: response.userText) + case UNNotificationDefaultActionIdentifier: + await delegate?.notificationTapped(self, + content: response.notification.request.content) + default: + break + } + } +} diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift new file mode 100644 index 000000000..452367a74 --- /dev/null +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift @@ -0,0 +1,42 @@ +// +// Copyright 2022 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 UserNotifications + +protocol NotificationManagerDelegate: AnyObject { + func authorizationStatusUpdated(_ service: NotificationManagerProtocol, + granted: Bool) + func shouldDisplayInAppNotification(_ service: NotificationManagerProtocol, + content: UNNotificationContent) -> Bool + func notificationTapped(_ service: NotificationManagerProtocol, + content: UNNotificationContent) async + func handleInlineReply(_ service: NotificationManagerProtocol, + content: UNNotificationContent, + replyText: String) async +} + +// MARK: - NotificationManagerProtocol + +protocol NotificationManagerProtocol { + var isAvailable: Bool { get } + var delegate: NotificationManagerDelegate? { get set } + + func start() + func register(with deviceToken: Data) + func registrationFailed(with error: Error) + func showLocalNotification(with title: String, subtitle: String?) +} diff --git a/ElementX/Sources/Services/Notification/NotificationConstants.swift b/ElementX/Sources/Services/Notification/NotificationConstants.swift new file mode 100644 index 000000000..018cdc5f5 --- /dev/null +++ b/ElementX/Sources/Services/Notification/NotificationConstants.swift @@ -0,0 +1,34 @@ +// +// Copyright 2022 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 + +enum NotificationConstants { + enum UserInfoKey { + static let roomIdentifier = "room_id" + static let eventIdentifier = "event_id" + static let unreadCount = "unread_count" + } + + enum Category { + static let discard = "discard" + static let reply = "reply" + } + + enum Action { + static let inlineReply = "inline-reply" + } +} diff --git a/ElementX/Sources/Services/UserSession/FileManager.swift b/ElementX/Sources/Services/Notification/Proxy/MockNotificationServiceProxy.swift similarity index 74% rename from ElementX/Sources/Services/UserSession/FileManager.swift rename to ElementX/Sources/Services/Notification/Proxy/MockNotificationServiceProxy.swift index 0d009e802..2b13fcb4d 100644 --- a/ElementX/Sources/Services/UserSession/FileManager.swift +++ b/ElementX/Sources/Services/Notification/Proxy/MockNotificationServiceProxy.swift @@ -16,9 +16,8 @@ import Foundation -extension FileManager { - /// The URL of the primary app group container. - @objc var appGroupContainerURL: URL? { - containerURL(forSecurityApplicationGroupIdentifier: ElementInfoPlist.appGroupIdentifier) +class MockNotificationServiceProxy: NotificationServiceProxyProtocol { + func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy? { + nil } } diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift new file mode 100644 index 000000000..740655139 --- /dev/null +++ b/ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift @@ -0,0 +1,73 @@ +// +// Copyright 2022 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 MatrixRustSDK + +struct NotificationItemProxy { +// let notificationItem: NotificationItem +// +// init(notificationItem: NotificationItem) { +// self.notificationItem = notificationItem +// } +// +// var timelineItemProxy: TimelineItemProxy { +// .init(item: notificationItem.item) +// } +// +// var title: String { +// notificationItem.title +// } +// +// var subtitle: String? { +// notificationItem.subtitle +// } +// +// var isNoisy: Bool { +// notificationItem.isNoisy +// } +// +// var avatarUrl: String? { +// notificationItem.avatarUrl +// } +// +// var avatarMediaSource: MediaSourceProxy? { +// guard let avatarUrl else { +// return nil +// } +// return .init(urlString: avatarUrl) +// } + + var title: String { + "Title" + } + + var subtitle: String? { + nil + } + + var isNoisy: Bool { + true + } + + var avatarUrl: String? { + nil + } + + var avatarMediaSource: MediaSourceProxy? { + nil + } +} diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift new file mode 100644 index 000000000..619acaa29 --- /dev/null +++ b/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift @@ -0,0 +1,37 @@ +// +// Copyright 2022 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 MatrixRustSDK + +class NotificationServiceProxy: NotificationServiceProxyProtocol { +// private let service: NotificationServiceProtocol + + init(basePath: String, + userId: String) { +// service = NotificationService(basePath: basePath, userId: userId) + } + + func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy? { + nil +// try await Task.dispatch(on: .global()) { +// guard let item = try self.service.getNotificationItem(roomId: roomId, eventId: eventId) else { +// return nil +// } +// return .init(notificationItem: item) +// } + } +} diff --git a/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxyProtocol.swift b/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxyProtocol.swift new file mode 100644 index 000000000..b5bda3a9d --- /dev/null +++ b/ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxyProtocol.swift @@ -0,0 +1,21 @@ +// +// Copyright 2022 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 + +protocol NotificationServiceProxyProtocol { + func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxy? +} diff --git a/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift index b7a58e7a2..45ae09551 100644 --- a/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimeLineItemContent/MessageTimelineItem.swift @@ -113,8 +113,8 @@ extension MatrixRustSDK.ImageMessageContent: MessageContentProtocol { } /// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.image`. extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent { - var source: MediaSource { - MediaSource(source: content.source) + var source: MediaSourceProxy { + .init(source: content.source) } var width: CGFloat? { @@ -134,15 +134,15 @@ extension MatrixRustSDK.VideoMessageContent: MessageContentProtocol { } /// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.video`. extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent { - var source: MediaSource { - MediaSource(source: content.source) + var source: MediaSourceProxy { + .init(source: content.source) } - var thumbnailSource: MediaSource? { + var thumbnailSource: MediaSourceProxy? { guard let src = content.info?.thumbnailSource else { return nil } - return MediaSource(source: src) + return .init(source: src) } var duration: UInt64 { @@ -166,14 +166,14 @@ extension MatrixRustSDK.FileMessageContent: MessageContentProtocol { } /// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.file`. extension MessageTimelineItem where Content == MatrixRustSDK.FileMessageContent { - var source: MediaSource { - MediaSource(source: content.source) + var source: MediaSourceProxy { + .init(source: content.source) } - var thumbnailSource: MediaSource? { + var thumbnailSource: MediaSourceProxy? { guard let src = content.info?.thumbnailSource else { return nil } - return MediaSource(source: src) + return .init(source: src) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/FileRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/FileRoomTimelineItem.swift index e5e4f74d5..35f9c773e 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/FileRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/FileRoomTimelineItem.swift @@ -29,8 +29,8 @@ struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equat var senderDisplayName: String? var senderAvatar: UIImage? - let source: MediaSource? - let thumbnailSource: MediaSource? + let source: MediaSourceProxy? + let thumbnailSource: MediaSourceProxy? var cachedFileURL: URL? var properties = RoomTimelineItemProperties() diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift index 3353f220e..38242b75c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/ImageRoomTimelineItem.swift @@ -29,7 +29,7 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa var senderDisplayName: String? var senderAvatar: UIImage? - let source: MediaSource? + let source: MediaSourceProxy? var image: UIImage? var width: CGFloat? diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/VideoRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/VideoRoomTimelineItem.swift index 23ed01f6e..4577e768d 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/VideoRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/VideoRoomTimelineItem.swift @@ -30,8 +30,8 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equa var senderAvatar: UIImage? let duration: UInt64 - let source: MediaSource? - let thumbnailSource: MediaSource? + let source: MediaSourceProxy? + let thumbnailSource: MediaSourceProxy? var image: UIImage? var cachedVideoURL: URL? diff --git a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift index c60e31643..ec3dc614e 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift @@ -42,7 +42,17 @@ class UserSessionFlowCoordinator: CoordinatorProtocol { func start() { stateMachine.processEvent(.start) } - + + func stop() { } + + func isDisplayingRoomScreen(withRoomId roomId: String) -> Bool { + stateMachine.isDisplayingRoomScreen(withRoomId: roomId) + } + + func tryDisplayingRoomScreen(roomId: String) { + stateMachine.processEvent(.showRoomScreen(roomId: roomId)) + } + // MARK: - Private // swiftlint:disable:next cyclomatic_complexity diff --git a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift index c2e46f0de..bbea9f7be 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinatorStateMachine.swift @@ -150,4 +150,14 @@ class UserSessionFlowCoordinatorStateMachine { func addErrorHandler(_ handler: @escaping StateMachine.Handler) { stateMachine.addErrorHandler(handler: handler) } + + /// Flag indicating the machine is displaying room screen with given room identifier + func isDisplayingRoomScreen(withRoomId roomId: String) -> Bool { + switch stateMachine.state { + case .roomScreen(let displayedRoomId): + return roomId == displayedRoomId + default: + return false + } + } } diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index f7a865beb..a099ab509 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -26,26 +26,14 @@ class UserSessionStore: UserSessionStoreProtocol { var hasSessions: Bool { !keychainController.restorationTokens().isEmpty } /// The base directory where all session data is stored. - private(set) lazy var baseDirectory: URL = { - guard let appGroupContainerURL = FileManager.default.appGroupContainerURL else { - fatalError("Should always be able to retrieve the container directory") - } - - let url = appGroupContainerURL - .appendingPathComponent("Library", isDirectory: true) - .appendingPathComponent("Caches", isDirectory: true) - .appendingPathComponent("Sessions", isDirectory: true) - - try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) - - MXLog.debug("Setup base directory at: \(url)") - - return url - }() + let baseDirectory: URL - init(bundleIdentifier: String, backgroundTaskService: BackgroundTaskServiceProtocol) { - keychainController = KeychainController(identifier: bundleIdentifier) + init(backgroundTaskService: BackgroundTaskServiceProtocol) { + keychainController = KeychainController(service: .sessions, + accessGroup: InfoPlistReader.target.appGroupIdentifier) self.backgroundTaskService = backgroundTaskService + baseDirectory = .sessionsBaseDirectory + MXLog.debug("Setup base directory at: \(baseDirectory)") } func restoreUserSession() async -> Result { @@ -58,7 +46,7 @@ class UserSessionStore: UserSessionStoreProtocol { switch await restorePreviousLogin(credentials) { case .success(let clientProxy): return .success(UserSession(clientProxy: clientProxy, - mediaProvider: MediaProvider(clientProxy: clientProxy, + mediaProvider: MediaProvider(mediaProxy: clientProxy, imageCache: .onlyInMemory, fileCache: .default, backgroundTaskService: backgroundTaskService))) @@ -77,7 +65,7 @@ class UserSessionStore: UserSessionStoreProtocol { switch await setupProxyForClient(client) { case .success(let clientProxy): return .success(UserSession(clientProxy: clientProxy, - mediaProvider: MediaProvider(clientProxy: clientProxy, + mediaProvider: MediaProvider(mediaProxy: clientProxy, imageCache: .onlyInMemory, fileCache: .default, backgroundTaskService: backgroundTaskService))) diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 6843165af..cf1ea69ba 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -17,8 +17,9 @@ import SwiftUI import UIKit -class UITestsAppCoordinator: CoordinatorProtocol { +class UITestsAppCoordinator: AppCoordinatorProtocol { private let navigationController: NavigationController + let notificationManager: NotificationManagerProtocol? = nil init() { navigationController = NavigationController() diff --git a/ElementX/SupportingFiles/ElementX.entitlements b/ElementX/SupportingFiles/ElementX.entitlements index 81b37208f..641b4256d 100644 --- a/ElementX/SupportingFiles/ElementX.entitlements +++ b/ElementX/SupportingFiles/ElementX.entitlements @@ -2,6 +2,10 @@ + aps-environment + development + com.apple.developer.usernotifications.communication + com.apple.security.app-sandbox com.apple.security.application-groups diff --git a/ElementX/SupportingFiles/Info.plist b/ElementX/SupportingFiles/Info.plist index 9aadd8a1a..02d3abf79 100644 --- a/ElementX/SupportingFiles/Info.plist +++ b/ElementX/SupportingFiles/Info.plist @@ -22,6 +22,10 @@ $(CURRENT_PROJECT_VERSION) ITSAppUsesNonExemptEncryption + NSUserActivityTypes + + INSendMessageIntent + UILaunchStoryboardName LaunchScreen UISupportedInterfaceOrientations @@ -33,5 +37,7 @@ appGroupIdentifier $(APP_GROUP_IDENTIFIER) + baseBundleIdentifier + $(BASE_BUNDLE_IDENTIFIER) diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 9b5c74d91..528f17c7e 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -46,15 +46,19 @@ targets: UIInterfaceOrientationLandscapeRight ] appGroupIdentifier: $(APP_GROUP_IDENTIFIER) + baseBundleIdentifier: $(BASE_BUNDLE_IDENTIFIER) ITSAppUsesNonExemptEncryption: false + NSUserActivityTypes: [ + INSendMessageIntent + ] settings: base: PRODUCT_NAME: ElementX PRODUCT_BUNDLE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER) - MARKETING_VERSION: 1.0.9 - CURRENT_PROJECT_VERSION: 1 - DEVELOPMENT_TEAM: 7J4U792NQT + MARKETING_VERSION: $(MARKETING_VERSION) + CURRENT_PROJECT_VERSION: $(CURRENT_PROJECT_VERSION) + DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM) CODE_SIGN_ENTITLEMENTS: ElementX/SupportingFiles/ElementX.entitlements SWIFT_OBJC_BRIDGING_HEADER: ElementX/SupportingFiles/ElementX-Bridging-Header.h SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h @@ -97,6 +101,7 @@ targets: fi dependencies: + - target: NSE - package: MatrixRustSDK - package: DesignKit - package: AnalyticsEvents diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift new file mode 100644 index 000000000..cf7a28be2 --- /dev/null +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -0,0 +1,165 @@ +// +// Copyright 2022 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 Intents +import MatrixRustSDK +import UserNotifications + +class NotificationServiceExtension: UNNotificationServiceExtension { + private lazy var keychainController = KeychainController(service: .sessions, + accessGroup: InfoPlistReader.target.appGroupIdentifier) + var handler: ((UNNotificationContent) -> Void)? + var modifiedContent: UNMutableNotificationContent? + + override init() { + // Use `en` as fallback language + Bundle.elementFallbackLanguage = "en" + + super.init() + } + + override func didReceive(_ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + guard !DataProtectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory), + let roomId = request.roomId, + let eventId = request.eventId, + let credentials = keychainController.restorationTokens().first else { + // We cannot process this notification, it might be due to one of these: + // - Device rebooted and locked + // - Not a Matrix notification + // - User is not signed in + return contentHandler(request.content) + } + + handler = contentHandler + modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent + + NSELogger.configure() + + NSELogger.logMemory(with: tag) + + MXLog.debug("\(tag) #########################################") + MXLog.debug("\(tag) Payload came: \(request.content.userInfo)") + + Task { + try await run(with: credentials, + roomId: roomId, + eventId: eventId) + } + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + MXLog.debug("\(tag) serviceExtensionTimeWillExpire") + notify() + } + + private func run(with credentials: KeychainCredentials, + roomId: String, + eventId: String) async throws { + MXLog.debug("\(tag) run with roomId: \(roomId), eventId: \(eventId)") + + let service = NotificationServiceProxy(basePath: URL.sessionsBaseDirectory.path, + userId: credentials.userID) + + guard let itemProxy = try await service.notificationItem(roomId: roomId, + eventId: eventId) else { + MXLog.debug("\(tag) got no notification item") + + // Notification should be discarded + return discard() + } + + // First process without a media proxy. + // After this some properties of the notification should be set, like title, subtitle, sound etc. + guard let firstContent = try await itemProxy.process(with: roomId, + mediaProvider: nil) else { + MXLog.debug("\(tag) not even first content") + + // Notification should be discarded + return discard() + } + + // After the first processing, update the modified content + modifiedContent = firstContent + + guard itemProxy.requiresMediaProvider else { + MXLog.debug("\(tag) no media needed") + + // We've processed the item and no media operations needed, so no need to go further + return notify() + } + + MXLog.debug("\(tag) process with media") + + // There is some media to load, process it again + if let latestContent = try await itemProxy.process(with: roomId, + mediaProvider: try createMediaProvider(with: credentials)) { + // Processing finished, hopefully with some media + modifiedContent = latestContent + return notify() + } else { + // This is not very likely, as it should discard the notification sooner + return discard() + } + } + + private func createMediaProvider(with credentials: KeychainCredentials) throws -> MediaProviderProtocol { + let builder = ClientBuilder() + .basePath(path: URL.sessionsBaseDirectory.path) + .username(username: credentials.userID) + + let client = try builder.build() + try client.restoreSession(session: credentials.restorationToken.session) + + MXLog.debug("\(tag) creating media provider") + + return MediaProvider(mediaProxy: MediaProxy(client: client), + imageCache: .onlyOnDisk, + fileCache: .default, + backgroundTaskService: nil) + } + + private func notify() { + MXLog.debug("\(tag) notify") + + guard let modifiedContent else { + MXLog.debug("\(tag) notify: no modified content") + return + } + handler?(modifiedContent) + handler = nil + self.modifiedContent = nil + } + + private func discard() { + MXLog.debug("\(tag) discard") + + handler?(UNMutableNotificationContent()) + handler = nil + modifiedContent = nil + } + + private var tag: String { + "[NSE][\(Unmanaged.passUnretained(self).toOpaque())][\(Unmanaged.passUnretained(Thread.current).toOpaque())]" + } + + deinit { + NSELogger.logMemory(with: tag) + MXLog.debug("\(tag) deinit") + } +} diff --git a/NSE/Sources/Other/DataProtectionManager.swift b/NSE/Sources/Other/DataProtectionManager.swift new file mode 100644 index 000000000..6a519aa0a --- /dev/null +++ b/NSE/Sources/Other/DataProtectionManager.swift @@ -0,0 +1,45 @@ +// +// Copyright 2022 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 + +final class DataProtectionManager { + /// Detects after reboot, before unlocked state. Does this by trying to write a file to the filesystem (to the Caches directory) and read it back. + /// - Parameter containerURL: Container url to write the file. + /// - Returns: true if the state detected + static func isDeviceLockedAfterReboot(containerURL: URL) -> Bool { + let dummyString = ProcessInfo.processInfo.globallyUniqueString + guard let dummyData = dummyString.data(using: .utf8) else { + return true + } + + do { + // add a unique filename + let url = containerURL.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString) + + try dummyData.write(to: url, options: .completeFileProtectionUntilFirstUserAuthentication) + let readData = try Data(contentsOf: url) + let readString = String(data: readData, encoding: .utf8) + try FileManager.default.removeItem(at: url) + if readString != dummyString { + return true + } + } catch { + return true + } + return false + } +} diff --git a/NSE/Sources/Other/NSELogger.swift b/NSE/Sources/Other/NSELogger.swift new file mode 100644 index 000000000..85e594f20 --- /dev/null +++ b/NSE/Sources/Other/NSELogger.swift @@ -0,0 +1,110 @@ +// +// Copyright 2022 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 MatrixRustSDK + +class NSELogger { + private static var isConfigured = false + + /// Memory formatter, uses exact 2 fraction digits and no grouping + private static var numberFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.alwaysShowsDecimalSeparator = true + formatter.decimalSeparator = "." + formatter.groupingSeparator = "" + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 2 + return formatter + } + + private static var formattedMemoryAvailable: String { + let freeBytes = os_proc_available_memory() + let freeMB = Double(freeBytes) / 1024 / 1024 + guard let formattedStr = numberFormatter.string(from: NSNumber(value: freeMB)) else { + return "" + } + return "\(formattedStr) MB" + } + + /// Details: https://developer.apple.com/forums/thread/105088 + /// - Returns: Current memory footprint + private static var memoryFootprint: Float? { + // The `TASK_VM_INFO_COUNT` and `TASK_VM_INFO_REV1_COUNT` macros are too + // complex for the Swift C importer, so we have to define them ourselves. + let TASK_VM_INFO_COUNT = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) + guard let offset = MemoryLayout.offset(of: \task_vm_info_data_t.min_address) else { + return nil + } + let TASK_VM_INFO_REV1_COUNT = mach_msg_type_number_t(offset / MemoryLayout.size) + var info = task_vm_info_data_t() + var count = TASK_VM_INFO_COUNT + let kr = withUnsafeMutablePointer(to: &info) { infoPtr in + infoPtr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr in + task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), intPtr, &count) + } + } + guard kr == KERN_SUCCESS, count >= TASK_VM_INFO_REV1_COUNT else { + return nil + } + + return Float(info.phys_footprint) + } + + /// Formatted memory footprint for debugging purposes + /// - Returns: Memory footprint in MBs as a readable string + public static var formattedMemoryFootprint: String { + let usedBytes = UInt64(memoryFootprint ?? 0) + let usedMB = Double(usedBytes) / 1024 / 1024 + guard let formattedStr = numberFormatter.string(from: NSNumber(value: usedMB)) else { + return "" + } + return "\(formattedStr) MB" + } + + static func configure() { + guard !isConfigured else { + return + } + isConfigured = true + + let configuration = MXLogConfiguration() + configuration.maxLogFilesCount = 10 + configuration.subLogName = "nse" + + #if DEBUG + // This exposes the full Rust side tracing subscriber filter for more flexibility. + // We can filter by level, crate and even file. See more details here: + // https://docs.rs/tracing-subscriber/0.2.7/tracing_subscriber/filter/struct.EnvFilter.html#examples + setupTracing(filter: "warn,hyper=warn,sled=warn,matrix_sdk_sled=warn") + configuration.logLevel = .debug + #else + setupTracing(filter: "info,hyper=warn,sled=warn,matrix_sdk_sled=warn") + configuration.logLevel = .info + #endif + + // Avoid redirecting NSLogs to files if we are attached to a debugger. + if isatty(STDERR_FILENO) == 0 { + configuration.redirectLogsToFiles = true + } + + MXLog.configure(configuration) + } + + static func logMemory(with tag: String) { + MXLog.debug("\(tag) Memory: footprint: \(formattedMemoryFootprint) - available: \(formattedMemoryAvailable)") + } +} diff --git a/NSE/Sources/Other/NotificationItemProxy+NSE.swift b/NSE/Sources/Other/NotificationItemProxy+NSE.swift new file mode 100644 index 000000000..60a0f8dba --- /dev/null +++ b/NSE/Sources/Other/NotificationItemProxy+NSE.swift @@ -0,0 +1,222 @@ +// +// Copyright 2022 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 MatrixRustSDK +import UserNotifications + +extension NotificationItemProxy { + var requiresMediaProvider: Bool { + false +// if avatarUrl != nil { +// return true +// } +// switch timelineItemProxy { +// case .event(let eventItem): +// guard eventItem.isMessage else { +// // To be handled in the future +// return false +// } +// guard let message = eventItem.content.asMessage() else { +// fatalError("Only handled messages") +// } +// switch message.msgtype() { +// case .image, .video: +// return true +// default: +// return false +// } +// case .virtual: +// return false +// case .other: +// return false +// } + } + + /// Process the receiver item proxy + /// - Parameters: + /// - roomId: Room identifier + /// - mediaProvider: Media provider to process also media. May be passed nil to ignore media operations. + /// - Returns: A notification content object if the notification should be displayed. Otherwise nil. + func process(with roomId: String, + mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? { + nil +// switch timelineItemProxy { +// case .event(let eventItem): +// guard eventItem.isMessage else { +// // To be handled in the future +// return nil +// } +// guard let message = eventItem.content.asMessage() else { +// fatalError("Item must be a message") +// } +// +// return try await process(message: message, +// senderId: eventItem.sender, +// roomId: roomId, +// mediaProvider: mediaProvider) +// case .virtual: +// return nil +// case .other: +// return nil +// } + } + + // MARK: - Private + + // MARK: Common + + private func process(message: Message, + senderId: String, + roomId: String, + mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? { + switch message.msgtype() { + case .text(content: let content): + return try await processText(content: content, + senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + case .image(content: let content): + return try await processImage(content: content, + senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + case .video(content: let content): + return try await processVideo(content: content, + senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + case .file(content: let content): + return try await processFile(content: content, + senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + case .notice(content: let content): + return try await processNotice(content: content, + senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + case .emote(content: let content): + return try await processEmote(content: content, + senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + case .none: + return nil + } + } + + private func processCommon(senderId: String, + roomId: String, + mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + var notification = UNMutableNotificationContent() + notification.title = title + if let subtitle = subtitle { + notification.subtitle = subtitle + } + notification.threadIdentifier = roomId + notification.categoryIdentifier = NotificationConstants.Category.reply + notification.sound = isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil + + notification = try await notification.addSenderIcon(using: mediaProvider, + senderId: senderId, + senderName: title, + mediaSource: avatarMediaSource, + roomId: roomId) + + return notification + } + + // MARK: Message Types + + private func processText(content: TextMessageContent, + senderId: String, + roomId: String, + mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + let notification = try await processCommon(senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + notification.body = content.body + + return notification + } + + private func processImage(content: ImageMessageContent, + senderId: String, + roomId: String, + mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + var notification = try await processCommon(senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + notification.body = "📷 " + content.body + + notification = try await notification.addMediaAttachment(using: mediaProvider, + mediaSource: .init(source: content.source)) + + return notification + } + + private func processVideo(content: VideoMessageContent, + senderId: String, + roomId: String, + mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + var notification = try await processCommon(senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + notification.body = "📹 " + content.body + + notification = try await notification.addMediaAttachment(using: mediaProvider, + mediaSource: .init(source: content.source)) + + return notification + } + + private func processFile(content: FileMessageContent, + senderId: String, + roomId: String, + mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + let notification = try await processCommon(senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + notification.body = "📄 " + content.body + + return notification + } + + private func processNotice(content: NoticeMessageContent, + senderId: String, + roomId: String, + mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + let notification = try await processCommon(senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + notification.body = "❕ " + content.body + + return notification + } + + private func processEmote(content: EmoteMessageContent, + senderId: String, + roomId: String, + mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent { + let notification = try await processCommon(senderId: senderId, + roomId: roomId, + mediaProvider: mediaProvider) + notification.body = "🫥 " + content.body + + return notification + } +} diff --git a/NSE/Sources/Other/UNMutableNotificationContent.swift b/NSE/Sources/Other/UNMutableNotificationContent.swift new file mode 100644 index 000000000..d28842da9 --- /dev/null +++ b/NSE/Sources/Other/UNMutableNotificationContent.swift @@ -0,0 +1,91 @@ +// +// Copyright 2022 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 Intents +import UserNotifications + +extension UNMutableNotificationContent { + func addMediaAttachment(using mediaProvider: MediaProviderProtocol?, + mediaSource: MediaSourceProxy) async throws -> UNMutableNotificationContent { + guard let mediaProvider else { + return self + } + + switch await mediaProvider.loadFileFromSource(mediaSource, fileExtension: "") { + case .success(let url): + let attachment = try UNNotificationAttachment(identifier: ProcessInfo.processInfo.globallyUniqueString, + url: url, + options: nil) + attachments.append(attachment) + case .failure(let error): + MXLog.debug("Couldn't add media attachment: \(error)") + } + + return self + } + + func addSenderIcon(using mediaProvider: MediaProviderProtocol?, + senderId: String, + senderName: String, + mediaSource: MediaSourceProxy?, + roomId: String) async throws -> UNMutableNotificationContent { + guard let mediaProvider, let mediaSource else { + return self + } + + switch await mediaProvider.loadFileFromSource(mediaSource, fileExtension: "jpg") { + case .success(let url): + // Initialize only the sender for a one-to-one message intent. + let handle = INPersonHandle(value: senderId, type: .unknown) + let sender = INPerson(personHandle: handle, + nameComponents: nil, + displayName: senderName, + image: INImage(imageData: try Data(contentsOf: url)), + contactIdentifier: nil, + customIdentifier: nil) + + // Because this communication is incoming, you can infer that the current user is + // a recipient. Don't include the current user when initializing the intent. + let intent = INSendMessageIntent(recipients: nil, + outgoingMessageType: .outgoingMessageText, + content: nil, + speakableGroupName: nil, + conversationIdentifier: roomId, + serviceName: nil, + sender: sender, + attachments: nil) + + // Use the intent to initialize the interaction. + let interaction = INInteraction(intent: intent, response: nil) + + // Interaction direction is incoming because the user is + // receiving this message. + interaction.direction = .incoming + + // Donate the interaction before updating notification content. + try await interaction.donate() + // Update notification content before displaying the + // communication notification. + let updatedContent = try updating(from: intent) + + return updatedContent.mutableCopy() as! UNMutableNotificationContent + case .failure(let error): + MXLog.debug("Couldn't add sender icon: \(error)") + return self + } + } +} diff --git a/NSE/Sources/Other/UNNotificationRequest.swift b/NSE/Sources/Other/UNNotificationRequest.swift new file mode 100644 index 000000000..102437230 --- /dev/null +++ b/NSE/Sources/Other/UNNotificationRequest.swift @@ -0,0 +1,32 @@ +// +// Copyright 2022 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 UserNotifications + +extension UNNotificationRequest { + var roomId: String? { + content.userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String + } + + var eventId: String? { + content.userInfo[NotificationConstants.UserInfoKey.eventIdentifier] as? String + } + + var unreadCount: Int? { + content.userInfo[NotificationConstants.UserInfoKey.unreadCount] as? Int + } +} diff --git a/NSE/SupportingFiles/Info.plist b/NSE/SupportingFiles/Info.plist new file mode 100644 index 000000000..e29d5b909 --- /dev/null +++ b/NSE/SupportingFiles/Info.plist @@ -0,0 +1,33 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationServiceExtension + + appGroupIdentifier + $(APP_GROUP_IDENTIFIER) + baseBundleIdentifier + $(BASE_BUNDLE_IDENTIFIER) + + diff --git a/NSE/SupportingFiles/NSE.entitlements b/NSE/SupportingFiles/NSE.entitlements new file mode 100644 index 000000000..d18babef6 --- /dev/null +++ b/NSE/SupportingFiles/NSE.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.developer.usernotifications.filtering + + com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + + + diff --git a/NSE/SupportingFiles/target.yml b/NSE/SupportingFiles/target.yml new file mode 100644 index 000000000..f236ace60 --- /dev/null +++ b/NSE/SupportingFiles/target.yml @@ -0,0 +1,89 @@ +name: NSE + +schemes: + NSE: + analyze: + config: Debug + archive: + config: Release + build: + targets: + NSE: + - running + - testing + - profiling + - analyzing + - archiving + profile: + config: Release + run: + askForAppToLaunch: true + config: Debug + debugEnabled: false + disableMainThreadChecker: false + launchAutomaticallySubstyle: 2 + test: + config: Debug + disableMainThreadChecker: false + +targets: + NSE: + type: app-extension + platform: iOS + + dependencies: + - package: MatrixRustSDK + - package: SwiftyBeaver + - package: KeychainAccess + - package: Kingfisher + + info: + path: ../SupportingFiles/Info.plist + properties: + CFBundleShortVersionString: $(MARKETING_VERSION) + CFBundleVersion: $(CURRENT_PROJECT_VERSION) + appGroupIdentifier: $(APP_GROUP_IDENTIFIER) + baseBundleIdentifier: $(BASE_BUNDLE_IDENTIFIER) + NSExtension: + NSExtensionPointIdentifier: com.apple.usernotifications.service + NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).NotificationServiceExtension + + settings: + base: + PRODUCT_NAME: NSE + PRODUCT_BUNDLE_IDENTIFIER: ${BASE_BUNDLE_IDENTIFIER}.nse + MARKETING_VERSION: $(MARKETING_VERSION) + CURRENT_PROJECT_VERSION: $(CURRENT_PROJECT_VERSION) + DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM) + CODE_SIGN_ENTITLEMENTS: NSE/SupportingFiles/NSE.entitlements + SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h + debug: + release: + + sources: + - path: ../Sources + - path: ../SupportingFiles + - path: ../../ElementX/Sources/Services/Timeline/TimelineItemProxy.swift + - path: ../../ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift + - path: ../../ElementX/Sources/Services/Keychain/KeychainController.swift + - path: ../../ElementX/Sources/Services/UserSession/RestorationToken.swift + - path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxyProtocol.swift + - path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift + - path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift + - path: ../../ElementX/Sources/Services/Notification/NotificationConstants.swift + - path: ../../ElementX/Sources/Services/Media/MediaProxyProtocol.swift + - path: ../../ElementX/Sources/Services/Media/MediaProxy.swift + - path: ../../ElementX/Sources/Services/Media/MediaProviderProtocol.swift + - path: ../../ElementX/Sources/Services/Media/MediaProvider.swift + - path: ../../ElementX/Sources/Services/Media/MediaSourceProxy.swift + - path: ../../ElementX/Sources/Services/Background/BackgroundTaskServiceProtocol.swift + - path: ../../ElementX/Sources/Services/Background/BackgroundTaskProtocol.swift + - path: ../../ElementX/Sources/Services/Cache/FileCache.swift + - path: ../../ElementX/Sources/Other/Logging + - path: ../../ElementX/Sources/Other/Extensions/Task.swift + - path: ../../ElementX/Sources/Other/Extensions/FileManager.swift + - path: ../../ElementX/Sources/Other/Extensions/URL.swift + - path: ../../ElementX/Sources/Other/Extensions/Bundle.swift + - path: ../../ElementX/Sources/Other/Extensions/ImageCache.swift + - path: ../../ElementX/Sources/Other/AvatarSize.swift + - path: ../../ElementX/Sources/Other/InfoPlistReader.swift diff --git a/Tools/SwiftGen/swiftgen-config.yml b/Tools/SwiftGen/swiftgen-config.yml index c138508cd..43144c5d3 100755 --- a/Tools/SwiftGen/swiftgen-config.yml +++ b/Tools/SwiftGen/swiftgen-config.yml @@ -24,10 +24,3 @@ strings: params: enumName: ElementL10n publicAccess: true -plist: - inputs: SupportingFiles/Info.plist - outputs: - templatePath: Templates/Plists/runtime-swift5-element-info.stencil - output: InfoPlist.swift - params: - enumName: ElementInfoPlist diff --git a/UITests/SupportingFiles/target.yml b/UITests/SupportingFiles/target.yml index db5426f52..561254e3e 100644 --- a/UITests/SupportingFiles/target.yml +++ b/UITests/SupportingFiles/target.yml @@ -70,8 +70,8 @@ targets: basedOnDependencyAnalysis: false shell: /bin/sh script: | - python3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPhone 13 Pro Max' --version 'iOS.16.0' - python3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPad (9th generation)' --version 'iOS.16.0' + python3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPhone 13 Pro Max' --version 'iOS.16.1' + python3 $PROJECT_DIR/Tools/Scripts/bootTestSimulator.py --name 'iPad (9th generation)' --version 'iOS.16.1' sources: - path: ../Sources @@ -85,7 +85,8 @@ targets: - path: ../../ElementX/Sources/UITests/UITestScreenIdentifier.swift - path: ../../ElementX/Sources/Generated/Strings.swift - path: ../../ElementX/Sources/Generated/Strings+Untranslated.swift - - path: ../../ElementX/Sources/Generated/InfoPlist.swift - path: ../../ElementX/Resources - path: ../../ElementX/Sources/Other/Extensions/Bundle.swift + - path: ../../ElementX/Sources/Other/Extensions/FileManager.swift + - path: ../../ElementX/Sources/Other/InfoPlistReader.swift - path: ../../ElementX/Sources/Other/Extensions/URL.swift diff --git a/UnitTests/Sources/KeychainControllerTests.swift b/UnitTests/Sources/KeychainControllerTests.swift index 2647294bb..a40faf698 100644 --- a/UnitTests/Sources/KeychainControllerTests.swift +++ b/UnitTests/Sources/KeychainControllerTests.swift @@ -21,7 +21,8 @@ class KeychainControllerTests: XCTestCase { var keychain: KeychainController! override func setUp() { - keychain = KeychainController(identifier: "\(ElementInfoPlist.cfBundleIdentifier).tests") + keychain = KeychainController(service: .tests, + accessGroup: InfoPlistReader.target.appGroupIdentifier) keychain.removeAllRestorationTokens() } diff --git a/UnitTests/Sources/UserAgentBuilderTests.swift b/UnitTests/Sources/UserAgentBuilderTests.swift index 93ee1d756..2909c770b 100644 --- a/UnitTests/Sources/UserAgentBuilderTests.swift +++ b/UnitTests/Sources/UserAgentBuilderTests.swift @@ -25,11 +25,11 @@ class UserAgentBuilderTests: XCTestCase { func testContainsClientName() { let userAgent = UserAgentBuilder.makeASCIIUserAgent() - XCTAssert(userAgent?.contains(ElementInfoPlist.cfBundleDisplayName) == true, "\(userAgent ?? "nil") does not contain client name") + XCTAssert(userAgent?.contains(InfoPlistReader.target.bundleDisplayName) == true, "\(userAgent ?? "nil") does not contain client name") } func testContainsClientVersion() { let userAgent = UserAgentBuilder.makeASCIIUserAgent() - XCTAssert(userAgent?.contains(ElementInfoPlist.cfBundleShortVersionString) == true, "\(userAgent ?? "nil") does not contain client version") + XCTAssert(userAgent?.contains(InfoPlistReader.target.bundleShortVersionString) == true, "\(userAgent ?? "nil") does not contain client version") } } diff --git a/UnitTests/SupportingFiles/target.yml b/UnitTests/SupportingFiles/target.yml index fe8c9d445..de6e1f9c9 100644 --- a/UnitTests/SupportingFiles/target.yml +++ b/UnitTests/SupportingFiles/target.yml @@ -49,4 +49,4 @@ targets: - path: ../SupportingFiles - path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit - path: ../Resources - \ No newline at end of file + - path: ../../ElementX/Sources/Other/InfoPlistReader.swift diff --git a/changelog.d/243.feature b/changelog.d/243.feature new file mode 100644 index 000000000..f8d5a134e --- /dev/null +++ b/changelog.d/243.feature @@ -0,0 +1 @@ +NSE: Configure target with commented code blocks. diff --git a/project.yml b/project.yml index 949e28a46..f53f81fe2 100644 --- a/project.yml +++ b/project.yml @@ -25,12 +25,16 @@ settings: BASE_APP_GROUP_IDENTIFIER: io.element APP_GROUP_IDENTIFIER: group.$(BASE_APP_GROUP_IDENTIFIER) BASE_BUNDLE_IDENTIFIER: io.element.elementx + MARKETING_VERSION: 1.0.9 + CURRENT_PROJECT_VERSION: 1 + DEVELOPMENT_TEAM: 7J4U792NQT include: - path: ElementX/SupportingFiles/target.yml - path: UnitTests/SupportingFiles/target.yml - path: UITests/SupportingFiles/target.yml - path: IntegrationTests/SupportingFiles/target.yml + - path: NSE/SupportingFiles/target.yml packages: MatrixRustSDK: