From 7e58f719fee4f31022a122e25a2792d0717e17a5 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 26 Jan 2024 10:06:26 +0100 Subject: [PATCH] Add 'send private read receipts' option in advanced settings (#2290) * Add 'send private read receipts' option in advanced settings * Create `SessionPreferencesStore` that stores the settings for the current use separate from those of the app. * Rename `PreferencesStore` to `AppPreferencesStore` to split the preferences. --------- Co-authored-by: ElementBot --- .../io/element/android/x/di/AppBindings.kt | 4 +- changelog.d/2204.feature | 1 + .../features/call/ui/ElementCallActivity.kt | 6 +- .../call/utils/DefaultCallWidgetProvider.kt | 6 +- .../utils/DefaultCallWidgetProviderTest.kt | 12 ++-- .../messages/impl/MessagesPresenter.kt | 6 +- .../impl/actionlist/ActionListPresenter.kt | 6 +- .../impl/timeline/TimelinePresenter.kt | 10 ++- .../messages/impl/MessagesPresenterTest.kt | 11 ++-- .../actionlist/ActionListPresenterTest.kt | 6 +- .../impl/timeline/TimelinePresenterTest.kt | 44 +++++++++++-- .../impl/advanced/AdvancedSettingsEvents.kt | 1 + .../advanced/AdvancedSettingsPresenter.kt | 25 +++++--- .../impl/advanced/AdvancedSettingsState.kt | 1 + .../advanced/AdvancedSettingsStateProvider.kt | 3 + .../impl/advanced/AdvancedSettingsView.kt | 12 ++++ .../developer/DeveloperSettingsPresenter.kt | 8 +-- .../impl/src/main/res/values/localazy.xml | 2 + .../advanced/AdvancedSettingsPresenterTest.kt | 39 +++++++++--- .../DeveloperSettingsPresenterTest.kt | 8 +-- .../test/timeline/FakeMatrixTimeline.kt | 4 +- ...erencesStore.kt => AppPreferencesStore.kt} | 2 +- .../api/store/SessionPreferencesStore.kt | 26 ++++++++ libraries/preferences/impl/build.gradle.kts | 2 + ...Store.kt => DefaultAppPreferencesStore.kt} | 6 +- .../store/DefaultSessionPreferencesStore.kt | 63 +++++++++++++++++++ ...tore.kt => InMemoryAppPreferencesStore.kt} | 6 +- .../test/InMemorySessionPreferencesStore.kt | 41 ++++++++++++ ...ngsView-Day-1_2_null_0,NEXUS_5,1.0,en].png | 4 +- ...ngsView-Day-1_2_null_1,NEXUS_5,1.0,en].png | 4 +- ...ngsView-Day-1_2_null_2,NEXUS_5,1.0,en].png | 4 +- ...ngsView-Day-1_2_null_3,NEXUS_5,1.0,en].png | 4 +- ...ngsView-Day-1_2_null_4,NEXUS_5,1.0,en].png | 3 + ...sView-Night-1_3_null_0,NEXUS_5,1.0,en].png | 4 +- ...sView-Night-1_3_null_1,NEXUS_5,1.0,en].png | 4 +- ...sView-Night-1_3_null_2,NEXUS_5,1.0,en].png | 4 +- ...sView-Night-1_3_null_3,NEXUS_5,1.0,en].png | 4 +- ...sView-Night-1_3_null_4,NEXUS_5,1.0,en].png | 3 + 38 files changed, 314 insertions(+), 85 deletions(-) create mode 100644 changelog.d/2204.feature rename libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/{PreferencesStore.kt => AppPreferencesStore.kt} (97%) create mode 100644 libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/SessionPreferencesStore.kt rename libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/{DefaultPreferencesStore.kt => DefaultAppPreferencesStore.kt} (95%) create mode 100644 libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt rename libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/{InMemoryPreferencesStore.kt => InMemoryAppPreferencesStore.kt} (93%) create mode 100644 libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemorySessionPreferencesStore.kt create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_4,NEXUS_5,1.0,en].png diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt index a6ce26c237..0934771501 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt @@ -18,7 +18,7 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesTo import io.element.android.features.lockscreen.api.LockScreenService -import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.di.AppScope @@ -34,5 +34,5 @@ interface AppBindings { fun lockScreenService(): LockScreenService - fun preferencesStore(): PreferencesStore + fun preferencesStore(): AppPreferencesStore } diff --git a/changelog.d/2204.feature b/changelog.d/2204.feature new file mode 100644 index 0000000000..0a26acc5de --- /dev/null +++ b/changelog.d/2204.feature @@ -0,0 +1 @@ +Add 'send private read receipts' option in advanced settings diff --git a/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt b/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt index 2729d71a7e..8eea4c814f 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/ui/ElementCallActivity.kt @@ -45,7 +45,7 @@ import io.element.android.features.call.CallForegroundService import io.element.android.features.call.CallType import io.element.android.features.call.di.CallBindings import io.element.android.features.call.utils.CallIntentDataParser -import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.libraries.architecture.bindings import javax.inject.Inject @@ -67,7 +67,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator { @Inject lateinit var callIntentDataParser: CallIntentDataParser @Inject lateinit var presenterFactory: CallScreenPresenter.Factory - @Inject lateinit var preferencesStore: PreferencesStore + @Inject lateinit var appPreferencesStore: AppPreferencesStore private lateinit var presenter: CallScreenPresenter @@ -101,7 +101,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator { setContent { val theme by remember { - preferencesStore.getThemeFlow().mapToTheme() + appPreferencesStore.getThemeFlow().mapToTheme() } .collectAsState(initial = Theme.System) val state = presenter.present() diff --git a/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt b/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt index f3cb9cbcd5..cc6e2a299f 100644 --- a/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt +++ b/features/call/src/main/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProvider.kt @@ -18,7 +18,7 @@ package io.element.android.features.call.utils import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.ElementCallConfig -import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.RoomId @@ -31,7 +31,7 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultCallWidgetProvider @Inject constructor( private val matrixClientsProvider: MatrixClientProvider, - private val preferencesStore: PreferencesStore, + private val appPreferencesStore: AppPreferencesStore, private val callWidgetSettingsProvider: CallWidgetSettingsProvider, ) : CallWidgetProvider { override suspend fun getWidget( @@ -42,7 +42,7 @@ class DefaultCallWidgetProvider @Inject constructor( theme: String?, ): Result> = runCatching { val room = matrixClientsProvider.getOrRestore(sessionId).getOrThrow().getRoom(roomId) ?: error("Room not found") - val baseUrl = preferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() ?: ElementCallConfig.DEFAULT_BASE_URL + val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() ?: ElementCallConfig.DEFAULT_BASE_URL val widgetSettings = callWidgetSettingsProvider.provide(baseUrl) val callUrl = room.generateWidgetWebViewUrl(widgetSettings, clientId, languageTag, theme).getOrThrow() room.getWidgetDriver(widgetSettings).getOrThrow() to callUrl diff --git a/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt b/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt index 50d8c54998..31f6327d2f 100644 --- a/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt +++ b/features/call/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt @@ -17,8 +17,8 @@ package io.element.android.features.call.utils import com.google.common.truth.Truth.assertThat -import io.element.android.features.preferences.api.store.PreferencesStore -import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore +import io.element.android.features.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -94,14 +94,14 @@ class DefaultCallWidgetProviderTest { val client = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, room) } - val preferencesStore = InMemoryPreferencesStore().apply { + val preferencesStore = InMemoryAppPreferencesStore().apply { setCustomElementCallBaseUrl("https://custom.element.io") } val settingsProvider = FakeCallWidgetSettingsProvider() val provider = createProvider( matrixClientProvider = FakeMatrixClientProvider { Result.success(client) }, callWidgetSettingsProvider = settingsProvider, - preferencesStore = preferencesStore, + appPreferencesStore = preferencesStore, ) provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme") @@ -110,11 +110,11 @@ class DefaultCallWidgetProviderTest { private fun createProvider( matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(), - preferencesStore: PreferencesStore = InMemoryPreferencesStore(), + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider() ) = DefaultCallWidgetProvider( matrixClientProvider, - preferencesStore, + appPreferencesStore, callWidgetSettingsProvider, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 07eeda2efa..262f6cc6b2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -63,7 +63,7 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus -import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -108,7 +108,7 @@ class MessagesPresenter @AssistedInject constructor( private val messageSummaryFormatter: MessageSummaryFormatter, private val dispatchers: CoroutineDispatchers, private val clipboardHelper: ClipboardHelper, - private val preferencesStore: PreferencesStore, + private val appPreferencesStore: AppPreferencesStore, private val featureFlagsService: FeatureFlagService, private val htmlConverterProvider: HtmlConverterProvider, @Assisted private val navigator: MessagesNavigator, @@ -178,7 +178,7 @@ class MessagesPresenter @AssistedInject constructor( timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId)) } - val enableTextFormatting by preferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true) + val enableTextFormatting by appPreferencesStore.isRichTextEditorEnabledFlow().collectAsState(initial = true) var enableVoiceMessages by remember { mutableStateOf(false) } LaunchedEffect(featureFlagsService) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index f753266062..c5d449b288 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -31,7 +31,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.canBeCopied import io.element.android.features.messages.impl.timeline.model.event.canReact -import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.libraries.architecture.Presenter import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope @@ -39,7 +39,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject class ActionListPresenter @Inject constructor( - private val preferencesStore: PreferencesStore, + private val appPreferencesStore: AppPreferencesStore, ) : Presenter { @Composable override fun present(): ActionListState { @@ -49,7 +49,7 @@ class ActionListPresenter @Inject constructor( mutableStateOf(ActionListState.Target.None) } - val isDeveloperModeEnabled by preferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false) + val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false) fun handleEvents(event: ActionListEvents) { when (event) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index f1f934c4ba..a0d7ebf771 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction +import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.EventId @@ -73,6 +74,7 @@ class TimelinePresenter @AssistedInject constructor( private val redactedVoiceMessageManager: RedactedVoiceMessageManager, private val sendPollResponseAction: SendPollResponseAction, private val endPollAction: EndPollAction, + private val sessionPreferencesStore: SessionPreferencesStore, ) : Presenter { @AssistedFactory interface Factory { @@ -103,6 +105,8 @@ class TimelinePresenter @AssistedInject constructor( val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState() val keyBackupState by encryptionService.backupStateStateFlow.collectAsState() + val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true) + val sessionState by remember { derivedStateOf { SessionState( @@ -124,7 +128,8 @@ class TimelinePresenter @AssistedInject constructor( firstVisibleIndex = event.firstIndex, timelineItems = timelineItems, lastReadReceiptIndex = lastReadReceiptIndex, - lastReadReceiptId = lastReadReceiptId + lastReadReceiptId = lastReadReceiptId, + readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE, ) } is TimelineEvents.PollAnswerSelected -> appScope.launch { @@ -223,13 +228,14 @@ class TimelinePresenter @AssistedInject constructor( timelineItems: ImmutableList, lastReadReceiptIndex: MutableState, lastReadReceiptId: MutableState, + readReceiptType: ReceiptType, ) = launch(dispatchers.computation) { // Get last valid EventId seen by the user, as the first index might refer to a Virtual item val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) { lastReadReceiptIndex.value = firstVisibleIndex lastReadReceiptId.value = eventId - timeline.sendReadReceipt(eventId = eventId, receiptType = ReceiptType.READ) + timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 67bbb98e73..08eaf15f7b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -59,7 +59,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore +import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore +import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -668,6 +669,8 @@ class MessagesPresenterTest { ): MessagesPresenter { val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom) val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) + val appPreferencesStore = InMemoryAppPreferencesStore(isRichTextEditorEnabled = true) + val sessionPreferencesStore = InMemorySessionPreferencesStore() val messageComposerPresenter = MessageComposerPresenter( appCoroutineScope = this, room = matrixRoom, @@ -702,14 +705,14 @@ class MessagesPresenterTest { redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), endPollAction = FakeEndPollAction(), sendPollResponseAction = FakeSendPollResponseAction(), + sessionPreferencesStore = sessionPreferencesStore, ) val timelinePresenterFactory = object : TimelinePresenter.Factory { override fun create(navigator: MessagesNavigator): TimelinePresenter { return timelinePresenter } } - val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true) - val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore) + val actionListPresenter = ActionListPresenter(appPreferencesStore = appPreferencesStore) val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) @@ -729,7 +732,7 @@ class MessagesPresenterTest { messageSummaryFormatter = FakeMessageSummaryFormatter(), navigator = navigator, clipboardHelper = clipboardHelper, - preferencesStore = preferencesStore, + appPreferencesStore = appPreferencesStore, featureFlagsService = FakeFeatureFlagService(), buildMeta = aBuildMeta(), dispatchers = coroutineDispatchers, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index 0132b449df..981e8fc8ac 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -30,7 +30,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList -import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore +import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.tests.testutils.WarmUpRule import kotlinx.collections.immutable.persistentListOf @@ -747,6 +747,6 @@ class ActionListPresenterTest { } private fun createActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter { - val preferencesStore = InMemoryPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled) - return ActionListPresenter(preferencesStore = preferencesStore) + val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled) + return ActionListPresenter(appPreferencesStore = preferencesStore) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index be06965fa6..c01135fb5f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -35,9 +35,11 @@ import io.element.android.features.poll.api.actions.SendPollResponseAction import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.event.Receipt @@ -134,13 +136,41 @@ class TimelinePresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(timeline.sendReadReceiptCount).isEqualTo(0) + assertThat(timeline.sentReadReceipts).isEmpty() val initialState = awaitFirstItem() awaitWithLatch { latch -> timeline.sendReadReceiptLatch = latch initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) } - assertThat(timeline.sendReadReceiptCount).isEqualTo(1) + assertThat(timeline.sentReadReceipts).isNotEmpty() + assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - on scroll finished send a private read receipt if an event is before the index and public read receipts are disabled`() = runTest { + val timeline = FakeMatrixTimeline( + initialTimelineItems = listOf( + MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()) + ) + ) + val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false) + val presenter = createTimelinePresenter( + timeline = timeline, + sessionPreferencesStore = sessionPreferencesStore, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(timeline.sentReadReceipts).isEmpty() + val initialState = awaitFirstItem() + awaitWithLatch { latch -> + timeline.sendReadReceiptLatch = latch + initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) + } + assertThat(timeline.sentReadReceipts).isNotEmpty() + assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ_PRIVATE) cancelAndIgnoreRemainingEvents() } } @@ -156,13 +186,13 @@ class TimelinePresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(timeline.sendReadReceiptCount).isEqualTo(0) + assertThat(timeline.sentReadReceipts).isEmpty() val initialState = awaitFirstItem() awaitWithLatch { latch -> timeline.sendReadReceiptLatch = latch initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) } - assertThat(timeline.sendReadReceiptCount).isEqualTo(0) + assertThat(timeline.sentReadReceipts).isEmpty() cancelAndIgnoreRemainingEvents() } } @@ -178,13 +208,13 @@ class TimelinePresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - assertThat(timeline.sendReadReceiptCount).isEqualTo(0) + assertThat(timeline.sentReadReceipts).isEmpty() val initialState = awaitFirstItem() awaitWithLatch { latch -> timeline.sendReadReceiptLatch = latch initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) } - assertThat(timeline.sendReadReceiptCount).isEqualTo(0) + assertThat(timeline.sentReadReceipts).isEmpty() cancelAndIgnoreRemainingEvents() } } @@ -418,6 +448,7 @@ class TimelinePresenterTest { messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), endPollAction: EndPollAction = FakeEndPollAction(), sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), + sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), ): TimelinePresenter { return TimelinePresenter( timelineItemsFactory = timelineItemsFactory, @@ -430,6 +461,7 @@ class TimelinePresenterTest { redactedVoiceMessageManager = redactedVoiceMessageManager, endPollAction = endPollAction, sendPollResponseAction = sendPollResponseAction, + sessionPreferencesStore = sessionPreferencesStore, ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt index f22a653f6d..d67a594dd2 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt @@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme sealed interface AdvancedSettingsEvents { data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents + data class SetSendPublicReadReceiptsEnabled(val enabled: Boolean) : AdvancedSettingsEvents data object ChangeTheme : AdvancedSettingsEvents data object CancelChangeTheme : AdvancedSettingsEvents data class SetTheme(val theme: Theme) : AdvancedSettingsEvents diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt index ce5de0b8e8..2ac5b664b5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -25,40 +25,48 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import io.element.android.compound.theme.Theme import io.element.android.compound.theme.mapToTheme -import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.features.preferences.api.store.AppPreferencesStore +import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.launch import javax.inject.Inject class AdvancedSettingsPresenter @Inject constructor( - private val preferencesStore: PreferencesStore, + private val appPreferencesStore: AppPreferencesStore, + private val sessionPreferencesStore: SessionPreferencesStore, ) : Presenter { @Composable override fun present(): AdvancedSettingsState { val localCoroutineScope = rememberCoroutineScope() - val isRichTextEditorEnabled by preferencesStore + val isRichTextEditorEnabled by appPreferencesStore .isRichTextEditorEnabledFlow() .collectAsState(initial = false) - val isDeveloperModeEnabled by preferencesStore + val isDeveloperModeEnabled by appPreferencesStore .isDeveloperModeEnabledFlow() .collectAsState(initial = false) + val isSendPublicReadReceiptsEnabled by sessionPreferencesStore + .isSendPublicReadReceiptsEnabled() + .collectAsState(initial = true) val theme by remember { - preferencesStore.getThemeFlow().mapToTheme() + appPreferencesStore.getThemeFlow().mapToTheme() } .collectAsState(initial = Theme.System) var showChangeThemeDialog by remember { mutableStateOf(false) } fun handleEvents(event: AdvancedSettingsEvents) { when (event) { is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch { - preferencesStore.setRichTextEditorEnabled(event.enabled) + appPreferencesStore.setRichTextEditorEnabled(event.enabled) } is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch { - preferencesStore.setDeveloperModeEnabled(event.enabled) + appPreferencesStore.setDeveloperModeEnabled(event.enabled) + } + is AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled -> localCoroutineScope.launch { + sessionPreferencesStore.setSendPublicReadReceipts(event.enabled) } AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch { - preferencesStore.setTheme(event.theme.name) + appPreferencesStore.setTheme(event.theme.name) showChangeThemeDialog = false } } @@ -67,6 +75,7 @@ class AdvancedSettingsPresenter @Inject constructor( return AdvancedSettingsState( isRichTextEditorEnabled = isRichTextEditorEnabled, isDeveloperModeEnabled = isDeveloperModeEnabled, + isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled, theme = theme, showChangeThemeDialog = showChangeThemeDialog, eventSink = { handleEvents(it) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt index 01d702224f..0ea04185f7 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt @@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme data class AdvancedSettingsState( val isRichTextEditorEnabled: Boolean, val isDeveloperModeEnabled: Boolean, + val isSendPublicReadReceiptsEnabled: Boolean, val theme: Theme, val showChangeThemeDialog: Boolean, val eventSink: (AdvancedSettingsEvents) -> Unit diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt index aadf27dd20..acfd9bb026 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt @@ -26,16 +26,19 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider { @Composable override fun present(): DeveloperSettingsState { @@ -69,7 +69,7 @@ class DeveloperSettingsPresenter @Inject constructor( val clearCacheAction = remember { mutableStateOf>(AsyncData.Uninitialized) } - val customElementCallBaseUrl by preferencesStore + val customElementCallBaseUrl by appPreferencesStore .getCustomElementCallBaseUrlFlow() .collectAsState(initial = null) @@ -100,7 +100,7 @@ class DeveloperSettingsPresenter @Inject constructor( is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch { // If the URL is either empty or the default one, we want to save 'null' to remove the custom URL val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL } - preferencesStore.setCustomElementCallBaseUrl(urlToSave) + appPreferencesStore.setCustomElementCallBaseUrl(urlToSave) } DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction) } diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index 55c43f05f4..607329332f 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -6,6 +6,8 @@ "Developer mode" "Enable to have access to features and functionality for developers." "Disable the rich text editor to type Markdown manually." + "Read receipts" + "If turned off, your read receipts won\'t be sent to anyone. You will still receive read receipts from other users." "Enable option to view message source in the timeline." "Display name" "Your display name" diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index 6d7877de32..a042d792b8 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -21,7 +21,8 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.compound.theme.Theme -import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore +import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore +import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest @@ -34,8 +35,7 @@ class AdvancedSettingsPresenterTest { @Test fun `present - initial state`() = runTest { - val store = InMemoryPreferencesStore() - val presenter = AdvancedSettingsPresenter(store) + val presenter = createAdvancedSettingsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -43,14 +43,14 @@ class AdvancedSettingsPresenterTest { assertThat(initialState.isDeveloperModeEnabled).isFalse() assertThat(initialState.isRichTextEditorEnabled).isFalse() assertThat(initialState.showChangeThemeDialog).isFalse() + assertThat(initialState.isSendPublicReadReceiptsEnabled).isTrue() assertThat(initialState.theme).isEqualTo(Theme.System) } } @Test fun `present - developer mode on off`() = runTest { - val store = InMemoryPreferencesStore() - val presenter = AdvancedSettingsPresenter(store) + val presenter = createAdvancedSettingsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -65,8 +65,7 @@ class AdvancedSettingsPresenterTest { @Test fun `present - rich text editor on off`() = runTest { - val store = InMemoryPreferencesStore() - val presenter = AdvancedSettingsPresenter(store) + val presenter = createAdvancedSettingsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -79,10 +78,24 @@ class AdvancedSettingsPresenterTest { } } + @Test + fun `present - send public read receipts off on`() = runTest { + val presenter = createAdvancedSettingsPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitLastSequentialItem() + assertThat(initialState.isSendPublicReadReceiptsEnabled).isTrue() + initialState.eventSink.invoke(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(false)) + assertThat(awaitItem().isSendPublicReadReceiptsEnabled).isFalse() + initialState.eventSink.invoke(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(true)) + assertThat(awaitItem().isSendPublicReadReceiptsEnabled).isTrue() + } + } + @Test fun `present - change theme`() = runTest { - val store = InMemoryPreferencesStore() - val presenter = AdvancedSettingsPresenter(store) + val presenter = createAdvancedSettingsPresenter() moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { @@ -102,4 +115,12 @@ class AdvancedSettingsPresenterTest { assertThat(withNewTheme.theme).isEqualTo(Theme.Light) } } + + private fun createAdvancedSettingsPresenter( + appPreferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), + sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(), + ) = AdvancedSettingsPresenter( + appPreferencesStore = appPreferencesStore, + sessionPreferencesStore = sessionPreferencesStore, + ) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 50bf1ab426..19b1614500 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -29,7 +29,7 @@ import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataSto import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore +import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.awaitLastSequentialItem import kotlinx.coroutines.test.runTest @@ -114,7 +114,7 @@ class DeveloperSettingsPresenterTest { @Test fun `present - custom element call base url`() = runTest { - val preferencesStore = InMemoryPreferencesStore() + val preferencesStore = InMemoryAppPreferencesStore() val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -149,14 +149,14 @@ class DeveloperSettingsPresenterTest { cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(), clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(), rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()), - preferencesStore: InMemoryPreferencesStore = InMemoryPreferencesStore(), + preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), ): DeveloperSettingsPresenter { return DeveloperSettingsPresenter( featureFlagService = featureFlagService, computeCacheSizeUseCase = cacheSizeUseCase, clearCacheUseCase = clearCacheUseCase, rageshakePresenter = rageshakePresenter, - preferencesStore = preferencesStore, + appPreferencesStore = preferencesStore, ) } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt index 5530bb8d13..83ea98df6d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt @@ -39,7 +39,7 @@ class FakeMatrixTimeline( private val _paginationState: MutableStateFlow = MutableStateFlow(initialPaginationState) private val _timelineItems: MutableStateFlow> = MutableStateFlow(initialTimelineItems) - var sendReadReceiptCount = 0 + var sentReadReceipts = mutableListOf>() private set var sendReadReceiptLatch: CompletableDeferred? = null @@ -81,7 +81,7 @@ class FakeMatrixTimeline( eventId: EventId, receiptType: ReceiptType, ): Result = simulateLongTask { - sendReadReceiptCount++ + sentReadReceipts.add(eventId to receiptType) sendReadReceiptLatch?.complete(Unit) Result.success(Unit) } diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/AppPreferencesStore.kt similarity index 97% rename from libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt rename to libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/AppPreferencesStore.kt index 2bd1fc6064..4e78978873 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/PreferencesStore.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/AppPreferencesStore.kt @@ -18,7 +18,7 @@ package io.element.android.features.preferences.api.store import kotlinx.coroutines.flow.Flow -interface PreferencesStore { +interface AppPreferencesStore { suspend fun setRichTextEditorEnabled(enabled: Boolean) fun isRichTextEditorEnabledFlow(): Flow diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/SessionPreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/SessionPreferencesStore.kt new file mode 100644 index 0000000000..0174d8d1eb --- /dev/null +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/SessionPreferencesStore.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 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. + */ + +package io.element.android.features.preferences.api.store + +import kotlinx.coroutines.flow.Flow + +interface SessionPreferencesStore { + suspend fun setSendPublicReadReceipts(enabled: Boolean) + fun isSendPublicReadReceiptsEnabled(): Flow + + suspend fun clear() +} diff --git a/libraries/preferences/impl/build.gradle.kts b/libraries/preferences/impl/build.gradle.kts index 9c31d83481..3fa6324699 100644 --- a/libraries/preferences/impl/build.gradle.kts +++ b/libraries/preferences/impl/build.gradle.kts @@ -31,6 +31,8 @@ dependencies { api(projects.libraries.preferences.api) implementation(libs.dagger) implementation(libs.androidx.datastore.preferences) + implementation(projects.libraries.androidutils) implementation(projects.libraries.di) implementation(projects.libraries.core) + implementation(projects.libraries.matrix.api) } diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt similarity index 95% rename from libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt rename to libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt index d00b7505d7..fdbd7dde8c 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt @@ -24,7 +24,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.features.preferences.api.store.AppPreferencesStore import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType @@ -42,10 +42,10 @@ private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseU private val themeKey = stringPreferencesKey("theme") @ContributesBinding(AppScope::class) -class DefaultPreferencesStore @Inject constructor( +class DefaultAppPreferencesStore @Inject constructor( @ApplicationContext context: Context, private val buildMeta: BuildMeta, -) : PreferencesStore { +) : AppPreferencesStore { private val store = context.dataStore override suspend fun setRichTextEditorEnabled(enabled: Boolean) { diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt new file mode 100644 index 0000000000..0d10d02b62 --- /dev/null +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 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. + */ + +package io.element.android.libraries.preferences.impl.store + +import android.content.Context +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStoreFile +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.preferences.api.store.SessionPreferencesStore +import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.androidutils.hash.hash +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@ContributesBinding(SessionScope::class) +@SingleIn(SessionScope::class) +class DefaultSessionPreferencesStore @Inject constructor( + @ApplicationContext context: Context, + currentSessionIdHolder: CurrentSessionIdHolder, +) : SessionPreferencesStore { + private val sendPublicReadReceiptsKey = booleanPreferencesKey("sendPublicReadReceipts") + private val hashedUserId = currentSessionIdHolder.current.value.hash().take(16) + + private val dataStoreFile = context.preferencesDataStoreFile("session_${hashedUserId}_preferences") + private val store = PreferenceDataStoreFactory.create { dataStoreFile } + + override suspend fun setSendPublicReadReceipts(enabled: Boolean) = update(sendPublicReadReceiptsKey, enabled) + override fun isSendPublicReadReceiptsEnabled(): Flow = get(sendPublicReadReceiptsKey, true) + + override suspend fun clear() { + dataStoreFile.safeDelete() + } + + private suspend fun update(key: Preferences.Key, value: T) { + store.edit { prefs -> prefs[key] = value } + } + + private fun get(key: Preferences.Key, default: T): Flow { + return store.data.map { prefs -> prefs[key] ?: default } + } +} diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryAppPreferencesStore.kt similarity index 93% rename from libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt rename to libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryAppPreferencesStore.kt index c143b3ff6c..c065622f3f 100644 --- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryPreferencesStore.kt +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemoryAppPreferencesStore.kt @@ -16,16 +16,16 @@ package io.element.android.libraries.featureflag.test -import io.element.android.features.preferences.api.store.PreferencesStore +import io.element.android.features.preferences.api.store.AppPreferencesStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -class InMemoryPreferencesStore( +class InMemoryAppPreferencesStore( isRichTextEditorEnabled: Boolean = false, isDeveloperModeEnabled: Boolean = false, customElementCallBaseUrl: String? = null, theme: String? = null, -) : PreferencesStore { +) : AppPreferencesStore { private val isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled) private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled) private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl) diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemorySessionPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemorySessionPreferencesStore.kt new file mode 100644 index 0000000000..1f6f7a6724 --- /dev/null +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemorySessionPreferencesStore.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 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. + */ + +package io.element.android.libraries.featureflag.test + +import io.element.android.features.preferences.api.store.SessionPreferencesStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class InMemorySessionPreferencesStore( + isSendPublicReadReceiptsEnabled: Boolean = true, +) : SessionPreferencesStore { + private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled) + var clearCallCount = 0 + private set + + override suspend fun setSendPublicReadReceipts(enabled: Boolean) { + isSendPublicReadReceiptsEnabled.tryEmit(enabled) + } + override fun isSendPublicReadReceiptsEnabled(): Flow { + return isSendPublicReadReceiptsEnabled + } + + override suspend fun clear() { + clearCallCount++ + isSendPublicReadReceiptsEnabled.tryEmit(true) + } +} diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_0,NEXUS_5,1.0,en].png index a5e5ee3ebe..da1d8ddbfd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9792c65780ccbabc55476cd15f0ef1a00e43d4b49ded546350271f52bc50ac5 -size 39898 +oid sha256:da3f8614862ddacfd9b54afcd3bf091bec335e1489d7cbffc826059a89ae5478 +size 58453 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_1,NEXUS_5,1.0,en].png index 4e99b875cb..c99349ac0c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33121ba1e716f4a22ba66a2648a1c66acbab8ffe721be2d52e556dd341818696 -size 39392 +oid sha256:1fd599f2e93432d5d5555f086b60059e84452a0da3c2a69d932042b7ee4d3bac +size 57922 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_2,NEXUS_5,1.0,en].png index cca7799a9b..b228d561bd 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2081e18691471601c80bed114bc795c6fe4491186282db4a81f0fb186d286d30 -size 39402 +oid sha256:9e9641228e0482c8e5500bb999b180e79cbfcb6a13e121f3be68aae5c58a6839 +size 57953 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_3,NEXUS_5,1.0,en].png index 336d6c8b81..3df61a1ef4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61201245ab79d39b505f27d9fd4d1a5842111bac1e067c2bd450f633c9e92ec4 -size 35497 +oid sha256:1c1c5b00098e5860d4c69ef9a652483ea1192d9fa10f5449fb817769ab054183 +size 36128 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..50da3b5d6c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fe6625641cf06c45a817e9d10bc0d7296aa59160fc5a3e9dc28304e5b794125 +size 57943 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_0,NEXUS_5,1.0,en].png index 01afa74c7f..47fbdb6c0f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a24874553e4228ba8c254487c1bdd4d1a5556ecc2d4dfe26ace20548f5f9d38 -size 37203 +oid sha256:e85b252d0d6c166f987974ebcf9a6b58b924316733b868ad0ed5a61413bda381 +size 54577 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_1,NEXUS_5,1.0,en].png index 43cfc64ba6..10d7a6c5e1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08d8714914bef5a1e5f353c43d5b7dbec7ff5c549402c723a0e875e1a8de2181 -size 36807 +oid sha256:4f246ce9f60b25d9739d73e2aed98132dfb3a0a694acbf320ff796f35f805fdc +size 54273 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_2,NEXUS_5,1.0,en].png index 7dafa3459b..4b03ace56b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3eb3cbf3f0eeced3753b4b574cfcf1e8a5d86e7789e970f0a07089656cd16a18 -size 36818 +oid sha256:d0403a4bb8da3d36922cb5362977af9b3052624525def05e5236f4f922384c31 +size 54297 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_3,NEXUS_5,1.0,en].png index 83b3fee634..01e0e907a6 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a74667729b5980edce4103b5ce38bd97dbb4df0e50642489a1822128b4dcd75 -size 31280 +oid sha256:824774a1e19974090de4cb32e06a7f984b5db238c3dc8d04a35d3457e647ac84 +size 31999 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..449fb69524 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0820b462a0dc103b66aef78528ae5ce13a55e2614a7a197e06713366c422893 +size 54313