From e755be4cda82369db1c4bae68b84854124444ddc Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:47:33 +0100 Subject: [PATCH] Settings to enable and disable sending public RRs (#2384) --- .../en.lproj/Localizable.strings | 7 +++ .../Sources/Application/AppSettings.swift | 6 +++ ElementX/Sources/Generated/Strings.swift | 14 ++++++ .../Mocks/Generated/GeneratedMocks.swift | 28 ++++++------ .../InvitesScreen/View/InvitesScreen.swift | 2 +- .../AdvancedSettingsScreenModels.swift | 1 + .../View/AdvancedSettingsScreen.swift | 4 ++ .../MockRoomTimelineController.swift | 2 +- .../RoomTimelineController.swift | 3 +- .../Services/Timeline/TimelineProxy.swift | 4 +- .../Timeline/TimelineProxyProtocol.swift | 2 +- .../Sources/RoomScreenViewModelTests.swift | 44 ++++++++++++------- .../test_advancedSettingsScreen.1.png | 4 +- changelog.d/2319.feature | 1 + 14 files changed, 84 insertions(+), 38 deletions(-) create mode 100644 changelog.d/2319.feature diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 28198c20a..f015cee8a 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -120,6 +120,11 @@ "common_everyone" = "Everyone"; "common_face_id_ios" = "Face ID"; "common_file" = "File"; +"common_filter_favourites" = "Favourites"; +"common_filter_low_priority" = "Low Priority"; +"common_filter_people" = "People"; +"common_filter_rooms" = "Rooms"; +"common_filter_unreads" = "Unreads"; "common_forward_message" = "Forward message"; "common_gif" = "GIF"; "common_image" = "Image"; @@ -298,6 +303,8 @@ "screen_advanced_settings_developer_mode" = "Developer mode"; "screen_advanced_settings_developer_mode_description" = "Enable to have access to features and functionality for developers."; "screen_advanced_settings_rich_text_editor_description" = "Disable the rich text editor to type Markdown manually."; +"screen_advanced_settings_send_read_receipts" = "Read receipts"; +"screen_advanced_settings_send_read_receipts_description" = "If turned off, your read receipts won't be sent to anyone. You will still receive read receipts from other users."; "screen_advanced_settings_view_source_description" = "Enable option to view message source in the timeline."; "screen_analytics_prompt_data_usage" = "We won't record or profile any personal data"; "screen_analytics_prompt_help_us_improve" = "Share anonymous usage data to help us identify issues."; diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 9a097fead..0c11c83ad 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -37,6 +37,7 @@ final class AppSettings { case viewSourceEnabled case richTextEditorEnabled case appAppearance + case sendReadReceiptsEnabled case elementCallBaseURL case elementCallEncryptionEnabled @@ -261,6 +262,11 @@ final class AppSettings { // maptiler api key let mapTilerApiKey = InfoPlistReader.main.mapLibreAPIKey + // MARK: - Read Receipts + + @UserPreference(key: UserDefaultsKeys.sendReadReceiptsEnabled, defaultValue: true, storageType: .userDefaults(store)) + var sendReadReceiptsEnabled + // MARK: - Feature Flags @UserPreference(key: UserDefaultsKeys.userSuggestionsEnabled, defaultValue: false, storageType: .volatile) diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 57672c75b..a4094cf36 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -274,6 +274,16 @@ public enum L10n { public static var commonFaceIdIos: String { return L10n.tr("Localizable", "common_face_id_ios") } /// File public static var commonFile: String { return L10n.tr("Localizable", "common_file") } + /// Favourites + public static var commonFilterFavourites: String { return L10n.tr("Localizable", "common_filter_favourites") } + /// Low Priority + public static var commonFilterLowPriority: String { return L10n.tr("Localizable", "common_filter_low_priority") } + /// People + public static var commonFilterPeople: String { return L10n.tr("Localizable", "common_filter_people") } + /// Rooms + public static var commonFilterRooms: String { return L10n.tr("Localizable", "common_filter_rooms") } + /// Unreads + public static var commonFilterUnreads: String { return L10n.tr("Localizable", "common_filter_unreads") } /// Forward message public static var commonForwardMessage: String { return L10n.tr("Localizable", "common_forward_message") } /// GIF @@ -718,6 +728,10 @@ public enum L10n { public static var screenAdvancedSettingsElementCallBaseUrlValidationError: String { return L10n.tr("Localizable", "screen_advanced_settings_element_call_base_url_validation_error") } /// Disable the rich text editor to type Markdown manually. public static var screenAdvancedSettingsRichTextEditorDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_rich_text_editor_description") } + /// Read receipts + public static var screenAdvancedSettingsSendReadReceipts: String { return L10n.tr("Localizable", "screen_advanced_settings_send_read_receipts") } + /// If turned off, your read receipts won't be sent to anyone. You will still receive read receipts from other users. + public static var screenAdvancedSettingsSendReadReceiptsDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_send_read_receipts_description") } /// Enable option to view message source in the timeline. public static var screenAdvancedSettingsViewSourceDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_view_source_description") } /// We won't record or profile any personal data diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 9a81861dd..8a593edb4 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2789,23 +2789,23 @@ class TimelineProxyMock: TimelineProxyProtocol { } //MARK: - sendReadReceipt - var sendReadReceiptForCallsCount = 0 - var sendReadReceiptForCalled: Bool { - return sendReadReceiptForCallsCount > 0 + var sendReadReceiptForTypeCallsCount = 0 + var sendReadReceiptForTypeCalled: Bool { + return sendReadReceiptForTypeCallsCount > 0 } - var sendReadReceiptForReceivedEventID: String? - var sendReadReceiptForReceivedInvocations: [String] = [] - var sendReadReceiptForReturnValue: Result! - var sendReadReceiptForClosure: ((String) async -> Result)? + var sendReadReceiptForTypeReceivedArguments: (eventID: String, type: ReceiptType)? + var sendReadReceiptForTypeReceivedInvocations: [(eventID: String, type: ReceiptType)] = [] + var sendReadReceiptForTypeReturnValue: Result! + var sendReadReceiptForTypeClosure: ((String, ReceiptType) async -> Result)? - func sendReadReceipt(for eventID: String) async -> Result { - sendReadReceiptForCallsCount += 1 - sendReadReceiptForReceivedEventID = eventID - sendReadReceiptForReceivedInvocations.append(eventID) - if let sendReadReceiptForClosure = sendReadReceiptForClosure { - return await sendReadReceiptForClosure(eventID) + func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result { + sendReadReceiptForTypeCallsCount += 1 + sendReadReceiptForTypeReceivedArguments = (eventID: eventID, type: type) + sendReadReceiptForTypeReceivedInvocations.append((eventID: eventID, type: type)) + if let sendReadReceiptForTypeClosure = sendReadReceiptForTypeClosure { + return await sendReadReceiptForTypeClosure(eventID, type) } else { - return sendReadReceiptForReturnValue + return sendReadReceiptForTypeReturnValue } } //MARK: - sendMessageEventContent diff --git a/ElementX/Sources/Screens/InvitesScreen/View/InvitesScreen.swift b/ElementX/Sources/Screens/InvitesScreen/View/InvitesScreen.swift index 67c73bfc6..34279f936 100644 --- a/ElementX/Sources/Screens/InvitesScreen/View/InvitesScreen.swift +++ b/ElementX/Sources/Screens/InvitesScreen/View/InvitesScreen.swift @@ -73,7 +73,7 @@ struct InvitesScreen_Previews: PreviewProvider, TestablePreview { NavigationView { InvitesScreen(context: InvitesScreenViewModel.someInvite.context) } - .snapshot(delay: 2.0) + .snapshot(delay: 3.0) .previewDisplayName("Some Invite") } } diff --git a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift index 645c78904..7bed31358 100644 --- a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/AdvancedSettingsScreenModels.swift @@ -42,6 +42,7 @@ protocol AdvancedSettingsProtocol: AnyObject { var viewSourceEnabled: Bool { get set } var richTextEditorEnabled: Bool { get set } var appAppearance: AppAppearance { get set } + var sendReadReceiptsEnabled: Bool { get set } } extension AppSettings: AdvancedSettingsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift index 42976285e..79ebe7eed 100644 --- a/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/AvancedOptionsScreen/View/AdvancedSettingsScreen.swift @@ -36,6 +36,10 @@ struct AdvancedSettingsScreen: View { ListRow(label: .plain(title: L10n.actionViewSource), kind: .toggle($context.viewSourceEnabled)) + + ListRow(label: .plain(title: L10n.screenAdvancedSettingsSendReadReceipts, + description: L10n.screenAdvancedSettingsSendReadReceiptsDescription), + kind: .toggle($context.sendReadReceiptsEnabled)) } } .compoundList() diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index 7ce73fa9a..df749860f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -57,7 +57,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result { guard let roomProxy, let eventID = itemID.eventID else { return .failure(.generic) } - switch await roomProxy.timeline.sendReadReceipt(for: eventID) { + switch await roomProxy.timeline.sendReadReceipt(for: eventID, type: .read) { case .success: return .success(()) case .failure: diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index b33f6f95a..fcb186582 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -93,7 +93,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol { let eventID = itemID.eventID else { return .success(()) } - switch await roomProxy.timeline.sendReadReceipt(for: eventID) { + switch await roomProxy.timeline.sendReadReceipt(for: eventID, + type: appSettings.sendReadReceiptsEnabled ? .read : .readPrivate) { case .success: return .success(()) case .failure: diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 06044ae98..5c8e8cfbf 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -453,7 +453,7 @@ final class TimelineProxy: TimelineProxyProtocol { } } - func sendReadReceipt(for eventID: String) async -> Result { + func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result { MXLog.info("Sending read receipt for eventID: \(eventID)") sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) @@ -463,7 +463,7 @@ final class TimelineProxy: TimelineProxyProtocol { return await Task.dispatch(on: lowPriorityDispatchQueue) { do { - try self.timeline.sendReadReceipt(receiptType: .read, eventId: eventID) + try self.timeline.sendReadReceipt(receiptType: type, eventId: eventID) MXLog.info("Finished sending read receipt for eventID: \(eventID)") return .success(()) } catch { diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index 55acf2884..40599195b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -102,7 +102,7 @@ protocol TimelineProxyProtocol { progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result - func sendReadReceipt(for eventID: String) async -> Result + func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index cb2fa123a..28965f49b 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -25,6 +25,7 @@ class RoomScreenViewModelTests: XCTestCase { var cancellables = Set() override func setUp() async throws { + AppSettings.reset() cancellables.removeAll() userIndicatorControllerMock = UserIndicatorControllerMock.default } @@ -456,8 +457,10 @@ class RoomScreenViewModelTests: XCTestCase { try await Task.sleep(for: .milliseconds(100)) // Then the receipt should be sent. - XCTAssertEqual(timelineProxy.sendReadReceiptForCalled, true) - XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t3") + XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCalled, true) + let arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments + XCTAssertEqual(arguments?.eventID, "t3") + XCTAssertEqual(arguments?.type, .read) // And the notifications should be cleared. XCTAssertEqual(notificationCenter.postNameObjectReceivedArguments?.aName, .roomMarkedAsRead) @@ -473,16 +476,20 @@ class RoomScreenViewModelTests: XCTestCase { let (viewModel, _, timelineProxy, timelineController, _) = readReceiptsConfiguration(with: items) viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) try await Task.sleep(for: .milliseconds(100)) - XCTAssertEqual(timelineProxy.sendReadReceiptForCallsCount, 1) - XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t3") + XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCallsCount, 1) + var arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments + XCTAssertEqual(arguments?.eventID, "t3") + XCTAssertEqual(arguments?.type, .read) // When sending a receipt for the first item in the timeline. viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.first!.id)) try await Task.sleep(for: .milliseconds(100)) // Then the request should be ignored. - XCTAssertEqual(timelineProxy.sendReadReceiptForCallsCount, 1) - XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t3") + XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCallsCount, 1) + arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments + XCTAssertEqual(arguments?.eventID, "t3") + XCTAssertEqual(arguments?.type, .read) // When a new message is received and marked as read. let newMessage = TextRoomTimelineItem(eventID: "t4") @@ -494,8 +501,10 @@ class RoomScreenViewModelTests: XCTestCase { try await Task.sleep(for: .milliseconds(100)) // Then the request should be made. - XCTAssertEqual(timelineProxy.sendReadReceiptForCallsCount, 2) - XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t4") + XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCallsCount, 2) + arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments + XCTAssertEqual(arguments?.eventID, "t4") + XCTAssertEqual(arguments?.type, .read) } func testSendReadReceiptWithoutEvents() async throws { @@ -510,7 +519,7 @@ class RoomScreenViewModelTests: XCTestCase { try await Task.sleep(for: .milliseconds(100)) // Then nothing should be sent. - XCTAssertEqual(timelineProxy.sendReadReceiptForCalled, false) + XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCalled, false) } func testSendReadReceiptVirtualLast() async throws { @@ -525,8 +534,10 @@ class RoomScreenViewModelTests: XCTestCase { try await Task.sleep(for: .milliseconds(100)) // Then a read receipt should be sent for the item before it. - XCTAssertEqual(timelineProxy.sendReadReceiptForCalled, true) - XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t2") + XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCalled, true) + let arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments + XCTAssertEqual(arguments?.eventID, "t2") + XCTAssertEqual(arguments?.type, .read) } func testSendReadReceiptMultipleRequests() async throws { @@ -537,15 +548,17 @@ class RoomScreenViewModelTests: XCTestCase { let (viewModel, _, timelineProxy, _, _) = readReceiptsConfiguration(with: items) viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) try await Task.sleep(for: .milliseconds(100)) - XCTAssertEqual(timelineProxy.sendReadReceiptForCallsCount, 1) - XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t2") + XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCallsCount, 1) + let arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments + XCTAssertEqual(arguments?.eventID, "t2") + XCTAssertEqual(arguments?.type, .read) // When sending the same receipt again viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) try await Task.sleep(for: .milliseconds(100)) // Then the second call should be ignored. - XCTAssertEqual(timelineProxy.sendReadReceiptForCallsCount, 1) + XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCallsCount, 1) } // swiftlint:enable force_unwrapping @@ -564,7 +577,7 @@ class RoomScreenViewModelTests: XCTestCase { roomProxy.timeline = timelineProxy let timelineController = MockRoomTimelineController() - timelineProxy.sendReadReceiptForReturnValue = .success(()) + timelineProxy.sendReadReceiptForTypeReturnValue = .success(()) roomProxy.underlyingHasUnreadNotifications = true timelineController.timelineItems = items @@ -580,7 +593,6 @@ class RoomScreenViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, notificationCenter: notificationCenter) - return (viewModel, roomProxy, timelineProxy, timelineController, notificationCenter) } diff --git a/UnitTests/__Snapshots__/PreviewTests/test_advancedSettingsScreen.1.png b/UnitTests/__Snapshots__/PreviewTests/test_advancedSettingsScreen.1.png index 7ae24fda1..5937c15d5 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_advancedSettingsScreen.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_advancedSettingsScreen.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef4d0a11568fed0fe4849557b05a6cffd4cc53092ac6f6255e7b41268419da1b -size 114023 +oid sha256:ca4fa009a4a584047cdec1c4c58abd2d0ae6dc7feefa1ed4133174e3d1beba89 +size 144629 diff --git a/changelog.d/2319.feature b/changelog.d/2319.feature new file mode 100644 index 000000000..2a9266990 --- /dev/null +++ b/changelog.d/2319.feature @@ -0,0 +1 @@ +Added an advanced setting to turn off sending read receipts. \ No newline at end of file