From 75a3751f787ae6a286025cc829df3d27bbd21408 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Thu, 8 Jun 2023 12:05:56 +0200 Subject: [PATCH] Read Receipts display with the user avatar (#1038) * added the read receipt into the code * fixed some things lost during merge * implemented the status view to be always at the trailing side * preview code for better testing * removed timelineStyle modifier * removed also the group style * added some UI tests for the read receipts --- ElementX.xcodeproj/project.pbxproj | 30 +++--- .../xcshareddata/swiftpm/Package.resolved | 4 +- ElementX/Sources/Other/AvatarSize.swift | 3 + .../Screens/RoomScreen/RoomScreenModels.swift | 7 ++ .../RoomScreen/RoomScreenViewModel.swift | 14 +++ .../Screens/RoomScreen/View/RoomScreen.swift | 3 +- .../Style/TimelineItemBubbledStylerView.swift | 38 +++++--- .../Style/TimelineItemPlainStylerView.swift | 4 +- .../RoomScreen/View/Style/TimelineStyle.swift | 15 ++- ...iew.swift => TimelineItemStatusView.swift} | 13 ++- .../TimelineReadReceiptsView.swift | 97 +++++++++++++++++++ .../View/Timeline/AudioRoomTimelineView.swift | 4 +- .../View/Timeline/EmoteRoomTimelineView.swift | 4 +- .../Timeline/EncryptedRoomTimelineView.swift | 4 +- .../View/Timeline/FileRoomTimelineView.swift | 4 +- .../View/Timeline/FormattedBodyText.swift | 2 +- .../View/Timeline/ImageRoomTimelineView.swift | 4 +- .../Timeline/NoticeRoomTimelineView.swift | 4 +- .../View/Timeline/StateRoomTimelineView.swift | 3 +- .../Timeline/StickerRoomTimelineView.swift | 4 +- .../View/Timeline/TextRoomTimelineView.swift | 4 +- .../UnsupportedRoomTimelineView.swift | 4 +- .../View/Timeline/VideoRoomTimelineView.swift | 4 +- .../DeveloperOptionsScreenModels.swift | 2 + .../DeveloperOptionsScreenViewModel.swift | 5 +- .../View/DeveloperOptionsScreen.swift | 7 ++ .../Fixtures/RoomTimelineItemFixtures.swift | 39 +++++++- .../TimelineItemContent/ReadReceipt.swift | 26 +++++ .../RoomTimelineItemProperties.swift | 2 + .../RoomTimelineItemFactory.swift | 37 +++++-- .../RoomTimelineViewProvider.swift | 2 +- .../UITests/UITestsAppCoordinator.swift | 12 +++ .../UITests/UITestsScreenIdentifier.swift | 1 + UITests/Sources/RoomScreenUITests.swift | 7 ++ ...tion.roomSmallTimelineWithReadReceipts.png | 3 + ...e-14.roomSmallTimelineWithReadReceipts.png | 3 + ...tion.roomSmallTimelineWithReadReceipts.png | 3 + ...e-14.roomSmallTimelineWithReadReceipts.png | 3 + 38 files changed, 360 insertions(+), 65 deletions(-) rename ElementX/Sources/Screens/RoomScreen/View/Supplementary/{TimelineReceiptView.swift => TimelineItemStatusView.swift} (85%) create mode 100644 ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift create mode 100644 ElementX/Sources/Services/Timeline/TimelineItemContent/ReadReceipt.swift create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomSmallTimelineWithReadReceipts.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomSmallTimelineWithReadReceipts.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomSmallTimelineWithReadReceipts.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomSmallTimelineWithReadReceipts.png diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 37a5d7fa0..70dc5cc48 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -99,7 +99,6 @@ 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 */; }; @@ -175,6 +174,7 @@ 48FE5F0E3921146DBF4E61E7 /* OnboardingBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06A27D9C70E0DCC1E199163 /* OnboardingBackgroundView.swift */; }; 492274DA6691EE985C2FCCAA /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 67E7A6F388D3BF85767609D9 /* Sentry */; }; 496CC9D59ACFAB84FD9B3B5F /* AnalyticsPromptScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840E86A67DB2C92C09771EAD /* AnalyticsPromptScreenModels.swift */; }; + 49814A48470F347426513B07 /* TimelineReadReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1877038D1AD3D5A029F8AE2C /* TimelineReadReceiptsView.swift */; }; 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; }; 4A618590DEB72C4F186BFED4 /* UserSessionFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */; }; 4A85928E27D4C1A548A06EE9 /* StartChatScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */; }; @@ -360,6 +360,7 @@ 8F2FAA98457750D9D664136F /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 9573B94B1C86C6DF751AF3FD /* SwiftState */; }; 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; + 91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; 92B95779840CD749117B3615 /* EmojiMartStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38AE3617D7619EF30CDD229 /* EmojiMartStore.swift */; }; 92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */; }; @@ -656,6 +657,7 @@ FC10228E73323BDC09526F97 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; }; FCD3F2B82CAB29A07887A127 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; }; FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; }; + FDFB75880BC21B297E66AB28 /* TimelineItemStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A78A43F3971B1BE8CF642E /* TimelineItemStatusView.swift */; }; FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; }; FF149F0A3550A54C50ECBE7A /* AppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843CF833BF6485B64AC87E1 /* AppRouter.swift */; }; FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */; }; @@ -761,7 +763,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; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; @@ -774,6 +776,7 @@ 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; + 1877038D1AD3D5A029F8AE2C /* TimelineReadReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReadReceiptsView.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactoryProtocol.swift; sourceTree = ""; }; 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; @@ -825,6 +828,7 @@ 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = ""; }; 303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModel.swift; sourceTree = ""; }; 30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerTests.swift; sourceTree = ""; }; + 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = ""; }; 31B35311C7FED04B0E1B80C2 /* RoomMemberDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetails.swift; sourceTree = ""; }; 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = ""; }; 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyProtocol.swift; sourceTree = ""; }; @@ -845,7 +849,6 @@ 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 = ""; }; @@ -880,8 +883,9 @@ 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = ""; }; - 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; }; + 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; }; 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = ""; }; + 47A78A43F3971B1BE8CF642E /* TimelineItemStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemStatusView.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = ""; }; 4959CECEC984B3995616F427 /* DataProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = ""; }; @@ -1033,7 +1037,7 @@ 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; 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 = ""; }; @@ -1132,7 +1136,7 @@ B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModel.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; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; 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 = ""; }; B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsSignalling.swift; sourceTree = ""; }; @@ -1196,7 +1200,7 @@ CD6B0C4639E066915B5E6463 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; 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 = ""; }; @@ -1262,7 +1266,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; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; 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 = ""; }; @@ -2158,6 +2162,7 @@ children = ( B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */, 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */, + 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */, 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */, BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */, ); @@ -2964,7 +2969,8 @@ children = ( D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */, 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */, - 3A5457B0320D028A676C248F /* TimelineReceiptView.swift */, + 1877038D1AD3D5A029F8AE2C /* TimelineReadReceiptsView.swift */, + 47A78A43F3971B1BE8CF642E /* TimelineItemStatusView.swift */, ); path = Supplementary; sourceTree = ""; @@ -4005,6 +4011,7 @@ 2835FD52F3F618D07F799B3D /* Publisher.swift in Sources */, 743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */, 8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */, + 91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */, C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */, AF19D65A9C60C6B2646F3210 /* RedactedRoomTimelineItem.swift in Sources */, 13853973A5E24374FCEDE8A3 /* RedactedRoomTimelineView.swift in Sources */, @@ -4147,11 +4154,12 @@ 9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */, 1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */, 9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */, - 28A94A7DF684F2A29C4ADFE9 /* TimelineReceiptView.swift in Sources */, + 49814A48470F347426513B07 /* TimelineReadReceiptsView.swift in Sources */, 2A90DD14DE5C891BFA433950 /* TimelineReplyView.swift in Sources */, ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */, C4FE0E11A907C8999F92D5A8 /* TimelineStartRoomTimelineItem.swift in Sources */, 6FF51EB400DBA0668FC38B97 /* TimelineStartRoomTimelineView.swift in Sources */, + FDFB75880BC21B297E66AB28 /* TimelineItemStatusView.swift in Sources */, 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */, FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */, 702694459B649B9D3A3C34F8 /* TimelineTableViewController.swift in Sources */, diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 244796746..f7c054f43 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -165,8 +165,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-parsing", "state" : { - "revision" : "c6e2241daa46e5c6e5027a93b161bca6ba692bcc", - "version" : "0.12.0" + "revision" : "27c941bbd22a4bbc53005a15a0440443fd892f70", + "version" : "0.12.1" } }, { diff --git a/ElementX/Sources/Other/AvatarSize.swift b/ElementX/Sources/Other/AvatarSize.swift index 351a30246..74a0b462b 100644 --- a/ElementX/Sources/Other/AvatarSize.swift +++ b/ElementX/Sources/Other/AvatarSize.swift @@ -49,9 +49,12 @@ enum UserAvatarSizeOnScreen { case startChat case memberDetails case inviteUsers + case readReceipt var value: CGFloat { switch self { + case .readReceipt: + return 16 case .timeline: return 32 case .home: diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index b7729934f..4d0a6a780 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -77,10 +77,12 @@ struct RoomScreenViewState: BindableState { var roomTitle = "" var roomAvatarURL: URL? var items: [RoomTimelineViewProvider] = [] + var members: [String: RoomMemberState] = [:] var canBackPaginate = true var isBackPaginating = false var showLoading = false var timelineStyle: TimelineStyle + var readReceiptsEnabled: Bool var bindings: RoomScreenViewStateBindings @@ -123,3 +125,8 @@ enum RoomScreenErrorType: Hashable { /// A specific error message shown in a toast. case toast(String) } + +struct RoomMemberState { + let displayName: String? + let avatarURL: URL? +} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 76371c007..6c32383ac 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -44,6 +44,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol roomTitle: roomProxy.roomTitle, roomAvatarURL: roomProxy.avatarURL, timelineStyle: ServiceLocator.shared.settings.timelineStyle, + readReceiptsEnabled: ServiceLocator.shared.settings.readReceiptsEnabled, bindings: .init(composerText: "", composerFocused: false)), imageProvider: mediaProvider) @@ -88,6 +89,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol ServiceLocator.shared.settings.$timelineStyle .weakAssign(to: \.state.timelineStyle, on: self) .store(in: &cancellables) + + ServiceLocator.shared.settings.$readReceiptsEnabled + .weakAssign(to: \.state.readReceiptsEnabled, on: self) + .store(in: &cancellables) + + roomProxy.membersPublisher + .map { members in + members.reduce(into: [String: RoomMemberState]()) { dictionary, member in + dictionary[member.userID] = RoomMemberState(displayName: member.displayName, avatarURL: member.avatarURL) + } + } + .weakAssign(to: \.state.members, on: self) + .store(in: &cancellables) buildTimelineViews() } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 7b16085c2..f347f80e1 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -68,7 +68,8 @@ struct RoomScreen: View { TimelineView() .id(context.viewState.roomId) .environmentObject(context) - .timelineStyle(context.viewState.timelineStyle) + .environment(\.timelineStyle, context.viewState.timelineStyle) + .environment(\.readReceiptsEnabled, context.viewState.readReceiptsEnabled) .overlay(alignment: .bottomTrailing) { scrollToBottomButton } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index 31345cc08..118660f0b 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -42,16 +42,28 @@ struct TimelineItemBubbledStylerView: View { header .zIndex(1) } - - HStack { - if timelineItem.isOutgoing { - Spacer() + + VStack(alignment: alignment, spacing: 0) { + HStack { + if timelineItem.isOutgoing { + Spacer() + } + + messageBubbleWithReactions } + .padding(timelineItem.isOutgoing ? .leading : .trailing, 40) // Extra padding to differentiate alignment. - messageBubbleWithReactions + HStack(spacing: 0) { + if !timelineItem.isOutgoing { + Spacer() + } + TimelineItemStatusView(timelineItem: timelineItem) + .environmentObject(context) + .padding(.top, 8) + .padding(.bottom, 3) + } } - .padding(.horizontal, 16.0) - .padding(timelineItem.isOutgoing ? .leading : .trailing, 40) // Extra padding to differentiate alignment. + .padding(.horizontal, 16) } } } @@ -92,11 +104,6 @@ struct TimelineItemBubbledStylerView: View { context.send(viewAction: .sendReaction(key: key, eventID: timelineItem.id)) } } - - TimelineReceiptView(timelineItem: timelineItem) - .environmentObject(context) - .padding(.top, 10) - .padding(.bottom, 3) } } @@ -120,7 +127,7 @@ struct TimelineItemBubbledStylerView: View { } .padding(.top, messageBubbleTopPadding) } - + @ViewBuilder var styledContent: some View { if shouldAvoidBubbling { @@ -254,6 +261,9 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider { static var previews: some View { mockTimeline .previewDisplayName("Mock Timeline") + mockTimeline + .environment(\.readReceiptsEnabled, true) + .previewDisplayName("Mock Timeline with read receipts") mockTimeline .environment(\.layoutDirection, .rightToLeft) .previewDisplayName("Mock Timeline RTL") @@ -267,7 +277,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider { item.padding(TimelineStyle.bubbles.rowInsets) // Insets added in the table view cells } } - .timelineStyle(.bubbles) + .environment(\.timelineStyle, .bubbles) .previewLayout(.sizeThatFits) .environmentObject(viewModel.context) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift index e278a11a6..a1d778dcb 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift @@ -42,7 +42,7 @@ struct TimelineItemPlainStylerView: View { supplementaryViews } } - TimelineReceiptView(timelineItem: timelineItem) + TimelineItemStatusView(timelineItem: timelineItem) .environmentObject(context) } } @@ -139,7 +139,7 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider { .padding(TimelineStyle.plain.rowInsets) // Insets added in the table view cells } } - .timelineStyle(.plain) + .environment(\.timelineStyle, .plain) .previewLayout(.sizeThatFits) .environmentObject(viewModel.context) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyle.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyle.swift index 12175a3e4..72c4e13ec 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyle.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyle.swift @@ -58,6 +58,10 @@ private struct TimelineGroupStyleKey: EnvironmentKey { static let defaultValue = TimelineGroupStyle.single } +private struct ReadReceiptsEnabledKey: EnvironmentKey { + static let defaultValue = false +} + extension EnvironmentValues { var timelineStyle: TimelineStyle { get { self[TimelineStyleKey.self] } @@ -68,14 +72,9 @@ extension EnvironmentValues { get { self[TimelineGroupStyleKey.self] } set { self[TimelineGroupStyleKey.self] = newValue } } -} -extension View { - func timelineStyle(_ style: TimelineStyle) -> some View { - environment(\.timelineStyle, style) - } - - func timelineGroupStyle(_ groupStyle: TimelineGroupStyle) -> some View { - environment(\.timelineGroupStyle, groupStyle) + var readReceiptsEnabled: Bool { + get { self[ReadReceiptsEnabledKey.self] } + set { self[ReadReceiptsEnabledKey.self] = newValue } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReceiptView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemStatusView.swift similarity index 85% rename from ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReceiptView.swift rename to ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemStatusView.swift index c6873c715..dbe0a89ed 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReceiptView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemStatusView.swift @@ -16,10 +16,10 @@ import SwiftUI -// This is also going to be used for read receipts in the future -struct TimelineReceiptView: View { +struct TimelineItemStatusView: View { let timelineItem: EventBasedTimelineItemProtocol @Environment(\.timelineStyle) private var style + @Environment(\.readReceiptsEnabled) private var readReceiptsEnabled @EnvironmentObject private var context: RoomScreenViewModel.Context @State private var shouldShowDeliveryStatus = true @@ -34,7 +34,9 @@ struct TimelineReceiptView: View { } var body: some View { - if shouldShowDeliveryStatus { + if !timelineItem.properties.orderedReadReceipts.isEmpty, readReceiptsEnabled { + readReceipts + } else if shouldShowDeliveryStatus { deliveryStatus .onChange(of: timelineItem.properties.deliveryStatus) { newValue in if newValue == .sent, !isLast { @@ -71,4 +73,9 @@ struct TimelineReceiptView: View { // The bubbles handle the failure internally } } + + var readReceipts: some View { + TimelineReadReceiptsView(timelineItem: timelineItem) + .environmentObject(context) + } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift new file mode 100644 index 000000000..67abebd40 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift @@ -0,0 +1,97 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct TimelineReadReceiptsView: View { + let displayNumber = 3 + let timelineItem: EventBasedTimelineItemProtocol + @EnvironmentObject private var context: RoomScreenViewModel.Context + + var body: some View { + HStack(spacing: 2) { + HStack(spacing: -4) { + let receiptsToDisplay = timelineItem.properties.orderedReadReceipts.prefix(displayNumber) + ForEach(0.. displayNumber { + let remaining = timelineItem.properties.orderedReadReceipts.count - displayNumber + Text("+\(remaining)") + .font(.compound.bodySM) + .foregroundColor(.element.primaryContent) + } + } + } +} + +struct TimelineReadReceiptsView_Previews: PreviewProvider { + static let members: [RoomMemberProxyMock] = [ + .mockAlice, + .mockBob, + .mockCharlie, + .mockDan, + .mockMe + ] + + static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(), + mediaProvider: MockMediaProvider(), + roomProxy: RoomProxyMock(with: .init(displayName: "Test", + members: members))) + + static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")] + static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"), + ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before")] + static let tripleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"), + ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before"), + ReadReceipt(userID: RoomMemberProxyMock.mockCharlie.userID, formattedTimestamp: "Way before")] + static let quadrupleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"), + ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before"), + ReadReceipt(userID: RoomMemberProxyMock.mockCharlie.userID, formattedTimestamp: "Way before"), + ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")] + + static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem { + TextRoomTimelineItem(id: UUID().uuidString, + timestamp: "Now", + isOutgoing: true, + isEditable: false, sender: .init(id: UUID().uuidString), content: .init(body: "Test"), + properties: .init(orderedReadReceipts: receipts)) + } + + static var previews: some View { + VStack { + TimelineReadReceiptsView(timelineItem: mockTimelineItem(with: singleReceipt)) + .environmentObject(viewModel.context) + TimelineReadReceiptsView(timelineItem: mockTimelineItem(with: doubleReceipt)) + .environmentObject(viewModel.context) + TimelineReadReceiptsView(timelineItem: mockTimelineItem(with: tripleReceipt)) + .environmentObject(viewModel.context) + TimelineReadReceiptsView(timelineItem: mockTimelineItem(with: quadrupleReceipt)) + .environmentObject(viewModel.context) + } + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/AudioRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/AudioRoomTimelineView.swift index 948d16206..7ec3dfb7b 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/AudioRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/AudioRoomTimelineView.swift @@ -38,7 +38,9 @@ struct AudioRoomTimelineView_Previews: PreviewProvider { static var previews: some View { body.environmentObject(viewModel.context) - body.timelineStyle(.plain).environmentObject(viewModel.context) + body + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) } static var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift index 7e0c8985f..2b73a8abc 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift @@ -37,7 +37,9 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider { static var previews: some View { body.environmentObject(viewModel.context) - body.timelineStyle(.plain).environmentObject(viewModel.context) + body + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) } static var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift index 8a459659c..c214e9a2c 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift @@ -47,7 +47,9 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider { static var previews: some View { body.environmentObject(viewModel.context) - body.timelineStyle(.plain).environmentObject(viewModel.context) + body + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) } static var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift index 814ec7b63..51fd6cddd 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift @@ -38,7 +38,9 @@ struct FileRoomTimelineView_Previews: PreviewProvider { static var previews: some View { body.environmentObject(viewModel.context) - body.timelineStyle(.plain).environmentObject(viewModel.context) + body + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) } static var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift index d08f3be64..ff2873a43 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift @@ -134,7 +134,7 @@ struct FormattedBodyText_Previews: PreviewProvider { static var previews: some View { body body - .timelineStyle(.plain) + .environment(\.timelineStyle, .plain) } @ViewBuilder diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift index 3dc2af91c..20b97db59 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift @@ -58,7 +58,9 @@ struct ImageRoomTimelineView_Previews: PreviewProvider { static var previews: some View { body.environmentObject(viewModel.context) - body.timelineStyle(.plain).environmentObject(viewModel.context) + body + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) } static var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift index 00ee435a1..eac68e359 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift @@ -48,7 +48,9 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider { static var previews: some View { body.environmentObject(viewModel.context) - body.timelineStyle(.plain).environmentObject(viewModel.context) + body + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) } static var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift index 7614601e5..5193ff5fe 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift @@ -33,7 +33,8 @@ struct StateRoomTimelineView: View { struct StateRoomTimelineView_Previews: PreviewProvider { static var previews: some View { body - body.timelineStyle(.plain) + body + .environment(\.timelineStyle, .plain) } static var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift index 5a42edcc6..969cec336 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift @@ -51,7 +51,9 @@ struct StickerRoomTimelineView_Previews: PreviewProvider { static var previews: some View { body.environmentObject(viewModel.context) - body.timelineStyle(.plain).environmentObject(viewModel.context) + body + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) } static var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift index d1e09e637..27b12c199 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift @@ -37,7 +37,9 @@ struct TextRoomTimelineView_Previews: PreviewProvider { static var previews: some View { body.environmentObject(viewModel.context) - body.timelineStyle(.plain).environmentObject(viewModel.context) + body + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) } static var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift index cac8c4d5f..6bb7b1475 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift @@ -43,7 +43,9 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider { static var previews: some View { body.environmentObject(viewModel.context) - body.timelineStyle(.plain).environmentObject(viewModel.context) + body + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) } static var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift index ef3e19fe0..c88384499 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift @@ -70,7 +70,9 @@ struct VideoRoomTimelineView_Previews: PreviewProvider { static var previews: some View { body.environmentObject(viewModel.context) - body.timelineStyle(.plain).environmentObject(viewModel.context) + body + .environment(\.timelineStyle, .plain) + .environmentObject(viewModel.context) } static var body: some View { diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 40dfc5391..c1d60d68b 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -27,10 +27,12 @@ struct DeveloperOptionsScreenViewState: BindableState { struct DeveloperOptionsScreenViewStateBindings { var shouldCollapseRoomStateEvents: Bool var userSuggestionsEnabled: Bool + var readReceiptsEnabled: Bool } enum DeveloperOptionsScreenViewAction { case changedShouldCollapseRoomStateEvents case changedUserSuggestionsEnabled + case changedReadReceiptsEnabled case clearCache } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift index 86b65a169..db9245684 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -26,7 +26,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve init() { appSettings = ServiceLocator.shared.settings let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: appSettings.shouldCollapseRoomStateEvents, - userSuggestionsEnabled: appSettings.userSuggestionsEnabled) + userSuggestionsEnabled: appSettings.userSuggestionsEnabled, + readReceiptsEnabled: appSettings.readReceiptsEnabled) let state = DeveloperOptionsScreenViewState(bindings: bindings) super.init(initialViewState: state) @@ -42,6 +43,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve appSettings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents case .changedUserSuggestionsEnabled: appSettings.userSuggestionsEnabled = state.bindings.userSuggestionsEnabled + case .changedReadReceiptsEnabled: + appSettings.readReceiptsEnabled = state.bindings.readReceiptsEnabled case .clearCache: callback?(.clearCache) } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 939361864..2531edcef 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -36,6 +36,13 @@ struct DeveloperOptionsScreen: View { .onChange(of: context.userSuggestionsEnabled) { _ in context.send(viewAction: .changedUserSuggestionsEnabled) } + + Toggle(isOn: $context.readReceiptsEnabled) { + Text("Show read receipts") + } + .onChange(of: context.readReceiptsEnabled) { _ in + context.send(viewAction: .changedReadReceiptsEnabled) + } } Section { diff --git a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift index b6bf5ee9f..6d9fb9a80 100644 --- a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift +++ b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift @@ -52,7 +52,8 @@ enum RoomTimelineItemFixtures { isOutgoing: false, isEditable: false, sender: .init(id: "", displayName: "Helena"), - content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!")), + content: .init(body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!"), + properties: RoomTimelineItemProperties(orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil)])), TextRoomTimelineItem(id: UUID().uuidString, timestamp: "5 PM", isOutgoing: true, @@ -68,7 +69,11 @@ enum RoomTimelineItemFixtures { properties: RoomTimelineItemProperties(reactions: [ AggregatedReaction(key: "🙏", count: 1, isHighlighted: false), AggregatedReaction(key: "😁", count: 3, isHighlighted: false) - ])), + ], + orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil), + ReadReceipt(userID: "bob", formattedTimestamp: nil), + ReadReceipt(userID: "charlie", formattedTimestamp: nil), + ReadReceipt(userID: "dan", formattedTimestamp: nil)])), TextRoomTimelineItem(id: UUID().uuidString, timestamp: "5 PM", isOutgoing: false, @@ -85,6 +90,28 @@ enum RoomTimelineItemFixtures { TextRoomTimelineItem(text: "How are you?", senderDisplayName: "Alice")] } + + /// A small chunk of events, containing 2 text items. + static var smallChunkWithReadReceipts: [RoomTimelineItemProtocol] { + [TextRoomTimelineItem(text: "Hey there 👋", + senderDisplayName: "Alice") + .withReadReceipts([ReadReceipt(userID: "a1", formattedTimestamp: nil)]), + TextRoomTimelineItem(text: "How are you?", + senderDisplayName: "Alice") + .withReadReceipts([ReadReceipt(userID: "a2", formattedTimestamp: nil), + ReadReceipt(userID: "b2", formattedTimestamp: nil)]), + TextRoomTimelineItem(text: "Fine, Thanks!", + senderDisplayName: "Bob") + .withReadReceipts([ReadReceipt(userID: "a3", formattedTimestamp: nil), + ReadReceipt(userID: "b3", formattedTimestamp: nil), + ReadReceipt(userID: "c3", formattedTimestamp: nil)]), + TextRoomTimelineItem(text: "What about you?", + senderDisplayName: "Bob") + .withReadReceipts([ReadReceipt(userID: "a4", formattedTimestamp: nil), + ReadReceipt(userID: "b4", formattedTimestamp: nil), + ReadReceipt(userID: "c4", formattedTimestamp: nil), + ReadReceipt(userID: "d4", formattedTimestamp: nil)])] + } /// A chunk of events that contains a single text item. static var singleMessageChunk: [RoomTimelineItemProtocol] { @@ -194,3 +221,11 @@ private extension TextRoomTimelineItem { content: .init(body: text)) } } + +private extension TextRoomTimelineItem { + func withReadReceipts(_ receipts: [ReadReceipt]) -> TextRoomTimelineItem { + var newSelf = self + newSelf.properties.orderedReadReceipts = receipts + return newSelf + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/ReadReceipt.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/ReadReceipt.swift new file mode 100644 index 000000000..546e26a5c --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/ReadReceipt.swift @@ -0,0 +1,26 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct ReadReceipt: Hashable { + let userID: String + let formattedTimestamp: String? +} + +extension ReadReceipt: Identifiable { + var id: String { userID } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift index bc2034c1e..ddbe6a044 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/RoomTimelineItemProperties.swift @@ -24,4 +24,6 @@ struct RoomTimelineItemProperties: Hashable { var reactions: [AggregatedReaction] = [] /// The delivery status for this item. If a sent message is echoed the value is nil. var deliveryStatus: TimelineItemDeliveryStatus? + /// The read receipts of the item, ordered from newest to oldest + var orderedReadReceipts: [ReadReceipt] = [] } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index da032370e..f114d47ef 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -132,7 +132,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { aspectRatio: aspectRatio, blurhash: imageInfo.blurhash, properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions), - deliveryStatus: eventItemProxy.deliveryStatus)) + deliveryStatus: eventItemProxy.deliveryStatus, + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy, @@ -182,7 +183,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), - deliveryStatus: eventItemProxy.deliveryStatus)) + deliveryStatus: eventItemProxy.deliveryStatus, + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -198,7 +200,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), - deliveryStatus: eventItemProxy.deliveryStatus)) + deliveryStatus: eventItemProxy.deliveryStatus, + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -214,7 +217,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), - deliveryStatus: eventItemProxy.deliveryStatus)) + deliveryStatus: eventItemProxy.deliveryStatus, + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -230,7 +234,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), - deliveryStatus: eventItemProxy.deliveryStatus)) + deliveryStatus: eventItemProxy.deliveryStatus, + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -246,7 +251,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), - deliveryStatus: eventItemProxy.deliveryStatus)) + deliveryStatus: eventItemProxy.deliveryStatus, + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -262,7 +268,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), - deliveryStatus: eventItemProxy.deliveryStatus)) + deliveryStatus: eventItemProxy.deliveryStatus, + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy, @@ -278,7 +285,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { replyDetails: buildReplyToDetailsFrom(details: messageTimelineItem.inReplyTo()), properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(), reactions: aggregateReactions(eventItemProxy.reactions), - deliveryStatus: eventItemProxy.deliveryStatus)) + deliveryStatus: eventItemProxy.deliveryStatus, + orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts))) } private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] { @@ -287,6 +295,19 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { return AggregatedReaction(key: reaction.key, count: Int(reaction.count), isHighlighted: isHighlighted) } } + + private func orderReadReceipts(_ receipts: [String: Receipt]) -> [ReadReceipt] { + receipts + .sorted { firstElement, secondElement in + // If there is no timestamp we order them as last + let firstTimestamp = firstElement.value.dateTimestamp ?? Date(timeIntervalSince1970: 0) + let secondTimestamp = secondElement.value.dateTimestamp ?? Date(timeIntervalSince1970: 0) + return firstTimestamp > secondTimestamp + } + .map { key, receipt in + ReadReceipt(userID: key, formattedTimestamp: receipt.dateTimestamp?.formatted(date: .omitted, time: .shortened)) + } + } // MARK: - Message events content diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift index be6d47139..781186458 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineViewProvider.swift @@ -139,7 +139,7 @@ enum RoomTimelineViewProvider: Identifiable, Hashable { extension RoomTimelineViewProvider: View { var body: some View { timelineView - .timelineGroupStyle(timelineGroupStyle) + .environment(\.timelineGroupStyle, timelineGroupStyle) } @ViewBuilder private var timelineView: some View { diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 19c9bcef9..ba2d219e9 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -197,6 +197,18 @@ class MockScreen: Identifiable { let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator + case .roomSmallTimelineWithReadReceipts: + ServiceLocator.shared.settings.readReceiptsEnabled = true + let navigationStackCoordinator = NavigationStackCoordinator() + let timelineController = MockRoomTimelineController() + timelineController.timelineItems = RoomTimelineItemFixtures.smallChunkWithReadReceipts + let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(with: .init(displayName: "New room", avatarURL: URL.picturesDirectory)), + timelineController: timelineController, + mediaProvider: MockMediaProvider(), + emojiProvider: EmojiProvider()) + let coordinator = RoomScreenCoordinator(parameters: parameters) + navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator case .roomSmallTimelineIncomingAndSmallPagination: let navigationStackCoordinator = NavigationStackCoordinator() diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index 53bd0453e..f707c63ca 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -36,6 +36,7 @@ enum UITestsScreenIdentifier: String { case roomPlainNoAvatar case roomEncryptedWithAvatar case roomSmallTimeline + case roomSmallTimelineWithReadReceipts case roomSmallTimelineIncomingAndSmallPagination case roomSmallTimelineLargePagination case roomLayoutTop diff --git a/UITests/Sources/RoomScreenUITests.swift b/UITests/Sources/RoomScreenUITests.swift index 75f5c939e..5039989ca 100644 --- a/UITests/Sources/RoomScreenUITests.swift +++ b/UITests/Sources/RoomScreenUITests.swift @@ -153,6 +153,13 @@ class RoomScreenUITests: XCTestCase { try await app.assertScreenshot(.roomLayoutBottom, step: 1) } + + func testTimelineReadReceipts() async throws { + let app = Application.launch(.roomSmallTimelineWithReadReceipts) + + // The messages should be bottom aligned. + try await app.assertScreenshot(.roomSmallTimelineWithReadReceipts) + } // MARK: - Helper Methods diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomSmallTimelineWithReadReceipts.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomSmallTimelineWithReadReceipts.png new file mode 100644 index 000000000..48c44ef82 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomSmallTimelineWithReadReceipts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1a51a634a8e8f9c4c55a8068d7f0cbf6b6658dd31fd8dfc82bb59dd83a9b31c +size 123189 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomSmallTimelineWithReadReceipts.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomSmallTimelineWithReadReceipts.png new file mode 100644 index 000000000..b4228bba2 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomSmallTimelineWithReadReceipts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a64e6084d03b2b61b52528525f39042f95d1a01544d729af236ef81e9359540b +size 188024 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomSmallTimelineWithReadReceipts.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomSmallTimelineWithReadReceipts.png new file mode 100644 index 000000000..cc3df45ec --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomSmallTimelineWithReadReceipts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bace67ffd6d2f7df36127fb6499ce7f9af2182d49acb89ab475bc9269e7b7447 +size 124167 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomSmallTimelineWithReadReceipts.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomSmallTimelineWithReadReceipts.png new file mode 100644 index 000000000..5e480cd77 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomSmallTimelineWithReadReceipts.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:126045309c9d7d8f7ebf5a55076cdc2258ff96c4a91fb018639096445a7e7b62 +size 189910