Merge pull request #2349 from element-hq/feature/bma/disableTyping

"Share presence" setting
This commit is contained in:
Benoit Marty
2024-02-07 10:29:43 +01:00
committed by GitHub
35 changed files with 197 additions and 58 deletions

2
changelog.d/2241.feature Normal file
View File

@@ -0,0 +1,2 @@
Change "Read receipts" advanced setting used to send private Read Receipt to "Share presence" settings.
When disabled, private Read Receipts will be sent, and no typing notification will be sent. Also Read Receipts and typing notifications will not be rendered in the timeline.

View File

@@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
@@ -37,6 +38,7 @@ import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
import io.element.android.features.messages.impl.mentions.MentionSuggestion
import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
@@ -86,6 +88,7 @@ class MessageComposerPresenter @Inject constructor(
private val room: MatrixRoom,
private val mediaPickerProvider: PickerProvider,
private val featureFlagService: FeatureFlagService,
private val sessionPreferencesStore: SessionPreferencesStore,
private val localMediaFactory: LocalMediaFactory,
private val mediaSender: MediaSender,
private val snackbarDispatcher: SnackbarDispatcher,
@@ -147,6 +150,8 @@ class MessageComposerPresenter @Inject constructor(
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
var showTextFormatting: Boolean by remember { mutableStateOf(false) }
val sendTypingNotifications by sessionPreferencesStore.isSendTypingNotificationsEnabled().collectAsState(initial = true)
LaunchedEffect(messageComposerContext.composerMode) {
when (val modeValue = messageComposerContext.composerMode) {
is MessageComposerMode.Edit ->
@@ -212,7 +217,9 @@ class MessageComposerPresenter @Inject constructor(
// Declare that the user is not typing anymore when the composer is disposed
onDispose {
appCoroutineScope.launch {
room.typingNotice(false)
if (sendTypingNotifications) {
room.typingNotice(false)
}
}
}
}
@@ -310,8 +317,10 @@ class MessageComposerPresenter @Inject constructor(
analyticsService.trackError(event.error)
}
is MessageComposerEvents.TypingNotice -> {
localCoroutineScope.launch {
room.typingNotice(event.isTyping)
if (sendTypingNotifications) {
localCoroutineScope.launch {
room.typingNotice(event.isTyping)
}
}
}
is MessageComposerEvents.SuggestionReceived -> {

View File

@@ -106,6 +106,7 @@ class TimelinePresenter @AssistedInject constructor(
val keyBackupState by encryptionService.backupStateStateFlow.collectAsState()
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
val sessionState by remember {
derivedStateOf {
@@ -183,6 +184,7 @@ class TimelinePresenter @AssistedInject constructor(
highlightedEventId = highlightedEventId.value,
paginationState = paginationState,
timelineItems = timelineItems,
renderReadReceipts = renderReadReceipts,
newEventState = newItemState.value,
sessionState = sessionState,
eventSink = { handleEvents(it) }

View File

@@ -28,6 +28,7 @@ import kotlinx.collections.immutable.ImmutableList
data class TimelineState(
val timelineItems: ImmutableList<TimelineItem>,
val timelineRoomInfo: TimelineRoomInfo,
val renderReadReceipts: Boolean,
val highlightedEventId: EventId?,
val paginationState: MatrixTimeline.PaginationState,
val newEventState: NewEventState,

View File

@@ -48,6 +48,7 @@ import kotlin.random.Random
fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState(
timelineItems = timelineItems,
timelineRoomInfo = aTimelineRoomInfo(),
renderReadReceipts = false,
paginationState = MatrixTimeline.PaginationState(
isBackPaginating = false,
hasMoreToLoadBackwards = true,

View File

@@ -120,6 +120,7 @@ fun TimelineView(
TimelineItemRow(
timelineItem = timelineItem,
timelineRoomInfo = state.timelineRoomInfo,
renderReadReceipts = state.renderReadReceipts,
isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true &&
state.timelineItems.first().identifier() == timelineItem.identifier(),
highlightedItem = state.highlightedEventId?.value,

View File

@@ -26,11 +26,13 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
internal fun ATimelineItemEventRow(
event: TimelineItem.Event,
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
renderReadReceipts: Boolean = false,
isLastOutgoingMessage: Boolean = false,
isHighlighted: Boolean = false,
) = TimelineItemEventRow(
event = event,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = isHighlighted,
onClick = {},

View File

@@ -114,6 +114,7 @@ import kotlin.math.roundToInt
fun TimelineItemEventRow(
event: TimelineItem.Event,
timelineRoomInfo: TimelineRoomInfo,
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
isHighlighted: Boolean,
onClick: () -> Unit,
@@ -223,6 +224,7 @@ fun TimelineItemEventRow(
isLastOutgoingMessage = isLastOutgoingMessage,
receipts = event.readReceiptState.receipts,
),
renderReadReceipts = renderReadReceipts,
onReadReceiptsClicked = { onReadReceiptClick(event) },
modifier = Modifier.padding(top = 4.dp),
)

View File

@@ -47,6 +47,7 @@ internal fun TimelineItemEventRowWithRRPreview(
timelineItemReactions = aTimelineItemReactions(count = 0),
readReceiptState = TimelineItemReadReceipts(state.receipts),
),
renderReadReceipts = true,
isLastOutgoingMessage = false,
)
// A message from current user
@@ -60,6 +61,7 @@ internal fun TimelineItemEventRowWithRRPreview(
timelineItemReactions = aTimelineItemReactions(count = 0),
readReceiptState = TimelineItemReadReceipts(state.receipts),
),
renderReadReceipts = true,
isLastOutgoingMessage = false,
)
// Another message from current user
@@ -73,6 +75,7 @@ internal fun TimelineItemEventRowWithRRPreview(
timelineItemReactions = aTimelineItemReactions(count = 0),
readReceiptState = TimelineItemReadReceipts(state.receipts),
),
renderReadReceipts = true,
isLastOutgoingMessage = true,
)
}

View File

@@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.core.UserId
fun TimelineItemGroupedEventsRow(
timelineItem: TimelineItem.GroupedEvents,
timelineRoomInfo: TimelineRoomInfo,
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
highlightedItem: String?,
sessionState: SessionState,
@@ -70,6 +71,7 @@ fun TimelineItemGroupedEventsRow(
timelineItem = timelineItem,
timelineRoomInfo = timelineRoomInfo,
highlightedItem = highlightedItem,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
sessionState = sessionState,
onClick = onClick,
@@ -93,6 +95,7 @@ private fun TimelineItemGroupedEventsRowContent(
timelineItem: TimelineItem.GroupedEvents,
timelineRoomInfo: TimelineRoomInfo,
highlightedItem: String?,
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
sessionState: SessionState,
onClick: (TimelineItem.Event) -> Unit,
@@ -124,6 +127,7 @@ private fun TimelineItemGroupedEventsRowContent(
TimelineItemRow(
timelineItem = subGroupEvent,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
highlightedItem = highlightedItem,
sessionState = sessionState,
@@ -141,13 +145,14 @@ private fun TimelineItemGroupedEventsRowContent(
)
}
}
} else {
} else if (renderReadReceipts) {
TimelineItemReadReceiptView(
state = ReadReceiptViewState(
sendState = null,
isLastOutgoingMessage = false,
receipts = timelineItem.aggregatedReadReceipts,
),
renderReadReceipts = true,
onReadReceiptsClicked = onExpandGroupClick
)
}
@@ -163,6 +168,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
timelineItem = aGroupedEvents(withReadReceipts = true),
timelineRoomInfo = aTimelineRoomInfo(),
highlightedItem = null,
renderReadReceipts = true,
isLastOutgoingMessage = false,
sessionState = aSessionState(),
onClick = {},
@@ -187,6 +193,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
timelineItem = aGroupedEvents(withReadReceipts = true),
timelineRoomInfo = aTimelineRoomInfo(),
highlightedItem = null,
renderReadReceipts = true,
isLastOutgoingMessage = false,
sessionState = aSessionState(),
onClick = {},

View File

@@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.core.UserId
internal fun TimelineItemRow(
timelineItem: TimelineItem,
timelineRoomInfo: TimelineRoomInfo,
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
highlightedItem: String?,
sessionState: SessionState,
@@ -58,6 +59,7 @@ internal fun TimelineItemRow(
if (timelineItem.content is TimelineItemStateContent) {
TimelineItemStateEventRow(
event = timelineItem,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = highlightedItem == timelineItem.identifier(),
onClick = { onClick(timelineItem) },
@@ -70,6 +72,7 @@ internal fun TimelineItemRow(
TimelineItemEventRow(
event = timelineItem,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = highlightedItem == timelineItem.identifier(),
onClick = { onClick(timelineItem) },
@@ -91,6 +94,7 @@ internal fun TimelineItemRow(
TimelineItemGroupedEventsRow(
timelineItem = timelineItem,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
highlightedItem = highlightedItem,
sessionState = sessionState,

View File

@@ -47,6 +47,7 @@ import kotlinx.collections.immutable.toPersistentList
@Composable
fun TimelineItemStateEventRow(
event: TimelineItem.Event,
renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean,
isHighlighted: Boolean,
onClick: () -> Unit,
@@ -90,6 +91,7 @@ fun TimelineItemStateEventRow(
isLastOutgoingMessage = isLastOutgoingMessage,
receipts = event.readReceiptState.receipts,
),
renderReadReceipts = renderReadReceipts,
onReadReceiptsClicked = { onReadReceiptsClick(event) },
)
}
@@ -107,6 +109,7 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview {
receipts = listOf(aReadReceiptData(0)).toPersistentList(),
)
),
renderReadReceipts = true,
isLastOutgoingMessage = false,
isHighlighted = false,
onClick = {},

View File

@@ -58,20 +58,23 @@ import kotlinx.collections.immutable.ImmutableList
@Composable
fun TimelineItemReadReceiptView(
state: ReadReceiptViewState,
renderReadReceipts: Boolean,
onReadReceiptsClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
if (state.receipts.isNotEmpty()) {
ReadReceiptsRow(modifier = modifier) {
ReadReceiptsAvatars(
receipts = state.receipts,
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.clickable {
onReadReceiptsClicked()
}
.padding(2.dp)
)
if (renderReadReceipts) {
ReadReceiptsRow(modifier = modifier) {
ReadReceiptsAvatars(
receipts = state.receipts,
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.clickable {
onReadReceiptsClicked()
}
.padding(2.dp)
)
}
}
} else {
when (state.sendState) {
@@ -206,6 +209,7 @@ internal fun TimelineItemReactionsViewPreview(
) = ElementPreview {
TimelineItemReadReceiptView(
state = state,
renderReadReceipts = true,
onReadReceiptsClicked = {},
)
}

View File

@@ -675,6 +675,7 @@ class MessagesPresenterTest {
room = matrixRoom,
mediaPickerProvider = FakePickerProvider(),
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.NotificationSettings.key to true)),
sessionPreferencesStore = InMemorySessionPreferencesStore(),
localMediaFactory = FakeLocalMediaFactory(mockMediaUrl),
mediaSender = mediaSender,
snackbarDispatcher = SnackbarDispatcher(),

View File

@@ -32,11 +32,13 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.media.ImageInfo
@@ -888,6 +890,24 @@ class MessageComposerPresenterTest {
}
}
@Test
fun `present - handle typing notice event when sending typing notice is disabled`() = runTest {
val room = FakeMatrixRoom()
val store = InMemorySessionPreferencesStore(
isSendTypingNotificationsEnabled = false
)
val presenter = createPresenter(room = room, sessionPreferencesStore = store, coroutineScope = this)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitFirstItem()
assertThat(room.typingRecord).isEmpty()
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(true))
initialState.eventSink.invoke(MessageComposerEvents.TypingNotice(false))
assertThat(room.typingRecord).isEmpty()
}
}
private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0): MessageComposerState {
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
skipItems(skipCount)
@@ -901,6 +921,7 @@ class MessageComposerPresenterTest {
room: MatrixRoom = FakeMatrixRoom(),
pickerProvider: PickerProvider = this.pickerProvider,
featureFlagService: FeatureFlagService = this.featureFlagService,
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
permissionPresenter: PermissionsPresenter = FakePermissionsPresenter(),
@@ -909,6 +930,7 @@ class MessageComposerPresenterTest {
room,
pickerProvider,
featureFlagService,
sessionPreferencesStore,
localMediaFactory,
MediaSender(mediaPreProcessor, room),
snackbarDispatcher,

View File

@@ -21,7 +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 class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data object ChangeTheme : AdvancedSettingsEvents
data object CancelChangeTheme : AdvancedSettingsEvents
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents

View File

@@ -44,8 +44,8 @@ class AdvancedSettingsPresenter @Inject constructor(
val isDeveloperModeEnabled by appPreferencesStore
.isDeveloperModeEnabledFlow()
.collectAsState(initial = false)
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore
.isSendPublicReadReceiptsEnabled()
val isSharePresenceEnabled by sessionPreferencesStore
.isSharePresenceEnabled()
.collectAsState(initial = true)
val theme by remember {
appPreferencesStore.getThemeFlow().mapToTheme()
@@ -60,8 +60,8 @@ class AdvancedSettingsPresenter @Inject constructor(
is AdvancedSettingsEvents.SetDeveloperModeEnabled -> localCoroutineScope.launch {
appPreferencesStore.setDeveloperModeEnabled(event.enabled)
}
is AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled -> localCoroutineScope.launch {
sessionPreferencesStore.setSendPublicReadReceipts(event.enabled)
is AdvancedSettingsEvents.SetSharePresenceEnabled -> localCoroutineScope.launch {
sessionPreferencesStore.setSharePresence(event.enabled)
}
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
@@ -75,7 +75,7 @@ class AdvancedSettingsPresenter @Inject constructor(
return AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled,
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
isSharePresenceEnabled = isSharePresenceEnabled,
theme = theme,
showChangeThemeDialog = showChangeThemeDialog,
eventSink = { handleEvents(it) }

View File

@@ -21,7 +21,7 @@ import io.element.android.compound.theme.Theme
data class AdvancedSettingsState(
val isRichTextEditorEnabled: Boolean,
val isDeveloperModeEnabled: Boolean,
val isSendPublicReadReceiptsEnabled: Boolean,
val isSharePresenceEnabled: Boolean,
val theme: Theme,
val showChangeThemeDialog: Boolean,
val eventSink: (AdvancedSettingsEvents) -> Unit

View File

@@ -38,7 +38,7 @@ fun aAdvancedSettingsState(
) = AdvancedSettingsState(
isRichTextEditorEnabled = isRichTextEditorEnabled,
isDeveloperModeEnabled = isDeveloperModeEnabled,
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
isSharePresenceEnabled = isSendPublicReadReceiptsEnabled,
theme = Theme.System,
showChangeThemeDialog = showChangeThemeDialog,
eventSink = {}

View File

@@ -83,15 +83,15 @@ fun AdvancedSettingsView(
)
ListItem(
headlineContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_send_read_receipts))
Text(text = stringResource(id = R.string.screen_advanced_settings_share_presence))
},
supportingContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_send_read_receipts_description))
Text(text = stringResource(id = R.string.screen_advanced_settings_share_presence_description))
},
trailingContent = ListItemContent.Switch(
checked = state.isSendPublicReadReceiptsEnabled,
checked = state.isSharePresenceEnabled,
),
onClick = { state.eventSink(AdvancedSettingsEvents.SetSendPublicReadReceiptsEnabled(!state.isSendPublicReadReceiptsEnabled)) }
onClick = { state.eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(!state.isSharePresenceEnabled)) }
)
}

View File

@@ -8,6 +8,8 @@
<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_share_presence">"Share presence"</string>
<string name="screen_advanced_settings_share_presence_description">"If turned off, you wont be able to send or receive read receipts or typing notifications"</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_placeholder">"Your display name"</string>

View File

@@ -43,7 +43,7 @@ class AdvancedSettingsPresenterTest {
assertThat(initialState.isDeveloperModeEnabled).isFalse()
assertThat(initialState.isRichTextEditorEnabled).isFalse()
assertThat(initialState.showChangeThemeDialog).isFalse()
assertThat(initialState.isSendPublicReadReceiptsEnabled).isTrue()
assertThat(initialState.isSharePresenceEnabled).isTrue()
assertThat(initialState.theme).isEqualTo(Theme.System)
}
}
@@ -79,17 +79,17 @@ class AdvancedSettingsPresenterTest {
}
@Test
fun `present - send public read receipts off on`() = runTest {
fun `present - share presence 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()
assertThat(initialState.isSharePresenceEnabled).isTrue()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetSharePresenceEnabled(false))
assertThat(awaitItem().isSharePresenceEnabled).isFalse()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetSharePresenceEnabled(true))
assertThat(awaitItem().isSharePresenceEnabled).isTrue()
}
}

View File

@@ -19,8 +19,20 @@ package io.element.android.features.preferences.api.store
import kotlinx.coroutines.flow.Flow
interface SessionPreferencesStore {
suspend fun setSharePresence(enabled: Boolean)
fun isSharePresenceEnabled(): Flow<Boolean>
suspend fun setSendPublicReadReceipts(enabled: Boolean)
fun isSendPublicReadReceiptsEnabled(): Flow<Boolean>
suspend fun setRenderReadReceipts(enabled: Boolean)
fun isRenderReadReceiptsEnabled(): Flow<Boolean>
suspend fun setSendTypingNotifications(enabled: Boolean)
fun isSendTypingNotificationsEnabled(): Flow<Boolean>
suspend fun setRenderTypingNotifications(enabled: Boolean)
fun isRenderTypingNotificationsEnabled(): Flow<Boolean>
suspend fun clear()
}

View File

@@ -29,7 +29,9 @@ import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import java.io.File
class DefaultSessionPreferencesStore(
@@ -43,13 +45,41 @@ class DefaultSessionPreferencesStore(
return context.preferencesDataStoreFile("session_${hashedUserId}_preferences")
}
}
private val sharePresenceKey = booleanPreferencesKey("sharePresence")
private val sendPublicReadReceiptsKey = booleanPreferencesKey("sendPublicReadReceipts")
private val renderReadReceiptsKey = booleanPreferencesKey("renderReadReceipts")
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
private val dataStoreFile = storeFile(context, sessionId)
private val store = PreferenceDataStoreFactory.create(scope = sessionCoroutineScope) { dataStoreFile }
override suspend fun setSharePresence(enabled: Boolean) {
update(sharePresenceKey, enabled)
// Also update all the other settings
setSendPublicReadReceipts(enabled)
setRenderReadReceipts(enabled)
setSendTypingNotifications(enabled)
setRenderTypingNotifications(enabled)
}
override fun isSharePresenceEnabled(): Flow<Boolean> {
// Migration, if sendPublicReadReceiptsKey was false, consider that sharing presence is false.
return get(sharePresenceKey) { runBlocking { isSendPublicReadReceiptsEnabled().first() } }
}
override suspend fun setSendPublicReadReceipts(enabled: Boolean) = update(sendPublicReadReceiptsKey, enabled)
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> = get(sendPublicReadReceiptsKey, true)
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> = get(sendPublicReadReceiptsKey) { true }
override suspend fun setRenderReadReceipts(enabled: Boolean) = update(renderReadReceiptsKey, enabled)
override fun isRenderReadReceiptsEnabled(): Flow<Boolean> = get(renderReadReceiptsKey) { true }
override suspend fun setSendTypingNotifications(enabled: Boolean) = update(sendTypingNotificationsKey, enabled)
override fun isSendTypingNotificationsEnabled(): Flow<Boolean> = get(sendTypingNotificationsKey) { true }
override suspend fun setRenderTypingNotifications(enabled: Boolean) = update(renderTypingNotificationsKey, enabled)
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = get(renderTypingNotificationsKey) { true }
override suspend fun clear() {
dataStoreFile.safeDelete()
@@ -59,7 +89,7 @@ class DefaultSessionPreferencesStore(
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 }
private fun <T> get(key: Preferences.Key<T>, default: () -> T): Flow<T> {
return store.data.map { prefs -> prefs[key] ?: default() }
}
}

View File

@@ -21,19 +21,50 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class InMemorySessionPreferencesStore(
isSharePresenceEnabled: Boolean = true,
isSendPublicReadReceiptsEnabled: Boolean = true,
isRenderReadReceiptsEnabled: Boolean = true,
isSendTypingNotificationsEnabled: Boolean = true,
isRenderTypingNotificationsEnabled: Boolean = true,
) : SessionPreferencesStore {
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
private val isRenderReadReceiptsEnabled = MutableStateFlow(isRenderReadReceiptsEnabled)
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
var clearCallCount = 0
private set
override suspend fun setSharePresence(enabled: Boolean) {
isSharePresenceEnabled.tryEmit(enabled)
}
override fun isSharePresenceEnabled(): Flow<Boolean> = isSharePresenceEnabled
override suspend fun setSendPublicReadReceipts(enabled: Boolean) {
isSendPublicReadReceiptsEnabled.tryEmit(enabled)
}
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> {
return isSendPublicReadReceiptsEnabled
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> = isSendPublicReadReceiptsEnabled
override suspend fun setRenderReadReceipts(enabled: Boolean) {
isRenderReadReceiptsEnabled.tryEmit(enabled)
}
override fun isRenderReadReceiptsEnabled(): Flow<Boolean> = isRenderReadReceiptsEnabled
override suspend fun setSendTypingNotifications(enabled: Boolean) {
isSendTypingNotificationsEnabled.tryEmit(enabled)
}
override fun isSendTypingNotificationsEnabled(): Flow<Boolean> = isSendTypingNotificationsEnabled
override suspend fun setRenderTypingNotifications(enabled: Boolean) {
isRenderTypingNotificationsEnabled.tryEmit(enabled)
}
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = isRenderTypingNotificationsEnabled
override suspend fun clear() {
clearCallCount++
isSendPublicReadReceiptsEnabled.tryEmit(true)