diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 1e3fa1b0e..b92ce6cde 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -100,6 +100,7 @@ 281BED345D59A9A6A99E9D98 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; }; 282A5F3375DDC774AE09B0C3 /* TracingConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */; }; 2835FD52F3F618D07F799B3D /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7310D8DFE01AF45F0689C3AA /* Publisher.swift */; }; + 28A94A7DF684F2A29C4ADFE9 /* TimelineReceiptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5457B0320D028A676C248F /* TimelineReceiptView.swift */; }; 290FDB0FFDC2F1DDF660343E /* TestMeasurementParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4048041C1A6B20CB97FD18 /* TestMeasurementParser.swift */; }; 292827744227DF61C930BDDB /* CreateRoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB0D6CB491777E7FC6B5BA12 /* CreateRoomScreen.swift */; }; 2955F4C160CFD7794D819C64 /* EffectsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024F7398C5FC12586FB10E9D /* EffectsScene.swift */; }; @@ -138,6 +139,7 @@ 39929D29B265C3F6606047DE /* AlignedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */; }; 3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */; }; 3A64A93A651A3CB8774ADE8E /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = BA93CD75CCE486660C9040BD /* Collections */; }; + 3A7DD0D13B0FB8876D69D829 /* TextBasedRoomTimelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */; }; 3B0F9B57D25B07E66F15762A /* MediaUploadPreviewScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2E7C987AE5DC9087BB19F7D /* MediaUploadPreviewScreenModels.swift */; }; 3B28408450BCAED911283AA2 /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FA991289149D31F4286747 /* UserPreference.swift */; }; 3C549A0BF39F8A854D45D9FD /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */; }; @@ -302,6 +304,7 @@ 8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */; }; 828EA5009557C2B9DCD4CA0F /* UserDiscoverySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */; }; 829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */; }; + 8317E1314C00DCCC99D30DA8 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9227F7495DA43324050A863 /* TextBasedRoomTimelineItem.swift */; }; 83E05DB56BBD6C151602881E /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E0ADE4FAA5A4DB91CB07737 /* SettingsScreen.swift */; }; 84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; }; 84C0CF78BCE085C08CB94D86 /* TimelineEventProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B62EE933FC3D5651AF4607 /* TimelineEventProxy.swift */; }; @@ -333,6 +336,7 @@ 8BC8EF6705A78946C1F22891 /* SoftLogoutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71A7D4DDEEE5D2CA0C8D63CD /* SoftLogoutScreen.swift */; }; 8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; }; 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; }; + 8D0C5BC670D514760CC84E2A /* TextBasedRoomTimelineViewMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A542BC40D6EC2E66BC5659B /* TextBasedRoomTimelineViewMock.swift */; }; 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */; }; 8D605456793F243649EC96AA /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = CD6B0C4639E066915B5E6463 /* target.yml */; }; 8D71E5E53F372202379BECCE /* BugReportScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */; }; @@ -394,6 +398,7 @@ A216C83ADCF32BA5EF8A6FBC /* InviteUsersViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845DDBDE5A0887E73D38B826 /* InviteUsersViewModelTests.swift */; }; A23B8B27A1436A1049EEF68E /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; }; + A2A5AB2E8B3F5CA769E531FA /* TextBasedRoomTimelineViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E47F18A9A077E351CEA10D4 /* TextBasedRoomTimelineViewProtocol.swift */; }; A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; A371629728E597C5FCA3C2B2 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73FC861755C6388F62B9280A /* Analytics.swift */; }; A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287FC98AF2664EAD79C0D902 /* UIDevice.swift */; }; @@ -408,11 +413,6 @@ A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; }; A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; A6F713461DB62AC06293E7B7 /* FilePreviewScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 820637A0F9C2F562FF40CBC8 /* FilePreviewScreenModels.swift */; }; - A733C86A2A1D17590055ECD6 /* TimelineReceiptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A733C8692A1D17590055ECD6 /* TimelineReceiptView.swift */; }; - A733C86C2A1E149E0055ECD6 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A733C86B2A1E149E0055ECD6 /* TextBasedRoomTimelineItem.swift */; }; - A733C86E2A1E1C190055ECD6 /* TextBasedRoomTimelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A733C86D2A1E1C190055ECD6 /* TextBasedRoomTimelineTests.swift */; }; - A7C152962A1F4E4C0089FF9D /* TextBasedRoomTimelineViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C152952A1F4E4C0089FF9D /* TextBasedRoomTimelineViewProtocol.swift */; }; - A7C152982A1F4E710089FF9D /* TextBasedRoomTimelineViewMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C152972A1F4E710089FF9D /* TextBasedRoomTimelineViewMock.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; }; A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; }; @@ -726,7 +726,7 @@ 1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 127A57D053CE8C87B5EFB089 /* Consumable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Consumable.swift; sourceTree = ""; }; 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = ""; }; - 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 13673F95EBA78D40C09CCE35 /* MockUserIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserIndicatorController.swift; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -774,6 +774,7 @@ 28D116D4633E177BE1AC0E71 /* AnalyticsSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModel.swift; sourceTree = ""; }; 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = ""; }; 2A96A67AD0E32C48941EFBB3 /* SessionVerificationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenCoordinator.swift; sourceTree = ""; }; + 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineTests.swift; sourceTree = ""; }; 2AFEF3AC64B1358083F76B8B /* List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = List.swift; sourceTree = ""; }; 2BB385E148DE55C85C0A02D6 /* SoftLogoutScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenModels.swift; sourceTree = ""; }; 2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerProtocol.swift; sourceTree = ""; }; @@ -806,6 +807,7 @@ 3948D16F021DFDB2CD26EAA8 /* MockBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBackgroundTaskService.swift; sourceTree = ""; }; 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = ""; }; 39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenUITests.swift; sourceTree = ""; }; + 3A5457B0320D028A676C248F /* TimelineReceiptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReceiptView.swift; sourceTree = ""; }; 3B5E97E9615A158C76B2AB77 /* DateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTests.swift; sourceTree = ""; }; 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenUITests.swift; sourceTree = ""; }; 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategory.swift; sourceTree = ""; }; @@ -837,7 +839,7 @@ 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = ""; }; 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; - 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; }; + 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; }; 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = ""; }; 47E6DD75A81D07CD91997D8C /* SettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; @@ -845,6 +847,7 @@ 4959CECEC984B3995616F427 /* DataProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = ""; }; 49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = ""; }; + 4A542BC40D6EC2E66BC5659B /* TextBasedRoomTimelineViewMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewMock.swift; sourceTree = ""; }; 4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = ""; }; 4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = ""; }; 4B5046BB295AEAFA6FB81655 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = ""; }; @@ -852,6 +855,7 @@ 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenUITests.swift; sourceTree = ""; }; 4E2245243369B99216C7D84E /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; + 4E47F18A9A077E351CEA10D4 /* TextBasedRoomTimelineViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewProtocol.swift; sourceTree = ""; }; 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxyProtocol.swift; sourceTree = ""; }; 4F1DFE6E746539F33042D3A9 /* FormSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormSection.swift; sourceTree = ""; }; 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; @@ -978,7 +982,7 @@ 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; 8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = ""; }; 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = ""; }; 8F61A0DD8243B395499C99A2 /* InvitesScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenUITests.swift; sourceTree = ""; }; @@ -1036,12 +1040,7 @@ A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = ""; }; A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; A6F5CDE754D53A9A403EDBA9 /* DeveloperOptionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModelProtocol.swift; sourceTree = ""; }; - A733C8692A1D17590055ECD6 /* TimelineReceiptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReceiptView.swift; sourceTree = ""; }; - A733C86B2A1E149E0055ECD6 /* TextBasedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineItem.swift; sourceTree = ""; }; - A733C86D2A1E1C190055ECD6 /* TextBasedRoomTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineTests.swift; sourceTree = ""; }; A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = ""; }; - A7C152952A1F4E4C0089FF9D /* TextBasedRoomTimelineViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewProtocol.swift; sourceTree = ""; }; - A7C152972A1F4E710089FF9D /* TextBasedRoomTimelineViewMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewMock.swift; sourceTree = ""; }; A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = ""; }; A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = ""; }; A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = ""; }; @@ -1077,7 +1076,7 @@ B43AF03660F5FD4FFFA7F1CE /* TimelineItemContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemContextMenu.swift; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; B7DBA101D643B31E813F3AC1 /* AnalyticsSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreen.swift; sourceTree = ""; }; @@ -1087,6 +1086,7 @@ 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 = ""; }; + B9227F7495DA43324050A863 /* TextBasedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineItem.swift; sourceTree = ""; }; B99E13633862847D8B7E2815 /* StartChatScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenModels.swift; sourceTree = ""; }; BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferenceTests.swift; sourceTree = ""; }; BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenModels.swift; sourceTree = ""; }; @@ -1141,7 +1141,7 @@ CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CECF45B5E8E795666B8C5013 /* SettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenModels.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = ""; }; D06A27D9C70E0DCC1E199163 /* OnboardingBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackgroundView.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; @@ -1203,7 +1203,7 @@ ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -2188,6 +2188,7 @@ 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */, 6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */, 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */, + 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */, 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */, EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */, 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */, @@ -2197,7 +2198,6 @@ 7583EAC171059A86B767209F /* MediaProvider */, 7DBC911559934065993A5FF4 /* NotificationManager */, 1C62F5382CC9D9F7DCEC344A /* UserDiscoveryService */, - A733C86D2A1E1C190055ECD6 /* TextBasedRoomTimelineTests.swift */, ); path = Sources; sourceTree = ""; @@ -2717,13 +2717,13 @@ 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */, ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */, 612EF972F2A1800682D32C5E /* StickerRoomTimelineView.swift */, + B9227F7495DA43324050A863 /* TextBasedRoomTimelineItem.swift */, + 4A542BC40D6EC2E66BC5659B /* TextBasedRoomTimelineViewMock.swift */, + 4E47F18A9A077E351CEA10D4 /* TextBasedRoomTimelineViewProtocol.swift */, F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */, F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */, A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */, 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */, - A733C86B2A1E149E0055ECD6 /* TextBasedRoomTimelineItem.swift */, - A7C152952A1F4E4C0089FF9D /* TextBasedRoomTimelineViewProtocol.swift */, - A7C152972A1F4E710089FF9D /* TextBasedRoomTimelineViewMock.swift */, ); path = Timeline; sourceTree = ""; @@ -2834,7 +2834,7 @@ children = ( D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */, 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */, - A733C8692A1D17590055ECD6 /* TimelineReceiptView.swift */, + 3A5457B0320D028A676C248F /* TimelineReceiptView.swift */, ); path = Supplementary; sourceTree = ""; @@ -3531,7 +3531,6 @@ 864C69CF951BF36D25BE0C03 /* DeveloperOptionsScreenViewModelTests.swift in Sources */, 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */, 501304F26B52DF7024011B6C /* EmojiMartJSONLoaderTests.swift in Sources */, - A733C86E2A1E1C190055ECD6 /* TextBasedRoomTimelineTests.swift in Sources */, 25618589E0DE0F1E95FC7B5C /* EmojiProviderTests.swift in Sources */, 71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */, CA45758F08DF42D41D8A4B29 /* FilePreviewViewModelTests.swift in Sources */, @@ -3573,6 +3572,7 @@ 6189B4ABD535CE526FA1107B /* StartChatViewModelTests.swift in Sources */, 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */, E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */, + 3A7DD0D13B0FB8876D69D829 /* TextBasedRoomTimelineTests.swift in Sources */, 282A5F3375DDC774AE09B0C3 /* TracingConfigurationTests.swift in Sources */, 8E650379587C31D7912ED67B /* UNNotification+Creator.swift in Sources */, AF33B9044498211C3D82F1E1 /* UNTextInputNotificationResponse+Creator.swift in Sources */, @@ -3652,7 +3652,6 @@ B98A20A093A4FB785BFCCA53 /* BugReportScreenCoordinator.swift in Sources */, 4FFDC274824F7CC0BBDF581E /* BugReportScreenModels.swift in Sources */, 8D71E5E53F372202379BECCE /* BugReportScreenViewModel.swift in Sources */, - A733C86C2A1E149E0055ECD6 /* TextBasedRoomTimelineItem.swift in Sources */, B4A0C69370E6008A971463E7 /* BugReportScreenViewModelProtocol.swift in Sources */, 3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */, 172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */, @@ -3889,7 +3888,6 @@ 77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */, CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */, B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */, - A7C152962A1F4E4C0089FF9D /* TextBasedRoomTimelineViewProtocol.swift in Sources */, 50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */, 0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */, 0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */, @@ -3945,6 +3943,9 @@ 275EDE8849A2AC1D9309ED7C /* TemplateScreenViewModel.swift in Sources */, 2C4C750D0039AFABDF24236C /* TemplateScreenViewModelProtocol.swift in Sources */, D85D4FA590305180B4A41795 /* Tests.swift in Sources */, + 8317E1314C00DCCC99D30DA8 /* TextBasedRoomTimelineItem.swift in Sources */, + 8D0C5BC670D514760CC84E2A /* TextBasedRoomTimelineViewMock.swift in Sources */, + A2A5AB2E8B3F5CA769E531FA /* TextBasedRoomTimelineViewProtocol.swift in Sources */, BB784A02BADB03C820617A46 /* TextRoomTimelineItem.swift in Sources */, 53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */, 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */, @@ -3954,13 +3955,12 @@ 157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */, 01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */, FBCCF1EA25A071324FCD8544 /* TimelineItemDebugView.swift in Sources */, - A733C86A2A1D17590055ECD6 /* TimelineReceiptView.swift in Sources */, - A7C152982A1F4E710089FF9D /* TextBasedRoomTimelineViewMock.swift in Sources */, F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */, 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */, 9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */, 1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */, 9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */, + 28A94A7DF684F2A29C4ADFE9 /* TimelineReceiptView.swift in Sources */, 2A90DD14DE5C891BFA433950 /* TimelineReplyView.swift in Sources */, ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */, C4FE0E11A907C8999F92D5A8 /* TimelineStartRoomTimelineItem.swift in Sources */, diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index a131f68f5..744cb5349 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -419,6 +419,11 @@ class RoomProxyMock: RoomProxyProtocol { set(value) { underlyingMembersPublisher = value } } var underlyingMembersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never>! + var updatesPublisher: AnyPublisher { + get { return underlyingUpdatesPublisher } + set(value) { underlyingUpdatesPublisher = value } + } + var underlyingUpdatesPublisher: AnyPublisher! var invitedMembersCount: UInt { get { return underlyingInvitedMembersCount } set(value) { underlyingInvitedMembersCount = value } @@ -477,39 +482,23 @@ class RoomProxyMock: RoomProxyProtocol { return loadDisplayNameForUserIdReturnValue } } - //MARK: - addTimelineListener + //MARK: - registerTimelineListenerIfNeeded - var addTimelineListenerListenerCallsCount = 0 - var addTimelineListenerListenerCalled: Bool { - return addTimelineListenerListenerCallsCount > 0 + var registerTimelineListenerIfNeededCallsCount = 0 + var registerTimelineListenerIfNeededCalled: Bool { + return registerTimelineListenerIfNeededCallsCount > 0 } - var addTimelineListenerListenerReceivedListener: TimelineListener? - var addTimelineListenerListenerReceivedInvocations: [TimelineListener] = [] - var addTimelineListenerListenerReturnValue: Result<[TimelineItem], RoomProxyError>! - var addTimelineListenerListenerClosure: ((TimelineListener) -> Result<[TimelineItem], RoomProxyError>)? + var registerTimelineListenerIfNeededReturnValue: Result<[TimelineItem], RoomProxyError>! + var registerTimelineListenerIfNeededClosure: (() -> Result<[TimelineItem], RoomProxyError>)? - func addTimelineListener(listener: TimelineListener) -> Result<[TimelineItem], RoomProxyError> { - addTimelineListenerListenerCallsCount += 1 - addTimelineListenerListenerReceivedListener = listener - addTimelineListenerListenerReceivedInvocations.append(listener) - if let addTimelineListenerListenerClosure = addTimelineListenerListenerClosure { - return addTimelineListenerListenerClosure(listener) + func registerTimelineListenerIfNeeded() -> Result<[TimelineItem], RoomProxyError> { + registerTimelineListenerIfNeededCallsCount += 1 + if let registerTimelineListenerIfNeededClosure = registerTimelineListenerIfNeededClosure { + return registerTimelineListenerIfNeededClosure() } else { - return addTimelineListenerListenerReturnValue + return registerTimelineListenerIfNeededReturnValue } } - //MARK: - removeTimelineListener - - var removeTimelineListenerCallsCount = 0 - var removeTimelineListenerCalled: Bool { - return removeTimelineListenerCallsCount > 0 - } - var removeTimelineListenerClosure: (() -> Void)? - - func removeTimelineListener() { - removeTimelineListenerCallsCount += 1 - removeTimelineListenerClosure?() - } //MARK: - paginateBackwards var paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount = 0 diff --git a/ElementX/Sources/Mocks/RoomProxyMock.swift b/ElementX/Sources/Mocks/RoomProxyMock.swift index 184925713..1f7c9cd03 100644 --- a/ElementX/Sources/Mocks/RoomProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomProxyMock.swift @@ -73,5 +73,7 @@ extension RoomProxyMock { updateMembersClosure = { } acceptInvitationClosure = { .success(()) } + registerTimelineListenerIfNeededClosure = { .success([]) } + underlyingUpdatesPublisher = Empty(completeImmediately: false).eraseToAnyPublisher() } } diff --git a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift index f8e56c5bd..b94780f9b 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift @@ -53,6 +53,9 @@ struct LoadableAvatarImage: View { } placeholder: { PlaceholderAvatarImage(name: name, contentID: contentID) } + // Binds the lifecycle of the LoadableImage to the associated URL. + // This fixes the problem of the cache returning old values after a change in the URL. + .id(url) } else { PlaceholderAvatarImage(name: name, contentID: contentID) } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 9cb1e01fe..2cce10457 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -36,7 +36,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr canonicalAlias: roomProxy.canonicalAlias, isEncrypted: roomProxy.isEncrypted, isDirect: roomProxy.isDirect, - title: roomProxy.displayName ?? roomProxy.name ?? "Unknown Room", + title: roomProxy.roomTitle, topic: roomProxy.topic, avatarURL: roomProxy.avatarURL, permalink: roomProxy.permalink, @@ -81,6 +81,13 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr // MARK: - Private private func setupSubscriptions() { + switch roomProxy.registerTimelineListenerIfNeeded() { + case .success, .failure(.roomListenerAlreadyRegistered): + break + case .failure: + MXLog.error("Failed to register a room listener in room's details for the room \(roomProxy.id)") + } + roomProxy.membersPublisher .sink { [weak self] members in guard let self else { return } @@ -102,6 +109,16 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } } .store(in: &cancellables) + + roomProxy.updatesPublisher + .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) + .sink { [weak self] _ in + guard let self else { return } + self.state.title = self.roomProxy.roomTitle + self.state.topic = self.roomProxy.topic + self.state.avatarURL = self.roomProxy.avatarURL + } + .store(in: &cancellables) } private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index dd1edc642..2194c801e 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -44,8 +44,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { viewModel = RoomScreenViewModel(timelineController: parameters.timelineController, mediaProvider: parameters.mediaProvider, - roomName: parameters.roomProxy.displayName ?? parameters.roomProxy.name, - roomAvatarUrl: parameters.roomProxy.avatarURL) + roomProxy: parameters.roomProxy) } // MARK: - Public @@ -76,7 +75,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol { } func stop() { - parameters.roomProxy.removeTimelineListener() viewModel.context.send(viewAction: .markRoomAsRead) } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index b7eb92f79..5d833fbeb 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -27,17 +27,18 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol static let toastErrorID = "RoomScreenToastError" } + private let roomProxy: RoomProxyProtocol private let timelineController: RoomTimelineControllerProtocol init(timelineController: RoomTimelineControllerProtocol, mediaProvider: MediaProviderProtocol, - roomName: String?, - roomAvatarUrl: URL? = nil) { + roomProxy: RoomProxyProtocol) { + self.roomProxy = roomProxy self.timelineController = timelineController super.init(initialViewState: RoomScreenViewState(roomId: timelineController.roomID, - roomTitle: roomName ?? "Unknown room 💥", - roomAvatarURL: roomAvatarUrl, + roomTitle: roomProxy.roomTitle, + roomAvatarURL: roomProxy.avatarURL, timelineStyle: ServiceLocator.shared.settings.timelineStyle, bindings: .init(composerText: "", composerFocused: false)), imageProvider: mediaProvider) @@ -70,6 +71,16 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol return self.contextMenuActionsForItemId(itemId) } + roomProxy + .updatesPublisher + .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) + .sink { [weak self] _ in + guard let self else { return } + self.state.roomTitle = roomProxy.roomTitle + self.state.roomAvatarURL = roomProxy.avatarURL + } + .store(in: &cancellables) + ServiceLocator.shared.settings.$timelineStyle .weakAssign(to: \.state.timelineStyle, on: self) .store(in: &cancellables) @@ -406,5 +417,5 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol extension RoomScreenViewModel { static let mock = RoomScreenViewModel(timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), - roomName: "Preview room") + roomProxy: RoomProxyMock(with: .init(displayName: "Preview room"))) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift index 5ab8f9827..90ac897db 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift @@ -59,8 +59,7 @@ struct RoomHeaderView_Previews: PreviewProvider { static var bodyPlain: some View { let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), - roomName: "Some Room name", - roomAvatarUrl: URL.picturesDirectory) + roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name", avatarURL: URL.picturesDirectory))) RoomHeaderView(context: viewModel.context) .previewLayout(.sizeThatFits) @@ -71,8 +70,7 @@ struct RoomHeaderView_Previews: PreviewProvider { static var bodyEncrypted: some View { let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), - roomName: "Some Room name", - roomAvatarUrl: nil) + roomProxy: RoomProxyMock(with: .init(displayName: "Some Room name"))) RoomHeaderView(context: viewModel.context) .previewLayout(.sizeThatFits) diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 340a3a765..3e587a057 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -133,7 +133,7 @@ struct RoomScreen: View { struct RoomScreen_Previews: PreviewProvider { static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), - roomName: "Preview room") + roomProxy: RoomProxyMock(with: .init(displayName: "Preview room"))) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift index a5e6d38ed..bd091a8cf 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift @@ -84,7 +84,7 @@ struct TimelineView: UIViewControllerRepresentable { struct TimelineTableView_Previews: PreviewProvider { static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), - roomName: "Preview room") + roomProxy: RoomProxyMock(with: .init(displayName: "Preview room"))) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 88276caf8..c7e7cc604 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -41,7 +41,29 @@ class RoomProxy: RoomProxyProtocol { var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> { membersSubject.eraseToAnyPublisher() } - + + private var timelineListener: RoomTimelineListener? + private let updatesSubject = PassthroughSubject() + var updatesPublisher: AnyPublisher { + updatesSubject.eraseToAnyPublisher() + } + + deinit { + Task { @MainActor [roomTimelineObservationToken, slidingSyncRoom] in + roomTimelineObservationToken?.cancel() + + let task = ExpiringTaskRunner { + let unsubscribeTask = slidingSyncRoom.unsubscribeFromRoom() + + while !unsubscribeTask.isFinished() { + try await Task.sleep(for: .seconds(2)) + } + } + + try? await task.run(timeout: .seconds(30)) + } + } + init(slidingSyncRoom: SlidingSyncRoomProtocol, room: RoomProtocol, backgroundTaskService: BackgroundTaskServiceProtocol) { @@ -139,15 +161,27 @@ class RoomProxy: RoomProxyProtocol { } } - func addTimelineListener(listener: TimelineListener) -> Result<[TimelineItem], RoomProxyError> { - let settings = RoomSubscription(requiredState: [RequiredState(key: "m.room.topic", value: ""), + func registerTimelineListenerIfNeeded() -> Result<[TimelineItem], RoomProxyError> { + guard timelineListener == nil else { + return .failure(.roomListenerAlreadyRegistered) + } + + let settings = RoomSubscription(requiredState: [RequiredState(key: "m.room.name", value: ""), + RequiredState(key: "m.room.topic", value: ""), + RequiredState(key: "m.room.avatar", value: ""), RequiredState(key: "m.room.canonical_alias", value: ""), RequiredState(key: "m.room.join_rules", value: "")], timelineLimit: UInt32(SlidingSyncConstants.timelinePrecachingTimelineLimit)) roomSubscriptionObservationToken = slidingSyncRoom.subscribeToRoom(settings: settings) + let listener = RoomTimelineListener { [weak self] timelineDiff in + self?.updatesSubject.send(timelineDiff) + } + if let result = try? slidingSyncRoom.addTimelineListener(listener: listener) { roomTimelineObservationToken = result.taskHandle + timelineListener = listener + Task { await fetchMembers() await updateMembers() @@ -158,15 +192,6 @@ class RoomProxy: RoomProxyProtocol { } } - func removeTimelineListener() { - roomTimelineObservationToken?.cancel() - roomTimelineObservationToken = nil - - roomSubscriptionObservationToken = nil - - roomUnsubscriptionObservationToken = slidingSyncRoom.unsubscribeFromRoom() - } - func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result { do { try await Task.dispatch(on: .global()) { @@ -487,3 +512,15 @@ class RoomProxy: RoomProxyProtocol { self.displayName = displayName } } + +private class RoomTimelineListener: TimelineListener { + private let onUpdateClosure: (TimelineDiff) -> Void + + init(_ onUpdateClosure: @escaping (TimelineDiff) -> Void) { + self.onUpdateClosure = onUpdateClosure + } + + func onUpdate(update: TimelineDiff) { + onUpdateClosure(update) + } +} diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index bb5a7daca..f7f9dd750 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -20,6 +20,7 @@ import MatrixRustSDK enum RoomProxyError: Error { case noMoreMessagesToBackPaginate + case roomListenerAlreadyRegistered case failedPaginatingBackwards case failedRetrievingMemberAvatarURL case failedRetrievingMemberDisplayName @@ -59,14 +60,19 @@ protocol RoomProxyProtocol { var avatarURL: URL? { get } var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> { get } + + /// Publishes the room's updates. + /// The publisher starts publishing after the first call to `registerTimelineListenerIfNeeded()` + /// The thread on which this publisher sends the output isn't defined. + var updatesPublisher: AnyPublisher { get } func loadAvatarURLForUserId(_ userId: String) async -> Result func loadDisplayNameForUserId(_ userId: String) async -> Result - func addTimelineListener(listener: TimelineListener) -> Result<[TimelineItem], RoomProxyError> - - func removeTimelineListener() + /// Registers a timeline listener if not registered already. + /// Updates for this object will be published on the `updatesPublisher` publisher. + func registerTimelineListenerIfNeeded() -> Result<[TimelineItem], RoomProxyError> func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result @@ -130,4 +136,10 @@ extension RoomProxyProtocol { func sendMessage(_ message: String) async -> Result { await sendMessage(message, inReplyTo: nil) } + + // Avoids to duplicate the same logic around in the app + // Probably this should be done in rust. + var roomTitle: String { + displayName ?? name ?? "Unknown room 💥" + } } diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift index f22282b0f..f3da99937 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift @@ -18,14 +18,6 @@ import Combine import Foundation import MatrixRustSDK -private class RoomTimelineListener: TimelineListener { - let itemsUpdatePublisher = PassthroughSubject() - - func onUpdate(update: TimelineDiff) { - itemsUpdatePublisher.send(update) - } -} - class RoomTimelineProvider: RoomTimelineProviderProtocol { private let roomProxy: RoomProxyProtocol private var cancellables = Set() @@ -48,21 +40,21 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol { serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility) itemProxies = [] - Task { - let roomTimelineListener = RoomTimelineListener() - - roomTimelineListener - .itemsUpdatePublisher + Task { @MainActor in + roomProxy + .updatesPublisher .collect(.byTime(serialDispatchQueue, 0.1)) .sink { [weak self] in self?.updateItemsWithDiffs($0) } .store(in: &cancellables) - switch await roomProxy.addTimelineListener(listener: roomTimelineListener) { - case .success(let items): + switch roomProxy.registerTimelineListenerIfNeeded() { + case let .success(items): itemProxies = items.map(TimelineItemProxy.init) MXLog.info("Added timeline listener, current items (\(items.count)) : \(items.map(\.debugIdentifier))") + case .failure(.roomListenerAlreadyRegistered): + MXLog.info("Listener already registered for the room: \(roomProxy.id)") case .failure: - let roomID = await roomProxy.id + let roomID = roomProxy.id MXLog.error("Failed adding timeline listener on room with identifier: \(roomID)") } } diff --git a/UnitTests/Sources/RoomDetailsViewModelTests.swift b/UnitTests/Sources/RoomDetailsViewModelTests.swift index 70b371f40..1b4818b9b 100644 --- a/UnitTests/Sources/RoomDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsViewModelTests.swift @@ -214,4 +214,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase { await Task.yield() XCTAssertTrue(callbackCorrectlyCalled) } + + func testRoomSubscription() async { + await context.nextViewState() + XCTAssertEqual(roomProxyMock.registerTimelineListenerIfNeededCallsCount, 1) + } } diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index cd921506a..ade55710d 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -35,7 +35,7 @@ class RoomScreenViewModelTests: XCTestCase { timelineController.timelineItems = items let viewModel = RoomScreenViewModel(timelineController: timelineController, mediaProvider: MockMediaProvider(), - roomName: nil) + roomProxy: RoomProxyMock(with: .init(displayName: ""))) // Then the messages should be grouped together. XCTAssertEqual(viewModel.state.items[0].timelineGroupStyle, .first, "Nothing should prevent the first message from being grouped.") @@ -65,7 +65,7 @@ class RoomScreenViewModelTests: XCTestCase { timelineController.timelineItems = items let viewModel = RoomScreenViewModel(timelineController: timelineController, mediaProvider: MockMediaProvider(), - roomName: nil) + roomProxy: RoomProxyMock(with: .init(displayName: ""))) // Then the messages should be grouped by sender. XCTAssertEqual(viewModel.state.items[0].timelineGroupStyle, .single, "A message should not be grouped when the sender changes.") @@ -93,7 +93,7 @@ class RoomScreenViewModelTests: XCTestCase { timelineController.timelineItems = items let viewModel = RoomScreenViewModel(timelineController: timelineController, mediaProvider: MockMediaProvider(), - roomName: nil) + roomProxy: RoomProxyMock(with: .init(displayName: ""))) // Then the first message should not be grouped but the other two should. XCTAssertEqual(viewModel.state.items[0].timelineGroupStyle, .single, "When the first message has reactions it should not be grouped.") @@ -118,7 +118,7 @@ class RoomScreenViewModelTests: XCTestCase { timelineController.timelineItems = items let viewModel = RoomScreenViewModel(timelineController: timelineController, mediaProvider: MockMediaProvider(), - roomName: nil) + roomProxy: RoomProxyMock(with: .init(displayName: ""))) // Then the first and second messages should be grouped and the last one should not. XCTAssertEqual(viewModel.state.items[0].timelineGroupStyle, .first, "Nothing should prevent the first message from being grouped.") @@ -143,7 +143,7 @@ class RoomScreenViewModelTests: XCTestCase { timelineController.timelineItems = items let viewModel = RoomScreenViewModel(timelineController: timelineController, mediaProvider: MockMediaProvider(), - roomName: nil) + roomProxy: RoomProxyMock(with: .init(displayName: ""))) // Then the messages should be grouped together. XCTAssertEqual(viewModel.state.items[0].timelineGroupStyle, .first, "Nothing should prevent the first message from being grouped.")