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 <benoitm+elementbot@element.io>
This commit is contained in:
parent
e22ac6a48d
commit
17f22d143b
38 changed files with 314 additions and 85 deletions
|
|
@ -18,7 +18,7 @@ package io.element.android.x.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
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.features.rageshake.api.reporter.BugReporter
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
@ -34,5 +34,5 @@ interface AppBindings {
|
||||||
|
|
||||||
fun lockScreenService(): LockScreenService
|
fun lockScreenService(): LockScreenService
|
||||||
|
|
||||||
fun preferencesStore(): PreferencesStore
|
fun preferencesStore(): AppPreferencesStore
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
changelog.d/2204.feature
Normal file
1
changelog.d/2204.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Add 'send private read receipts' option in advanced settings
|
||||||
|
|
@ -45,7 +45,7 @@ import io.element.android.features.call.CallForegroundService
|
||||||
import io.element.android.features.call.CallType
|
import io.element.android.features.call.CallType
|
||||||
import io.element.android.features.call.di.CallBindings
|
import io.element.android.features.call.di.CallBindings
|
||||||
import io.element.android.features.call.utils.CallIntentDataParser
|
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 io.element.android.libraries.architecture.bindings
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
|
||||||
|
|
||||||
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
@Inject lateinit var callIntentDataParser: CallIntentDataParser
|
||||||
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
@Inject lateinit var presenterFactory: CallScreenPresenter.Factory
|
||||||
@Inject lateinit var preferencesStore: PreferencesStore
|
@Inject lateinit var appPreferencesStore: AppPreferencesStore
|
||||||
|
|
||||||
private lateinit var presenter: CallScreenPresenter
|
private lateinit var presenter: CallScreenPresenter
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ class ElementCallActivity : NodeComponentActivity(), CallScreenNavigator {
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val theme by remember {
|
val theme by remember {
|
||||||
preferencesStore.getThemeFlow().mapToTheme()
|
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||||
}
|
}
|
||||||
.collectAsState(initial = Theme.System)
|
.collectAsState(initial = Theme.System)
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.call.utils
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.appconfig.ElementCallConfig
|
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.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -31,7 +31,7 @@ import javax.inject.Inject
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultCallWidgetProvider @Inject constructor(
|
class DefaultCallWidgetProvider @Inject constructor(
|
||||||
private val matrixClientsProvider: MatrixClientProvider,
|
private val matrixClientsProvider: MatrixClientProvider,
|
||||||
private val preferencesStore: PreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
|
||||||
) : CallWidgetProvider {
|
) : CallWidgetProvider {
|
||||||
override suspend fun getWidget(
|
override suspend fun getWidget(
|
||||||
|
|
@ -42,7 +42,7 @@ class DefaultCallWidgetProvider @Inject constructor(
|
||||||
theme: String?,
|
theme: String?,
|
||||||
): Result<Pair<MatrixWidgetDriver, String>> = runCatching {
|
): Result<Pair<MatrixWidgetDriver, String>> = runCatching {
|
||||||
val room = matrixClientsProvider.getOrRestore(sessionId).getOrThrow().getRoom(roomId) ?: error("Room not found")
|
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 widgetSettings = callWidgetSettingsProvider.provide(baseUrl)
|
||||||
val callUrl = room.generateWidgetWebViewUrl(widgetSettings, clientId, languageTag, theme).getOrThrow()
|
val callUrl = room.generateWidgetWebViewUrl(widgetSettings, clientId, languageTag, theme).getOrThrow()
|
||||||
room.getWidgetDriver(widgetSettings).getOrThrow() to callUrl
|
room.getWidgetDriver(widgetSettings).getOrThrow() to callUrl
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
package io.element.android.features.call.utils
|
package io.element.android.features.call.utils
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.preferences.api.store.PreferencesStore
|
import io.element.android.features.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.featureflag.test.InMemoryPreferencesStore
|
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
|
@ -94,14 +94,14 @@ class DefaultCallWidgetProviderTest {
|
||||||
val client = FakeMatrixClient().apply {
|
val client = FakeMatrixClient().apply {
|
||||||
givenGetRoomResult(A_ROOM_ID, room)
|
givenGetRoomResult(A_ROOM_ID, room)
|
||||||
}
|
}
|
||||||
val preferencesStore = InMemoryPreferencesStore().apply {
|
val preferencesStore = InMemoryAppPreferencesStore().apply {
|
||||||
setCustomElementCallBaseUrl("https://custom.element.io")
|
setCustomElementCallBaseUrl("https://custom.element.io")
|
||||||
}
|
}
|
||||||
val settingsProvider = FakeCallWidgetSettingsProvider()
|
val settingsProvider = FakeCallWidgetSettingsProvider()
|
||||||
val provider = createProvider(
|
val provider = createProvider(
|
||||||
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
|
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
|
||||||
callWidgetSettingsProvider = settingsProvider,
|
callWidgetSettingsProvider = settingsProvider,
|
||||||
preferencesStore = preferencesStore,
|
appPreferencesStore = preferencesStore,
|
||||||
)
|
)
|
||||||
provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme")
|
provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme")
|
||||||
|
|
||||||
|
|
@ -110,11 +110,11 @@ class DefaultCallWidgetProviderTest {
|
||||||
|
|
||||||
private fun createProvider(
|
private fun createProvider(
|
||||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||||
preferencesStore: PreferencesStore = InMemoryPreferencesStore(),
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider()
|
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider()
|
||||||
) = DefaultCallWidgetProvider(
|
) = DefaultCallWidgetProvider(
|
||||||
matrixClientProvider,
|
matrixClientProvider,
|
||||||
preferencesStore,
|
appPreferencesStore,
|
||||||
callWidgetSettingsProvider,
|
callWidgetSettingsProvider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
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.androidutils.clipboard.ClipboardHelper
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -108,7 +108,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
private val messageSummaryFormatter: MessageSummaryFormatter,
|
private val messageSummaryFormatter: MessageSummaryFormatter,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val clipboardHelper: ClipboardHelper,
|
private val clipboardHelper: ClipboardHelper,
|
||||||
private val preferencesStore: PreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val featureFlagsService: FeatureFlagService,
|
private val featureFlagsService: FeatureFlagService,
|
||||||
private val htmlConverterProvider: HtmlConverterProvider,
|
private val htmlConverterProvider: HtmlConverterProvider,
|
||||||
@Assisted private val navigator: MessagesNavigator,
|
@Assisted private val navigator: MessagesNavigator,
|
||||||
|
|
@ -178,7 +178,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
|
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) }
|
var enableVoiceMessages by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(featureFlagsService) {
|
LaunchedEffect(featureFlagsService) {
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItemVoiceContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
|
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.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 io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -39,7 +39,7 @@ import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ActionListPresenter @Inject constructor(
|
class ActionListPresenter @Inject constructor(
|
||||||
private val preferencesStore: PreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
) : Presenter<ActionListState> {
|
) : Presenter<ActionListState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ActionListState {
|
override fun present(): ActionListState {
|
||||||
|
|
@ -49,7 +49,7 @@ class ActionListPresenter @Inject constructor(
|
||||||
mutableStateOf(ActionListState.Target.None)
|
mutableStateOf(ActionListState.Target.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDeveloperModeEnabled by preferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
|
val isDeveloperModeEnabled by appPreferencesStore.isDeveloperModeEnabledFlow().collectAsState(initial = false)
|
||||||
|
|
||||||
fun handleEvents(event: ActionListEvents) {
|
fun handleEvents(event: ActionListEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
|
||||||
|
|
@ -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.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
import io.element.android.features.poll.api.actions.EndPollAction
|
import io.element.android.features.poll.api.actions.EndPollAction
|
||||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -73,6 +74,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
private val redactedVoiceMessageManager: RedactedVoiceMessageManager,
|
||||||
private val sendPollResponseAction: SendPollResponseAction,
|
private val sendPollResponseAction: SendPollResponseAction,
|
||||||
private val endPollAction: EndPollAction,
|
private val endPollAction: EndPollAction,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -103,6 +105,8 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState()
|
val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState()
|
||||||
val keyBackupState by encryptionService.backupStateStateFlow.collectAsState()
|
val keyBackupState by encryptionService.backupStateStateFlow.collectAsState()
|
||||||
|
|
||||||
|
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
|
|
||||||
val sessionState by remember {
|
val sessionState by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
SessionState(
|
SessionState(
|
||||||
|
|
@ -124,7 +128,8 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
firstVisibleIndex = event.firstIndex,
|
firstVisibleIndex = event.firstIndex,
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
lastReadReceiptIndex = lastReadReceiptIndex,
|
lastReadReceiptIndex = lastReadReceiptIndex,
|
||||||
lastReadReceiptId = lastReadReceiptId
|
lastReadReceiptId = lastReadReceiptId,
|
||||||
|
readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
||||||
|
|
@ -223,13 +228,14 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
timelineItems: ImmutableList<TimelineItem>,
|
timelineItems: ImmutableList<TimelineItem>,
|
||||||
lastReadReceiptIndex: MutableState<Int>,
|
lastReadReceiptIndex: MutableState<Int>,
|
||||||
lastReadReceiptId: MutableState<EventId?>,
|
lastReadReceiptId: MutableState<EventId?>,
|
||||||
|
readReceiptType: ReceiptType,
|
||||||
) = launch(dispatchers.computation) {
|
) = launch(dispatchers.computation) {
|
||||||
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
|
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
|
||||||
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
||||||
if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) {
|
if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) {
|
||||||
lastReadReceiptIndex.value = firstVisibleIndex
|
lastReadReceiptIndex.value = firstVisibleIndex
|
||||||
lastReadReceiptId.value = eventId
|
lastReadReceiptId.value = eventId
|
||||||
timeline.sendReadReceipt(eventId = eventId, receiptType = ReceiptType.READ)
|
timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
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.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
|
|
@ -668,6 +669,8 @@ class MessagesPresenterTest {
|
||||||
): MessagesPresenter {
|
): MessagesPresenter {
|
||||||
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
|
val mediaSender = MediaSender(FakeMediaPreProcessor(), matrixRoom)
|
||||||
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
|
val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter)
|
||||||
|
val appPreferencesStore = InMemoryAppPreferencesStore(isRichTextEditorEnabled = true)
|
||||||
|
val sessionPreferencesStore = InMemorySessionPreferencesStore()
|
||||||
val messageComposerPresenter = MessageComposerPresenter(
|
val messageComposerPresenter = MessageComposerPresenter(
|
||||||
appCoroutineScope = this,
|
appCoroutineScope = this,
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
|
|
@ -702,14 +705,14 @@ class MessagesPresenterTest {
|
||||||
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||||
endPollAction = FakeEndPollAction(),
|
endPollAction = FakeEndPollAction(),
|
||||||
sendPollResponseAction = FakeSendPollResponseAction(),
|
sendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
)
|
)
|
||||||
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
||||||
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
||||||
return timelinePresenter
|
return timelinePresenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val preferencesStore = InMemoryPreferencesStore(isRichTextEditorEnabled = true)
|
val actionListPresenter = ActionListPresenter(appPreferencesStore = appPreferencesStore)
|
||||||
val actionListPresenter = ActionListPresenter(preferencesStore = preferencesStore)
|
|
||||||
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
|
||||||
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
|
||||||
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
|
||||||
|
|
@ -729,7 +732,7 @@ class MessagesPresenterTest {
|
||||||
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
clipboardHelper = clipboardHelper,
|
clipboardHelper = clipboardHelper,
|
||||||
preferencesStore = preferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
featureFlagsService = FakeFeatureFlagService(),
|
featureFlagsService = FakeFeatureFlagService(),
|
||||||
buildMeta = aBuildMeta(),
|
buildMeta = aBuildMeta(),
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
|
|
|
||||||
|
|
@ -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.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
|
||||||
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList
|
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.libraries.matrix.test.A_MESSAGE
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
@ -747,6 +747,6 @@ class ActionListPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter {
|
private fun createActionListPresenter(isDeveloperModeEnabled: Boolean): ActionListPresenter {
|
||||||
val preferencesStore = InMemoryPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
|
val preferencesStore = InMemoryAppPreferencesStore(isDeveloperModeEnabled = isDeveloperModeEnabled)
|
||||||
return ActionListPresenter(preferencesStore = preferencesStore)
|
return ActionListPresenter(appPreferencesStore = preferencesStore)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.FakeEndPollAction
|
||||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
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.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
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.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.EventReaction
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
|
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
||||||
|
|
@ -134,13 +136,41 @@ class TimelinePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
awaitWithLatch { latch ->
|
awaitWithLatch { latch ->
|
||||||
timeline.sendReadReceiptLatch = latch
|
timeline.sendReadReceiptLatch = latch
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
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()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -156,13 +186,13 @@ class TimelinePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
awaitWithLatch { latch ->
|
awaitWithLatch { latch ->
|
||||||
timeline.sendReadReceiptLatch = latch
|
timeline.sendReadReceiptLatch = latch
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||||
}
|
}
|
||||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,13 +208,13 @@ class TimelinePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
awaitWithLatch { latch ->
|
awaitWithLatch { latch ->
|
||||||
timeline.sendReadReceiptLatch = latch
|
timeline.sendReadReceiptLatch = latch
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||||
}
|
}
|
||||||
assertThat(timeline.sendReadReceiptCount).isEqualTo(0)
|
assertThat(timeline.sentReadReceipts).isEmpty()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -418,6 +448,7 @@ class TimelinePresenterTest {
|
||||||
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||||
endPollAction: EndPollAction = FakeEndPollAction(),
|
endPollAction: EndPollAction = FakeEndPollAction(),
|
||||||
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
|
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
): TimelinePresenter {
|
): TimelinePresenter {
|
||||||
return TimelinePresenter(
|
return TimelinePresenter(
|
||||||
timelineItemsFactory = timelineItemsFactory,
|
timelineItemsFactory = timelineItemsFactory,
|
||||||
|
|
@ -430,6 +461,7 @@ class TimelinePresenterTest {
|
||||||
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
||||||
endPollAction = endPollAction,
|
endPollAction = endPollAction,
|
||||||
sendPollResponseAction = sendPollResponseAction,
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme
|
||||||
sealed interface AdvancedSettingsEvents {
|
sealed interface AdvancedSettingsEvents {
|
||||||
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
data class SetRichTextEditorEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||||
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||||
|
data class SetSendPublicReadReceiptsEnabled(val enabled: Boolean) : AdvancedSettingsEvents
|
||||||
data object ChangeTheme : AdvancedSettingsEvents
|
data object ChangeTheme : AdvancedSettingsEvents
|
||||||
data object CancelChangeTheme : AdvancedSettingsEvents
|
data object CancelChangeTheme : AdvancedSettingsEvents
|
||||||
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents
|
||||||
|
|
|
||||||
|
|
@ -25,40 +25,48 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.compound.theme.Theme
|
import io.element.android.compound.theme.Theme
|
||||||
import io.element.android.compound.theme.mapToTheme
|
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 io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AdvancedSettingsPresenter @Inject constructor(
|
class AdvancedSettingsPresenter @Inject constructor(
|
||||||
private val preferencesStore: PreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
) : Presenter<AdvancedSettingsState> {
|
) : Presenter<AdvancedSettingsState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): AdvancedSettingsState {
|
override fun present(): AdvancedSettingsState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val isRichTextEditorEnabled by preferencesStore
|
val isRichTextEditorEnabled by appPreferencesStore
|
||||||
.isRichTextEditorEnabledFlow()
|
.isRichTextEditorEnabledFlow()
|
||||||
.collectAsState(initial = false)
|
.collectAsState(initial = false)
|
||||||
val isDeveloperModeEnabled by preferencesStore
|
val isDeveloperModeEnabled by appPreferencesStore
|
||||||
.isDeveloperModeEnabledFlow()
|
.isDeveloperModeEnabledFlow()
|
||||||
.collectAsState(initial = false)
|
.collectAsState(initial = false)
|
||||||
|
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore
|
||||||
|
.isSendPublicReadReceiptsEnabled()
|
||||||
|
.collectAsState(initial = true)
|
||||||
val theme by remember {
|
val theme by remember {
|
||||||
preferencesStore.getThemeFlow().mapToTheme()
|
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||||
}
|
}
|
||||||
.collectAsState(initial = Theme.System)
|
.collectAsState(initial = Theme.System)
|
||||||
var showChangeThemeDialog by remember { mutableStateOf(false) }
|
var showChangeThemeDialog by remember { mutableStateOf(false) }
|
||||||
fun handleEvents(event: AdvancedSettingsEvents) {
|
fun handleEvents(event: AdvancedSettingsEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
|
is AdvancedSettingsEvents.SetRichTextEditorEnabled -> localCoroutineScope.launch {
|
||||||
preferencesStore.setRichTextEditorEnabled(event.enabled)
|
appPreferencesStore.setRichTextEditorEnabled(event.enabled)
|
||||||
}
|
}
|
||||||
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
|
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.CancelChangeTheme -> showChangeThemeDialog = false
|
||||||
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
|
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
|
||||||
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
|
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
|
||||||
preferencesStore.setTheme(event.theme.name)
|
appPreferencesStore.setTheme(event.theme.name)
|
||||||
showChangeThemeDialog = false
|
showChangeThemeDialog = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,6 +75,7 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||||
return AdvancedSettingsState(
|
return AdvancedSettingsState(
|
||||||
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
||||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||||
|
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
|
||||||
theme = theme,
|
theme = theme,
|
||||||
showChangeThemeDialog = showChangeThemeDialog,
|
showChangeThemeDialog = showChangeThemeDialog,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.compound.theme.Theme
|
||||||
data class AdvancedSettingsState(
|
data class AdvancedSettingsState(
|
||||||
val isRichTextEditorEnabled: Boolean,
|
val isRichTextEditorEnabled: Boolean,
|
||||||
val isDeveloperModeEnabled: Boolean,
|
val isDeveloperModeEnabled: Boolean,
|
||||||
|
val isSendPublicReadReceiptsEnabled: Boolean,
|
||||||
val theme: Theme,
|
val theme: Theme,
|
||||||
val showChangeThemeDialog: Boolean,
|
val showChangeThemeDialog: Boolean,
|
||||||
val eventSink: (AdvancedSettingsEvents) -> Unit
|
val eventSink: (AdvancedSettingsEvents) -> Unit
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,19 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
|
||||||
aAdvancedSettingsState(isRichTextEditorEnabled = true),
|
aAdvancedSettingsState(isRichTextEditorEnabled = true),
|
||||||
aAdvancedSettingsState(isDeveloperModeEnabled = true),
|
aAdvancedSettingsState(isDeveloperModeEnabled = true),
|
||||||
aAdvancedSettingsState(showChangeThemeDialog = true),
|
aAdvancedSettingsState(showChangeThemeDialog = true),
|
||||||
|
aAdvancedSettingsState(isSendPublicReadReceiptsEnabled = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aAdvancedSettingsState(
|
fun aAdvancedSettingsState(
|
||||||
isRichTextEditorEnabled: Boolean = false,
|
isRichTextEditorEnabled: Boolean = false,
|
||||||
isDeveloperModeEnabled: Boolean = false,
|
isDeveloperModeEnabled: Boolean = false,
|
||||||
|
isSendPublicReadReceiptsEnabled: Boolean = false,
|
||||||
showChangeThemeDialog: Boolean = false,
|
showChangeThemeDialog: Boolean = false,
|
||||||
) = AdvancedSettingsState(
|
) = AdvancedSettingsState(
|
||||||
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
||||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||||
|
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
|
||||||
theme = Theme.System,
|
theme = Theme.System,
|
||||||
showChangeThemeDialog = showChangeThemeDialog,
|
showChangeThemeDialog = showChangeThemeDialog,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,18 @@ fun AdvancedSettingsView(
|
||||||
onChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) },
|
onChange = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(it)) },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = stringResource(id = R.string.screen_advanced_settings_send_read_receipts))
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(text = stringResource(id = R.string.screen_advanced_settings_send_read_receipts_description))
|
||||||
|
},
|
||||||
|
trailingContent = ListItemContent.Switch(
|
||||||
|
checked = state.isSendPublicReadReceiptsEnabled,
|
||||||
|
onChange = { state.eventSink(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(it)) },
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.showChangeThemeDialog) {
|
if (state.showChangeThemeDialog) {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import io.element.android.appconfig.ElementCallConfig
|
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.features.preferences.impl.tasks.ClearCacheUseCase
|
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
||||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
|
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesPresenter
|
||||||
|
|
@ -51,7 +51,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
|
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
|
||||||
private val clearCacheUseCase: ClearCacheUseCase,
|
private val clearCacheUseCase: ClearCacheUseCase,
|
||||||
private val rageshakePresenter: RageshakePreferencesPresenter,
|
private val rageshakePresenter: RageshakePreferencesPresenter,
|
||||||
private val preferencesStore: PreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
) : Presenter<DeveloperSettingsState> {
|
) : Presenter<DeveloperSettingsState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): DeveloperSettingsState {
|
override fun present(): DeveloperSettingsState {
|
||||||
|
|
@ -69,7 +69,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
val clearCacheAction = remember {
|
val clearCacheAction = remember {
|
||||||
mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
|
||||||
}
|
}
|
||||||
val customElementCallBaseUrl by preferencesStore
|
val customElementCallBaseUrl by appPreferencesStore
|
||||||
.getCustomElementCallBaseUrlFlow()
|
.getCustomElementCallBaseUrlFlow()
|
||||||
.collectAsState(initial = null)
|
.collectAsState(initial = null)
|
||||||
|
|
||||||
|
|
@ -100,7 +100,7 @@ class DeveloperSettingsPresenter @Inject constructor(
|
||||||
is DeveloperSettingsEvents.SetCustomElementCallBaseUrl -> coroutineScope.launch {
|
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
|
// 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 }
|
val urlToSave = event.baseUrl.takeIf { !it.isNullOrEmpty() && it != ElementCallConfig.DEFAULT_BASE_URL }
|
||||||
preferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
appPreferencesStore.setCustomElementCallBaseUrl(urlToSave)
|
||||||
}
|
}
|
||||||
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
|
DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
<string name="screen_advanced_settings_developer_mode">"Developer mode"</string>
|
<string name="screen_advanced_settings_developer_mode">"Developer mode"</string>
|
||||||
<string name="screen_advanced_settings_developer_mode_description">"Enable to have access to features and functionality for developers."</string>
|
<string name="screen_advanced_settings_developer_mode_description">"Enable to have access to features and functionality for developers."</string>
|
||||||
<string name="screen_advanced_settings_rich_text_editor_description">"Disable the rich text editor to type Markdown manually."</string>
|
<string name="screen_advanced_settings_rich_text_editor_description">"Disable the rich text editor to type Markdown manually."</string>
|
||||||
|
<string name="screen_advanced_settings_send_read_receipts">"Read receipts"</string>
|
||||||
|
<string name="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."</string>
|
||||||
<string name="screen_advanced_settings_view_source_description">"Enable option to view message source in the timeline."</string>
|
<string name="screen_advanced_settings_view_source_description">"Enable option to view message source in the timeline."</string>
|
||||||
<string name="screen_edit_profile_display_name">"Display name"</string>
|
<string name="screen_edit_profile_display_name">"Display name"</string>
|
||||||
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>
|
<string name="screen_edit_profile_display_name_placeholder">"Your display name"</string>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.compound.theme.Theme
|
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.WarmUpRule
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -34,8 +35,7 @@ class AdvancedSettingsPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val store = InMemoryPreferencesStore()
|
val presenter = createAdvancedSettingsPresenter()
|
||||||
val presenter = AdvancedSettingsPresenter(store)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -43,14 +43,14 @@ class AdvancedSettingsPresenterTest {
|
||||||
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
||||||
assertThat(initialState.isRichTextEditorEnabled).isFalse()
|
assertThat(initialState.isRichTextEditorEnabled).isFalse()
|
||||||
assertThat(initialState.showChangeThemeDialog).isFalse()
|
assertThat(initialState.showChangeThemeDialog).isFalse()
|
||||||
|
assertThat(initialState.isSendPublicReadReceiptsEnabled).isTrue()
|
||||||
assertThat(initialState.theme).isEqualTo(Theme.System)
|
assertThat(initialState.theme).isEqualTo(Theme.System)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - developer mode on off`() = runTest {
|
fun `present - developer mode on off`() = runTest {
|
||||||
val store = InMemoryPreferencesStore()
|
val presenter = createAdvancedSettingsPresenter()
|
||||||
val presenter = AdvancedSettingsPresenter(store)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -65,8 +65,7 @@ class AdvancedSettingsPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - rich text editor on off`() = runTest {
|
fun `present - rich text editor on off`() = runTest {
|
||||||
val store = InMemoryPreferencesStore()
|
val presenter = createAdvancedSettingsPresenter()
|
||||||
val presenter = AdvancedSettingsPresenter(store)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.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
|
@Test
|
||||||
fun `present - change theme`() = runTest {
|
fun `present - change theme`() = runTest {
|
||||||
val store = InMemoryPreferencesStore()
|
val presenter = createAdvancedSettingsPresenter()
|
||||||
val presenter = AdvancedSettingsPresenter(store)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -102,4 +115,12 @@ class AdvancedSettingsPresenterTest {
|
||||||
assertThat(withNewTheme.theme).isEqualTo(Theme.Light)
|
assertThat(withNewTheme.theme).isEqualTo(Theme.Light)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createAdvancedSettingsPresenter(
|
||||||
|
appPreferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
|
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
|
) = AdvancedSettingsPresenter(
|
||||||
|
appPreferencesStore = appPreferencesStore,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.architecture.AsyncData
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
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.WarmUpRule
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -114,7 +114,7 @@ class DeveloperSettingsPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - custom element call base url`() = runTest {
|
fun `present - custom element call base url`() = runTest {
|
||||||
val preferencesStore = InMemoryPreferencesStore()
|
val preferencesStore = InMemoryAppPreferencesStore()
|
||||||
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
|
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferencesStore)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -149,14 +149,14 @@ class DeveloperSettingsPresenterTest {
|
||||||
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
|
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),
|
||||||
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
|
clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(),
|
||||||
rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()),
|
rageshakePresenter: DefaultRageshakePreferencesPresenter = DefaultRageshakePreferencesPresenter(FakeRageShake(), FakeRageshakeDataStore()),
|
||||||
preferencesStore: InMemoryPreferencesStore = InMemoryPreferencesStore(),
|
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
): DeveloperSettingsPresenter {
|
): DeveloperSettingsPresenter {
|
||||||
return DeveloperSettingsPresenter(
|
return DeveloperSettingsPresenter(
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
computeCacheSizeUseCase = cacheSizeUseCase,
|
computeCacheSizeUseCase = cacheSizeUseCase,
|
||||||
clearCacheUseCase = clearCacheUseCase,
|
clearCacheUseCase = clearCacheUseCase,
|
||||||
rageshakePresenter = rageshakePresenter,
|
rageshakePresenter = rageshakePresenter,
|
||||||
preferencesStore = preferencesStore,
|
appPreferencesStore = preferencesStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class FakeMatrixTimeline(
|
||||||
private val _paginationState: MutableStateFlow<MatrixTimeline.PaginationState> = MutableStateFlow(initialPaginationState)
|
private val _paginationState: MutableStateFlow<MatrixTimeline.PaginationState> = MutableStateFlow(initialPaginationState)
|
||||||
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = MutableStateFlow(initialTimelineItems)
|
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = MutableStateFlow(initialTimelineItems)
|
||||||
|
|
||||||
var sendReadReceiptCount = 0
|
var sentReadReceipts = mutableListOf<Pair<EventId, ReceiptType>>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var sendReadReceiptLatch: CompletableDeferred<Unit>? = null
|
var sendReadReceiptLatch: CompletableDeferred<Unit>? = null
|
||||||
|
|
@ -81,7 +81,7 @@ class FakeMatrixTimeline(
|
||||||
eventId: EventId,
|
eventId: EventId,
|
||||||
receiptType: ReceiptType,
|
receiptType: ReceiptType,
|
||||||
): Result<Unit> = simulateLongTask {
|
): Result<Unit> = simulateLongTask {
|
||||||
sendReadReceiptCount++
|
sentReadReceipts.add(eventId to receiptType)
|
||||||
sendReadReceiptLatch?.complete(Unit)
|
sendReadReceiptLatch?.complete(Unit)
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.preferences.api.store
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface PreferencesStore {
|
interface AppPreferencesStore {
|
||||||
suspend fun setRichTextEditorEnabled(enabled: Boolean)
|
suspend fun setRichTextEditorEnabled(enabled: Boolean)
|
||||||
fun isRichTextEditorEnabledFlow(): Flow<Boolean>
|
fun isRichTextEditorEnabledFlow(): Flow<Boolean>
|
||||||
|
|
||||||
|
|
@ -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<Boolean>
|
||||||
|
|
||||||
|
suspend fun clear()
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,8 @@ dependencies {
|
||||||
api(projects.libraries.preferences.api)
|
api(projects.libraries.preferences.api)
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
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.bool.orTrue
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
|
|
@ -42,10 +42,10 @@ private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseU
|
||||||
private val themeKey = stringPreferencesKey("theme")
|
private val themeKey = stringPreferencesKey("theme")
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultPreferencesStore @Inject constructor(
|
class DefaultAppPreferencesStore @Inject constructor(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
) : PreferencesStore {
|
) : AppPreferencesStore {
|
||||||
private val store = context.dataStore
|
private val store = context.dataStore
|
||||||
|
|
||||||
override suspend fun setRichTextEditorEnabled(enabled: Boolean) {
|
override suspend fun setRichTextEditorEnabled(enabled: Boolean) {
|
||||||
|
|
@ -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<Boolean> = get(sendPublicReadReceiptsKey, true)
|
||||||
|
|
||||||
|
override suspend fun clear() {
|
||||||
|
dataStoreFile.safeDelete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <T> update(key: Preferences.Key<T>, value: T) {
|
||||||
|
store.edit { prefs -> prefs[key] = value }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> get(key: Preferences.Key<T>, default: T): Flow<T> {
|
||||||
|
return store.data.map { prefs -> prefs[key] ?: default }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,16 +16,16 @@
|
||||||
|
|
||||||
package io.element.android.libraries.featureflag.test
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
class InMemoryPreferencesStore(
|
class InMemoryAppPreferencesStore(
|
||||||
isRichTextEditorEnabled: Boolean = false,
|
isRichTextEditorEnabled: Boolean = false,
|
||||||
isDeveloperModeEnabled: Boolean = false,
|
isDeveloperModeEnabled: Boolean = false,
|
||||||
customElementCallBaseUrl: String? = null,
|
customElementCallBaseUrl: String? = null,
|
||||||
theme: String? = null,
|
theme: String? = null,
|
||||||
) : PreferencesStore {
|
) : AppPreferencesStore {
|
||||||
private val isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled)
|
private val isRichTextEditorEnabled = MutableStateFlow(isRichTextEditorEnabled)
|
||||||
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
|
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
|
||||||
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
|
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
|
||||||
|
|
@ -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<Boolean> {
|
||||||
|
return isSendPublicReadReceiptsEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clear() {
|
||||||
|
clearCallCount++
|
||||||
|
isSendPublicReadReceiptsEnabled.tryEmit(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b9792c65780ccbabc55476cd15f0ef1a00e43d4b49ded546350271f52bc50ac5
|
oid sha256:da3f8614862ddacfd9b54afcd3bf091bec335e1489d7cbffc826059a89ae5478
|
||||||
size 39898
|
size 58453
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:33121ba1e716f4a22ba66a2648a1c66acbab8ffe721be2d52e556dd341818696
|
oid sha256:1fd599f2e93432d5d5555f086b60059e84452a0da3c2a69d932042b7ee4d3bac
|
||||||
size 39392
|
size 57922
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2081e18691471601c80bed114bc795c6fe4491186282db4a81f0fb186d286d30
|
oid sha256:9e9641228e0482c8e5500bb999b180e79cbfcb6a13e121f3be68aae5c58a6839
|
||||||
size 39402
|
size 57953
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:61201245ab79d39b505f27d9fd4d1a5842111bac1e067c2bd450f633c9e92ec4
|
oid sha256:1c1c5b00098e5860d4c69ef9a652483ea1192d9fa10f5449fb817769ab054183
|
||||||
size 35497
|
size 36128
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:4fe6625641cf06c45a817e9d10bc0d7296aa59160fc5a3e9dc28304e5b794125
|
||||||
|
size 57943
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5a24874553e4228ba8c254487c1bdd4d1a5556ecc2d4dfe26ace20548f5f9d38
|
oid sha256:e85b252d0d6c166f987974ebcf9a6b58b924316733b868ad0ed5a61413bda381
|
||||||
size 37203
|
size 54577
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:08d8714914bef5a1e5f353c43d5b7dbec7ff5c549402c723a0e875e1a8de2181
|
oid sha256:4f246ce9f60b25d9739d73e2aed98132dfb3a0a694acbf320ff796f35f805fdc
|
||||||
size 36807
|
size 54273
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3eb3cbf3f0eeced3753b4b574cfcf1e8a5d86e7789e970f0a07089656cd16a18
|
oid sha256:d0403a4bb8da3d36922cb5362977af9b3052624525def05e5236f4f922384c31
|
||||||
size 36818
|
size 54297
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0a74667729b5980edce4103b5ce38bd97dbb4df0e50642489a1822128b4dcd75
|
oid sha256:824774a1e19974090de4cb32e06a7f984b5db238c3dc8d04a35d3457e647ac84
|
||||||
size 31280
|
size 31999
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c0820b462a0dc103b66aef78528ae5ce13a55e2614a7a197e06713366c422893
|
||||||
|
size 54313
|
||||||
Loading…
Add table
Add a link
Reference in a new issue