From 7b812a45c3869434ff0c715252c426430b5b3502 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:38:39 +0100 Subject: [PATCH] Read Receipts sheet + enabled RR by default (#2123) --- ElementX.xcodeproj/project.pbxproj | 48 +++++----- .../en.lproj/Localizable.strings | 1 + .../Sources/Application/AppSettings.swift | 2 +- ElementX/Sources/Generated/Strings.swift | 2 + ElementX/Sources/Other/AvatarSize.swift | 3 + .../Screens/RoomScreen/RoomScreenModels.swift | 9 ++ .../RoomScreen/RoomScreenViewModel.swift | 13 +++ .../View/ReadReceipts/ReadReceiptCell.swift | 87 +++++++++++++++++++ .../ReadReceiptsSummaryView.swift | 80 +++++++++++++++++ .../Screens/RoomScreen/View/RoomScreen.swift | 4 + .../TimelineReadReceiptsView.swift | 4 + .../Sources/RoomScreenViewModelTests.swift | 36 +++++++- .../test_readReceiptCell.Loading-Member.png | 3 + .../test_readReceiptCell.No-Image.png | 3 + .../test_readReceiptCell.With-Image.png | 3 + .../test_readReceiptsSummaryView.1.png | 3 + .../PreviewTests/test_roomScreen.1.png | 4 +- .../PreviewTests/test_timelineView.1.png | 4 +- .../PreviewTests/test_uITimelineView.1.png | 4 +- changelog.d/1053.feature | 1 + changelog.d/pr-2123.change | 1 + 21 files changed, 286 insertions(+), 29 deletions(-) create mode 100644 ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptCell.swift create mode 100644 ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.Loading-Member.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.No-Image.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.With-Image.png create mode 100644 UnitTests/__Snapshots__/PreviewTests/test_readReceiptsSummaryView.1.png create mode 100644 changelog.d/1053.feature create mode 100644 changelog.d/pr-2123.change diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index efb5df5c5b..618a912708 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -643,6 +643,8 @@ A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; }; A743841F91B62B0E56217B04 /* SecureBackupKeyBackupScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DCB219D7B7B0299358FF81 /* SecureBackupKeyBackupScreenUITests.swift */; }; A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; }; + A7609C5F2B0BAB9700E40AF2 /* ReadReceiptCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7609C5E2B0BAB9700E40AF2 /* ReadReceiptCell.swift */; }; + A7609C612B0BB7C100E40AF2 /* ReadReceiptsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7609C602B0BB7C100E40AF2 /* ReadReceiptsSummaryView.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 */; }; @@ -1042,7 +1044,7 @@ 033DB41C51865A2E83174E87 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 035177BCD8E8308B098AC3C2 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; 0376C429FAB1687C3D905F3E /* MockCoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCoder.swift; sourceTree = ""; }; - 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = ""; }; + 0392E3FDE372C9B56FEEED8B /* test_voice_message.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_voice_message.m4a; sourceTree = ""; }; 03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelTests.swift; sourceTree = ""; }; 044E501B8331B339874D1B96 /* CompoundIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompoundIcon.swift; sourceTree = ""; }; @@ -1103,7 +1105,7 @@ 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = ""; }; 12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModel.swift; sourceTree = ""; }; 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.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 = ""; }; @@ -1523,7 +1525,7 @@ 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.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 = ""; }; 8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = ""; }; @@ -1604,6 +1606,8 @@ A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriverProtocol.swift; sourceTree = ""; }; A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = ""; }; + A7609C5E2B0BAB9700E40AF2 /* ReadReceiptCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptCell.swift; sourceTree = ""; }; + A7609C602B0BB7C100E40AF2 /* ReadReceiptsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptsSummaryView.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 = ""; }; @@ -1659,7 +1663,7 @@ B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.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 = ""; }; B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = ""; }; B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1764,7 +1768,7 @@ CD95B3714F806AC9CF9A557B /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; 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 = ""; }; D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryRoomTimelineItem.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; @@ -1868,7 +1872,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 = ""; }; @@ -1883,7 +1887,7 @@ F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = ""; }; - F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; + F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = ""; }; F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = ""; }; F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = ""; }; @@ -3395,6 +3399,7 @@ 79023E5904B155E8E2B8B502 /* View */ = { isa = PBXGroup; children = ( + A7609C5D2B0BAB8400E40AF2 /* ReadReceipts */, 422724361B6555364C43281E /* RoomHeaderView.swift */, 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */, 4552D3466B1453F287223ADA /* SwipeRightAction.swift */, @@ -3933,6 +3938,15 @@ path = LayoutTests; sourceTree = ""; }; + A7609C5D2B0BAB8400E40AF2 /* ReadReceipts */ = { + isa = PBXGroup; + children = ( + A7609C5E2B0BAB9700E40AF2 /* ReadReceiptCell.swift */, + A7609C602B0BB7C100E40AF2 /* ReadReceiptsSummaryView.swift */, + ); + path = ReadReceipts; + sourceTree = ""; + }; A78C2592419CA4C76FBA8FD2 /* Application */ = { isa = PBXGroup; children = ( @@ -5590,6 +5604,7 @@ B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */, AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */, D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */, + A7609C5F2B0BAB9700E40AF2 /* ReadReceiptCell.swift in Sources */, F9842667B68DC6FA1F9ECCBB /* NSItemProvider.swift in Sources */, EA01A06EEDFEF4AE7652E5F3 /* NSRegularExpresion.swift in Sources */, FA2BBAE9FC5E2E9F960C0980 /* NavigationCoordinators.swift in Sources */, @@ -5769,6 +5784,7 @@ E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */, E77FE06B165A38BF1735509F /* SecureBackupScreenCoordinator.swift in Sources */, DA7E867F5EAFF8E20B2EE3B6 /* SecureBackupScreenModels.swift in Sources */, + A7609C612B0BB7C100E40AF2 /* ReadReceiptsSummaryView.swift in Sources */, 7BF368A78E6D9AFD222F25AF /* SecureBackupScreenViewModel.swift in Sources */, AC90434798E7894370E80E66 /* SecureBackupScreenViewModelProtocol.swift in Sources */, 14E99D27628B1A6F0CB46FEA /* SeparatorRoomTimelineItem.swift in Sources */, @@ -6140,9 +6156,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; @@ -6167,9 +6181,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -6195,9 +6207,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -6438,9 +6448,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index ee310a95e2..0b5de9c78c 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -10,6 +10,7 @@ "a11y_poll_end" = "Ended poll"; "a11y_read_receipts_multiple" = "Read by %1$@ and %2$@"; "a11y_read_receipts_single" = "Read by %1$@"; +"a11y_read_receipts_tap_to_show_all" = "Tap to show all"; "a11y_send_files" = "Send files"; "a11y_show_password" = "Show password"; "a11y_start_call" = "Start a call"; diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index fcf15158f3..014b757a06 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -266,7 +266,7 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.userSuggestionsEnabled, defaultValue: false, storageType: .volatile) var userSuggestionsEnabled - @UserPreference(key: UserDefaultsKeys.readReceiptsEnabled, defaultValue: false, storageType: .userDefaults(store)) + @UserPreference(key: UserDefaultsKeys.readReceiptsEnabled, defaultValue: true, storageType: .userDefaults(store)) var readReceiptsEnabled @UserPreference(key: UserDefaultsKeys.swiftUITimelineEnabled, defaultValue: false, storageType: .volatile) diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 19e2ddb648..9e1a28d2bc 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -44,6 +44,8 @@ public enum L10n { public static func a11yReadReceiptsSingle(_ p1: Any) -> String { return L10n.tr("Localizable", "a11y_read_receipts_single", String(describing: p1)) } + /// Tap to show all + public static var a11yReadReceiptsTapToShowAll: String { return L10n.tr("Localizable", "a11y_read_receipts_tap_to_show_all") } /// Send files public static var a11ySendFiles: String { return L10n.tr("Localizable", "a11y_send_files") } /// Show password diff --git a/ElementX/Sources/Other/AvatarSize.swift b/ElementX/Sources/Other/AvatarSize.swift index ebc70bd521..8ab890484f 100644 --- a/ElementX/Sources/Other/AvatarSize.swift +++ b/ElementX/Sources/Other/AvatarSize.swift @@ -50,6 +50,7 @@ enum UserAvatarSizeOnScreen { case memberDetails case inviteUsers case readReceipt + case readReceiptSheet case editUserDetails case suggestions @@ -57,6 +58,8 @@ enum UserAvatarSizeOnScreen { switch self { case .readReceipt: return 16 + case .readReceiptSheet: + return 32 case .timeline: return 32 case .home: diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index efedac85e8..1ef38a3d17 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -96,6 +96,8 @@ enum RoomScreenViewAction { case retrySend(itemID: TimelineItemIdentifier) case cancelSend(itemID: TimelineItemIdentifier) + case showReadReceipts(itemID: TimelineItemIdentifier) + case scrolledToBottom case poll(RoomScreenViewPollAction) @@ -160,6 +162,8 @@ struct RoomScreenViewStateBindings { var sendFailedConfirmationDialogInfo: SendFailedConfirmationDialogInfo? var reactionSummaryInfo: ReactionSummaryInfo? + + var readReceiptsSummaryInfo: ReadReceiptSummaryInfo? } struct TimelineItemActionMenuInfo: Equatable, Identifiable { @@ -189,6 +193,11 @@ struct ReactionSummaryInfo: Identifiable { } } +struct ReadReceiptSummaryInfo: Identifiable { + let orderedReceipts: [ReadReceipt] + let id: TimelineItemIdentifier +} + enum RoomScreenErrorType: Hashable { /// A specific error message shown in an alert. case alert(String) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 96642298f1..5380f21416 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -166,6 +166,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol processAudioAction(audioAction) case .presentCall: actionsSubject.send(.displayCallScreen) + case .showReadReceipts(itemID: let itemID): + showReadReceipts(for: itemID) } } @@ -655,6 +657,17 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.bindings.reactionSummaryInfo = .init(reactions: eventTimelineItem.properties.reactions, selectedKey: selectedKey) } + // MARK: - Read Receipts + + private func showReadReceipts(for itemID: TimelineItemIdentifier) { + guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), + let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { + return + } + + state.bindings.readReceiptsSummaryInfo = .init(orderedReceipts: eventTimelineItem.properties.orderedReadReceipts, id: eventTimelineItem.id) + } + // MARK: - User Indicators private func displayError(_ type: RoomScreenErrorType) { diff --git a/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptCell.swift b/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptCell.swift new file mode 100644 index 0000000000..a920bf5221 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptCell.swift @@ -0,0 +1,87 @@ +// +// 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 ReadReceiptCell: View { + let readReceipt: ReadReceipt + let memberState: RoomMemberState? + let imageProvider: ImageProviderProtocol? + + private var title: String { + memberState?.displayName ?? readReceipt.userID + } + + private var subtitle: String { + guard title != readReceipt.userID else { + return "" + } + return readReceipt.userID + } + + var body: some View { + HStack(spacing: 12) { + LoadableAvatarImage(url: memberState?.avatarURL, + name: memberState?.displayName, + contentID: readReceipt.userID, + avatarSize: .user(on: .readReceiptSheet), + imageProvider: imageProvider) + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 12) { + Text(title) + .font(.compound.bodyMDSemibold) + .foregroundColor(.compound.textPrimary) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + if let formattedTimestamp = readReceipt.formattedTimestamp { + Text(formattedTimestamp) + .font(.compound.bodyXS) + .foregroundColor(.compound.textSecondary) + .lineLimit(1) + } + } + Text(subtitle) + .font(.compound.bodySM) + .foregroundColor(.compound.textSecondary) + .lineLimit(1) + } + } + .padding(.vertical, 8) + .padding(.horizontal, 16) + } +} + +struct ReadReceiptCell_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org", + formattedTimestamp: "10:00"), + memberState: .init(displayName: "Test", + avatarURL: nil), + imageProvider: MockMediaProvider()) + .previewDisplayName("No Image") + ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org", + formattedTimestamp: "10:00"), + memberState: .init(displayName: "Test", + avatarURL: URL.documentsDirectory), + imageProvider: MockMediaProvider()) + .previewDisplayName("With Image") + ReadReceiptCell(readReceipt: .init(userID: "@test:matrix.org", + formattedTimestamp: "10:00"), + memberState: nil, + imageProvider: MockMediaProvider()) + .previewDisplayName("Loading Member") + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift b/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift new file mode 100644 index 0000000000..f32ffcf884 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift @@ -0,0 +1,80 @@ +// +// 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 ReadReceiptsSummaryView: View { + let orderedReadReceipts: [ReadReceipt] + @EnvironmentObject private var context: RoomScreenViewModel.Context + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text(L10n.commonSeenBy) + .font(.compound.bodyLGSemibold) + .foregroundColor(.compound.textPrimary) + .padding(.horizontal, 16) + ScrollView { + LazyVStack(spacing: 0) { + ForEach(orderedReadReceipts) { receipt in + ReadReceiptCell(readReceipt: receipt, + memberState: context.viewState.members[receipt.userID], + imageProvider: context.imageProvider) + } + } + } + } + .padding(.top, 24) + .presentationDetents([.medium, .large]) + .presentationBackground(Color.compound.bgCanvasDefault) + .presentationDragIndicator(.visible) + } +} + +struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview { + static let viewModel = { + let members: [RoomMemberProxyMock] = [ + .mockAlice, + .mockBob, + .mockCharlie, + .mockDan + ] + let roomProxyMock = RoomProxyMock(with: .init(displayName: "Room", members: members)) + let mock = RoomScreenViewModel(roomProxy: roomProxyMock, + timelineController: MockRoomTimelineController(), + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: UserIndicatorControllerMock(), + application: ApplicationMock(), + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics, + notificationCenter: NotificationCenterMock()) + return mock + }() + + static let orderedReadReceipts: [ReadReceipt] = [ + .init(userID: "@alice:matrix.org", formattedTimestamp: "10:00"), + .init(userID: "@bob:matrix.org", formattedTimestamp: "9:30"), + .init(userID: "@charlie:matrix.org", formattedTimestamp: "9:00"), + .init(userID: "@dan:matrix.org", formattedTimestamp: "8:30"), + .init(userID: "@loading:matrix.org", formattedTimestamp: "Long time ago") + ] + + static var previews: some View { + ReadReceiptsSummaryView(orderedReadReceipts: orderedReadReceipts) + .environmentObject(viewModel.context) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 9a3e809160..22a15d5424 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -65,6 +65,10 @@ struct RoomScreen: View { ReactionsSummaryView(reactions: $0.reactions, members: context.viewState.members, imageProvider: context.imageProvider, selectedReactionKey: $0.selectedKey) .edgesIgnoringSafeArea([.bottom]) } + .sheet(item: $context.readReceiptsSummaryInfo) { + ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts) + .environmentObject(context) + } .interactiveQuickLook(item: $context.mediaPreviewItem) .track(screen: .room) .onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift index c1249ffcde..5b0a202fb7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift @@ -45,8 +45,12 @@ struct TimelineReadReceiptsView: View { .foregroundColor(.compound.textPrimary) } } + .onTapGesture { + context.send(viewAction: .showReadReceipts(itemID: timelineItem.id)) + } .accessibilityElement(children: .ignore) .accessibilityLabel(accessibilityLabel) + .accessibilityHint(L10n.a11yReadReceiptsTapToShowAll) } private var remaining: Int { diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 596e57dac6..93f491b844 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -556,10 +556,42 @@ class RoomScreenViewModelTests: XCTestCase { return (viewModel, roomProxy, timelineController, notificationCenter) } + + func testShowReadReceipts() async throws { + let receipts: [ReadReceipt] = [.init(userID: "@alice:matrix.org", formattedTimestamp: "12:00"), + .init(userID: "@charlie:matrix.org", formattedTimestamp: "11:00")] + // Given 3 messages from Bob where the middle message has a reaction. + let message = TextRoomTimelineItem(text: "Test", + sender: "bob", + addReadReceipts: receipts) + let id = message.id + + // When showing them in a timeline. + let timelineController = MockRoomTimelineController() + timelineController.timelineItems = [message] + let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "", + members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])), + timelineController: timelineController, + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: userIndicatorControllerMock, + application: ApplicationMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics, + notificationCenter: NotificationCenterMock()) + + let deferred = deferFulfillment(viewModel.context.$viewState) { value in + value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts + } + + viewModel.context.send(viewAction: .showReadReceipts(itemID: id)) + try await deferred.fulfill() + } } private extension TextRoomTimelineItem { - init(text: String, sender: String, addReactions: Bool = false) { + init(text: String, sender: String, addReactions: Bool = false, addReadReceipts: [ReadReceipt] = []) { let reactions = addReactions ? [AggregatedReaction(accountOwnerID: "bob", key: "🦄", senders: [ReactionSender(senderID: sender, timestamp: Date())])] : [] self.init(id: .random, timestamp: "10:47 am", @@ -569,7 +601,7 @@ private extension TextRoomTimelineItem { isThreaded: false, sender: .init(id: "@\(sender):server.com", displayName: sender), content: .init(body: text), - properties: RoomTimelineItemProperties(reactions: reactions)) + properties: RoomTimelineItemProperties(reactions: reactions, orderedReadReceipts: addReadReceipts)) } } diff --git a/UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.Loading-Member.png b/UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.Loading-Member.png new file mode 100644 index 0000000000..29360965cb --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.Loading-Member.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2431b131a71e3ecc537771ac267f6a3c613fe469fd28cb78cb79af628e1205d6 +size 65811 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.No-Image.png b/UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.No-Image.png new file mode 100644 index 0000000000..dcbfc21d22 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.No-Image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf54a03ff167095cc424d2578fdf9c94274f22d9a172a93e04628bb5cbf504b7 +size 66209 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.With-Image.png b/UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.With-Image.png new file mode 100644 index 0000000000..abcd2a3b24 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_readReceiptCell.With-Image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbf4d823916f3dad79f996cf00380ac17df33c2774a65bc6a75bdaad534c3b32 +size 68826 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_readReceiptsSummaryView.1.png b/UnitTests/__Snapshots__/PreviewTests/test_readReceiptsSummaryView.1.png new file mode 100644 index 0000000000..081d15d7f0 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_readReceiptsSummaryView.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:098b0a6b4b4afea40281b37c0fa173f15ee28b05332050d045fe3b8bbb08b42d +size 111724 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomScreen.1.png b/UnitTests/__Snapshots__/PreviewTests/test_roomScreen.1.png index 75996840e1..27c8960bc8 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_roomScreen.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomScreen.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:610e0a0946e4bb26642f84c5dfb6fd91caf7d623290c501e70fbd83073a7e191 -size 318638 +oid sha256:349cab6a64a17c9c0ed6d76c902f06edec4763cd4b7a3304178efba6b733a321 +size 311524 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_timelineView.1.png b/UnitTests/__Snapshots__/PreviewTests/test_timelineView.1.png index 119284c7de..54b8766880 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_timelineView.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_timelineView.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf7addca5dd6f7347d38227a5b03e36a3ff9430e24bafc06976ffe7e478baa67 -size 315120 +oid sha256:265541e4885dd109da0212857cbb43393784782e54b6608851b7da1385a59f4c +size 308464 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_uITimelineView.1.png b/UnitTests/__Snapshots__/PreviewTests/test_uITimelineView.1.png index 119284c7de..54b8766880 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_uITimelineView.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_uITimelineView.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf7addca5dd6f7347d38227a5b03e36a3ff9430e24bafc06976ffe7e478baa67 -size 315120 +oid sha256:265541e4885dd109da0212857cbb43393784782e54b6608851b7da1385a59f4c +size 308464 diff --git a/changelog.d/1053.feature b/changelog.d/1053.feature new file mode 100644 index 0000000000..aab7b23cf7 --- /dev/null +++ b/changelog.d/1053.feature @@ -0,0 +1 @@ +Tapping on read receipts will open a detailed sheet of all the receipts. \ No newline at end of file diff --git a/changelog.d/pr-2123.change b/changelog.d/pr-2123.change new file mode 100644 index 0000000000..4f549d0fc2 --- /dev/null +++ b/changelog.d/pr-2123.change @@ -0,0 +1 @@ +Read Receipts are enabled by default. \ No newline at end of file