Browse Source

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

"Share presence" setting
pull/2358/head
Benoit Marty 8 months ago committed by GitHub
parent
commit
022d309eb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      changelog.d/2241.feature
  2. 15
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
  3. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
  4. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt
  5. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt
  6. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
  7. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt
  8. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
  9. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithRRPreview.kt
  10. 9
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
  11. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
  12. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt
  13. 24
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt
  14. 1
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
  15. 22
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt
  16. 2
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt
  17. 10
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt
  18. 2
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt
  19. 2
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt
  20. 8
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt
  21. 2
      features/preferences/impl/src/main/res/values/localazy.xml
  22. 14
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt
  23. 12
      libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/SessionPreferencesStore.kt
  24. 36
      libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt
  25. 35
      libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemorySessionPreferencesStore.kt
  26. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_0,NEXUS_5,1.0,en].png
  27. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_1,NEXUS_5,1.0,en].png
  28. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_2,NEXUS_5,1.0,en].png
  29. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_3,NEXUS_5,1.0,en].png
  30. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_4,NEXUS_5,1.0,en].png
  31. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_0,NEXUS_5,1.0,en].png
  32. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_1,NEXUS_5,1.0,en].png
  33. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_2,NEXUS_5,1.0,en].png
  34. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_3,NEXUS_5,1.0,en].png
  35. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_4,NEXUS_5,1.0,en].png

2
changelog.d/2241.feature

@ -0,0 +1,2 @@ @@ -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.

15
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable @@ -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 @@ -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( @@ -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( @@ -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( @@ -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( @@ -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 -> {

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt

@ -106,6 +106,7 @@ class TimelinePresenter @AssistedInject constructor( @@ -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( @@ -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) }

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt

@ -28,6 +28,7 @@ import kotlinx.collections.immutable.ImmutableList @@ -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,

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt

@ -48,6 +48,7 @@ import kotlin.random.Random @@ -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,

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt

@ -120,6 +120,7 @@ fun TimelineView( @@ -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,

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt

@ -26,11 +26,13 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -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 = {},

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt

@ -114,6 +114,7 @@ import kotlin.math.roundToInt @@ -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( @@ -223,6 +224,7 @@ fun TimelineItemEventRow(
isLastOutgoingMessage = isLastOutgoingMessage,
receipts = event.readReceiptState.receipts,
),
renderReadReceipts = renderReadReceipts,
onReadReceiptsClicked = { onReadReceiptClick(event) },
modifier = Modifier.padding(top = 4.dp),
)

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithRRPreview.kt

@ -47,6 +47,7 @@ internal fun TimelineItemEventRowWithRRPreview( @@ -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( @@ -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( @@ -73,6 +75,7 @@ internal fun TimelineItemEventRowWithRRPreview(
timelineItemReactions = aTimelineItemReactions(count = 0),
readReceiptState = TimelineItemReadReceipts(state.receipts),
),
renderReadReceipts = true,
isLastOutgoingMessage = true,
)
}

9
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt

@ -43,6 +43,7 @@ import io.element.android.libraries.matrix.api.core.UserId @@ -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( @@ -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( @@ -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( @@ -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( @@ -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 @@ -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 @@ -187,6 +193,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
timelineItem = aGroupedEvents(withReadReceipts = true),
timelineRoomInfo = aTimelineRoomInfo(),
highlightedItem = null,
renderReadReceipts = true,
isLastOutgoingMessage = false,
sessionState = aSessionState(),
onClick = {},

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt

@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.core.UserId @@ -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( @@ -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( @@ -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( @@ -91,6 +94,7 @@ internal fun TimelineItemRow(
TimelineItemGroupedEventsRow(
timelineItem = timelineItem,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
highlightedItem = highlightedItem,
sessionState = sessionState,

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt

@ -47,6 +47,7 @@ import kotlinx.collections.immutable.toPersistentList @@ -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( @@ -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 { @@ -107,6 +109,7 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview {
receipts = listOf(aReadReceiptData(0)).toPersistentList(),
)
),
renderReadReceipts = true,
isLastOutgoingMessage = false,
isHighlighted = false,
onClick = {},

24
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt

@ -58,20 +58,23 @@ import kotlinx.collections.immutable.ImmutableList @@ -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( @@ -206,6 +209,7 @@ internal fun TimelineItemReactionsViewPreview(
) = ElementPreview {
TimelineItemReadReceiptView(
state = state,
renderReadReceipts = true,
onReadReceiptsClicked = {},
)
}

1
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt

@ -675,6 +675,7 @@ class MessagesPresenterTest { @@ -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(),

22
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt

@ -32,11 +32,13 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer @@ -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 { @@ -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 { @@ -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 { @@ -909,6 +930,7 @@ class MessageComposerPresenterTest {
room,
pickerProvider,
featureFlagService,
sessionPreferencesStore,
localMediaFactory,
MediaSender(mediaPreProcessor, room),
snackbarDispatcher,

2
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt

@ -21,7 +21,7 @@ import io.element.android.compound.theme.Theme @@ -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

10
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt

@ -44,8 +44,8 @@ class AdvancedSettingsPresenter @Inject constructor( @@ -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( @@ -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( @@ -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) }

2
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt

@ -21,7 +21,7 @@ import io.element.android.compound.theme.Theme @@ -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

2
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt

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

8
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt

@ -83,15 +83,15 @@ fun AdvancedSettingsView( @@ -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)) }
)
}

2
features/preferences/impl/src/main/res/values/localazy.xml

@ -8,6 +8,8 @@ @@ -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>

14
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt

@ -43,7 +43,7 @@ class AdvancedSettingsPresenterTest { @@ -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 { @@ -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()
}
}

12
libraries/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/store/SessionPreferencesStore.kt

@ -19,8 +19,20 @@ package io.element.android.features.preferences.api.store @@ -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()
}

36
libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt

@ -29,7 +29,9 @@ import io.element.android.libraries.di.annotations.SessionCoroutineScope @@ -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( @@ -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( @@ -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() }
}
}

35
libraries/preferences/test/src/main/kotlin/io/element/android/libraries/featureflag/test/InMemorySessionPreferencesStore.kt

@ -21,19 +21,50 @@ import kotlinx.coroutines.flow.Flow @@ -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)

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Day-1_2_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_0,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_1,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_2,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_3,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.advanced_AdvancedSettingsView_null_AdvancedSettingsView-Night-1_3_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save