Typing notification: disable rendering when the user preference isRenderTypingNotificationsEnabled is false.

This commit is contained in:
Benoit Marty 2024-02-09 09:35:52 +01:00
parent 6f70e587cf
commit 996c5fad95
6 changed files with 80 additions and 18 deletions

View file

@ -18,10 +18,12 @@ package io.element.android.features.messages.impl.typing
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
@ -29,6 +31,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -37,30 +40,41 @@ import javax.inject.Inject
class TypingNotificationPresenter @Inject constructor( class TypingNotificationPresenter @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val sessionPreferencesStore: SessionPreferencesStore,
) : Presenter<TypingNotificationState> { ) : Presenter<TypingNotificationState> {
@Composable @Composable
override fun present(): TypingNotificationState { override fun present(): TypingNotificationState {
var typingMembers by remember { mutableStateOf(emptyList<RoomMember>()) } val typingMembersState = remember { mutableStateOf(emptyList<RoomMember>()) }
LaunchedEffect(Unit) { val renderTypingNotifications by sessionPreferencesStore.isRenderTypingNotificationsEnabled().collectAsState(initial = true)
combine(room.roomTypingMembersFlow, room.membersStateFlow) { typingMembers, membersState -> LaunchedEffect(renderTypingNotifications) {
typingMembers if (renderTypingNotifications) {
.map { userId -> observeRoomTypingMembers(typingMembersState)
membersState.roomMembers() } else {
?.firstOrNull { roomMember -> roomMember.userId == userId } typingMembersState.value = emptyList()
?: createDefaultRoomMemberForTyping(userId)
}
} }
.distinctUntilChanged()
.onEach { members ->
typingMembers = members
}
.launchIn(this)
} }
return TypingNotificationState( return TypingNotificationState(
typingMembers = typingMembers.toImmutableList(), renderTypingNotifications = renderTypingNotifications,
typingMembers = typingMembersState.value.toImmutableList(),
) )
} }
private fun CoroutineScope.observeRoomTypingMembers(typingMembersState: MutableState<List<RoomMember>>) {
combine(room.roomTypingMembersFlow, room.membersStateFlow) { typingMembers, membersState ->
typingMembers
.map { userId ->
membersState.roomMembers()
?.firstOrNull { roomMember -> roomMember.userId == userId }
?: createDefaultRoomMemberForTyping(userId)
}
}
.distinctUntilChanged()
.onEach { members ->
typingMembersState.value = members
}
.launchIn(this)
}
} }
/** /**

View file

@ -20,5 +20,6 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class TypingNotificationState( data class TypingNotificationState(
val renderTypingNotifications: Boolean,
val typingMembers: ImmutableList<RoomMember>, val typingMembers: ImmutableList<RoomMember>,
) )

View file

@ -74,6 +74,7 @@ class TypingNotificationStateProvider : PreviewParameterProvider<TypingNotificat
internal fun aTypingNotificationState( internal fun aTypingNotificationState(
typingMembers: List<RoomMember> = emptyList(), typingMembers: List<RoomMember> = emptyList(),
) = TypingNotificationState( ) = TypingNotificationState(
renderTypingNotifications = true,
typingMembers = typingMembers.toImmutableList(), typingMembers = typingMembers.toImmutableList(),
) )

View file

@ -42,7 +42,7 @@ fun TypingNotificationView(
state: TypingNotificationState, state: TypingNotificationState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (state.typingMembers.isEmpty()) return if (state.typingMembers.isEmpty() || !state.renderTypingNotifications) return
val typingNotificationText = computeTypingNotificationText(state.typingMembers) val typingNotificationText = computeTypingNotificationText(state.typingMembers)
Text( Text(
modifier = modifier modifier = modifier

View file

@ -731,7 +731,10 @@ class MessagesPresenterTest {
} }
} }
val actionListPresenter = ActionListPresenter(appPreferencesStore = appPreferencesStore) val actionListPresenter = ActionListPresenter(appPreferencesStore = appPreferencesStore)
val typingNotificationPresenter = TypingNotificationPresenter(matrixRoom) val typingNotificationPresenter = TypingNotificationPresenter(
room = matrixRoom,
sessionPreferencesStore = sessionPreferencesStore,
)
val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter()
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)

View file

@ -20,6 +20,8 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@ -49,10 +51,47 @@ class TypingNotificationPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.renderTypingNotifications).isTrue()
assertThat(initialState.typingMembers).isEmpty() assertThat(initialState.typingMembers).isEmpty()
} }
} }
@Test
fun `present - typing notification disabled`() = runTest {
val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2)
val room = FakeMatrixRoom()
val sessionPreferencesStore = InMemorySessionPreferencesStore(
isRenderTypingNotificationsEnabled = false
)
val presenter = createPresenter(
matrixRoom = room,
sessionPreferencesStore = sessionPreferencesStore,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.renderTypingNotifications).isFalse()
assertThat(initialState.typingMembers).isEmpty()
room.givenRoomTypingMembers(listOf(A_USER_ID_2))
expectNoEvents()
// Preferences changes
sessionPreferencesStore.setRenderTypingNotifications(true)
skipItems(1)
val oneMemberTypingState = awaitItem()
assertThat(oneMemberTypingState.renderTypingNotifications).isTrue()
assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1)
assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember)
// Preferences changes again
sessionPreferencesStore.setRenderTypingNotifications(false)
skipItems(1)
val finalState = awaitItem()
assertThat(finalState.renderTypingNotifications).isFalse()
assertThat(finalState.typingMembers).isEmpty()
}
}
@Test @Test
fun `present - state is updated when a member is typing, member is not known`() = runTest { fun `present - state is updated when a member is typing, member is not known`() = runTest {
val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2) val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2)
@ -136,9 +175,13 @@ class TypingNotificationPresenterTest {
matrixRoom: MatrixRoom = FakeMatrixRoom().apply { matrixRoom: MatrixRoom = FakeMatrixRoom().apply {
givenRoomInfo(aRoomInfo(id = roomId.value, name = "")) givenRoomInfo(aRoomInfo(id = roomId.value, name = ""))
}, },
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(
isRenderTypingNotificationsEnabled = true
),
): TypingNotificationPresenter { ): TypingNotificationPresenter {
return TypingNotificationPresenter( return TypingNotificationPresenter(
room = matrixRoom, room = matrixRoom,
sessionPreferencesStore = sessionPreferencesStore,
) )
} }