diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 3c8d612e79..6f4f71e3f0 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ /* Begin PBXBuildFile section */ 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.swift */; }; + 00C3023B6DF55024D8876B76 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3D8BEEFCA07BEA43F4F4BF77 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 01681E8B20AD6F0D237F2DC1 /* IdentityConfirmedScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6624240FFD32B7F0834229 /* IdentityConfirmedScreenViewModel.swift */; }; 0180C44B997EDA8D21F883AC /* RoomNotificationSettingsCustomSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */; }; 01B63F1A04A276B39AC17014 /* CallInviteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A3D3CFA199FA7897364547 /* CallInviteRoomTimelineItem.swift */; }; @@ -31,6 +32,7 @@ 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */; }; 037006FB6DF1374F94E4058D /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */; }; 038AB2E86960FD240231D4C2 /* GeneratedPreviewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A2E4BD7C0CAD25EF924A4C /* GeneratedPreviewTests.swift */; }; + 03BD83E8BDD23AE059802E0D /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; 03CDCA6243F89B194E3FAD17 /* EncryptionAuthenticity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */; }; 0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */; }; 044DD8F80231BC30570F7965 /* UserDiscoveryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */; }; @@ -39,7 +41,10 @@ 059173B3C77056C406906B6D /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = D4DA544B2520BFA65D6DB4BB /* target.yml */; }; 05BAB510CBC2ED35C154ADD0 /* AnalyticsPromptScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFD012C3A9F5EF276DDD4AA /* AnalyticsPromptScreenViewModelProtocol.swift */; }; 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */; }; + 05FF0CD80EDAB3A7C0D4700A /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; + 0638CBDE3098B1C3F23AFCFA /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; + 069358C2C825A19DE6CB127E /* TracingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */; }; 06B31F84CE52A7A7C271267C /* SecureBackupRecoveryKeyScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */; }; 06B55882911B4BF5B14E9851 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; }; @@ -109,6 +114,7 @@ 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; }; 1583E2D766E4485FF91662FC /* PermalinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3EB5B1848CF4F64E63C6B7 /* PermalinkTests.swift */; }; 15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */; }; + 1621BF6316FFFEF5AE067C77 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; }; 1653275750CE11F5CE94DDFD /* ReadReceiptsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */; }; 167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */; }; 16CBD087038DE3815CDA512C /* PollMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D38391154120264910D19528 /* PollMock.swift */; }; @@ -119,12 +125,14 @@ 17BC15DA08A52587466698C5 /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; }; 18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; 18E3786918486D4C9726BC84 /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */; }; + 18FDE4ED6D83B0771452B43D /* RoomSelectionScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F104596B0620CEFE5DFD31B1 /* RoomSelectionScreenCoordinator.swift */; }; 192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; 197441F1EF23A5DABACCA79F /* StickerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5338450E6783A576B5C16DD /* StickerRoomTimelineView.swift */; }; 19DED23340D0855B59693ED2 /* VoiceMessageRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */; }; 19DF5600A7F547B22DD7872A /* CompletionSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A12D3D8138F1B71AFA7C858 /* CompletionSuggestionService.swift */; }; 19FE025AE9BA2959B6589B0D /* RoomMemberDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */; }; + 1A3783005E6945F8583AF997 /* NSItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EFC8C634469F9262659C7 /* NSItemProvider.swift */; }; 1A3B073568D1DC8F76F1F3A0 /* UserProfileScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE69982BBA18C6D51AD08E /* UserProfileScreen.swift */; }; 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; }; 1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */; }; @@ -176,12 +184,14 @@ 25C4C1100B6EA79F5CC7CBB5 /* AppLockSetupPINScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */; }; 260FFC1475EE94F641C3F3F9 /* PollFormScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40F1985065500F0E7F61A27 /* PollFormScreenViewModelProtocol.swift */; }; 261261778DEFAEFC042B875E /* JoinedRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */; }; + 26252AA9AED64010788F4C26 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05A3E8741D199CD1A37F4CBF /* UIView.swift */; }; 2689D22EF1D10D22B0A4DAEA /* NotificationContentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */; }; 273AB64B9A26B61C51858867 /* AsyncSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A73A07BAEDD74C48795A996A /* AsyncSequence.swift */; }; 274CE3C986841D15FD530BF5 /* ShimmerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97CE98208321C4D66E363612 /* ShimmerModifier.swift */; }; 275EDE8849A2AC1D9309ED7C /* TemplateScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */; }; 2797C9D9BA642370F1C85D78 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; }; 27F015B0D5436633B5B3C8C3 /* SecureBackupRecoveryKeyScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7061BE2C0BF427C38AEDEF5E /* SecureBackupRecoveryKeyScreenViewModel.swift */; }; + 27FEF0F40750465195C9D6D6 /* RoomSelectionScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B9D191A81FFB0C72CE73E77 /* RoomSelectionScreenModels.swift */; }; 2814E7075BF3A5C0CCBC9F90 /* RoomDirectorySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AF32E4136FD6F159D86C2C /* RoomDirectorySearchView.swift */; }; 281BED345D59A9A6A99E9D98 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; }; 282A5F3375DDC774AE09B0C3 /* TracingConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */; }; @@ -214,6 +224,7 @@ 2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */; }; 2F09DF0CB213CAE86A3E3B67 /* EventTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B10423B9102086A2D9BFCBA /* EventTimelineItem.swift */; }; 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; }; + 2F2906AE9BC3D0E79A6F98F8 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 2F6207CB5C4715FE313B1E95 /* TimelineViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6509708F54FC883604DFDC95 /* TimelineViewModelTests.swift */; }; 2F623DA1122140A987B34D08 /* NotificationSettingsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */; }; 2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; }; @@ -243,6 +254,7 @@ 355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */; }; 3582056513A384F110EC8274 /* MediaPlayerProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */; }; 35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; }; + 36206F74DDEBF9BEAF6A6A1F /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; }; 366D5BFE52CB79E804C7D095 /* CallScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */; }; 36926D795D6D19177C7812F8 /* EncryptionResetPasswordScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6935A55AB3B0C94BC566DD6 /* EncryptionResetPasswordScreenCoordinator.swift */; }; 369BF960E52BBEE61F8A5BD1 /* BlockedUsersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */; }; @@ -264,6 +276,7 @@ 3982C505960006B341CFD0C6 /* UserDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D0EA07BD545CC9F234DB8D /* UserDetailsEditScreenModels.swift */; }; 3982E60F9C126437D5E488A3 /* PillContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A6314FDC51DA25712D9A81 /* PillContextTests.swift */; }; 39A987B3E41B976D1DF944C6 /* CallScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */; }; + 39DFC4B9EB6A8757210BDEC6 /* RoomSelectionScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD2A058F3566FEEBA1D11B3 /* RoomSelectionScreenViewModelProtocol.swift */; }; 3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */; }; 3A164187907DA43B7858F9EC /* CompletionSuggestionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */; }; 3A64A93A651A3CB8774ADE8E /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = BA93CD75CCE486660C9040BD /* Collections */; }; @@ -360,6 +373,7 @@ 4DAEE2468669848B6C9F55B4 /* TimelineReadReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33035418BB35754232985871 /* TimelineReadReceiptsView.swift */; }; 4DEEFB73181C3B023DB42686 /* NetworkMonitorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */; }; 4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */; }; + 4E22086585CB3B35FEEFBBB9 /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FA991289149D31F4286747 /* UserPreference.swift */; }; 4E36A66E0EDA74BF3A036FD0 /* RoomChangeRolesScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAD8C633AA57948B34EDCF7 /* RoomChangeRolesScreenViewModelProtocol.swift */; }; 4E8A2A2CFEB212F14E49E1A1 /* AppLockSetupSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */; }; 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; }; @@ -386,11 +400,11 @@ 53C1E7F6A7D6409D89F36ED7 /* AggregatedReactionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */; }; 53DEF39F0C4DE02E3FC56D91 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 800631D7250B7F93195035F1 /* KeychainAccess */; }; 53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */; }; - 5455147CAC63F71E48F7D699 /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D455BC2423D911A62ACFB2 /* NSELogger.swift */; }; 54AE8860D668AFD96E7E177B /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; 54C774874BED4A8FAD1F22FE /* AnalyticsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */; }; 5518DA4A6C9B4FC4B497EA9A /* LogViewerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B795AAAB7B8747FE2FF311 /* LogViewerScreenModels.swift */; }; 558E2673B04FDD06A1A12DD3 /* LogViewerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7463464054DDF194C54F0B04 /* LogViewerScreenViewModelProtocol.swift */; }; + 558F37B1A8F2C4CC9B1ACEDA /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 3262F08E1C3483C22A7A319F /* Compound */; }; 55CDD3968D95D1A820B5491E /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; }; 55D18AA4F4A2257642EBDB94 /* GlobalSearchScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38354164AF59C5006CD05878 /* GlobalSearchScreenViewModel.swift */; }; 562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 933B074F006F8E930DB98B4E /* TimelineMediaFrame.swift */; }; @@ -409,6 +423,8 @@ 5992EF10AA157EBD97D88910 /* AudioRecorderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6569593FA36B22259E806A67 /* AudioRecorderState.swift */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; }; + 5AA81A4E2D40A32A9E7F71F2 /* ShareExtensionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3ADF21BE301D0DA48F2A7E /* ShareExtensionView.swift */; }; + 5AC5CD6D893073EE4D9A277E /* ShareExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D27299A36536DBF91AE8FA6 /* ShareExtensionViewController.swift */; }; 5AE6404C4FD4848ACCFF9EDC /* SecureBackupLogoutConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */; }; 5B6E5AD224509E6C0B520D6E /* RoomMemberDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */; }; 5B7D24A318AFF75AD611A026 /* RoomDirectorySearchScreenScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */; }; @@ -421,6 +437,7 @@ 5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; }; 5D56CE09743C6B90C21B04C2 /* RoomMembersListScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E9E0929CEFA356090BE5FB8 /* RoomMembersListScreenViewModelTests.swift */; }; 5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; + 5D99F63CC88BB29383019FC6 /* ShareExtensionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */; }; 5DD0EF30070DC0A82C5CCD33 /* RoomMembersListManageMemberSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */; }; 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; }; 5EE1D4E316D66943E97FDCF2 /* BloomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BEB970F500BFB248443FA1 /* BloomView.swift */; }; @@ -463,6 +480,8 @@ 66357ECB73B1290E5490A012 /* WebRegistrationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F418426410F3823F3EB0828 /* WebRegistrationScreenViewModelProtocol.swift */; }; 663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; }; 6681D6D3ADF69EBD2625F29A /* KnockedRoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E8F4D7D61B80EBD5CB92F8A /* KnockedRoomProxyMock.swift */; }; + 66832DE7B5C2E861045265DC /* RoomSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D879DC5515B1D42577F96C94 /* RoomSelectionScreen.swift */; }; + 66E9202BED03B5BB00E812A1 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 67160204A8D362BB7D4AD259 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693E16574C6F7F9FA1015A8C /* Search.swift */; }; 6786C4B0936AC84D993B20BF /* NotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */; }; 6793E75E3EBE48EBB8F857AF /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; }; @@ -561,6 +580,7 @@ 7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */; }; 7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; }; 7A8B264506D3DDABC01B4EEB /* AppMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53AC78E49A297AC1D72A7CF /* AppMediator.swift */; }; + 7AED78DC086695E93F0647D2 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; 7B1605C6FFD4D195F264A684 /* RoomPollsHistoryScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */; }; 7B3A59786DB2F741A1743ED0 /* PinnedEventsTimelineScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510E89B989477E5EE8E503C0 /* PinnedEventsTimelineScreenViewModelProtocol.swift */; }; 7B5DAB915357BE596529BF25 /* MapTilerStaticMapProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */; }; @@ -630,6 +650,7 @@ 88CBF1595E39CE697928DE48 /* SFNumberedListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */; }; 88F348E2CB14FF71CBBB665D /* AudioRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7475C5AE20BA896930907EA8 /* AudioRoomTimelineItemContent.swift */; }; 890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C687844F60BFF532D49A994C /* AnalyticsTests.swift */; }; + 89198AE2649DD77673D5793B /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; }; 8944548A684F1C837CEC47F4 /* RoomMembersListScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D0946F77B696176E062D037 /* RoomMembersListScreenModels.swift */; }; 89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */; }; 899359A4D1147601F6C4E364 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; }; @@ -656,6 +677,7 @@ 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */; }; 8D71E5E53F372202379BECCE /* BugReportScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */; }; 8DC176CC5ABA24138EB443DD /* RoomMemberDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55679AF67545EF8087E47BE /* RoomMemberDetails.swift */; }; + 8DCA1F05C3BA6ED826F1599D /* RoomSelectionScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B4E3F1265581683E4997B8 /* RoomSelectionScreenViewModel.swift */; }; 8DCD9CC5361FF22A5B2C20F1 /* AppLockSetupSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9FCE4D1E3A81AC1CC5CB91 /* AppLockSetupSettingsScreenCoordinator.swift */; }; 8DDC6F28C797D8685F2F8E32 /* AnalyticsConsentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57B6B383F1FD04CC0E7B60C6 /* AnalyticsConsentState.swift */; }; 8E650379587C31D7912ED67B /* UNNotification+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */; }; @@ -694,7 +716,6 @@ 9603EEF6DE980BB1D15D4707 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05A3E8741D199CD1A37F4CBF /* UIView.swift */; }; 962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */; }; 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; - 968A5B890004526AB58A217C /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; 9696ECAFB4F0C079C5C2A526 /* AppLockSetupPINScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAF8C2226A57B9AB7446B31 /* AppLockSetupPINScreenCoordinator.swift */; }; 96B3606E30F824095B1DD022 /* NetworkMonitorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */; }; 97189E495F0E47805D1868DB /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 527578916BD388A09F5A8036 /* DTCoreText */; }; @@ -848,6 +869,7 @@ B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029D5701F80A9AF7167BB4D0 /* TimelineModels.swift */; }; B879446FD8E65A711EF8F9F7 /* AdvancedSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */; }; B89990DD875B0B603D4D4332 /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; }; + B8EC8A544162B0A41B9AB339 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.swift */; }; B93D7CE520088AD53FA6D53C /* SettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B663BE498BB39EADC24025D /* SettingsScreenModels.swift */; }; B93FA0DA1504B301CAEE141B /* NotificationSettingsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F5D66F158A6662F953733E /* NotificationSettingsProxy.swift */; }; B94368839BDB69172E28E245 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; @@ -870,9 +892,11 @@ BDA68E8D95B2B24B28825B8B /* LoginScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */; }; BDC4EB54CC3036730475CB8B /* QRCodeLoginScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */; }; BDED6DA7AD1E76018C424143 /* LegalInformationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */; }; + BE8E5985771DF9137C6CE89A /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */; }; BEA646DF302711A753F0D420 /* MapTilerStyleBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225EFCA26877E75CDFE7F48D /* MapTilerStyleBuilderProtocol.swift */; }; BFEB24336DFD5F196E6F3456 /* IntentionalMentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF5CBAF69BDF5DF31C661E1 /* IntentionalMentions.swift */; }; C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */; }; + C022284E2774A5E1EF683B4D /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; C051475DFF4C8EBDDF4DC8E4 /* StartChatScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99E13633862847D8B7E2815 /* StartChatScreenModels.swift */; }; C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */; }; C0B97FFEC0083F3A36609E61 /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A243A6E6207297123E60DE48 /* TimelineItemMacContextMenu.swift */; }; @@ -983,11 +1007,12 @@ D6DE764B17FB4A9A12C33BF4 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1DF3FFFE5ED2B8133F43A7 /* MessageComposer.swift */; }; D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; }; D8459AAD6969B1431ECBE990 /* UnsupportedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E535B3388755B65C34CD10 /* UnsupportedRoomTimelineView.swift */; }; - D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; D8CFA0EE46376F9FF04EEE45 /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4853C923A1AF43711D025EAF /* TextRoomTimelineView.swift */; }; + D8F1462EA00AFC939FF9ACCA /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 203D1ACC20287F8986C959D3 /* target.yml */; }; D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352359663A0E52BA20761EE /* LoadableImage.swift */; }; DA7E867F5EAFF8E20B2EE3B6 /* SecureBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3D16709ADD4F4BCC710B1E /* SecureBackupScreenModels.swift */; }; + DAF63A9CF9932CA8F6830F11 /* ShareExtensionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */; }; DB079D1929B5A5F52D207C83 /* RoomDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */; }; DB65401349C143DFF883E2B0 /* AnalyticsPromptScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C8EC6EA7EDFCE46710DA306 /* AnalyticsPromptScreenViewModel.swift */; }; DBC8D1DBFE9F9CA7662BC8AA /* RoomPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */; }; @@ -1097,8 +1122,10 @@ F12F6BED7B6D7EE4BEE55039 /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; }; F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57916A1578D8043BB0795441 /* GeneratedMocks.swift */; }; F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; + F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; }; F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */; }; F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7BAD55A4E2B8E5828CD64C /* SoftLogoutScreenViewModel.swift */; }; + F38D32C1B0232AAFE6A0822C /* ExtensionLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */; }; F3E2D3F7ACDED65A4E5CD8DE /* RoomMembersListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */; }; F3ECA377FF77E81A4F1FA062 /* TimelineItemSendInfoLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753B4C6C0EDDCBF0708DC384 /* TimelineItemSendInfoLabel.swift */; }; F3F38062C6CA21CF403C5C90 /* AudioConverterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2757B1BE23DF8AA239937243 /* AudioConverterProtocol.swift */; }; @@ -1112,6 +1139,7 @@ F5D2270B5021D521C0D22E11 /* FlowCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */; }; F656F92A63D3DC1978D79427 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 290FDEDA4D764B9F7EBE55A9 /* Algorithms */; }; F669B55BC237CDA5EC9332FE /* MentionSuggestionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4100DDE6BF3C566AB66B80CC /* MentionSuggestionItemView.swift */; }; + F66BBBE51B258BBB0B918C68 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C79D91A7F9F378CECEF64B5A /* MatrixRustSDK */; }; F66BCCC825D6CA51724A94D0 /* MediaPlayerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8A1F98AE670377B20679FF5 /* MediaPlayerProvider.swift */; }; F697284B9B5F2C00CFEA3B12 /* EmojiDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */; }; F6DFA23885980118AD7359C5 /* NotificationSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */; }; @@ -1149,6 +1177,7 @@ FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; }; FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; }; FDD5B4B616D9FF4DE3E9A418 /* QRCodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92DB574F954CC2B40F7BE892 /* QRCodeScannerView.swift */; }; + FDE47D4686BA0F86BB584633 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = CAA3B9DF998B397C9EE64E8B /* Collections */; }; FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; }; FEC03105D1BDE0F49BD7F243 /* PinnedEventsTimelineScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B6572E6EF5D5F4B0C338A40 /* PinnedEventsTimelineScreenModels.swift */; }; FEFD5290B31FCBA6999912C8 /* RoomChangePermissionsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2721D7B051F0159AA919DA05 /* RoomChangePermissionsScreenViewModelProtocol.swift */; }; @@ -1173,6 +1202,13 @@ remoteGlobalIDString = C0FAEB81CFD9776CD78CE489; remoteInfo = ElementX; }; + 52A426E590105174D83B9532 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AC22997D58D612146053154D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 19F0C845D67E9BEA4BE7133E; + remoteInfo = ShareExtension; + }; 6848AF4480814C5F810FB7EB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AC22997D58D612146053154D /* Project object */; @@ -1204,6 +1240,7 @@ dstSubfolderSpec = 13; files = ( EB88DBD77221E2CFE463018C /* NSE.appex in Embed Foundation Extensions */, + 00C3023B6DF55024D8876B76 /* ShareExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -1334,6 +1371,7 @@ 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenModels.swift; sourceTree = ""; }; 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreen.swift; sourceTree = ""; }; 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxyProtocol.swift; sourceTree = ""; }; + 1B9D191A81FFB0C72CE73E77 /* RoomSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenModels.swift; sourceTree = ""; }; 1BA5A62DA4B543827FF82354 /* LAContextMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LAContextMock.swift; sourceTree = ""; }; 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = ""; }; 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; @@ -1348,6 +1386,7 @@ 1D9F148717D74F73BE724434 /* LongPressWithFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressWithFeedback.swift; sourceTree = ""; }; 1DA7E93C2E148B96EF6A8500 /* TimelineItemAccessibilityModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemAccessibilityModifier.swift; sourceTree = ""; }; 1DB2FC2AA9A07EE792DF65CF /* NotificationPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenModels.swift; sourceTree = ""; }; + 1DD2A058F3566FEEBA1D11B3 /* RoomSelectionScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenViewModelProtocol.swift; sourceTree = ""; }; 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenModels.swift; sourceTree = ""; }; 1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelProtocol.swift; sourceTree = ""; }; 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposedViewSize.swift; sourceTree = ""; }; @@ -1358,6 +1397,7 @@ 1FAF8C2226A57B9AB7446B31 /* AppLockSetupPINScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenCoordinator.swift; sourceTree = ""; }; 1FD51B4D5173F7FC886F5360 /* NoticeRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItemContent.swift; sourceTree = ""; }; 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLoaderProtocol.swift; sourceTree = ""; }; + 203D1ACC20287F8986C959D3 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStaticMapProtocol.swift; sourceTree = ""; }; 20E69F67D2A70ABD08CA6D54 /* NotificationPermissionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenViewModelProtocol.swift; sourceTree = ""; }; 2141693488CE5446BB391964 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; @@ -1484,15 +1524,18 @@ 3BDCCD2F6B405C14B9BCE94E /* JoinRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenCoordinator.swift; sourceTree = ""; }; 3BF8E5D4C95974B96A18C80E /* EncryptionSettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSettingsUITests.swift; sourceTree = ""; }; 3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; sourceTree = ""; }; + 3C3ADF21BE301D0DA48F2A7E /* ShareExtensionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionView.swift; sourceTree = ""; }; 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenModels.swift; sourceTree = ""; }; 3CCD41CD67DB5DA0D436BFE9 /* VoiceMessageRoomPlaybackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomPlaybackView.swift; sourceTree = ""; }; 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenModels.swift; sourceTree = ""; }; 3CFD5EB0B0EEA4549FB49784 /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextField.swift; sourceTree = ""; }; + 3D27299A36536DBF91AE8FA6 /* ShareExtensionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionViewController.swift; sourceTree = ""; }; 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = ""; }; 3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = ""; }; 3D65BCC659FD9087E49B3C25 /* AppAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAppearance.swift; sourceTree = ""; }; 3D75941CBD7D336F831924EC /* ReadReceiptCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptCell.swift; sourceTree = ""; }; + 3D8BEEFCA07BEA43F4F4BF77 /* ShareExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 3D9B45D584D232CB9E5C7734 /* RoomChangeRolesScreenSelectedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenSelectedItem.swift; sourceTree = ""; }; 3D9FCE4D1E3A81AC1CC5CB91 /* AppLockSetupSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenCoordinator.swift; sourceTree = ""; }; 3DBE70FFB7936F35811772C1 /* IdentityConfirmedScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenModels.swift; sourceTree = ""; }; @@ -1512,6 +1555,7 @@ 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenFooterView.swift; sourceTree = ""; }; 4176C3E20C772DE8D182863C /* LegalInformationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreen.swift; sourceTree = ""; }; 419957D7B1C983D7B3B93678 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = ""; }; + 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionLogger.swift; sourceTree = ""; }; 41BB37D96C3EA18F3CE8675D /* RoomDirectorySearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenModels.swift; sourceTree = ""; }; 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenViewModelTests.swift; sourceTree = ""; }; 421E716C521F96D24ECE69B3 /* NoticeRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItem.swift; sourceTree = ""; }; @@ -1581,6 +1625,7 @@ 4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyProtocol.swift; sourceTree = ""; }; 502F986D57158674172C58E3 /* AppLockSetupSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenModels.swift; sourceTree = ""; }; 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = ""; }; + 505ADA084C0B38A0C4AD2574 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = ""; }; 50D685B4DB38BB5BD87C956A /* AuthenticationStartScreenBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenBackgroundImage.swift; sourceTree = ""; }; 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixUserShareLink.swift; sourceTree = ""; }; @@ -1788,6 +1833,7 @@ 8319173DD66C07F45DC48848 /* IdentityConfirmedScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenViewModelProtocol.swift; sourceTree = ""; }; 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModel.swift; sourceTree = ""; }; 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedCornerShape.swift; sourceTree = ""; }; + 83B4E3F1265581683E4997B8 /* RoomSelectionScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenViewModel.swift; sourceTree = ""; }; 84311D707B09854D67F78BBF /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; 845DDBDE5A0887E73D38B826 /* InviteUsersViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersViewModelTests.swift; sourceTree = ""; }; 848F69921527D31CAACB93AF /* SecureBackupLogoutConfirmationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenViewModelTests.swift; sourceTree = ""; }; @@ -2034,6 +2080,7 @@ B81B6170DB690013CEB646F4 /* MapLibreModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreModels.swift; sourceTree = ""; }; B8516302ACCA94A0E680AB3B /* VoiceMessageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageButton.swift; sourceTree = ""; }; B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = ""; }; + B88CE0A058727BC68EEEC6B6 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = ""; }; B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = ""; }; B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; @@ -2068,6 +2115,7 @@ C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = ""; }; C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = ""; }; C0FF08D0BD7D0B4B6877AB7D /* SecureBackupRecoveryKeyScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModelTests.swift; sourceTree = ""; }; + C142248014E08E885E323E56 /* Avatars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatars.swift; sourceTree = ""; }; C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutDirection.swift; sourceTree = ""; }; C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationModeProxy.swift; sourceTree = ""; }; C15E0017717EAE3A1D02D005 /* StaticLocationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenCoordinator.swift; sourceTree = ""; }; @@ -2150,7 +2198,6 @@ D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; D38391154120264910D19528 /* PollMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollMock.swift; sourceTree = ""; }; D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreen.swift; sourceTree = ""; }; - D3D455BC2423D911A62ACFB2 /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = ""; }; D3F219838588C62198E726E3 /* LABiometryType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LABiometryType.swift; sourceTree = ""; }; D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreen.swift; sourceTree = ""; }; D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorderProtocol.swift; sourceTree = ""; }; @@ -2173,6 +2220,7 @@ D79BB714D28C9F588DD69353 /* SecureBackupScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelProtocol.swift; sourceTree = ""; }; D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentBuilder.swift; sourceTree = ""; }; D7BEB970F500BFB248443FA1 /* BloomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomView.swift; sourceTree = ""; }; + D879DC5515B1D42577F96C94 /* RoomSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreen.swift; sourceTree = ""; }; D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModel.swift; sourceTree = ""; }; D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineItem.swift; sourceTree = ""; }; D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenInviteCell.swift; sourceTree = ""; }; @@ -2187,6 +2235,7 @@ DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = ""; }; DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = ""; }; DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollInteractionHandler.swift; sourceTree = ""; }; + DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionModels.swift; sourceTree = ""; }; DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenModels.swift; sourceTree = ""; }; DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceTests.swift; sourceTree = ""; }; DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelTests.swift; sourceTree = ""; }; @@ -2205,7 +2254,6 @@ E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreen.swift; sourceTree = ""; }; E1E0B4A34E69BD2132BEC521 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; E1ED17433ADC77287F8904F9 /* CallNotificationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallNotificationRoomTimelineItem.swift; sourceTree = ""; }; - E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; E26C69EC1157D71CC61ADAE4 /* ScaledPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledPaddingModifier.swift; sourceTree = ""; }; E2B1CC9AA154F4D5435BF60A /* Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comparable.swift; sourceTree = ""; }; E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyMock.swift; sourceTree = ""; }; @@ -2284,6 +2332,7 @@ F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelTests.swift; sourceTree = ""; }; F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStyle.swift; sourceTree = ""; }; F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModel.swift; sourceTree = ""; }; + F104596B0620CEFE5DFD31B1 /* RoomSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenCoordinator.swift; sourceTree = ""; }; F134D2D91DFF732FB75B2CB7 /* UserProfileScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenViewModelProtocol.swift; sourceTree = ""; }; F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; @@ -2355,6 +2404,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7E8EB7CD881C54161D4474E5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F66BBBE51B258BBB0B918C68 /* MatrixRustSDK in Frameworks */, + FDE47D4686BA0F86BB584633 /* Collections in Frameworks */, + 558F37B1A8F2C4CC9B1ACEDA /* Compound in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; BF59B36A7B2DB184B62826F6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2495,6 +2554,7 @@ 06501F0E978B2D5C92771DC7 /* Logging */ = { isa = PBXGroup; children = ( + 41A8571A8A071FB41778C016 /* ExtensionLogger.swift */, 111B698739E3410E2CDB7144 /* MXLog.swift */, 542D4F49FABA056DEEEB3400 /* RustTracing.swift */, ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */, @@ -2866,6 +2926,18 @@ path = View; sourceTree = ""; }; + 2E42D43DB6835A58D88B2F91 /* RoomSelectionScreen */ = { + isa = PBXGroup; + children = ( + F104596B0620CEFE5DFD31B1 /* RoomSelectionScreenCoordinator.swift */, + 1B9D191A81FFB0C72CE73E77 /* RoomSelectionScreenModels.swift */, + 83B4E3F1265581683E4997B8 /* RoomSelectionScreenViewModel.swift */, + 1DD2A058F3566FEEBA1D11B3 /* RoomSelectionScreenViewModelProtocol.swift */, + FF654D7FD6693839E3185FAD /* View */, + ); + path = RoomSelectionScreen; + sourceTree = ""; + }; 2ECFF6B05DAA37EB10DBF7E8 /* UITests */ = { isa = PBXGroup; children = ( @@ -3221,10 +3293,21 @@ 823ED0EC3F1B6CF47D284011 /* Tools */, B04B538A859CD012755DC19C /* NSE */, 1803CD2B96BF06009334BB61 /* PreviewTests */, + D0111119CDF3E28E6D7768E8 /* ShareExtension */, 681566846AF307E9BA4C72C6 /* Products */, ); sourceTree = ""; }; + 40D9A816C45E0278C29DF883 /* SupportingFiles */ = { + isa = PBXGroup; + children = ( + 505ADA084C0B38A0C4AD2574 /* Info.plist */, + B88CE0A058727BC68EEEC6B6 /* ShareExtension.entitlements */, + 203D1ACC20287F8986C959D3 /* target.yml */, + ); + path = SupportingFiles; + sourceTree = ""; + }; 40E6246F03D1FE377BC5D963 /* Room */ = { isa = PBXGroup; children = ( @@ -3518,11 +3601,18 @@ path = BugReportScreen; sourceTree = ""; }; + 557C534BD2052BFFD810CE3D /* ShareExtension */ = { + isa = PBXGroup; + children = ( + DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */, + ); + path = ShareExtension; + sourceTree = ""; + }; 566F2B84465726112B830CF6 /* Other */ = { isa = PBXGroup; children = ( 4959CECEC984B3995616F427 /* DataProtectionManager.swift */, - D3D455BC2423D911A62ACFB2 /* NSELogger.swift */, EEAA2832D93EC7D2608703FB /* NSEUserSession.swift */, 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */, ); @@ -3698,6 +3788,7 @@ 9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */, 0D8F620C8B314840D8602E3F /* NSE.appex */, D95E8C0EFEC0C6F96EDAA71A /* PreviewTests.xctest */, + 3D8BEEFCA07BEA43F4F4BF77 /* ShareExtension.appex */, F506C6ADB1E1DA6638078E11 /* UITests.xctest */, AAC9344689121887B74877AF /* UnitTests.xctest */, ); @@ -4144,6 +4235,14 @@ path = Replies; sourceTree = ""; }; + 7FF02C3DED8CD9890375D9FF /* View */ = { + isa = PBXGroup; + children = ( + 3C3ADF21BE301D0DA48F2A7E /* ShareExtensionView.swift */, + ); + path = View; + sourceTree = ""; + }; 8039515BAA53B7C3275AC64A /* Client */ = { isa = PBXGroup; children = ( @@ -4908,7 +5007,7 @@ children = ( 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */, 3D65BCC659FD9087E49B3C25 /* AppAppearance.swift */, - E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */, + C142248014E08E885E323E56 /* Avatars.swift */, E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */, 9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */, AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */, @@ -5105,6 +5204,15 @@ path = Layout; sourceTree = ""; }; + D0111119CDF3E28E6D7768E8 /* ShareExtension */ = { + isa = PBXGroup; + children = ( + F08E29610C82E4201463C4A5 /* Sources */, + 40D9A816C45E0278C29DF883 /* SupportingFiles */, + ); + path = ShareExtension; + sourceTree = ""; + }; D382E465AF067C1BF888BF8E /* View */ = { isa = PBXGroup; children = ( @@ -5295,6 +5403,7 @@ D57B3BC211BB74420C9138D7 /* RoomPollsHistoryScreen */, 7B890CCD20B037760BFDF957 /* RoomRolesAndPermissionsScreen */, 679E9837ECA8D6776079D16E /* RoomScreen */, + 2E42D43DB6835A58D88B2F91 /* RoomSelectionScreen */, 2565414373E6F68005966B8E /* SecureBackup */, 70B74A432C241E56A7ACE610 /* Settings */, EC4545C7E37E8294D3FE6800 /* StartChatScreen */, @@ -5334,6 +5443,7 @@ 22F9F1514B91803BB4B88894 /* AppHooks */, 337015ADFBA3AB96660DB3A6 /* Generated */, 31CE4DA53232AA534057F912 /* Mocks */, + 557C534BD2052BFFD810CE3D /* ShareExtension */, 4C826614718790C58C17117F /* UnitTests */, ); path = Sources; @@ -5426,6 +5536,15 @@ path = BlockedUsersScreen; sourceTree = ""; }; + F08E29610C82E4201463C4A5 /* Sources */ = { + isa = PBXGroup; + children = ( + 3D27299A36536DBF91AE8FA6 /* ShareExtensionViewController.swift */, + 7FF02C3DED8CD9890375D9FF /* View */, + ); + path = Sources; + sourceTree = ""; + }; F12966DF3DA87FEF21348D60 /* InviteUsersScreen */ = { isa = PBXGroup; children = ( @@ -5567,6 +5686,14 @@ path = View; sourceTree = ""; }; + FF654D7FD6693839E3185FAD /* View */ = { + isa = PBXGroup; + children = ( + D879DC5515B1D42577F96C94 /* RoomSelectionScreen.swift */, + ); + path = View; + sourceTree = ""; + }; FFD7C58CA6A7D6BBC2F584B5 /* JoinRoomScreen */ = { isa = PBXGroup; children = ( @@ -5616,6 +5743,28 @@ productReference = F506C6ADB1E1DA6638078E11 /* UITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + 19F0C845D67E9BEA4BE7133E /* ShareExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = A60414DDC2A95B206C91D4A4 /* Build configuration list for PBXNativeTarget "ShareExtension" */; + buildPhases = ( + 8431C24C3AE0AA27308F4185 /* Sources */, + 087F14F27D0A6FDFB80392A1 /* Resources */, + 7E8EB7CD881C54161D4474E5 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ShareExtension; + packageProductDependencies = ( + C79D91A7F9F378CECEF64B5A /* MatrixRustSDK */, + CAA3B9DF998B397C9EE64E8B /* Collections */, + 3262F08E1C3483C22A7A319F /* Compound */, + ); + productName = ShareExtension; + productReference = 3D8BEEFCA07BEA43F4F4BF77 /* ShareExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 32C23C8D224D46EFE62AFAD0 /* UnitTests */ = { isa = PBXNativeTarget; buildConfigurationList = 79663128986C62EFAC289176 /* Build configuration list for PBXNativeTarget "UnitTests" */; @@ -5672,6 +5821,7 @@ ); dependencies = ( 2C29670603B37E38705D5FF1 /* PBXTargetDependency */, + 58C473A5DEA945AACFEA8E9F /* PBXTargetDependency */, ); name = ElementX; packageProductDependencies = ( @@ -5774,6 +5924,9 @@ DevelopmentTeam = 7J4U792NQT; TestTargetID = C0FAEB81CFD9776CD78CE489; }; + 19F0C845D67E9BEA4BE7133E = { + DevelopmentTeam = "$(DEVELOPMENT_TEAM)"; + }; 32C23C8D224D46EFE62AFAD0 = { DevelopmentTeam = 7J4U792NQT; }; @@ -5862,6 +6015,7 @@ FEB53A5BC378C913769656D8 /* NSE */, F8E276FD6DC43EADB85241BC /* Periphery */, 7A17BE29BAC81ADBAC6349D9 /* PreviewTests */, + 19F0C845D67E9BEA4BE7133E /* ShareExtension */, 0E28CD62691FDBC63147D5E3 /* UITests */, 32C23C8D224D46EFE62AFAD0 /* UnitTests */, ); @@ -5869,6 +6023,14 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 087F14F27D0A6FDFB80392A1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D8F1462EA00AFC939FF9ACCA /* target.yml in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 215E1D91B98672C856F559D0 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -6071,7 +6233,7 @@ 484202C5D50983442D24D061 /* AttributedString.swift in Sources */, CDCA8A559E098503DDE29477 /* AttributedStringBuilder.swift in Sources */, BA43D782BE85C7F5F20C624A /* AttributedStringBuilderProtocol.swift in Sources */, - 968A5B890004526AB58A217C /* AvatarSize.swift in Sources */, + F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */, 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */, 238D561CA231339C6D4D06F3 /* ClientBuilder.swift in Sources */, 0BAF83521871E69D222EE8E4 /* ClientBuilderHook.swift in Sources */, @@ -6081,6 +6243,7 @@ 24A75F72EEB7561B82D726FD /* Date.swift in Sources */, 9F11B9F347F9E2D236799FB3 /* ElementCallServiceConstants.swift in Sources */, CFEC53440C572CEEABC4A6A0 /* ElementXAttributeScope.swift in Sources */, + 89198AE2649DD77673D5793B /* ExtensionLogger.swift in Sources */, A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */, 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */, EBE13FAB4E29738AC41BD3E5 /* InfoPlistReader.swift in Sources */, @@ -6095,7 +6258,6 @@ 9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */, 7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */, E2DB696117BAEABAD5718023 /* MediaSourceProxy.swift in Sources */, - 5455147CAC63F71E48F7D699 /* NSELogger.swift in Sources */, 30CC4F796B27BE8B1DFDBF5A /* NSEUserSession.swift in Sources */, 1D5DC685CED904386C89B7DA /* NSRegularExpresion.swift in Sources */, 94F0B78928E952689ACDB271 /* NetworkMonitor.swift in Sources */, @@ -6267,6 +6429,30 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8431C24C3AE0AA27308F4185 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B8EC8A544162B0A41B9AB339 /* AppSettings.swift in Sources */, + 2F2906AE9BC3D0E79A6F98F8 /* Bundle.swift in Sources */, + F38D32C1B0232AAFE6A0822C /* ExtensionLogger.swift in Sources */, + C022284E2774A5E1EF683B4D /* FileManager.swift in Sources */, + 05FF0CD80EDAB3A7C0D4700A /* InfoPlistReader.swift in Sources */, + 0638CBDE3098B1C3F23AFCFA /* MXLog.swift in Sources */, + 1A3783005E6945F8583AF997 /* NSItemProvider.swift in Sources */, + BE8E5985771DF9137C6CE89A /* ProcessInfo.swift in Sources */, + 7AED78DC086695E93F0647D2 /* RustTracing.swift in Sources */, + DAF63A9CF9932CA8F6830F11 /* ShareExtensionModels.swift in Sources */, + 5AA81A4E2D40A32A9E7F71F2 /* ShareExtensionView.swift in Sources */, + 5AC5CD6D893073EE4D9A277E /* ShareExtensionViewController.swift in Sources */, + 069358C2C825A19DE6CB127E /* TracingConfiguration.swift in Sources */, + 03BD83E8BDD23AE059802E0D /* UITestsScreenIdentifier.swift in Sources */, + 26252AA9AED64010788F4C26 /* UIView.swift in Sources */, + 66E9202BED03B5BB00E812A1 /* URL.swift in Sources */, + 4E22086585CB3B35FEEFBBB9 /* UserPreference.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9797D588420FCBBC228A63C9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -6371,7 +6557,7 @@ 874FEFB9D4A4AF447E0E086E /* AuthenticationStartScreenViewModelProtocol.swift in Sources */, 6146996D5C4DDD5DA816FC87 /* AuthenticationTextFieldStyle.swift in Sources */, 4AAA8606FBA290E23D15422E /* AvatarHeaderView.swift in Sources */, - D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */, + 1621BF6316FFFEF5AE067C77 /* Avatars.swift in Sources */, 7A25D6926A2C01DB8D0D67A5 /* BadgeLabel.swift in Sources */, A4B0BAD62A12ED76BD611B79 /* BadgeView.swift in Sources */, FC0EEFF630F34899953BB950 /* BigIcon.swift in Sources */, @@ -6509,6 +6695,7 @@ 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, 2F09DF0CB213CAE86A3E3B67 /* EventTimelineItem.swift in Sources */, 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */, + 36206F74DDEBF9BEAF6A6A1F /* ExtensionLogger.swift in Sources */, 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */, D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */, 37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */, @@ -6860,6 +7047,11 @@ 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */, 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */, 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */, + 66832DE7B5C2E861045265DC /* RoomSelectionScreen.swift in Sources */, + 18FDE4ED6D83B0771452B43D /* RoomSelectionScreenCoordinator.swift in Sources */, + 27FEF0F40750465195C9D6D6 /* RoomSelectionScreenModels.swift in Sources */, + 8DCA1F05C3BA6ED826F1599D /* RoomSelectionScreenViewModel.swift in Sources */, + 39DFC4B9EB6A8757210BDEC6 /* RoomSelectionScreenViewModelProtocol.swift in Sources */, 6C34237AFB808E38FC8776B9 /* RoomStateEventStringBuilder.swift in Sources */, 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */, 983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */, @@ -6945,6 +7137,7 @@ B93D7CE520088AD53FA6D53C /* SettingsScreenModels.swift in Sources */, E0B6A569AC3E81D233B43D60 /* SettingsScreenViewModel.swift in Sources */, A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */, + 5D99F63CC88BB29383019FC6 /* ShareExtensionModels.swift in Sources */, 1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */, 274CE3C986841D15FD530BF5 /* ShimmerModifier.swift in Sources */, 77920AFA8091AC6B9F190C90 /* Signposter.swift in Sources */, @@ -7174,6 +7367,11 @@ target = C0FAEB81CFD9776CD78CE489 /* ElementX */; targetProxy = 6848AF4480814C5F810FB7EB /* PBXContainerItemProxy */; }; + 58C473A5DEA945AACFEA8E9F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 19F0C845D67E9BEA4BE7133E /* ShareExtension */; + targetProxy = 52A426E590105174D83B9532 /* PBXContainerItemProxy */; + }; 8E24DC048A099AAFEE13B4F5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C0FAEB81CFD9776CD78CE489 /* ElementX */; @@ -7445,6 +7643,27 @@ }; name = Debug; }; + 7620CDAB1B38B30431DA8878 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = ShareExtension/SupportingFiles/ShareExtension.entitlements; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + INFOPLIST_FILE = ShareExtension/SupportingFiles/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = "$(MARKETING_VERSION)"; + PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.shareextension"; + PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; + PRODUCT_NAME = ShareExtension; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; 7A90A3EBE1ABAB9EAE0952F0 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -7697,6 +7916,27 @@ }; name = Release; }; + E57C898C511BBC8215673DEF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = ShareExtension/SupportingFiles/ShareExtension.entitlements; + CURRENT_PROJECT_VERSION = "$(CURRENT_PROJECT_VERSION)"; + DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + INFOPLIST_FILE = ShareExtension/SupportingFiles/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = "$(MARKETING_VERSION)"; + PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.shareextension"; + PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; + PRODUCT_NAME = ShareExtension; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; F0A74453D306F668178A859E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -7754,6 +7994,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + A60414DDC2A95B206C91D4A4 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7620CDAB1B38B30431DA8878 /* Debug */, + E57C898C511BBC8215673DEF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; B15427F8699AD5A5FC75C17E /* Build configuration list for PBXNativeTarget "ElementX" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -8068,6 +8317,11 @@ package = EC6D0C817B1C21D9D096505A /* XCRemoteSwiftPackageReference "Version" */; productName = Version; }; + 3262F08E1C3483C22A7A319F /* Compound */ = { + isa = XCSwiftPackageProductDependency; + package = F71C70A4404CC6D9C4AF35F2 /* XCRemoteSwiftPackageReference "compound-ios" */; + productName = Compound; + }; 36B7FC232711031AA2B0D188 /* DTCoreText */ = { isa = XCSwiftPackageProductDependency; package = C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */; @@ -8253,11 +8507,21 @@ package = 6FC4820D8D4559CEECA064D7 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; productName = MatrixRustSDK; }; + C79D91A7F9F378CECEF64B5A /* MatrixRustSDK */ = { + isa = XCSwiftPackageProductDependency; + package = 6FC4820D8D4559CEECA064D7 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; + productName = MatrixRustSDK; + }; CA07D57389DACE18AEB6A5E2 /* WysiwygComposer */ = { isa = XCSwiftPackageProductDependency; package = EE40B0E16A55BD23ECBFFD22 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */; productName = WysiwygComposer; }; + CAA3B9DF998B397C9EE64E8B /* Collections */ = { + isa = XCSwiftPackageProductDependency; + package = F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = Collections; + }; CCE5BF78B125320CBF3BB834 /* PostHog */ = { isa = XCSwiftPackageProductDependency; package = 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */; diff --git a/ElementX.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme b/ElementX.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme new file mode 100644 index 0000000000..6bbbe2daaa --- /dev/null +++ b/ElementX.xcodeproj/xcshareddata/xcschemes/ShareExtension.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index d86e6ca0a8..5d4ad70d7e 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -235,6 +235,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg } else { handleAppRoute(.childEventOnRoomAlias(eventID: eventID, alias: alias)) } + case .share: + guard isExternalURL else { + MXLog.error("Received unexpected internal share route") + break + } + handleAppRoute(route) default: break } diff --git a/ElementX/Sources/Application/Navigation/AppRoutes.swift b/ElementX/Sources/Application/Navigation/AppRoutes.swift index d90b9282a5..842bb436b0 100644 --- a/ElementX/Sources/Application/Navigation/AppRoutes.swift +++ b/ElementX/Sources/Application/Navigation/AppRoutes.swift @@ -8,7 +8,7 @@ import Foundation import MatrixRustSDK -enum AppRoute: Equatable { +enum AppRoute: Equatable, Hashable { /// The app's home screen. case roomList /// A room, shown as the root of the stack (popping any child rooms). @@ -41,6 +41,8 @@ enum AppRoute: Equatable { case settings /// The setting screen for key backup. case chatBackupSettings + /// An external share request e.g. from the ShareExtension + case share(ShareExtensionPayload) } struct AppRouteURLParser { @@ -48,6 +50,7 @@ struct AppRouteURLParser { init(appSettings: AppSettings) { urlParsers = [ + AppGroupURLParser(), MatrixPermalinkParser(), ElementWebURLParser(domains: appSettings.elementWebHosts), ElementCallURLParser() @@ -73,6 +76,30 @@ protocol URLParser { func route(from url: URL) -> AppRoute? } +struct AppGroupURLParser: URLParser { + func route(from url: URL) -> AppRoute? { + guard let scheme = url.scheme, + scheme == InfoPlistReader.app.appScheme, + url.pathComponents.last == ShareExtensionConstants.urlPath else { + return nil + } + + guard let query = url.query(percentEncoded: false), + let queryData = query.data(using: .utf8) else { + MXLog.error("Failed processing share parameters") + return nil + } + + do { + let payload = try JSONDecoder().decode(ShareExtensionPayload.self, from: queryData) + return .share(payload) + } catch { + MXLog.error("Failed decoding share payload with error: \(error)") + return nil + } + } +} + /// The parser for Element Call links. This always returns a `.genericCallLink`. struct ElementCallURLParser: URLParser { private let knownHosts = ["call.element.io"] diff --git a/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift index af620ef40b..d8be441aca 100644 --- a/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift @@ -83,7 +83,7 @@ class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol { case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias, .roomDetails, .roomMemberDetails, .userProfile, .event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias, - .call, .genericCallLink, .settings: + .call, .genericCallLink, .settings, .share: // These routes aren't in this flow so clear the entire stack. clearRoute(animated: animated) case .chatBackupSettings: diff --git a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift index cea5f952c5..8fd08c9223 100644 --- a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift @@ -293,9 +293,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { let coordinator = SecureBackupRecoveryKeyScreenCoordinator(parameters: parameters) coordinator.actions - .sink { [weak self] action in - guard let self else { return } - + .sink { action in switch action { case .complete: break // Moving to next state is Handled by the global session verification listener diff --git a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift index f900329307..fcaf4fb7dc 100644 --- a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift @@ -65,7 +65,9 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) - guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) else { + guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, + timelineItemFactory: timelineItemFactory, + mediaProvider: userSession.mediaProvider) else { fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil") } diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index fd84208489..51163c8c43 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -34,6 +34,8 @@ enum RoomFlowCoordinatorEntryPoint: Hashable { case eventID(String) /// The flow will start by showing the room's details. case roomDetails + /// An external media share request + case share(ShareExtensionPayload) var isEventID: Bool { guard case .eventID = self else { return false } @@ -41,6 +43,32 @@ enum RoomFlowCoordinatorEntryPoint: Hashable { } } +struct FocusEvent: Hashable { + /// The event ID that the timeline should be focussed around + let eventID: String + /// if the focus is coming from the pinned timeline, this should also update the pin banner + let shouldSetPin: Bool +} + +private enum PinnedEventsTimelineSource: Hashable { + case room + case details(isRoot: Bool) +} + +private enum PresentationAction: Hashable { + case eventFocus(FocusEvent) + case share(ShareExtensionPayload) + + var focusedEvent: FocusEvent? { + switch self { + case .eventFocus(let focusEvent): + return focusEvent + default: + return nil + } + } +} + // swiftlint:disable:next type_body_length class RoomFlowCoordinator: FlowCoordinatorProtocol { private let roomID: String @@ -112,6 +140,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { fatalError("This flow coordinator expect a route") } + // swiftlint:disable:next cyclomatic_complexity func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { guard stateMachine.state != .complete else { fatalError("This flow coordinator is `finished` ☠️") @@ -152,7 +181,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated)) } case .event(let eventID, let roomID, let via): - Task { await handleRoomRoute(roomID: roomID, via: via, focussedEventID: eventID, animated: animated) } + Task { + await handleRoomRoute(roomID: roomID, + via: via, + presentationAction: .eventFocus(.init(eventID: eventID, shouldSetPin: false)), + animated: animated) + } case .childEvent(let eventID, let roomID, let via): if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator { childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated) @@ -161,6 +195,21 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } else { roomScreenCoordinator?.focusOnEvent(.init(eventID: eventID, shouldSetPin: false)) } + case .share(let payload): + guard case let .mediaFile(roomID, _) = payload else { + return + } + + guard let roomID, roomID == self.roomID else { + fatalError("Navigation route doesn't belong to this room flow.") + } + + Task { + await handleRoomRoute(roomID: roomID, + via: [], + presentationAction: .share(payload), + animated: animated) + } case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias: break // These are converted to a room ID route one level above. case .roomList, .userProfile, .call, .genericCallLink, .settings, .chatBackupSettings: @@ -176,7 +225,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) } - private func handleRoomRoute(roomID: String, via: [String], focussedEventID: String? = nil, animated: Bool) async { + private func handleRoomRoute(roomID: String, via: [String], presentationAction: PresentationAction? = nil, animated: Bool) async { guard roomID == self.roomID else { fatalError("Navigation route doesn't belong to this room flow.") } guard let room = await userSession.clientProxy.roomForIdentifier(roomID) else { @@ -187,8 +236,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { switch room { case .joined(let roomProxy): await storeAndSubscribeToRoomProxy(roomProxy) - let focussedEvent = focussedEventID.map { FocusEvent(eventID: $0, shouldSetPin: false) } - stateMachine.tryEvent(.presentRoom(focussedEvent: focussedEvent), userInfo: EventUserInfo(animated: animated)) + stateMachine.tryEvent(.presentRoom(presentationAction: presentationAction), userInfo: EventUserInfo(animated: animated)) default: stateMachine.tryEvent(.presentJoinRoomScreen(via: via), userInfo: EventUserInfo(animated: animated)) } @@ -376,9 +424,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { presentJoinRoomScreen(via: via, animated: true) case (_, .dismissJoinRoomScreen, .complete): dismissFlow(animated: animated) - - case (_, .presentRoom(let focussedEvent), .room): - Task { await self.presentRoom(fromState: context.fromState, focussedEvent: focussedEvent, animated: animated) } + + case (_, .presentRoom(let presentationAction), .room): + Task { + await self.presentRoom(fromState: context.fromState, + presentationAction: presentationAction, + animated: animated) + } case (_, .dismissFlow, .complete): dismissFlow(animated: animated) @@ -445,9 +497,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { break case (.mediaUploadPicker, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL)): - presentMediaUploadPreviewScreen(for: fileURL) + presentMediaUploadPreviewScreen(for: fileURL, animated: animated) case (.room, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL)): - presentMediaUploadPreviewScreen(for: fileURL) + presentMediaUploadPreviewScreen(for: fileURL, animated: animated) case (.mediaUploadPreview, .dismissMediaUploadPreview, .room): break @@ -542,7 +594,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { /// - fromState: The state that asked for the room presentation. /// - focussedEvent: An (optional) struct that contains the event ID that the timeline should be focussed around, and a boolean telling if such event should update the pinned events banner /// - animated: whether it should animate the transition - private func presentRoom(fromState: State, focussedEvent: FocusEvent?, animated: Bool) async { + private func presentRoom(fromState: State, presentationAction: PresentationAction?, animated: Bool) async { // If any sheets are presented dismiss them, rely on their dismissal callbacks to transition the state machine // through the correct states before presenting the room navigationStackCoordinator.setSheetCoordinator(nil) @@ -559,9 +611,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { default: // The room is already on the stack, no need to present it again - // Check if we need to focus on an event - if let focussedEvent { - roomScreenCoordinator?.focusOnEvent(focussedEvent) + switch presentationAction { + case .eventFocus(let focusedEvent): + roomScreenCoordinator?.focusOnEvent(focusedEvent) + case .share(.mediaFile(_, let mediaFile)): + stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url)) + default: + break } return @@ -580,8 +636,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, - initialFocussedEventID: focussedEvent?.eventID, - timelineItemFactory: timelineItemFactory) + initialFocussedEventID: presentationAction?.focusedEvent?.eventID, + timelineItemFactory: timelineItemFactory, + mediaProvider: userSession.mediaProvider) self.timelineController = timelineController analytics.trackViewRoom(isDM: roomProxy.infoPublisher.value.isDirect, isSpace: roomProxy.infoPublisher.value.isSpace) @@ -592,7 +649,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let parameters = RoomScreenCoordinatorParameters(clientProxy: userSession.clientProxy, roomProxy: roomProxy, - focussedEvent: focussedEvent, + focussedEvent: presentationAction?.focusedEvent, timelineController: timelineController, mediaProvider: userSession.mediaProvider, mediaPlayerProvider: MediaPlayerProvider(), @@ -655,6 +712,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { self?.stateMachine.tryEvent(.dismissFlow) } } + + switch presentationAction { + case .share(.mediaFile(_, let mediaFile)): + stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: mediaFile.url), userInfo: EventUserInfo(animated: animated)) + default: + break + } } private func presentJoinRoomScreen(via: [String], animated: Bool) { @@ -679,7 +743,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) { await storeAndSubscribeToRoomProxy(roomProxy) - stateMachine.tryEvent(.presentRoom(focussedEvent: nil), userInfo: EventUserInfo(animated: animated)) + stateMachine.tryEvent(.presentRoom(presentationAction: nil), userInfo: EventUserInfo(animated: animated)) analytics.trackJoinedRoom(isDM: roomProxy.infoPublisher.value.isDirect, isSpace: roomProxy.infoPublisher.value.isSpace, @@ -894,7 +958,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } - private func presentMediaUploadPreviewScreen(for url: URL) { + private func presentMediaUploadPreviewScreen(for url: URL, animated: Bool) { let stackCoordinator = NavigationStackCoordinator() let parameters = MediaUploadPreviewScreenCoordinatorParameters(userIndicatorController: userIndicatorController, @@ -918,7 +982,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stackCoordinator.setRootCoordinator(mediaUploadPreviewScreenCoordinator) - navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in + navigationStackCoordinator.setSheetCoordinator(stackCoordinator, animated: animated) { [weak self] in self?.stateMachine.tryEvent(.dismissMediaUploadPreview) } } @@ -1091,7 +1155,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let roomTimelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, initialFocussedEventID: nil, - timelineItemFactory: timelineItemFactory) + timelineItemFactory: timelineItemFactory, + mediaProvider: userSession.mediaProvider) let parameters = RoomPollsHistoryScreenCoordinatorParameters(pollInteractionHandler: PollInteractionHandler(analyticsService: analytics, roomProxy: roomProxy), roomTimelineController: roomTimelineController) @@ -1361,7 +1426,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room)) case .displayRoomScreenWithFocussedPin(let eventID): navigationStackCoordinator.setSheetCoordinator(nil) - stateMachine.tryEvent(.presentRoom(focussedEvent: .init(eventID: eventID, shouldSetPin: true))) + stateMachine.tryEvent(.presentRoom(presentationAction: .eventFocus(.init(eventID: eventID, shouldSetPin: true)))) } } .store(in: &cancellables) @@ -1427,6 +1492,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { coordinator.handleAppRoute(.event(eventID: eventID, roomID: roomID, via: via), animated: true) case .roomDetails: coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: true) + case .share(let payload): + coordinator.handleAppRoute(.share(payload), animated: true) } } } @@ -1483,7 +1550,7 @@ private extension RoomFlowCoordinator { case presentJoinRoomScreen(via: [String]) case dismissJoinRoomScreen - case presentRoom(focussedEvent: FocusEvent?) + case presentRoom(presentationAction: PresentationAction?) case dismissFlow case presentReportContent(itemID: TimelineItemIdentifier, senderID: String) @@ -1559,15 +1626,3 @@ private extension Result { } } } - -private enum PinnedEventsTimelineSource: Hashable { - case room - case details(isRoot: Bool) -} - -struct FocusEvent: Hashable { - /// The event ID that the timeline should be focussed around - let eventID: String - /// if the focus is coming from the pinned timeline, this should also update the pin banner - let shouldSetPin: Bool -} diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index ff62ac4e2b..d6ff185383 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -206,6 +206,18 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { presentCallScreen(genericCallLink: url) case .settings, .chatBackupSettings: settingsFlowCoordinator.handleAppRoute(appRoute, animated: animated) + case .share(let payload): + switch payload { + case .mediaFile(let roomID, _): + if let roomID { + stateMachine.processEvent(.selectRoom(roomID: roomID, + via: [], + entryPoint: .share(payload)), + userInfo: .init(animated: animated)) + } else { + stateMachine.processEvent(.showShareExtensionRoomList(sharePayload: payload), userInfo: .init(animated: animated)) + } + } } } @@ -243,6 +255,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case .room: .room(roomID: roomID, via: via) case .roomDetails: .roomDetails(roomID: roomID) case .eventID(let eventID): .event(eventID: eventID, roomID: roomID, via: via) // ignored. + case .share(let payload): .share(payload) } roomFlowCoordinator.handleAppRoute(route, animated: animated) } else { @@ -296,6 +309,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case (.userProfileScreen, .dismissedUserProfileScreen, .roomList): break + case (.roomList, .showShareExtensionRoomList, .shareExtensionRoomList(let sharePayload)): + clearRoute(animated: animated) + presentRoomSelectionScreen(sharePayload: sharePayload, animated: animated) + case (.shareExtensionRoomList, .dismissedShareExtensionRoomList, .roomList): + dismissRoomSelectionScreen() + default: fatalError("Unknown transition: \(context)") } @@ -583,6 +602,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { coordinator.handleAppRoute(.event(eventID: eventID, roomID: roomID, via: via), animated: animated) case .roomDetails: coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: animated) + case .share(let payload): + coordinator.handleAppRoute(.share(payload), animated: animated) } Task { @@ -894,6 +915,52 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { } } + // MARK: Sharing + + private func presentRoomSelectionScreen(sharePayload: ShareExtensionPayload, animated: Bool) { + guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider else { + fatalError() + } + + let stackCoordinator = NavigationStackCoordinator() + + let coordinator = RoomSelectionScreenCoordinator(parameters: .init(clientProxy: userSession.clientProxy, + roomSummaryProvider: roomSummaryProvider, + mediaProvider: userSession.mediaProvider)) + + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + + switch action { + case .dismiss: + navigationSplitCoordinator.setSheetCoordinator(nil) + case .confirm(let roomID): + let sharePayload = switch sharePayload { + case .mediaFile(_, let mediaFile): + ShareExtensionPayload.mediaFile(roomID: roomID, mediaFile: mediaFile) + } + + navigationSplitCoordinator.setSheetCoordinator(nil) + + stateMachine.processEvent(.selectRoom(roomID: roomID, + via: [], + entryPoint: .share(sharePayload)), + userInfo: .init(animated: animated)) + } + } + .store(in: &cancellables) + + stackCoordinator.setRootCoordinator(coordinator) + + navigationSplitCoordinator.setSheetCoordinator(stackCoordinator, animated: animated) { [weak self] in + self?.stateMachine.processEvent(.dismissedShareExtensionRoomList) + } + } + + private func dismissRoomSelectionScreen() { + navigationSplitCoordinator.setSheetCoordinator(nil) + } + // MARK: Toasts and loading indicators private static let loadingIndicatorIdentifier = "\(UserSessionFlowCoordinator.self)-Loading" diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift index 5fe2d18c6f..c07dc5adad 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift @@ -42,10 +42,12 @@ class UserSessionFlowCoordinatorStateMachine { /// Showing the user profile screen. This screen clears the navigation. case userProfileScreen + case shareExtensionRoomList(sharePayload: ShareExtensionPayload) + /// The selected room ID from the state if available. var selectedRoomID: String? { switch self { - case .initial, .userProfileScreen: + case .initial, .userProfileScreen, .shareExtensionRoomList: nil case .roomList(let selectedRoomID), .feedbackScreen(let selectedRoomID), @@ -116,6 +118,9 @@ class UserSessionFlowCoordinatorStateMachine { case showUserProfileScreen(userID: String) /// The user profile screen has been dismissed. case dismissedUserProfileScreen + + case showShareExtensionRoomList(sharePayload: ShareExtensionPayload) + case dismissedShareExtensionRoomList } private let stateMachine: StateMachine @@ -184,6 +189,11 @@ class UserSessionFlowCoordinatorStateMachine { case (.userProfileScreen, .dismissedUserProfileScreen): return .roomList(selectedRoomID: nil) + case (.roomList, .showShareExtensionRoomList(let sharePayload)): + return .shareExtensionRoomList(sharePayload: sharePayload) + case (.shareExtensionRoomList, .dismissedShareExtensionRoomList): + return .roomList(selectedRoomID: nil) + default: return nil } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 51c386fda5..6dac05ec44 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -12939,15 +12939,15 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { //MARK: - buildRoomTimelineController - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = 0 - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount: Int { + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0 + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount: Int { get { if Thread.isMainThread { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount + returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingCallsCount } return returnValue! @@ -12955,29 +12955,29 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue } } } } - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCalled: Bool { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount > 0 + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCalled: Bool { + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount > 0 } - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)? - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = [] + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)? + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = [] - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol! - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol! { + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingReturnValue: RoomTimelineControllerProtocol! + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReturnValue: RoomTimelineControllerProtocol! { get { if Thread.isMainThread { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingReturnValue } else { var returnValue: RoomTimelineControllerProtocol? = nil DispatchQueue.main.sync { - returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue + returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingReturnValue } return returnValue! @@ -12985,39 +12985,39 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue } } } } - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure: ((JoinedRoomProxyProtocol, String?, RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol)? + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderClosure: ((JoinedRoomProxyProtocol, String?, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) -> RoomTimelineControllerProtocol)? - func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount += 1 - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments = (roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory) + func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol { + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount += 1 + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments = (roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider) DispatchQueue.main.async { - self.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory)) + self.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedInvocations.append((roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)) } - if let buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure(roomProxy, initialFocussedEventID, timelineItemFactory) + if let buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderClosure = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderClosure { + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderClosure(roomProxy, initialFocussedEventID, timelineItemFactory, mediaProvider) } else { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReturnValue } } //MARK: - buildRoomPinnedTimelineController - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = 0 - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCallsCount: Int { + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = 0 + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount: Int { get { if Thread.isMainThread { - return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount + returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount } return returnValue! @@ -13025,29 +13025,29 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingCallsCount = newValue } } } } - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCalled: Bool { - return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCallsCount > 0 + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCalled: Bool { + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount > 0 } - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol)? - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = [] + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)? + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = [] - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol? - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol? { + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: RoomTimelineControllerProtocol? + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue: RoomTimelineControllerProtocol? { get { if Thread.isMainThread { - return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue } else { var returnValue: RoomTimelineControllerProtocol?? = nil DispatchQueue.main.sync { - returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue + returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue } return returnValue! @@ -13055,26 +13055,26 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue = newValue + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue = newValue + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue = newValue } } } } - var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure: ((JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol?)? + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure: ((JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> RoomTimelineControllerProtocol?)? - func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? { - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCallsCount += 1 - buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedArguments = (roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) + func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? { + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1 + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider) DispatchQueue.main.async { - self.buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, timelineItemFactory: timelineItemFactory)) + self.buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations.append((roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)) } - if let buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure { - return await buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure(roomProxy, timelineItemFactory) + if let buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure { + return await buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure(roomProxy, timelineItemFactory, mediaProvider) } else { - return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReturnValue + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue } } } diff --git a/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift b/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift index 6dd55f8e35..8c99f36ec7 100644 --- a/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift +++ b/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift @@ -15,7 +15,7 @@ extension RoomTimelineControllerFactoryMock { convenience init(configuration: RoomTimelineControllerFactoryMockConfiguration) { self.init() - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue = configuration.timelineController ?? { + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReturnValue = configuration.timelineController ?? { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk return timelineController diff --git a/ElementX/Sources/Other/AvatarSize.swift b/ElementX/Sources/Other/Avatars.swift similarity index 55% rename from ElementX/Sources/Other/AvatarSize.swift rename to ElementX/Sources/Other/Avatars.swift index 4aec55c4c1..fab60ce616 100644 --- a/ElementX/Sources/Other/AvatarSize.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -6,29 +6,55 @@ // import Foundation -import UIKit +import SwiftUI -enum AvatarSize { - case user(on: UserAvatarSizeOnScreen) - case room(on: RoomAvatarSizeOnScreen) - // custom - case custom(CGFloat) +enum Avatars { + enum Size { + case user(on: UserAvatarSizeOnScreen) + case room(on: RoomAvatarSizeOnScreen) + // custom + case custom(CGFloat) - /// Value in UIKit points - var value: CGFloat { - switch self { - case .user(let screen): - return screen.value - case .room(let screen): - return screen.value - case .custom(let val): - return val + /// Value in UIKit points + var value: CGFloat { + switch self { + case .user(let screen): + return screen.value + case .room(let screen): + return screen.value + case .custom(let val): + return val + } } - } - /// Value in pixels by using the scale of the main screen - var scaledValue: CGFloat { - value * UIScreen.main.scale + /// Value in pixels by using the scale of the main screen + var scaledValue: CGFloat { + value * UIScreen.main.scale + } + + var scaledSize: CGSize { + CGSize(width: scaledValue, height: scaledValue) + } + } + + @MainActor + static func generatePlaceholderAvatarImageData(name: String, id: String, size: CGSize) -> Data? { + let image = PlaceholderAvatarImage(name: name, contentID: id) + .clipShape(Circle()) + .frame(width: size.width, height: size.height) + + let renderer = ImageRenderer(content: image) + + // Specify the scale so the image is rendered correctly. We don't have access to the screen + // here so a hardcoded 3.0 will have to do + renderer.scale = 3.0 + + guard let image = renderer.uiImage else { + MXLog.info("Generating notification icon placeholder failed") + return nil + } + + return image.pngData() } } @@ -87,6 +113,7 @@ enum RoomAvatarSizeOnScreen { case home case messageForwarding case globalSearch + case roomSelection case details case notificationSettings case roomDirectorySearch @@ -104,6 +131,8 @@ enum RoomAvatarSizeOnScreen { return 36 case .globalSearch: return 36 + case .roomSelection: + return 36 case .home: return 52 case .details: @@ -113,9 +142,3 @@ enum RoomAvatarSizeOnScreen { } } } - -extension AvatarSize { - var scaledSize: CGSize { - CGSize(width: scaledValue, height: scaledValue) - } -} diff --git a/ElementX/Sources/Other/Extensions/FileManager.swift b/ElementX/Sources/Other/Extensions/FileManager.swift index 0709dec3a4..a969d63265 100644 --- a/ElementX/Sources/Other/Extensions/FileManager.swift +++ b/ElementX/Sources/Other/Extensions/FileManager.swift @@ -38,7 +38,7 @@ extension FileManager { @discardableResult func writeDataToTemporaryDirectory(data: Data, fileName: String) throws -> URL { - let newURL = URL.temporaryDirectory.appendingPathComponent(fileName) + let newURL = URL.appGroupTemporaryDirectory.appendingPathComponent(fileName) try data.write(to: newURL) diff --git a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift index cc77eb22df..f9a45d8d77 100644 --- a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift +++ b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift @@ -183,37 +183,17 @@ extension UNMutableNotificationContent { @MainActor private func getPlaceholderAvatarImageData(name: String, id: String) async -> Data? { // The version value is used in case the design of the placeholder is updated to force a replacement - let shouldFlipAvatar = shouldFlipAvatar() - let prefix = "notification_placeholder\(shouldFlipAvatar ? "V9F" : "V9")" + let prefix = "notification_placeholderV9" + let fileName = "\(prefix)_\(name)_\(id).png" if let data = try? Data(contentsOf: URL.temporaryDirectory.appendingPathComponent(fileName)) { MXLog.info("Found existing notification icon placeholder") return data } - - MXLog.info("Generating notification icon placeholder") - let image = PlaceholderAvatarImage(name: name, - contentID: id) - .clipShape(Circle()) - .frame(width: 50, height: 50) - let renderer = ImageRenderer(content: image) - - // Specify the scale so the image is rendered correctly. We don't have access to the screen - // here so a hardcoded 3.0 will have to do - renderer.scale = 3.0 - - guard let image = renderer.uiImage else { - MXLog.info("Generating notification icon placeholder failed") - return nil - } - let data: Data? + MXLog.info("Generating notification icon placeholder") - if shouldFlipAvatar { - data = image.flippedVertically().pngData() - } else { - data = image.pngData() - } + let data = Avatars.generatePlaceholderAvatarImageData(name: name, id: id, size: .init(width: 50, height: 50)) if let data { do { @@ -224,44 +204,7 @@ extension UNMutableNotificationContent { return data } } - return data - } - - /// On simulators and macOS the image is rendered correctly - /// On devices before iOS 17 and iOS 17.2.0 it's rendered upside down and needs to be flipped - /// On all other versions it's rendered correctly and **doesn't** need to be flipped - private func shouldFlipAvatar() -> Bool { - #if targetEnvironment(simulator) - return false - #else - if ProcessInfo.processInfo.isiOSAppOnMac { - return false - } - - guard let version = Version(UIDevice.current.systemVersion) else { - return false - } - - if version < Version(17, 0, 0) { - return true - } - - if version == Version(17, 2, 0) { - return true - } - return false - #endif - } -} - -private extension UIImage { - func flippedVertically() -> UIImage { - let format = UIGraphicsImageRendererFormat() - format.scale = scale - return UIGraphicsImageRenderer(size: size, format: format).image { context in - context.cgContext.concatenate(CGAffineTransform(scaleX: 1, y: -1)) - self.draw(at: CGPoint(x: 0, y: -size.height)) - } + return data } } diff --git a/ElementX/Sources/Other/Extensions/URL.swift b/ElementX/Sources/Other/Extensions/URL.swift index 3506d46c5f..fc26b266b5 100644 --- a/ElementX/Sources/Other/Extensions/URL.swift +++ b/ElementX/Sources/Other/Extensions/URL.swift @@ -60,7 +60,7 @@ extension URL: @retroactive ExpressibleByStringLiteral { } /// The base directory where all application support data is stored. - static var cachesBaseDirectory: URL { + static var sessionCachesBaseDirectory: URL { let url = appGroupContainerDirectory .appendingPathComponent("Library", isDirectory: true) .appendingPathComponent("Caches", isDirectory: true) @@ -69,7 +69,20 @@ extension URL: @retroactive ExpressibleByStringLiteral { try? FileManager.default.createDirectoryIfNeeded(at: url) - // Caches are excluded from backups automatically anyway. + // Caches are excluded from backups automatically. + // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html + + return url + } + + /// The app group temporary directory + static var appGroupTemporaryDirectory: URL { + let url = appGroupContainerDirectory + .appendingPathComponent("tmp", isDirectory: true) + + try? FileManager.default.createDirectoryIfNeeded(at: url) + + // Temporary files are excluded from backups automatically. // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html return url diff --git a/NSE/Sources/Other/NSELogger.swift b/ElementX/Sources/Other/Logging/ExtensionLogger.swift similarity index 93% rename from NSE/Sources/Other/NSELogger.swift rename to ElementX/Sources/Other/Logging/ExtensionLogger.swift index 985038136a..3367d1c7ee 100644 --- a/NSE/Sources/Other/NSELogger.swift +++ b/ElementX/Sources/Other/Logging/ExtensionLogger.swift @@ -8,7 +8,7 @@ import Foundation import MatrixRustSDK -enum NSELogger { +enum ExtensionLogger { private static var isConfigured = false /// Memory formatter, uses exact 2 fraction digits and no grouping @@ -66,13 +66,13 @@ enum NSELogger { return "\(formattedStr) MB" } - static func configure(logLevel: TracingConfiguration.LogLevel) { + static func configure(currentTarget: String, logLevel: TracingConfiguration.LogLevel) { guard !isConfigured else { return } isConfigured = true - MXLog.configure(currentTarget: "nse", filePrefix: "nse", logLevel: logLevel) + MXLog.configure(currentTarget: currentTarget, filePrefix: currentTarget, logLevel: logLevel) } static func logMemory(with tag: String) { diff --git a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift index 2717af4a82..f78421bf94 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift @@ -25,13 +25,13 @@ struct AvatarHeaderView: View { private let subtitle: String? private let badges: [Badge] - private let avatarSize: AvatarSize + private let avatarSize: Avatars.Size private let mediaProvider: MediaProviderProtocol? private var onAvatarTap: ((URL) -> Void)? @ViewBuilder private var footer: () -> Footer init(room: RoomDetails, - avatarSize: AvatarSize, + avatarSize: Avatars.Size, mediaProvider: MediaProviderProtocol? = nil, onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { @@ -72,7 +72,7 @@ struct AvatarHeaderView: View { init(member: RoomMemberDetails, isVerified: Bool = false, - avatarSize: AvatarSize, + avatarSize: Avatars.Size, mediaProvider: MediaProviderProtocol? = nil, onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { @@ -88,7 +88,7 @@ struct AvatarHeaderView: View { init(user: UserProfileProxy, isVerified: Bool, - avatarSize: AvatarSize, + avatarSize: Avatars.Size, mediaProvider: MediaProviderProtocol? = nil, onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { diff --git a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift index 7daf4655de..1c048bd114 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift @@ -11,7 +11,7 @@ struct LoadableAvatarImage: View { private let url: URL? private let name: String? private let contentID: String? - private let avatarSize: AvatarSize + private let avatarSize: Avatars.Size private let mediaProvider: MediaProviderProtocol? private let onTap: ((URL) -> Void)? @@ -19,7 +19,7 @@ struct LoadableAvatarImage: View { init(url: URL?, name: String?, contentID: String?, - avatarSize: AvatarSize, + avatarSize: Avatars.Size, mediaProvider: MediaProviderProtocol?, onTap: ((URL) -> Void)? = nil) { self.url = url diff --git a/ElementX/Sources/Other/SwiftUI/Views/OverridableAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/OverridableAvatarImage.swift index 68137fdad4..7594c5b375 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/OverridableAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/OverridableAvatarImage.swift @@ -12,12 +12,12 @@ struct OverridableAvatarImage: View { private let url: URL? private let name: String? private let contentID: String? - private let avatarSize: AvatarSize + private let avatarSize: Avatars.Size private let mediaProvider: MediaProviderProtocol? @ScaledMetric private var frameSize: CGFloat - init(overrideURL: URL?, url: URL?, name: String?, contentID: String?, avatarSize: AvatarSize, mediaProvider: MediaProviderProtocol?) { + init(overrideURL: URL?, url: URL?, name: String?, contentID: String?, avatarSize: Avatars.Size, mediaProvider: MediaProviderProtocol?) { self.overrideURL = overrideURL self.url = url self.name = name diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift index 17cf55e783..f4b3d2bee1 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift @@ -22,7 +22,7 @@ enum RoomAvatar: Equatable { struct RoomAvatarImage: View { let avatar: RoomAvatar - let avatarSize: AvatarSize + let avatarSize: Avatars.Size let mediaProvider: MediaProviderProtocol? private(set) var onAvatarTap: ((URL) -> Void)? diff --git a/ElementX/Sources/Other/SwiftUI/Views/StackedAvatarsView.swift b/ElementX/Sources/Other/SwiftUI/Views/StackedAvatarsView.swift index b4cf190fef..b2ee66f1ce 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/StackedAvatarsView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/StackedAvatarsView.swift @@ -18,7 +18,7 @@ struct StackedAvatarsView: View { let lineWidth: CGFloat var shouldStackFromLast = false let avatars: [StackedAvatarInfo] - let avatarSize: AvatarSize + let avatarSize: Avatars.Size let mediaProvider: MediaProviderProtocol? var body: some View { diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift index 586a95647a..b6723a4b0a 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift @@ -30,7 +30,7 @@ enum GlobalSearchScreenViewAction { struct GlobalSearchRoom: Identifiable, Equatable { let id: String - let name: String - let alias: String? + let title: String + let description: String let avatar: RoomAvatar } diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift index ed85554fe0..6ea45004a9 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift @@ -36,7 +36,11 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch .map(\.bindings.searchQuery) .removeDuplicates() .sink { [weak self] searchQuery in - self?.roomSummaryProvider.setFilter(.search(query: searchQuery)) + if searchQuery.isEmpty { + self?.roomSummaryProvider.setFilter(.all(filters: [])) + } else { + self?.roomSummaryProvider.setFilter(.search(query: searchQuery)) + } } .store(in: &cancellables) @@ -66,8 +70,8 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch private func updateRooms(with summaries: [RoomSummary]) { state.rooms = summaries.compactMap { summary in GlobalSearchRoom(id: summary.id, - name: summary.name, - alias: summary.canonicalAlias, + title: summary.name, + description: summary.roomListDescription, avatar: summary.avatar) } } diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift index 6a9225facd..354e1cef80 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift @@ -16,8 +16,8 @@ struct GlobalSearchScreenListRow: View { var body: some View { ZStack { // The list row swallows listRowBackgrounds for some reason - ListRow(label: .avatar(title: room.name, - description: room.alias ?? room.id, + ListRow(label: .avatar(title: room.title, + description: room.description, icon: avatar), kind: .label) } @@ -42,8 +42,8 @@ struct GlobalSearchScreenListRow_Previews: PreviewProvider, TestablePreview { static var previews: some View { List { GlobalSearchScreenListRow(room: .init(id: "123", - name: "Tech central", - alias: "The best place in the whole wide world", + title: "Tech central", + description: "The best place in the whole wide world", avatar: .room(id: "123", name: "Tech central", avatarURL: .picturesDirectory)), diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift index 0a459a3f6e..cf0280f96d 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift @@ -33,8 +33,8 @@ enum MessageForwardingScreenViewAction { struct MessageForwardingRoom: Identifiable, Equatable { let id: String - let name: String - let alias: String? + let title: String + let description: String let avatar: RoomAvatar } diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift index ef27e2a2ae..d9ed472c12 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift @@ -45,8 +45,11 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me .map(\.bindings.searchQuery) .removeDuplicates() .sink { [weak self] searchQuery in - guard let self else { return } - self.roomSummaryProvider.setFilter(.search(query: searchQuery)) + if searchQuery.isEmpty { + self?.roomSummaryProvider.setFilter(.all(filters: [])) + } else { + self?.roomSummaryProvider.setFilter(.search(query: searchQuery)) + } } .store(in: &cancellables) @@ -79,8 +82,10 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me continue } - let room = MessageForwardingRoom(id: summary.id, name: summary.name, alias: summary.canonicalAlias, avatar: summary.avatar) - rooms.append(room) + rooms.append(.init(id: summary.id, + title: summary.name, + description: summary.roomListDescription, + avatar: summary.avatar)) } state.rooms = rooms diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift b/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift index 2801f66ef2..552c6c8be7 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift @@ -68,8 +68,8 @@ private struct MessageForwardingListRow: View { let context: MessageForwardingScreenViewModel.Context var body: some View { - ListRow(label: .avatar(title: room.name, - description: room.alias ?? room.id, + ListRow(label: .avatar(title: room.title, + description: room.description, icon: avatar), kind: .selection(isSelected: isSelected) { context.send(viewAction: .selectRoom(roomID: room.id)) diff --git a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenCoordinator.swift b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenCoordinator.swift new file mode 100644 index 0000000000..a89a8b4cc4 --- /dev/null +++ b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenCoordinator.swift @@ -0,0 +1,52 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import SwiftUI + +struct RoomSelectionScreenCoordinatorParameters { + let clientProxy: ClientProxyProtocol + let roomSummaryProvider: RoomSummaryProviderProtocol + let mediaProvider: MediaProviderProtocol +} + +enum RoomSelectionScreenCoordinatorAction { + case dismiss + case confirm(roomID: String) +} + +final class RoomSelectionScreenCoordinator: CoordinatorProtocol { + private var viewModel: RoomSelectionScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: RoomSelectionScreenCoordinatorParameters) { + viewModel = RoomSelectionScreenViewModel(clientProxy: parameters.clientProxy, + roomSummaryProvider: parameters.roomSummaryProvider, + mediaProvider: parameters.mediaProvider) + } + + func start() { + viewModel.actionsPublisher.sink { [weak self] action in + switch action { + case .dismiss: + self?.actionsSubject.send(.dismiss) + case .confirm(let roomID): + self?.actionsSubject.send(.confirm(roomID: roomID)) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(RoomSelectionScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenModels.swift b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenModels.swift new file mode 100644 index 0000000000..1d066b994d --- /dev/null +++ b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenModels.swift @@ -0,0 +1,39 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation +import MatrixRustSDK + +enum RoomSelectionScreenViewModelAction { + case dismiss + case confirm(roomID: String) +} + +struct RoomSelectionScreenViewState: BindableState { + var rooms: [RoomSelectionRoom] = [] + var selectedRoomID: String? + var bindings = RoomSelectionScreenViewStateBindings() +} + +struct RoomSelectionScreenViewStateBindings { + var searchQuery = "" +} + +enum RoomSelectionScreenViewAction { + case cancel + case confirm + case selectRoom(roomID: String) + case reachedTop + case reachedBottom +} + +struct RoomSelectionRoom: Identifiable, Equatable { + let id: String + let title: String + let description: String + let avatar: RoomAvatar +} diff --git a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift new file mode 100644 index 0000000000..3bad9ac694 --- /dev/null +++ b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift @@ -0,0 +1,105 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import SwiftUI + +typealias RoomSelectionScreenViewModelType = StateStoreViewModel + +class RoomSelectionScreenViewModel: RoomSelectionScreenViewModelType, RoomSelectionScreenViewModelProtocol { + private let clientProxy: ClientProxyProtocol + private let roomSummaryProvider: RoomSummaryProviderProtocol + + private var actionsSubject: PassthroughSubject = .init() + + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(clientProxy: ClientProxyProtocol, + roomSummaryProvider: RoomSummaryProviderProtocol, + mediaProvider: MediaProviderProtocol) { + self.clientProxy = clientProxy + self.roomSummaryProvider = roomSummaryProvider + + super.init(initialViewState: RoomSelectionScreenViewState(), mediaProvider: mediaProvider) + + roomSummaryProvider.roomListPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateRooms() + } + .store(in: &cancellables) + + context.$viewState + .map(\.bindings.searchQuery) + .removeDuplicates() + .sink { [weak self] searchQuery in + if searchQuery.isEmpty { + self?.roomSummaryProvider.setFilter(.all(filters: [])) + } else { + self?.roomSummaryProvider.setFilter(.search(query: searchQuery)) + } + } + .store(in: &cancellables) + + updateRooms() + } + + override func process(viewAction: RoomSelectionScreenViewAction) { + switch viewAction { + case .cancel: + actionsSubject.send(.dismiss) + roomSummaryProvider.setFilter(.all(filters: [])) + case .confirm: + guard let selectedRoomID = state.selectedRoomID else { + return + } + + actionsSubject.send(.confirm(roomID: selectedRoomID)) + case .selectRoom(let roomID): + state.selectedRoomID = roomID + case .reachedTop: + updateVisibleRange(edge: .top) + case .reachedBottom: + updateVisibleRange(edge: .bottom) + } + } + + // MARK: - Private + + private func updateRooms() { + var rooms = [RoomSelectionRoom]() + + for summary in roomSummaryProvider.roomListPublisher.value { + rooms.append(.init(id: summary.id, + title: summary.name, + description: summary.roomListDescription, + avatar: summary.avatar)) + } + + state.rooms = rooms + } + + /// The actual range values don't matter as long as they contain the lower + /// or upper bounds. updateVisibleRange is a hybrid API that powers both + /// sliding sync visible range update and list paginations + /// For lists other than the home screen one we don't care about visible ranges, + /// we just need the respective bounds to be there to trigger a next page load or + /// a reset to just one page + private func updateVisibleRange(edge: UIRectEdge) { + switch edge { + case .top: + roomSummaryProvider.updateVisibleRange(0..<0) + case .bottom: + let roomCount = roomSummaryProvider.roomListPublisher.value.count + roomSummaryProvider.updateVisibleRange(roomCount.. { get } + var context: RoomSelectionScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/RoomSelectionScreen/View/RoomSelectionScreen.swift b/ElementX/Sources/Screens/RoomSelectionScreen/View/RoomSelectionScreen.swift new file mode 100644 index 0000000000..8a1ea9113d --- /dev/null +++ b/ElementX/Sources/Screens/RoomSelectionScreen/View/RoomSelectionScreen.swift @@ -0,0 +1,104 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Compound +import SwiftUI + +struct RoomSelectionScreen: View { + @ObservedObject var context: RoomSelectionScreenViewModel.Context + + var body: some View { + Form { + Section { + ForEach(context.viewState.rooms) { room in + RoomSelectionListRow(room: room, + isSelected: context.viewState.selectedRoomID == room.id, + context: context) + } + // Replace these with ScrollView's `scrollPosition` when dropping iOS 16. + } header: { + emptyRectangle + .onAppear { + context.send(viewAction: .reachedTop) + } + } footer: { + emptyRectangle + .onAppear { + context.send(viewAction: .reachedBottom) + } + } + } + .compoundList() + .navigationTitle(L10n.screenRoomlistMainSpaceTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(L10n.actionCancel) { + context.send(viewAction: .cancel) + } + } + ToolbarItem(placement: .confirmationAction) { + Button(L10n.actionShare) { + context.send(viewAction: .confirm) + } + .disabled(context.viewState.selectedRoomID == nil) + } + } + .searchController(query: $context.searchQuery, showsCancelButton: false) + .compoundSearchField() + .disableAutocorrection(true) + } + + /// The greedy size of Rectangle can create an issue with the navigation bar when the search is highlighted, so is best to use a fixed frame instead of hidden() or EmptyView() + private var emptyRectangle: some View { + Rectangle() + .frame(width: 0, height: 0) + } +} + +private struct RoomSelectionListRow: View { + @Environment(\.dynamicTypeSize) var dynamicTypeSize + + let room: RoomSelectionRoom + let isSelected: Bool + let context: RoomSelectionScreenViewModel.Context + + var body: some View { + ListRow(label: .avatar(title: room.title, + description: room.description, + icon: avatar), + kind: .selection(isSelected: isSelected) { + context.send(viewAction: .selectRoom(roomID: room.id)) + }) + } + + @ViewBuilder @MainActor + var avatar: some View { + if dynamicTypeSize < .accessibility3 { + RoomAvatarImage(avatar: room.avatar, + avatarSize: .room(on: .roomSelection), + mediaProvider: context.mediaProvider) + .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) + .accessibilityHidden(true) + } + } +} + +// MARK: - Previews + +struct RoomSelectionScreen_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) + let viewModel = RoomSelectionScreenViewModel(clientProxy: ClientProxyMock(.init()), + roomSummaryProvider: summaryProvider, + mediaProvider: MediaProviderMock(configuration: .init())) + + NavigationStack { + RoomSelectionScreen(context: viewModel.context) + } + } +} diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index edf8200206..c8f948be3a 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -833,7 +833,7 @@ class ClientProxy: ClientProxyProtocol { alternateRoomSummaryProvider = RoomSummaryProvider(roomListService: roomListService, eventStringBuilder: eventStringBuilder, - name: "MessageForwarding", + name: "AlternateAllRooms", notificationSettings: notificationSettings, appSettings: appSettings) try await alternateRoomSummaryProvider?.setRoomList(roomListService.allRooms()) diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift index 429cba58ad..7b9b3a5ed7 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift @@ -69,6 +69,20 @@ extension RoomSummary: CustomStringConvertible { - notificationMode: \(notificationMode?.rawValue ?? "nil") """ } + + /// Used where summaries are shown in a list e.g. message forwarding, + /// global search, share destination list etc. + var roomListDescription: String { + if isDirect { + return canonicalAlias ?? "" + } + + if let alias = canonicalAlias { + return alias + } + + return heroes.compactMap(\.displayName).formatted(.list(type: .and)) + } } extension RoomSummary { diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 09681aa418..50ac8ef321 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -7,6 +7,7 @@ import Combine import Foundation +import IntentsUI import MatrixRustSDK import UIKit @@ -14,7 +15,9 @@ class RoomTimelineController: RoomTimelineControllerProtocol { private let roomProxy: JoinedRoomProxyProtocol private let liveTimelineProvider: RoomTimelineProviderProtocol private let timelineItemFactory: RoomTimelineItemFactoryProtocol + private let mediaProvider: MediaProviderProtocol private let appSettings: AppSettings + private let serialDispatchQueue: DispatchQueue let callbacks = PassthroughSubject() @@ -40,11 +43,14 @@ class RoomTimelineController: RoomTimelineControllerProtocol { timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol, + mediaProvider: MediaProviderProtocol, appSettings: AppSettings) { self.roomProxy = roomProxy liveTimelineProvider = timelineProxy.timelineProvider self.timelineItemFactory = timelineItemFactory + self.mediaProvider = mediaProvider self.appSettings = appSettings + serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility) activeTimeline = timelineProxy @@ -153,11 +159,63 @@ class RoomTimelineController: RoomTimelineControllerProtocol { intentionalMentions: intentionalMentions) { case .success: MXLog.info("Finished sending message") + await donateSendMessageIntent() case .failure(let error): MXLog.error("Failed sending message with error: \(error)") } } + private func donateSendMessageIntent() async { + guard let displayName = roomProxy.details.name ?? roomProxy.details.canonicalAlias, !displayName.isEmpty else { + MXLog.error("Failed donating send message intent, room missing name or alias.") + return + } + + let groupName = INSpeakableString(spokenPhrase: displayName) + + let sendMessageIntent = INSendMessageIntent(recipients: nil, + outgoingMessageType: .outgoingMessageText, + content: nil, + speakableGroupName: groupName, + conversationIdentifier: roomProxy.id, + serviceName: nil, + sender: nil, + attachments: nil) + + let avatarURL = switch roomProxy.details.avatar { + case .room(_, _, let avatarURL): + avatarURL + case .heroes(let userProfiles): + userProfiles.first?.avatarURL + } + + func addPlacehoder() { + if let imageData = Avatars.generatePlaceholderAvatarImageData(name: displayName, id: roomProxy.id, size: .init(width: 100, height: 100)) { + sendMessageIntent.setImage(INImage(imageData: imageData), forParameterNamed: \.speakableGroupName) + } + } + + if let avatarURL { + let mediaSource = MediaSourceProxy(url: avatarURL, mimeType: nil) + + if case let .success(avatarData) = await mediaProvider.loadThumbnailForSource(source: mediaSource, size: .init(width: 100, height: 100)) { + sendMessageIntent.setImage(INImage(imageData: avatarData), forParameterNamed: \.speakableGroupName) + } else { + addPlacehoder() + } + } else { + addPlacehoder() + } + + let interaction = INInteraction(intent: sendMessageIntent, response: nil) + + do { + try await interaction.donate() + } catch { + MXLog.error("Failed donating send message intent with error: \(error)") + } + } + func toggleReaction(_ reaction: String, to eventOrTransactionID: EventOrTransactionId) async { MXLog.info("Toggle reaction \(reaction) to \(eventOrTransactionID)") diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift index d090f7d90e..4e86953c25 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift @@ -10,16 +10,19 @@ import Foundation struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, - timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { + timelineItemFactory: RoomTimelineItemFactoryProtocol, + mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol { RoomTimelineController(roomProxy: roomProxy, timelineProxy: roomProxy.timeline, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory, + mediaProvider: mediaProvider, appSettings: ServiceLocator.shared.settings) } func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol, - timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? { + timelineItemFactory: RoomTimelineItemFactoryProtocol, + mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? { guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else { return nil } @@ -27,6 +30,7 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { timelineProxy: pinnedEventsTimeline, initialFocussedEventID: nil, timelineItemFactory: timelineItemFactory, + mediaProvider: mediaProvider, appSettings: ServiceLocator.shared.settings) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift index 71746fd849..0f1093c8f5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift @@ -11,9 +11,11 @@ import Foundation protocol RoomTimelineControllerFactoryProtocol { func buildRoomTimelineController(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, - timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol + timelineItemFactory: RoomTimelineItemFactoryProtocol, + mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol func buildRoomPinnedTimelineController(roomProxy: JoinedRoomProxyProtocol, - timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? + timelineItemFactory: RoomTimelineItemFactoryProtocol, + mediaProvider: MediaProviderProtocol) async -> RoomTimelineControllerProtocol? } // sourcery: AutoMockable diff --git a/ElementX/Sources/Services/UserSession/SessionDirectories.swift b/ElementX/Sources/Services/UserSession/SessionDirectories.swift index 29b1b076b3..ae768f30ea 100644 --- a/ElementX/Sources/Services/UserSession/SessionDirectories.swift +++ b/ElementX/Sources/Services/UserSession/SessionDirectories.swift @@ -72,19 +72,19 @@ extension SessionDirectories { init() { let sessionDirectoryName = UUID().uuidString dataDirectory = .sessionsBaseDirectory.appending(component: sessionDirectoryName) - cacheDirectory = .cachesBaseDirectory.appending(component: sessionDirectoryName) + cacheDirectory = .sessionCachesBaseDirectory.appending(component: sessionDirectoryName) } /// Creates the session directories for a user who signed in before the data directory was stored. init(userID: String) { dataDirectory = .legacySessionDirectory(for: userID) - cacheDirectory = .cachesBaseDirectory.appending(component: dataDirectory.lastPathComponent) + cacheDirectory = .sessionCachesBaseDirectory.appending(component: dataDirectory.lastPathComponent) } /// Creates the session directories for a user who has a single session directory stored without a separate caches directory. init(dataDirectory: URL) { self.dataDirectory = dataDirectory - cacheDirectory = .cachesBaseDirectory.appending(component: dataDirectory.lastPathComponent) + cacheDirectory = .sessionCachesBaseDirectory.appending(component: dataDirectory.lastPathComponent) } } diff --git a/ElementX/Sources/ShareExtension/ShareExtensionModels.swift b/ElementX/Sources/ShareExtension/ShareExtensionModels.swift new file mode 100644 index 0000000000..3324554c65 --- /dev/null +++ b/ElementX/Sources/ShareExtension/ShareExtensionModels.swift @@ -0,0 +1,21 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Foundation + +enum ShareExtensionConstants { + static let urlPath = "share" +} + +enum ShareExtensionPayload: Hashable, Codable { + case mediaFile(roomID: String?, mediaFile: ShareExtensionMediaFile) +} + +struct ShareExtensionMediaFile: Hashable, Codable { + let url: URL + let suggestedName: String? +} diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 01bea2b808..73c4910754 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -684,6 +684,7 @@ class MockScreen: Identifiable { timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org", attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")), + mediaProvider: MediaProviderMock(configuration: .init()), appSettings: ServiceLocator.shared.settings) let flowCoordinator = UserSessionFlowCoordinator(userSession: UserSessionMock(.init(clientProxy: clientProxy)), diff --git a/ElementX/SupportingFiles/Info.plist b/ElementX/SupportingFiles/Info.plist index fb51c555a9..39c81a8d9c 100644 --- a/ElementX/SupportingFiles/Info.plist +++ b/ElementX/SupportingFiles/Info.plist @@ -54,7 +54,7 @@ Application CFBundleURLSchemes - io.element + $(BASE_BUNDLE_IDENTIFIER) diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 33e3b1b850..e7b1587635 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -66,7 +66,7 @@ targets: CFBundleTypeRole: Editor, CFBundleURLName: "Application", CFBundleURLSchemes: [ - io.element + $(BASE_BUNDLE_IDENTIFIER) ] } ] @@ -189,6 +189,7 @@ targets: dependencies: - target: NSE + - target: ShareExtension # not used yet # - target: NCE - package: MatrixRustSDK diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index 3ea1ed8a6c..c55123454e 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -66,10 +66,10 @@ class NotificationServiceExtension: UNNotificationServiceExtension { handler = contentHandler modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent - NSELogger.configure(logLevel: settings.logLevel) + ExtensionLogger.configure(currentTarget: "nse", logLevel: settings.logLevel) MXLog.info("\(tag) #########################################") - NSELogger.logMemory(with: tag) + ExtensionLogger.logMemory(with: tag) MXLog.info("\(tag) Payload came: \(request.content.userInfo)") Self.serialQueue.sync { @@ -201,7 +201,7 @@ class NotificationServiceExtension: UNNotificationServiceExtension { deinit { cleanUp() - NSELogger.logMemory(with: tag) + ExtensionLogger.logMemory(with: tag) MXLog.info("\(tag) deinit") } diff --git a/NSE/SupportingFiles/target.yml b/NSE/SupportingFiles/target.yml index 417469aa13..dea122a13e 100644 --- a/NSE/SupportingFiles/target.yml +++ b/NSE/SupportingFiles/target.yml @@ -77,7 +77,7 @@ targets: - path: ../../ElementX/Sources/Application/AppSettings.swift - path: ../../ElementX/Sources/Generated/Assets.swift - path: ../../ElementX/Sources/Generated/Strings.swift - - path: ../../ElementX/Sources/Other/AvatarSize.swift + - path: ../../ElementX/Sources/Other/Avatars.swift - path: ../../ElementX/Sources/Other/CurrentValuePublisher.swift - path: ../../ElementX/Sources/Other/Extensions/AttributedString.swift - path: ../../ElementX/Sources/Other/Extensions/Bundle.swift diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 492fbd85f9..926ad7802a 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -701,6 +701,12 @@ extension PreviewTests { } } + func test_roomSelectionScreen() { + for preview in RoomSelectionScreen_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_sFNumberedListView() { for preview in SFNumberedListView_Previews._allPreviews { assertSnapshots(matching: preview) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPad-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPad-en-GB.1.png index 784d368a36..f498e2c853 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPad-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:481969f82410ed28b3c259d53e88310fd5669d1c6041020c99c47a7a64fde666 -size 151180 +oid sha256:ec8339fcf606ac520989a6915da4f3a15a43d049e49c3e93524ad70bd8097778 +size 148064 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPad-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPad-pseudo.1.png index fb1a5abde9..665b44f58c 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPad-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac9dc777f9bdaca948c7a5cf56aebb163ff80be491a124c78084b5d1309396af -size 151589 +oid sha256:aa3fb400d0af05b6c81181a040f3746c11fafe82703f7587ae9d98cdcce44386 +size 148467 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPhone-16-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPhone-16-en-GB.1.png index f80889fefc..c585a86568 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPhone-16-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPhone-16-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:656ab8534b3b28a11a09b7ee3bd735de4f9173687006d8346093ec8fa70caf54 -size 88511 +oid sha256:ed42c3e99db34f3dedb33b5faaf768caee7d15d8d5dfb50fbed0f97da2b990f0 +size 85859 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPhone-16-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPhone-16-pseudo.1.png index 84e27fc201..a82c51c502 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPhone-16-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_globalSearchScreen-iPhone-16-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dece45267d9514a73241f045de87eb76d7d87ccc236aed3d114ccfbe9507d74 -size 88837 +oid sha256:f33f9d8cc790b5a03c0da8423e948bc9faffa7580e587d509e13fb691a255eaf +size 86196 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPad-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPad-en-GB.1.png index 07c4385f19..a484a78c47 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPad-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7001c732a5f38e3b8093488f80504a474c0b8f4f494e68793029f387047522e8 -size 152484 +oid sha256:1705df7384f16656d63d4efca503b4533cd3d67f54733855f284ddc49cdf973f +size 149086 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPad-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPad-pseudo.1.png index b97167e9de..e2223527cd 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPad-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d213faa018f71016ebc6f31691ac3cbf97849e8a739937d9e351b92bef7877c -size 154392 +oid sha256:de6fe930505a8b4e9ab2b3052ebd1345118eaa64149381f28b3142447722d61a +size 151009 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPhone-16-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPhone-16-en-GB.1.png index 9bb41b57cf..8e24daa0ca 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPhone-16-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPhone-16-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee5ecf468c941282295014187f8fc03929c122f343465113496e20a8cca738d6 -size 101988 +oid sha256:96596029a45f8bc48da6f8cbd77f09eb65236ac111534d9dec7d45e0e083cf32 +size 99144 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPhone-16-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPhone-16-pseudo.1.png index 559fd39e1b..4c51e28475 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPhone-16-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_messageForwardingScreen-iPhone-16-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53268d051594a0dd4250a79b5b3093a4f57f4104250c990e6bb276c751093b94 -size 101810 +oid sha256:dd2bd5da5c119e827b8b195971a34b645d5e47f577bf622e5856412c91d616e2 +size 98976 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPad-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPad-en-GB.1.png new file mode 100644 index 0000000000..640b8a5dd1 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPad-en-GB.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75e855ee7f0e97644797ed467953cd618fbc64a1ca185e6784cd24aa5cdef613 +size 146330 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPad-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPad-pseudo.1.png new file mode 100644 index 0000000000..9d16fb4425 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPad-pseudo.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7749e68e119cf015e5110e889ef29f283af3b7f4f71840bba60b7abdfbea787b +size 147778 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPhone-16-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPhone-16-en-GB.1.png new file mode 100644 index 0000000000..47ca6ba736 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPhone-16-en-GB.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f53f2f4cc267e92bab7d8bf4a1fdfd307609312cf7e623ae43a6a254971c1f6 +size 97028 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPhone-16-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPhone-16-pseudo.1.png new file mode 100644 index 0000000000..e839f735b8 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_roomSelectionScreen-iPhone-16-pseudo.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e76ee2d1d8b3dee92997af404d846a889e9ccea0862b848c8d2386d4489220b +size 98291 diff --git a/ShareExtension/Sources/ShareExtensionViewController.swift b/ShareExtension/Sources/ShareExtensionViewController.swift new file mode 100644 index 0000000000..e2060491cf --- /dev/null +++ b/ShareExtension/Sources/ShareExtensionViewController.swift @@ -0,0 +1,141 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import IntentsUI +import SwiftUI + +class ShareExtensionViewController: UIViewController { + private let appSettings: CommonSettingsProtocol = AppSettings() + private let hostingController = UIHostingController(rootView: ShareExtensionView()) + + override func viewDidLoad() { + super.viewDidLoad() + + addChild(hostingController) + view.addMatchedSubview(hostingController.view) + hostingController.didMove(toParent: self) + + MXLog.configure(currentTarget: "shareextension", filePrefix: "shareextension", logLevel: appSettings.logLevel) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + Task { + if let payload = await prepareSharePayload() { + await self.openMainApp(payload: payload) + } + + self.dismiss() + } + } + + // MARK: - Private + + private func prepareSharePayload() async -> ShareExtensionPayload? { + guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem, + let itemProvider = extensionItem.attachments?.first else { + return nil + } + + guard let contentType = itemProvider.preferredContentType, + let preferredExtension = contentType.preferredFilenameExtension else { + MXLog.error("Invalid NSItemProvider: \(itemProvider)") + return nil + } + + let roomID = (extensionContext?.intent as? INSendMessageIntent)?.conversationIdentifier + let providerSuggestedName = itemProvider.suggestedName + let providerDescription = itemProvider.description + + let shareData: Data? = await withCheckedContinuation { continuation in + _ = itemProvider.loadDataRepresentation(for: contentType) { data, error in + if let error { + MXLog.error("Failed processing NSItemProvider: \(providerDescription) with error: \(error)") + continuation.resume(returning: nil) + return + } + + guard let data else { + MXLog.error("Invalid NSItemProvider data: \(providerDescription)") + continuation.resume(returning: nil) + return + } + + continuation.resume(returning: data) + } + } + + guard let shareData else { + return nil + } + + do { + let url: URL + if let filename = providerSuggestedName { + let hasExtension = !(filename as NSString).pathExtension.isEmpty + let filename = hasExtension ? filename : "\(filename).\(preferredExtension)" + url = try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename) + } else { + let filename = "\(UUID().uuidString).\(preferredExtension)" + url = try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename) + } + + return .mediaFile(roomID: roomID, mediaFile: .init(url: url, suggestedName: providerSuggestedName)) + } catch { + MXLog.error("Failed storing NSItemProvider data \(providerDescription) with error: \(error)") + return nil + } + } + + private func openMainApp(payload: ShareExtensionPayload) async { + guard let payload = urlEncodeSharePayload(payload) else { + MXLog.error("Failed preparing share payload") + return + } + + guard let url = URL(string: "\(InfoPlistReader.main.baseBundleIdentifier):/\(ShareExtensionConstants.urlPath)?\(payload)") else { + MXLog.error("Failed retrieving main application scheme") + return + } + + await openURL(url) + } + + private func urlEncodeSharePayload(_ payload: ShareExtensionPayload) -> String? { + let data: Data + do { + data = try JSONEncoder().encode(payload) + } catch { + MXLog.error("Failed encoding share payload with error: \(error)") + return nil + } + + guard let jsonString = String(data: data, encoding: .utf8) else { + MXLog.error("Invalid payload data") + return nil + } + + return jsonString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + } + + private func dismiss() { + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } + + private func openURL(_ url: URL) async { + var responder: UIResponder? = self + while responder != nil { + if let application = responder as? UIApplication { + await application.open(url) + return + } + + responder = responder?.next + } + } +} diff --git a/ShareExtension/Sources/View/ShareExtensionView.swift b/ShareExtension/Sources/View/ShareExtensionView.swift new file mode 100644 index 0000000000..9329e3c1ec --- /dev/null +++ b/ShareExtension/Sources/View/ShareExtensionView.swift @@ -0,0 +1,23 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Compound +import SwiftUI + +struct ShareExtensionView: View { + var body: some View { + ZStack { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .background(.compound.bgCanvasDefault) + } +} + +#Preview { + ShareExtensionView() +} diff --git a/ShareExtension/SupportingFiles/Info.plist b/ShareExtension/SupportingFiles/Info.plist new file mode 100644 index 0000000000..863d9f93c3 --- /dev/null +++ b/ShareExtension/SupportingFiles/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(PRODUCT_DISPLAY_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionAttributes + + IntentsSupported + + INSendMessageIntent + + NSExtensionActivationRule + + NSExtensionActivationSupportsFileWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 1 + NSExtensionActivationSupportsMovieWithMaxCount + 1 + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShareExtensionViewController + + appGroupIdentifier + $(APP_GROUP_IDENTIFIER) + baseBundleIdentifier + $(BASE_BUNDLE_IDENTIFIER) + keychainAccessGroupIdentifier + $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER) + productionAppName + $(PRODUCTION_APP_NAME) + + diff --git a/ShareExtension/SupportingFiles/ShareExtension.entitlements b/ShareExtension/SupportingFiles/ShareExtension.entitlements new file mode 100644 index 0000000000..8cb4a1a418 --- /dev/null +++ b/ShareExtension/SupportingFiles/ShareExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.io.element + + + diff --git a/ShareExtension/SupportingFiles/target.yml b/ShareExtension/SupportingFiles/target.yml new file mode 100644 index 0000000000..469de107a8 --- /dev/null +++ b/ShareExtension/SupportingFiles/target.yml @@ -0,0 +1,85 @@ +name: ShareExtension + +schemes: + ShareExtension: + analyze: + config: Debug + archive: + config: Release + build: + targets: + ShareExtension: + - 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: + ShareExtension: + type: app-extension + platform: iOS + + dependencies: + - package: MatrixRustSDK + - package: Collections + - package: Compound + + info: + path: ../SupportingFiles/Info.plist + properties: + CFBundleDisplayName: $(PRODUCT_DISPLAY_NAME) + CFBundleShortVersionString: $(MARKETING_VERSION) + CFBundleVersion: $(CURRENT_PROJECT_VERSION) + appGroupIdentifier: $(APP_GROUP_IDENTIFIER) + baseBundleIdentifier: $(BASE_BUNDLE_IDENTIFIER) + keychainAccessGroupIdentifier: $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER) + productionAppName: $(PRODUCTION_APP_NAME) + NSExtension: + NSExtensionPointIdentifier: com.apple.share-services + NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareExtensionViewController + NSExtensionAttributes: + IntentsSupported: [ + INSendMessageIntent, + ] + NSExtensionActivationRule: + NSExtensionActivationSupportsFileWithMaxCount: 1 + NSExtensionActivationSupportsImageWithMaxCount: 1 + NSExtensionActivationSupportsMovieWithMaxCount: 1 + + settings: + base: + PRODUCT_NAME: ShareExtension + PRODUCT_DISPLAY_NAME: $(APP_DISPLAY_NAME) + PRODUCT_BUNDLE_IDENTIFIER: ${BASE_BUNDLE_IDENTIFIER}.shareextension + MARKETING_VERSION: $(MARKETING_VERSION) + CURRENT_PROJECT_VERSION: $(CURRENT_PROJECT_VERSION) + DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM) + CODE_SIGN_ENTITLEMENTS: ShareExtension/SupportingFiles/ShareExtension.entitlements + + sources: + - path: ../Sources + - path: ../SupportingFiles + - path: ../../ElementX/Sources/ShareExtension + - path: ../../ElementX/Sources/Application/AppSettings.swift + - path: ../../ElementX/Sources/Other/Extensions/Bundle.swift + - path: ../../ElementX/Sources/Other/Extensions/FileManager.swift + - path: ../../ElementX/Sources/Other/Extensions/NSItemProvider.swift + - path: ../../ElementX/Sources/Other/Extensions/ProcessInfo.swift + - path: ../../ElementX/Sources/Other/Extensions/UIView.swift + - path: ../../ElementX/Sources/Other/Extensions/URL.swift + - path: ../../ElementX/Sources/Other/InfoPlistReader.swift + - path: ../../ElementX/Sources/Other/Logging + - path: ../../ElementX/Sources/Other/UserPreference.swift + - path: ../../ElementX/Sources/UITests/UITestsScreenIdentifier.swift diff --git a/UnitTests/Sources/MediaProvider/MediaProviderTests.swift b/UnitTests/Sources/MediaProvider/MediaProviderTests.swift index c33640482f..98712ba6c9 100644 --- a/UnitTests/Sources/MediaProvider/MediaProviderTests.swift +++ b/UnitTests/Sources/MediaProvider/MediaProviderTests.swift @@ -73,12 +73,12 @@ final class MediaProviderTests: XCTestCase { } func test_whenImageFromSourceWithSourceNil_nilReturned() throws { - let image = mediaProvider.imageFromSource(nil, size: AvatarSize.room(on: .timeline).scaledSize) + let image = mediaProvider.imageFromSource(nil, size: Avatars.Size.room(on: .timeline).scaledSize) XCTAssertNil(image) } func test_whenImageFromSourceWithSourceNotNilAndImageCacheContainsImage_ImageIsReturned() throws { - let avatarSize = AvatarSize.room(on: .timeline) + let avatarSize = Avatars.Size.room(on: .timeline) let url = URL.picturesDirectory let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}" let imageForKey = UIImage() @@ -90,12 +90,12 @@ final class MediaProviderTests: XCTestCase { func test_whenImageFromSourceWithSourceNotNilAndImageNotCached_nilReturned() throws { let image = mediaProvider.imageFromSource(MediaSourceProxy(url: URL.picturesDirectory, mimeType: "image/jpeg"), - size: AvatarSize.room(on: .timeline).scaledSize) + size: Avatars.Size.room(on: .timeline).scaledSize) XCTAssertNil(image) } func test_whenLoadImageFromSourceAndImageCacheContainsImage_successIsReturned() async throws { - let avatarSize = AvatarSize.room(on: .timeline) + let avatarSize = Avatars.Size.room(on: .timeline) let url = URL.picturesDirectory let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}" let imageForKey = UIImage() @@ -106,7 +106,7 @@ final class MediaProviderTests: XCTestCase { } func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageSucceeds_successIsReturned() async throws { - let avatarSize = AvatarSize.room(on: .timeline) + let avatarSize = Avatars.Size.room(on: .timeline) let url = URL.picturesDirectory let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}" let imageForKey = UIImage() @@ -117,7 +117,7 @@ final class MediaProviderTests: XCTestCase { } func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFails_imageThumbnailIsLoaded() async throws { - let avatarSize = AvatarSize.room(on: .timeline) + let avatarSize = Avatars.Size.room(on: .timeline) let expectedImage = try loadTestImage() mediaLoader.loadMediaThumbnailForSourceWidthHeightReturnValue = expectedImage.pngData() @@ -133,7 +133,7 @@ final class MediaProviderTests: XCTestCase { } func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFails_imageIsStored() async throws { - let avatarSize = AvatarSize.room(on: .timeline) + let avatarSize = Avatars.Size.room(on: .timeline) let url = URL.picturesDirectory let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}" let expectedImage = try loadTestImage() @@ -165,7 +165,7 @@ final class MediaProviderTests: XCTestCase { mediaLoader.loadMediaThumbnailForSourceWidthHeightThrowableError = MediaProviderTestsError.error let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory, mimeType: "image/jpeg"), - size: AvatarSize.room(on: .timeline).scaledSize) + size: Avatars.Size.room(on: .timeline).scaledSize) switch result { case .success: XCTFail("Should fail") @@ -191,7 +191,7 @@ final class MediaProviderTests: XCTestCase { mediaLoader.loadMediaThumbnailForSourceWidthHeightReturnValue = Data() let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory, mimeType: "image/jpeg"), - size: AvatarSize.room(on: .timeline).scaledSize) + size: Avatars.Size.room(on: .timeline).scaledSize) switch result { case .success: XCTFail("Should fail") diff --git a/UnitTests/Sources/RestorationTokenTests.swift b/UnitTests/Sources/RestorationTokenTests.swift index 404ae04ad3..72009d52ef 100644 --- a/UnitTests/Sources/RestorationTokenTests.swift +++ b/UnitTests/Sources/RestorationTokenTests.swift @@ -31,7 +31,7 @@ class RestorationTokenTests: XCTestCase { XCTAssertNil(decodedToken.pusherNotificationClientIdentifier, "There should not be a push notification client ID.") XCTAssertEqual(decodedToken.sessionDirectories.dataDirectory, .sessionsBaseDirectory.appending(component: "@user_example.com"), "The session directory should match the original location set by the Rust SDK from our base directory.") - XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .cachesBaseDirectory.appending(component: "@user_example.com"), + XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: "@user_example.com"), "The cache directory should be derived from the session directory but in the caches directory.") } @@ -60,7 +60,7 @@ class RestorationTokenTests: XCTestCase { "The push notification client identifier should not be changed.") XCTAssertEqual(decodedToken.sessionDirectories.dataDirectory, originalToken.sessionDirectory, "The session directory should not be changed.") - XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .cachesBaseDirectory.appending(component: sessionDirectoryName), + XCTAssertEqual(decodedToken.sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: sessionDirectoryName), "The cache directory should be derived from the session directory but in the caches directory.") } @@ -75,7 +75,7 @@ class RestorationTokenTests: XCTestCase { oidcData: "data-from-mas", slidingSyncVersion: .native), sessionDirectory: .sessionsBaseDirectory.appending(component: sessionDirectoryName), - cacheDirectory: .cachesBaseDirectory.appending(component: sessionDirectoryName), + cacheDirectory: .sessionCachesBaseDirectory.appending(component: sessionDirectoryName), passphrase: "passphrase", pusherNotificationClientIdentifier: "pusher-identifier") let data = try JSONEncoder().encode(originalToken) diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index 18e20f4524..6e2534b889 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -218,6 +218,31 @@ class RoomFlowCoordinatorTests: XCTestCase { XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomScreenCoordinator) } + func testShareRoute() async throws { + await setupRoomFlowCoordinator() + + try await process(route: .room(roomID: "1", via: [])) + XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0) + + let sharePayload: ShareExtensionPayload = .mediaFile(roomID: "1", mediaFile: .init(url: .picturesDirectory, suggestedName: nil)) + try await process(route: .share(sharePayload)) + + XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0) + + XCTAssertTrue((navigationStackCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator) + + try await process(route: .childRoom(roomID: "2", via: [])) + XCTAssertNil(navigationStackCoordinator.sheetCoordinator) + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1) + + try await process(route: .share(sharePayload)) + + XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0) + XCTAssertTrue((navigationStackCoordinator.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator) + } + // MARK: - Private private func process(route: AppRoute) async throws { diff --git a/UnitTests/Sources/SessionDirectoriesTests.swift b/UnitTests/Sources/SessionDirectoriesTests.swift index 62db895c7a..dfb934b648 100644 --- a/UnitTests/Sources/SessionDirectoriesTests.swift +++ b/UnitTests/Sources/SessionDirectoriesTests.swift @@ -21,7 +21,7 @@ class SessionDirectoriesTests: XCTestCase { // Then the directories should be generated in the correct location, using an escaped version of the user ID XCTAssertEqual(sessionDirectories.dataDirectory, .sessionsBaseDirectory.appending(component: "@user_matrix.org")) - XCTAssertEqual(sessionDirectories.cacheDirectory, .cachesBaseDirectory.appending(component: "@user_matrix.org")) + XCTAssertEqual(sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: "@user_matrix.org")) } func testInitWithDataDirectory() { @@ -34,7 +34,7 @@ class SessionDirectoriesTests: XCTestCase { // Then the data directory should remain unchanged and the caches directory should be generated. XCTAssertEqual(sessionDirectories.dataDirectory, sessionDirectory) - XCTAssertEqual(sessionDirectories.cacheDirectory, .cachesBaseDirectory.appending(component: sessionDirectoryName)) + XCTAssertEqual(sessionDirectories.cacheDirectory, .sessionCachesBaseDirectory.appending(component: sessionDirectoryName)) } func testPathOutput() { diff --git a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift index 83f79dfa92..30ed610786 100644 --- a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift +++ b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift @@ -211,8 +211,8 @@ class UserSessionFlowCoordinatorTests: XCTestCase { XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 1) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "1") + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 1) + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "1") // A child event route should push a new room screen onto the stack and focus on the event. userSessionFlowCoordinator.handleAppRoute(.childEvent(eventID: "2", roomID: "2", via: []), animated: true) @@ -221,27 +221,50 @@ class UserSessionFlowCoordinatorTests: XCTestCase { XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 1) XCTAssertTrue(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 2) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "2") + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 2) + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "2") // A subsequent regular event route should clear the stack and set the new room as the root of the stack. try await process(route: .event(eventID: "3", roomID: "3", via: []), expectedState: .roomList(selectedRoomID: "3")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 3) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "3") + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 3) + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "3") // A regular event route for the same room should set a new instance of the room as the root of the stack. try await process(route: .event(eventID: "4", roomID: "3", via: []), expectedState: .roomList(selectedRoomID: "3")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 4) - XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "4", + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderCallsCount, 4) + XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "4", "A new timeline should be created for the same room ID, so that the screen isn't stale while loading.") } + func testShareRouteWithoutRoom() async throws { + try await process(route: .settings, expectedState: .settingsScreen(selectedRoomID: nil)) + XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) + + let sharePayload: ShareExtensionPayload = .mediaFile(roomID: nil, mediaFile: .init(url: .picturesDirectory, suggestedName: nil)) + try await process(route: .share(sharePayload), + expectedState: .shareExtensionRoomList(sharePayload: sharePayload)) + + XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is RoomSelectionScreenCoordinator) + } + + func testShareRouteWithRoom() async throws { + try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + + let sharePayload: ShareExtensionPayload = .mediaFile(roomID: "2", mediaFile: .init(url: .picturesDirectory, suggestedName: nil)) + try await process(route: .share(sharePayload), + expectedState: .roomList(selectedRoomID: "2")) + + XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) + XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator) + } + // MARK: - Private private func process(route: AppRoute, expectedState: UserSessionFlowCoordinatorStateMachine.State) async throws { diff --git a/project.yml b/project.yml index 9c04fccc87..4363aa49b4 100644 --- a/project.yml +++ b/project.yml @@ -53,6 +53,7 @@ include: - path: UITests/SupportingFiles/target.yml - path: IntegrationTests/SupportingFiles/target.yml - path: NSE/SupportingFiles/target.yml +- path: ShareExtension/SupportingFiles/target.yml # - path: NCE/SupportingFiles/target.yml (not used yet) # - path: MyAppVariant/override.yml