From d389ce7ad71c3223e79cb5fb095cf391489d982d Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 21 Nov 2022 19:37:13 +0300 Subject: [PATCH] Notifications (#275) --- ElementX.xcodeproj/project.pbxproj | 471 ++++++++++++++++-- .../xcshareddata/xcschemes/NSE.xcscheme | 98 ++++ .../en.lproj/Untranslated.strings | 2 + ElementX/Resources/Sounds/message.caf | Bin 0 -> 30140 bytes .../Sources/Application/AppCoordinator.swift | 100 +++- .../Application/AppCoordinatorProtocol.swift | 21 + .../Sources/Application/AppDelegate.swift | 50 +- .../Sources/Application/Application.swift | 45 ++ .../Sources/Application/BuildSettings.swift | 22 +- ElementX/Sources/Generated/InfoPlist.swift | 66 --- .../Generated/Strings+Untranslated.swift | 2 + ElementX/Sources/Other/ElementSettings.swift | 17 +- .../Other/Extensions/FileManager.swift | 34 ++ .../Sources/Other/Extensions/ImageCache.swift | 6 + .../Sources/Other/Extensions/UIDevice.swift | 2 +- ElementX/Sources/Other/Extensions/URL.swift | 29 ++ ElementX/Sources/Other/InfoPlistReader.swift | 79 +++ ElementX/Sources/Other/Logging/MXLogger.swift | 8 +- ElementX/Sources/Other/UserAgentBuilder.swift | 4 +- .../AnalyticsPromptModels.swift | 4 +- .../View/AnalyticsPrompt.swift | 2 +- .../OnboardingScreen/OnboardingModels.swift | 2 +- .../Other/InviteFriendsCoordinator.swift | 2 +- .../RoomScreen/View/MessageComposer.swift | 4 +- .../Settings/View/SettingsScreen.swift | 2 +- .../Services/Analytics/Analytics.swift | 2 +- .../Services/BugReport/BugReportService.swift | 6 +- .../Sources/Services/Cache/FileCache.swift | 25 +- .../Sources/Services/Client/ClientProxy.swift | 74 ++- .../Services/Client/ClientProxyProtocol.swift | 45 +- .../Services/Client/MockClientProxy.swift | 22 +- .../KeychainController.swift | 31 +- .../KeychainControllerProtocol.swift | 0 .../Services/Media/MediaProvider.swift | 40 +- .../Media/MediaProviderProtocol.swift | 12 +- .../Sources/Services/Media/MediaProxy.swift | 49 ++ .../Services/Media/MediaProxyProtocol.swift | 25 + ...diaSource.swift => MediaSourceProxy.swift} | 24 +- .../Services/Media/MockMediaProvider.swift | 8 +- .../Manager/MockNotificationManager.swift | 37 ++ .../Manager/NotificationManager.swift | 166 ++++++ .../Manager/NotificationManagerProtocol.swift | 42 ++ .../Notification/NotificationConstants.swift | 34 ++ .../Proxy/MockNotificationServiceProxy.swift} | 7 +- .../Proxy/NotificationItemProxy.swift | 73 +++ .../Proxy/NotificationServiceProxy.swift | 37 ++ .../NotificationServiceProxyProtocol.swift | 21 + .../MessageTimelineItem.swift | 20 +- .../Items/FileRoomTimelineItem.swift | 4 +- .../Items/ImageRoomTimelineItem.swift | 2 +- .../Items/VideoRoomTimelineItem.swift | 4 +- .../UserSessionFlowCoordinator.swift | 12 +- ...erSessionFlowCoordinatorStateMachine.swift | 10 + .../UserSession/UserSessionStore.swift | 28 +- .../UITests/UITestsAppCoordinator.swift | 3 +- .../SupportingFiles/ElementX.entitlements | 4 + ElementX/SupportingFiles/Info.plist | 6 + ElementX/SupportingFiles/target.yml | 11 +- .../NotificationServiceExtension.swift | 165 ++++++ NSE/Sources/Other/DataProtectionManager.swift | 45 ++ NSE/Sources/Other/NSELogger.swift | 110 ++++ .../Other/NotificationItemProxy+NSE.swift | 222 +++++++++ .../Other/UNMutableNotificationContent.swift | 91 ++++ NSE/Sources/Other/UNNotificationRequest.swift | 32 ++ NSE/SupportingFiles/Info.plist | 33 ++ NSE/SupportingFiles/NSE.entitlements | 12 + NSE/SupportingFiles/target.yml | 89 ++++ Tools/SwiftGen/swiftgen-config.yml | 7 - UITests/SupportingFiles/target.yml | 7 +- .../Sources/KeychainControllerTests.swift | 3 +- UnitTests/Sources/UserAgentBuilderTests.swift | 4 +- UnitTests/SupportingFiles/target.yml | 2 +- changelog.d/243.feature | 1 + project.yml | 4 + 74 files changed, 2459 insertions(+), 324 deletions(-) create mode 100644 ElementX.xcodeproj/xcshareddata/xcschemes/NSE.xcscheme create mode 100644 ElementX/Resources/Sounds/message.caf create mode 100644 ElementX/Sources/Application/AppCoordinatorProtocol.swift create mode 100644 ElementX/Sources/Application/Application.swift delete mode 100644 ElementX/Sources/Generated/InfoPlist.swift create mode 100644 ElementX/Sources/Other/Extensions/FileManager.swift create mode 100644 ElementX/Sources/Other/InfoPlistReader.swift rename ElementX/Sources/Services/{UserSession => Keychain}/KeychainController.swift (89%) rename ElementX/Sources/Services/{UserSession => Keychain}/KeychainControllerProtocol.swift (100%) create mode 100644 ElementX/Sources/Services/Media/MediaProxy.swift create mode 100644 ElementX/Sources/Services/Media/MediaProxyProtocol.swift rename ElementX/Sources/Services/Media/{MediaSource.swift => MediaSourceProxy.swift} (70%) create mode 100644 ElementX/Sources/Services/Notification/Manager/MockNotificationManager.swift create mode 100644 ElementX/Sources/Services/Notification/Manager/NotificationManager.swift create mode 100644 ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift create mode 100644 ElementX/Sources/Services/Notification/NotificationConstants.swift rename ElementX/Sources/Services/{UserSession/FileManager.swift => Notification/Proxy/MockNotificationServiceProxy.swift} (74%) create mode 100644 ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift create mode 100644 ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift create mode 100644 ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxyProtocol.swift create mode 100644 NSE/Sources/NotificationServiceExtension.swift create mode 100644 NSE/Sources/Other/DataProtectionManager.swift create mode 100644 NSE/Sources/Other/NSELogger.swift create mode 100644 NSE/Sources/Other/NotificationItemProxy+NSE.swift create mode 100644 NSE/Sources/Other/UNMutableNotificationContent.swift create mode 100644 NSE/Sources/Other/UNNotificationRequest.swift create mode 100644 NSE/SupportingFiles/Info.plist create mode 100644 NSE/SupportingFiles/NSE.entitlements create mode 100644 NSE/SupportingFiles/target.yml create mode 100644 changelog.d/243.feature 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 0000000000000000000000000000000000000000..4c241b58666d398fbff465087574eebe6b28ec28 GIT binary patch literal 30140 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 literal 0 HcmV?d00001 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: