Merge pull request #2349 from element-hq/feature/bma/disableTyping
"Share presence" setting
This commit is contained in:
2
changelog.d/2241.feature
Normal file
2
changelog.d/2241.feature
Normal 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.
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -38,7 +38,7 @@ fun aAdvancedSettingsState(
|
||||
) = AdvancedSettingsState(
|
||||
isRichTextEditorEnabled = isRichTextEditorEnabled,
|
||||
isDeveloperModeEnabled = isDeveloperModeEnabled,
|
||||
isSendPublicReadReceiptsEnabled = isSendPublicReadReceiptsEnabled,
|
||||
isSharePresenceEnabled = isSendPublicReadReceiptsEnabled,
|
||||
theme = Theme.System,
|
||||
showChangeThemeDialog = showChangeThemeDialog,
|
||||
eventSink = {}
|
||||
|
||||
@@ -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)) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 won’t 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>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user