Settings to enable and disable sending public RRs (#2384)

This commit is contained in:
Mauro 2024-01-25 15:47:33 +01:00 committed by GitHub
parent 29c8049559
commit e755be4cda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 84 additions and 38 deletions

View File

@ -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.";

View File

@ -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)

View File

@ -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

View File

@ -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<Void, TimelineProxyError>!
var sendReadReceiptForClosure: ((String) async -> Result<Void, TimelineProxyError>)?
var sendReadReceiptForTypeReceivedArguments: (eventID: String, type: ReceiptType)?
var sendReadReceiptForTypeReceivedInvocations: [(eventID: String, type: ReceiptType)] = []
var sendReadReceiptForTypeReturnValue: Result<Void, TimelineProxyError>!
var sendReadReceiptForTypeClosure: ((String, ReceiptType) async -> Result<Void, TimelineProxyError>)?
func sendReadReceipt(for eventID: String) async -> Result<Void, TimelineProxyError> {
sendReadReceiptForCallsCount += 1
sendReadReceiptForReceivedEventID = eventID
sendReadReceiptForReceivedInvocations.append(eventID)
if let sendReadReceiptForClosure = sendReadReceiptForClosure {
return await sendReadReceiptForClosure(eventID)
func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result<Void, TimelineProxyError> {
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

View File

@ -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")
}
}

View File

@ -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 { }

View File

@ -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()

View File

@ -57,7 +57,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result<Void, RoomTimelineControllerError> {
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:

View File

@ -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:

View File

@ -453,7 +453,7 @@ final class TimelineProxy: TimelineProxyProtocol {
}
}
func sendReadReceipt(for eventID: String) async -> Result<Void, TimelineProxyError> {
func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result<Void, TimelineProxyError> {
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 {

View File

@ -102,7 +102,7 @@ protocol TimelineProxyProtocol {
progressSubject: CurrentValueSubject<Double, Never>?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, TimelineProxyError>
func sendReadReceipt(for eventID: String) async -> Result<Void, TimelineProxyError>
func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result<Void, TimelineProxyError>
func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result<Void, TimelineProxyError>

View File

@ -25,6 +25,7 @@ class RoomScreenViewModelTests: XCTestCase {
var cancellables = Set<AnyCancellable>()
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)
}

1
changelog.d/2319.feature Normal file
View File

@ -0,0 +1 @@
Added an advanced setting to turn off sending read receipts.