From 5d6716da6786040109e7c3790ad5cefb8ef1b208 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Jan 2024 14:33:07 +0100 Subject: [PATCH 01/11] Rendering typing notification #2242 --- changelog.d/2242.feature | 1 + .../messages/impl/MessagesPresenter.kt | 4 + .../features/messages/impl/MessagesState.kt | 2 + .../messages/impl/MessagesStateProvider.kt | 2 + .../features/messages/impl/MessagesView.kt | 1 + .../messages/impl/timeline/TimelineView.kt | 8 + .../typing/TypingNotificationPresenter.kt | 82 +++++++++ .../impl/typing/TypingNotificationState.kt | 24 +++ .../typing/TypingNotificationStateProvider.kt | 95 ++++++++++ .../impl/typing/TypingNotificationView.kt | 103 +++++++++++ .../messages/impl/MessagesPresenterTest.kt | 3 + .../typing/TypingNotificationPresenterTest.kt | 170 ++++++++++++++++++ .../libraries/matrix/api/room/MatrixRoom.kt | 1 + .../libraries/matrix/api/room/RoomMember.kt | 14 +- .../matrix/impl/room/RustMatrixRoom.kt | 16 ++ .../matrix/test/room/FakeMatrixRoom.kt | 7 + 16 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 changelog.d/2242.feature create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt diff --git a/changelog.d/2242.feature b/changelog.d/2242.feature new file mode 100644 index 0000000000..947661653f --- /dev/null +++ b/changelog.d/2242.feature @@ -0,0 +1 @@ +Rendering typing notification diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 1b264ac1f7..986dba4709 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -59,6 +59,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.typing.TypingNotificationPresenter import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.networkmonitor.api.NetworkMonitor @@ -97,6 +98,7 @@ class MessagesPresenter @AssistedInject constructor( private val composerPresenter: MessageComposerPresenter, private val voiceMessageComposerPresenter: VoiceMessageComposerPresenter, timelinePresenterFactory: TimelinePresenter.Factory, + private val typingNotificationPresenter: TypingNotificationPresenter, private val actionListPresenter: ActionListPresenter, private val customReactionPresenter: CustomReactionPresenter, private val reactionSummaryPresenter: ReactionSummaryPresenter, @@ -129,6 +131,7 @@ class MessagesPresenter @AssistedInject constructor( val composerState = composerPresenter.present() val voiceMessageComposerState = voiceMessageComposerPresenter.present() val timelineState = timelinePresenter.present() + val typingNotificationState = typingNotificationPresenter.present() val actionListState = actionListPresenter.present() val customReactionState = customReactionPresenter.present() val reactionSummaryState = reactionSummaryPresenter.present() @@ -233,6 +236,7 @@ class MessagesPresenter @AssistedInject constructor( composerState = composerState, voiceMessageComposerState = voiceMessageComposerState, timelineState = timelineState, + typingNotificationState = typingNotificationState, actionListState = actionListState, customReactionState = customReactionState, reactionSummaryState = reactionSummaryState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 8e4d1c484b..5c6e1c7ffa 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.components.customreact import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState +import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -42,6 +43,7 @@ data class MessagesState( val composerState: MessageComposerState, val voiceMessageComposerState: VoiceMessageComposerState, val timelineState: TimelineState, + val typingNotificationState: TypingNotificationState, val actionListState: ActionListState, val customReactionState: CustomReactionState, val reactionSummaryState: ReactionSummaryState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 9e36086c45..da278904a3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.timeline.components.reactionsum import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessageComposerState import io.element.android.features.messages.impl.voicemessages.composer.aVoiceMessagePreviewState @@ -117,6 +118,7 @@ fun aMessagesState( timelineState = aTimelineState( timelineItems = aTimelineItemList(aTimelineItemTextContent()), ), + typingNotificationState = aTypingNotificationState(), retrySendMenuState = RetrySendMenuState( selectedEvent = null, eventSink = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 1c1bca9179..e629571c3c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -384,6 +384,7 @@ private fun MessagesViewContent( modifier = Modifier.padding(paddingValues), state = state.timelineState, roomName = state.roomName.dataOrNull(), + typingNotificationState = state.typingNotificationState, onMessageClicked = onMessageClicked, onMessageLongClicked = onMessageLongClicked, onUserDataClicked = onUserDataClicked, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 72cf2be504..410e74faa4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -62,6 +62,9 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider +import io.element.android.features.messages.impl.typing.TypingNotificationState +import io.element.android.features.messages.impl.typing.TypingNotificationView +import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.libraries.designsystem.animation.alphaAnimation import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -75,6 +78,7 @@ import kotlinx.coroutines.launch @Composable fun TimelineView( state: TimelineState, + typingNotificationState: TypingNotificationState, roomName: String?, onUserDataClicked: (UserId) -> Unit, onMessageClicked: (TimelineItem.Event) -> Unit, @@ -112,6 +116,9 @@ fun TimelineView( reverseLayout = true, contentPadding = PaddingValues(vertical = 8.dp), ) { + item { + TypingNotificationView(state = typingNotificationState) + } items( items = state.timelineItems, contentType = { timelineItem -> timelineItem.contentType() }, @@ -256,6 +263,7 @@ internal fun TimelineViewPreview( TimelineView( state = aTimelineState(timelineItems), roomName = null, + typingNotificationState = aTypingNotificationState(), onMessageClicked = {}, onTimestampClicked = {}, onUserDataClicked = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt new file mode 100644 index 0000000000..fdbb698a02 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.typing + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Presenter +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.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.room.roomMembers +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +class TypingNotificationPresenter @Inject constructor( + private val room: MatrixRoom, +) : Presenter { + @Composable + override fun present(): TypingNotificationState { + var typingMembers by remember { mutableStateOf(emptyList()) } + LaunchedEffect(Unit) { + combine(room.roomTypingMembersFlow, room.membersStateFlow) { typingMembers, membersState -> + typingMembers + .map { userId -> + membersState.roomMembers() + ?.firstOrNull { roomMember -> roomMember.userId == userId } + ?: createDefaultRoomMemberForTyping(userId) + } + } + .distinctUntilChanged() + .onEach { members -> + typingMembers = members + } + .launchIn(this) + } + + return TypingNotificationState( + typingMembers = typingMembers.toImmutableList(), + ) + } +} + +/** + * Create a default [RoomMember] for typing events. + * In this case, only the userId will be used for rendering, other fields are not used, but keep them + * as close as possible to the actual data. + */ +private fun createDefaultRoomMemberForTyping(userId: UserId): RoomMember { + return RoomMember( + userId = userId, + displayName = null, + avatarUrl = null, + membership = RoomMembershipState.JOIN, + isNameAmbiguous = false, + powerLevel = 0, + normalizedPowerLevel = 0, + isIgnored = false, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt new file mode 100644 index 0000000000..d4398a6351 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.typing + +import io.element.android.libraries.matrix.api.room.RoomMember +import kotlinx.collections.immutable.ImmutableList + +data class TypingNotificationState( + val typingMembers: ImmutableList, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt new file mode 100644 index 0000000000..88a3c8677f --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.typing + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import kotlinx.collections.immutable.toImmutableList + +class TypingNotificationStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aTypingNotificationState(), + aTypingNotificationState( + typingMembers = listOf( + aTypingRoomMember(), + ), + ), + aTypingNotificationState( + typingMembers = listOf( + aTypingRoomMember(displayName = "Alice"), + ), + ), + aTypingNotificationState( + typingMembers = listOf( + aTypingRoomMember(displayName = "Alice", isNameAmbiguous = true), + ), + ), + aTypingNotificationState( + typingMembers = listOf( + aTypingRoomMember(displayName = "Alice"), + aTypingRoomMember(displayName = "Bob"), + ), + ), + aTypingNotificationState( + typingMembers = listOf( + aTypingRoomMember(displayName = "Alice"), + aTypingRoomMember(displayName = "Bob"), + aTypingRoomMember(displayName = "Charlie"), + ), + ), + aTypingNotificationState( + typingMembers = listOf( + aTypingRoomMember(displayName = "Alice"), + aTypingRoomMember(displayName = "Bob"), + aTypingRoomMember(displayName = "Charlie"), + aTypingRoomMember(displayName = "Dan"), + aTypingRoomMember(displayName = "Eve"), + ), + ), + aTypingNotificationState( + typingMembers = listOf( + aTypingRoomMember(displayName = "Alice with a very long display name"), + ), + ), + ) +} + +internal fun aTypingNotificationState( + typingMembers: List = emptyList(), +) = TypingNotificationState( + typingMembers = typingMembers.toImmutableList(), +) + +internal fun aTypingRoomMember( + userId: UserId = UserId("@alice:example.com"), + displayName: String? = null, + isNameAmbiguous: Boolean = false, +): RoomMember { + return RoomMember( + userId = userId, + displayName = displayName, + avatarUrl = null, + membership = RoomMembershipState.JOIN, + isNameAmbiguous = isNameAmbiguous, + powerLevel = 0, + normalizedPowerLevel = 0, + isIgnored = false, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt new file mode 100644 index 0000000000..7ad31abd9d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.typing + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.features.messages.impl.R +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.room.RoomMember + +@Composable +fun TypingNotificationView( + state: TypingNotificationState, + modifier: Modifier = Modifier, +) { + if (state.typingMembers.isEmpty()) return + val typingNotificationText = computeTypingNotificationText(state.typingMembers) + Text( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 68.dp, vertical = 2.dp), + text = typingNotificationText, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = ElementTheme.typography.fontBodySmRegular, + color = ElementTheme.colors.textSecondary, + ) +} + +@Composable +private fun computeTypingNotificationText(typingMembers: List): AnnotatedString { + val names = when (typingMembers.size) { + 0 -> "" // Cannot happen + 1 -> typingMembers[0].disambiguatedDisplayName + 2 -> stringResource( + id = R.string.screen_room_typing_two_members, + typingMembers[0].disambiguatedDisplayName, + typingMembers[1].disambiguatedDisplayName, + ) + else -> pluralStringResource( + id = R.plurals.screen_room_typing_many_members, + count = typingMembers.size - 2, + typingMembers[0].disambiguatedDisplayName, + typingMembers[1].disambiguatedDisplayName, + typingMembers.size - 2, + ) + } + // Get the translated string with a fake pattern + val tmpString = pluralStringResource( + id = R.plurals.screen_room_typing_notification, + count = typingMembers.size, + "<>", + ) + // Split the string in 3 parts + val parts = tmpString.split("<>") + // And rebuild the string with the names + return buildAnnotatedString { + append(parts[0]) + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(names) + } + append(parts[1]) + } +} + +@PreviewsDayNight +@Composable +internal fun TypingNotificationViewPreview( + @PreviewParameter(TypingNotificationStateProvider::class) state: TypingNotificationState, +) = ElementPreview { + TypingNotificationView( + state = state, + ) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index fe7e07e6f9..9e729e32e1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -42,6 +42,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent +import io.element.android.features.messages.impl.typing.TypingNotificationPresenter import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPlayer import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessageComposerPresenter import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager @@ -730,6 +731,7 @@ class MessagesPresenterTest { } } val actionListPresenter = ActionListPresenter(appPreferencesStore = appPreferencesStore) + val typingNotificationPresenter = TypingNotificationPresenter(matrixRoom) val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) @@ -739,6 +741,7 @@ class MessagesPresenterTest { composerPresenter = messageComposerPresenter, voiceMessageComposerPresenter = voiceMessageComposerPresenter, timelinePresenterFactory = timelinePresenterFactory, + typingNotificationPresenter = typingNotificationPresenter, actionListPresenter = actionListPresenter, customReactionPresenter = customReactionPresenter, reactionSummaryPresenter = reactionSummaryPresenter, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt new file mode 100644 index 0000000000..4bedd6f6a3 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.typing + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +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.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.A_USER_ID_4 +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +@Suppress("LargeClass") +class TypingNotificationPresenterTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `present - initial state`() = runTest { + val presenter = createPresenter() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.typingMembers).isEmpty() + } + } + + @Test + fun `present - state is updated when a member is typing, member is not known`() = runTest { + val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2) + val room = FakeMatrixRoom() + val presenter = createPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.typingMembers).isEmpty() + room.givenRoomTypingMembers(listOf(A_USER_ID_2)) + val oneMemberTypingState = awaitItem() + assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) + assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember) + // User stops typing + room.givenRoomTypingMembers(emptyList()) + val finalState = awaitItem() + assertThat(finalState.typingMembers).isEmpty() + } + } + + @Test + fun `present - state is updated when a member is typing, member is known`() = runTest { + val aKnownRoomMember = createKnownRoomMember(userId = A_USER_ID_2) + val room = FakeMatrixRoom().apply { + givenRoomMembersState( + MatrixRoomMembersState.Ready( + listOf( + createKnownRoomMember(A_USER_ID), + aKnownRoomMember, + createKnownRoomMember(A_USER_ID_3), + createKnownRoomMember(A_USER_ID_4), + ).toImmutableList() + ) + ) + } + val presenter = createPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.typingMembers).isEmpty() + room.givenRoomTypingMembers(listOf(A_USER_ID_2)) + val oneMemberTypingState = awaitItem() + assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) + assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aKnownRoomMember) + // User stops typing + room.givenRoomTypingMembers(emptyList()) + val finalState = awaitItem() + assertThat(finalState.typingMembers).isEmpty() + } + } + + @Test + fun `present - state is updated when a member is typing, member is not known, then known`() = runTest { + val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2) + val aKnownRoomMember = createKnownRoomMember(A_USER_ID_2) + val room = FakeMatrixRoom() + val presenter = createPresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.typingMembers).isEmpty() + room.givenRoomTypingMembers(listOf(A_USER_ID_2)) + val oneMemberTypingState = awaitItem() + assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) + assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember) + // User is getting known + room.givenRoomMembersState( + MatrixRoomMembersState.Ready( + listOf(aKnownRoomMember).toImmutableList() + ) + ) + val finalState = awaitItem() + assertThat(finalState.typingMembers.first()).isEqualTo(aKnownRoomMember) + } + } + + private fun createPresenter( + matrixRoom: MatrixRoom = FakeMatrixRoom().apply { + givenRoomInfo(aRoomInfo(id = roomId.value, name = "")) + }, + ): TypingNotificationPresenter { + return TypingNotificationPresenter( + room = matrixRoom, + ) + } + + private fun createDefaultRoomMember( + userId: UserId, + ) = RoomMember( + userId = userId, + displayName = null, + avatarUrl = null, + membership = RoomMembershipState.JOIN, + isNameAmbiguous = false, + powerLevel = 0, + normalizedPowerLevel = 0, + isIgnored = false, + ) + + private fun createKnownRoomMember( + userId: UserId, + ) = RoomMember( + userId = userId, + displayName = "Alice Doe", + avatarUrl = "an_avatar_url", + membership = RoomMembershipState.JOIN, + isNameAmbiguous = true, + powerLevel = 0, + normalizedPowerLevel = 0, + isIgnored = false, + ) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 051e0e419e..db5cd6c260 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -57,6 +57,7 @@ interface MatrixRoom : Closeable { val isDm: Boolean get() = isDirect && isOneToOne val roomInfoFlow: Flow + val roomTypingMembersFlow: Flow> /** * A one-to-one is a room with exactly 2 members. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt index 86cff93688..908390484b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt @@ -27,7 +27,19 @@ data class RoomMember( val powerLevel: Long, val normalizedPowerLevel: Long, val isIgnored: Boolean, -) +) { + /** + * Disambiguated display name for the RoomMember. + * If the display name is null, the user ID is returned. + * If the display name is ambiguous, the user ID is appended in parentheses. + * Otherwise, the display name is returned. + */ + val disambiguatedDisplayName: String = when { + displayName == null -> userId.value + isNameAmbiguous -> "$displayName ($userId)" + else -> displayName + } +} enum class RoomMembershipState { BAN, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 36b6cde936..65c648e5e2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -73,6 +73,7 @@ import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle +import org.matrix.rustcomponents.sdk.TypingNotificationsListener import org.matrix.rustcomponents.sdk.WidgetCapabilities import org.matrix.rustcomponents.sdk.WidgetCapabilitiesProvider import org.matrix.rustcomponents.sdk.messageEventContentFromHtml @@ -113,6 +114,21 @@ class RustMatrixRoom( }) } + override val roomTypingMembersFlow: Flow> = mxCallbackFlow { + launch { + val initial = emptyList() + channel.trySend(initial) + } + innerRoom.subscribeToTypingNotifications(object : TypingNotificationsListener { + override fun call(typingUsers: List) { + channel.trySend( + typingUsers + .filter { it != sessionData.userId } + .map(::UserId)) + } + }) + } + // Create a dispatcher for all room methods... private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index fdd0dbb6cb..60969cfd0b 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -170,6 +170,9 @@ class FakeMatrixRoom( private val _roomInfoFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) override val roomInfoFlow: Flow = _roomInfoFlow + private val _roomTypingMembersFlow: MutableSharedFlow> = MutableSharedFlow(replay = 1) + override val roomTypingMembersFlow: Flow> = _roomTypingMembersFlow + override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) override val roomNotificationSettingsStateFlow: MutableStateFlow = @@ -589,6 +592,10 @@ class FakeMatrixRoom( fun givenRoomInfo(roomInfo: MatrixRoomInfo) { _roomInfoFlow.tryEmit(roomInfo) } + + fun givenRoomTypingMembers(typingMembers: List) { + _roomTypingMembersFlow.tryEmit(typingMembers) + } } data class SendLocationInvocation( From dfaf4ab8c86a6a2edba438e2d9b1ab458528d8a9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Jan 2024 16:20:18 +0100 Subject: [PATCH 02/11] Typing: add some specific MessagesView preview. --- .../typing/MessagesViewWithTypingPreview.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt new file mode 100644 index 0000000000..3dbc21cb15 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.typing + +import androidx.compose.runtime.Composable +import io.element.android.features.messages.impl.MessagesView +import io.element.android.features.messages.impl.aMessagesState +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +@PreviewsDayNight +@Composable +internal fun MessagesViewWithTypingPreview() = ElementPreview { + MessagesView( + state = aMessagesState().copy( + typingNotificationState = aTypingNotificationState( + typingMembers = listOf( + aTypingRoomMember(displayName = "Alice"), + aTypingRoomMember(displayName = "Bob"), + ), + ), + ), + onBackPressed = {}, + onRoomDetailsClicked = {}, + onEventClicked = { false }, + onPreviewAttachments = {}, + onUserDataClicked = {}, + onSendLocationClicked = {}, + onCreatePollClicked = {}, + onJoinCallClicked = {}, + ) +} From 7b809ac50762259c59eac9aeba62225d09a8f58b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 29 Jan 2024 18:20:27 +0100 Subject: [PATCH 03/11] Add a way to hide the Jump to Bottom button for coherent preview --- .../android/features/messages/impl/MessagesView.kt | 5 +++++ .../features/messages/impl/timeline/TimelineView.kt | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index e629571c3c..fd1c6bf19b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -125,6 +125,7 @@ fun MessagesView( onCreatePollClicked: () -> Unit, onJoinCallClicked: () -> Unit, modifier: Modifier = Modifier, + forceJumpToBottomVisibility: Boolean = false ) { OnLifecycleEvent { _, event -> state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event)) @@ -224,6 +225,7 @@ fun MessagesView( onSwipeToReply = { targetEvent -> state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent)) }, + forceJumpToBottomVisibility = forceJumpToBottomVisibility, ) }, snackbarHost = { @@ -324,6 +326,7 @@ private fun MessagesViewContent( onTimestampClicked: (TimelineItem.Event) -> Unit, onSendLocationClicked: () -> Unit, onCreatePollClicked: () -> Unit, + forceJumpToBottomVisibility: Boolean, modifier: Modifier = Modifier, onSwipeToReply: (TimelineItem.Event) -> Unit, ) { @@ -394,6 +397,7 @@ private fun MessagesViewContent( onMoreReactionsClicked = onMoreReactionsClicked, onReadReceiptClick = onReadReceiptClick, onSwipeToReply = onSwipeToReply, + forceJumpToBottomVisibility = forceJumpToBottomVisibility, ) }, sheetContent = { subcomposing: Boolean -> @@ -574,5 +578,6 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) onSendLocationClicked = {}, onCreatePollClicked = {}, onJoinCallClicked = {}, + forceJumpToBottomVisibility = true, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 410e74faa4..3c0d7fc67b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -47,7 +47,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.rotate -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -90,6 +89,7 @@ fun TimelineView( onMoreReactionsClicked: (TimelineItem.Event) -> Unit, onReadReceiptClick: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier, + forceJumpToBottomVisibility: Boolean = false ) { fun onReachedLoadMore() { state.eventSink(TimelineEvents.LoadMore) @@ -164,6 +164,7 @@ fun TimelineView( TimelineScrollHelper( isTimelineEmpty = state.timelineItems.isEmpty(), lazyListState = lazyListState, + forceJumpToBottomVisibility = forceJumpToBottomVisibility, newEventState = state.newEventState, onScrollFinishedAt = ::onScrollFinishedAt ) @@ -175,6 +176,7 @@ private fun BoxScope.TimelineScrollHelper( isTimelineEmpty: Boolean, lazyListState: LazyListState, newEventState: NewEventState, + forceJumpToBottomVisibility: Boolean, onScrollFinishedAt: (Int) -> Unit, ) { val coroutineScope = rememberCoroutineScope() @@ -212,7 +214,7 @@ private fun BoxScope.TimelineScrollHelper( JumpToBottomButton( // Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered - isVisible = !canAutoScroll, + isVisible = !canAutoScroll || forceJumpToBottomVisibility, modifier = Modifier .align(Alignment.BottomEnd) .padding(end = 24.dp, bottom = 12.dp), @@ -228,7 +230,7 @@ private fun JumpToBottomButton( ) { AnimatedVisibility( modifier = modifier, - visible = isVisible || LocalInspectionMode.current, + visible = isVisible, enter = scaleIn(animationSpec = tween(100)), exit = scaleOut(animationSpec = tween(100)), ) { @@ -273,6 +275,7 @@ internal fun TimelineViewPreview( onMoreReactionsClicked = {}, onSwipeToReply = {}, onReadReceiptClick = {}, + forceJumpToBottomVisibility = true, ) } } From 656ef9d6d0dccb7c3e01d9389f80179cfa58705a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Feb 2024 18:17:57 +0100 Subject: [PATCH 04/11] Typing notification: reduce horizontal padding. --- .../messages/impl/typing/TypingNotificationStateProvider.kt | 2 +- .../features/messages/impl/typing/TypingNotificationView.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt index 88a3c8677f..1de315c8c5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt @@ -65,7 +65,7 @@ class TypingNotificationStateProvider : PreviewParameterProvider Date: Thu, 8 Feb 2024 18:31:37 +0100 Subject: [PATCH 05/11] Typing notification: fix test compilation issue after rebase. --- .../features/messages/impl/timeline/TimelineViewTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index f80f4ce4c2..78cd671e3f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EventsRecorder @@ -41,6 +42,7 @@ class TimelineViewTest { hasMoreToLoadBackwards = true, ) ), + typingNotificationState = aTypingNotificationState(), roomName = null, onUserDataClicked = EnsureNeverCalledWithParam(), onMessageClicked = EnsureNeverCalledWithParam(), @@ -67,6 +69,7 @@ class TimelineViewTest { hasMoreToLoadBackwards = false, ) ), + typingNotificationState = aTypingNotificationState(), roomName = null, onUserDataClicked = EnsureNeverCalledWithParam(), onMessageClicked = EnsureNeverCalledWithParam(), From 5213849f8bff26074219faf94eff99a23c63e83e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Feb 2024 09:35:52 +0100 Subject: [PATCH 06/11] Typing notification: disable rendering when the user preference `isRenderTypingNotificationsEnabled` is false. --- .../typing/TypingNotificationPresenter.kt | 46 ++++++++++++------- .../impl/typing/TypingNotificationState.kt | 1 + .../typing/TypingNotificationStateProvider.kt | 1 + .../impl/typing/TypingNotificationView.kt | 2 +- .../messages/impl/MessagesPresenterTest.kt | 5 +- .../typing/TypingNotificationPresenterTest.kt | 43 +++++++++++++++++ 6 files changed, 80 insertions(+), 18 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt index fdbb698a02..92cae4c4d9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenter.kt @@ -18,10 +18,12 @@ package io.element.android.features.messages.impl.typing import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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.matrix.api.core.UserId 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.roomMembers import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn @@ -37,30 +40,41 @@ import javax.inject.Inject class TypingNotificationPresenter @Inject constructor( private val room: MatrixRoom, + private val sessionPreferencesStore: SessionPreferencesStore, ) : Presenter { @Composable override fun present(): TypingNotificationState { - var typingMembers by remember { mutableStateOf(emptyList()) } - LaunchedEffect(Unit) { - combine(room.roomTypingMembersFlow, room.membersStateFlow) { typingMembers, membersState -> - typingMembers - .map { userId -> - membersState.roomMembers() - ?.firstOrNull { roomMember -> roomMember.userId == userId } - ?: createDefaultRoomMemberForTyping(userId) - } + val typingMembersState = remember { mutableStateOf(emptyList()) } + val renderTypingNotifications by sessionPreferencesStore.isRenderTypingNotificationsEnabled().collectAsState(initial = true) + LaunchedEffect(renderTypingNotifications) { + if (renderTypingNotifications) { + observeRoomTypingMembers(typingMembersState) + } else { + typingMembersState.value = emptyList() } - .distinctUntilChanged() - .onEach { members -> - typingMembers = members - } - .launchIn(this) } return TypingNotificationState( - typingMembers = typingMembers.toImmutableList(), + renderTypingNotifications = renderTypingNotifications, + typingMembers = typingMembersState.value.toImmutableList(), ) } + + private fun CoroutineScope.observeRoomTypingMembers(typingMembersState: MutableState>) { + 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) + } } /** diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt index d4398a6351..380586f9c7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationState.kt @@ -20,5 +20,6 @@ import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.collections.immutable.ImmutableList data class TypingNotificationState( + val renderTypingNotifications: Boolean, val typingMembers: ImmutableList, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt index 1de315c8c5..92d7e357d2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt @@ -74,6 +74,7 @@ class TypingNotificationStateProvider : PreviewParameterProvider = emptyList(), ) = TypingNotificationState( + renderTypingNotifications = true, typingMembers = typingMembers.toImmutableList(), ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt index 97ed4f85dd..633758c541 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt @@ -42,7 +42,7 @@ fun TypingNotificationView( state: TypingNotificationState, modifier: Modifier = Modifier, ) { - if (state.typingMembers.isEmpty()) return + if (state.typingMembers.isEmpty() || !state.renderTypingNotifications) return val typingNotificationText = computeTypingNotificationText(state.typingMembers) Text( modifier = modifier diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 9e729e32e1..44dc96d804 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -731,7 +731,10 @@ class MessagesPresenterTest { } } val actionListPresenter = ActionListPresenter(appPreferencesStore = appPreferencesStore) - val typingNotificationPresenter = TypingNotificationPresenter(matrixRoom) + val typingNotificationPresenter = TypingNotificationPresenter( + room = matrixRoom, + sessionPreferencesStore = sessionPreferencesStore, + ) val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt index 4bedd6f6a3..a879c5a4bc 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt @@ -20,6 +20,8 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test 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.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -49,10 +51,47 @@ class TypingNotificationPresenterTest { presenter.present() }.test { val initialState = awaitItem() + assertThat(initialState.renderTypingNotifications).isTrue() 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 fun `present - state is updated when a member is typing, member is not known`() = runTest { val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2) @@ -136,9 +175,13 @@ class TypingNotificationPresenterTest { matrixRoom: MatrixRoom = FakeMatrixRoom().apply { givenRoomInfo(aRoomInfo(id = roomId.value, name = "")) }, + sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore( + isRenderTypingNotificationsEnabled = true + ), ): TypingNotificationPresenter { return TypingNotificationPresenter( room = matrixRoom, + sessionPreferencesStore = sessionPreferencesStore, ) } From b229bb19ce824438d24ffb89cc710fdc837e09c3 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 9 Feb 2024 08:45:45 +0000 Subject: [PATCH 07/11] Update screenshots --- ...l_MessagesViewWithTyping-Day-59_59_null,NEXUS_5,1.0,en].png | 3 +++ ...MessagesViewWithTyping-Night-59_60_null,NEXUS_5,1.0,en].png | 3 +++ ...TypingNotificationView-Day-60_60_null_0,NEXUS_5,1.0,en].png | 3 +++ ...TypingNotificationView-Day-60_60_null_1,NEXUS_5,1.0,en].png | 3 +++ ...TypingNotificationView-Day-60_60_null_2,NEXUS_5,1.0,en].png | 3 +++ ...TypingNotificationView-Day-60_60_null_3,NEXUS_5,1.0,en].png | 3 +++ ...TypingNotificationView-Day-60_60_null_4,NEXUS_5,1.0,en].png | 3 +++ ...TypingNotificationView-Day-60_60_null_5,NEXUS_5,1.0,en].png | 3 +++ ...TypingNotificationView-Day-60_60_null_6,NEXUS_5,1.0,en].png | 3 +++ ...TypingNotificationView-Day-60_60_null_7,NEXUS_5,1.0,en].png | 3 +++ ...pingNotificationView-Night-60_61_null_0,NEXUS_5,1.0,en].png | 3 +++ ...pingNotificationView-Night-60_61_null_1,NEXUS_5,1.0,en].png | 3 +++ ...pingNotificationView-Night-60_61_null_2,NEXUS_5,1.0,en].png | 3 +++ ...pingNotificationView-Night-60_61_null_3,NEXUS_5,1.0,en].png | 3 +++ ...pingNotificationView-Night-60_61_null_4,NEXUS_5,1.0,en].png | 3 +++ ...pingNotificationView-Night-60_61_null_5,NEXUS_5,1.0,en].png | 3 +++ ...pingNotificationView-Night-60_61_null_6,NEXUS_5,1.0,en].png | 3 +++ ...pingNotificationView-Night-60_61_null_7,NEXUS_5,1.0,en].png | 3 +++ 18 files changed, 54 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-59_59_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-59_60_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_7,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-59_59_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-59_59_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9e5eb38198 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Day-59_59_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0dda5041c16d690fe8e0c13aaca813a06acbcee8e9ab6268256ad8925defe5d0 +size 55428 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-59_60_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-59_60_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..aeb62038c2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_MessagesViewWithTyping_null_MessagesViewWithTyping-Night-59_60_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6946a2d952a72e29aaa54543ce0e8a73b7ab7c6db2c6fd23ac833fa7f61f8425 +size 54141 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ae320c8dbc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff84eecce0c3e489e0596c524a361e277679fc0f6b1c3f3011e50bb666074574 +size 8953 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5b271ff6fb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c46396fd61ca8188bf3ea7745dee0456f3f944c2a1fcbf04402cfb1127d8bbef +size 6856 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..45e0208327 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:272038adab5288f287dcf81c815de34945092ca20c67dad15b7e0a3715663be4 +size 9932 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fc071c13d3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3200b2363f50db05157314f5c5d9d491ffb51860e66ffe28bd4c024b6857dda0 +size 8161 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..39402506e6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3409d9e8e10bae9e90f2f9088897261619ecd8d0e6e32441671396d50fed8ac0 +size 8940 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f4281d3fe3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ca0ccfd706c8dca696481f7d1f60de83ed15435b1096c46d9dc79e21573817b +size 9265 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..919be03513 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Day-60_60_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18f8d2c5c872598293ac759a074e88ba38062e54ca9348a12aff62f3d91c34f4 +size 11149 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fae8a6fca3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c89ac73df77c2bccb0c2aa80cee1420f78e7d07f0eda89a90bffef55e8cf753 +size 4464 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3e8ac12a44 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5c002a4929aa725cf22ca46840ff3374efe4c4e1757e3b76c3a064911525ce6 +size 8875 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a4bb07b2ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a227b3aca2f20d895c483ca72e3c3b49a3b6263e01f10cd59dda7147f750ecc8 +size 6840 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1e529c35db --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2d151892e95c633ef8bd6960f32eca0e00b2ddbcaba01cbca830fea356471f9 +size 9877 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..83ead64bd3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3e0ec1f6946fa12c56d17f7ff72e7fca50ad52e41864b0351a36842a220df02 +size 8093 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dc9eb04e55 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59ed8a9b93f09875de225096a7c7c044840c603c31e0f525522f5be13fa116a8 +size 8786 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4819f10183 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32087f3ad5e63e8075d2b981881ba36e7fb36464a10b3d7506ed1cf6d00c2e3d +size 9106 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4540dee44e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.typing_TypingNotificationView_null_TypingNotificationView-Night-60_61_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:833aa5ba2891ca8a0f81534ca2b36b6971dc851a2842507b1ae3f2f5b701961f +size 11004 From b3715ca9c48320f1841183b5251ff00e84adbde2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Feb 2024 10:11:33 +0100 Subject: [PATCH 08/11] Use ImmutableList type. --- .../features/messages/impl/typing/TypingNotificationView.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt index 633758c541..47200132d8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt @@ -36,6 +36,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.room.RoomMember +import kotlinx.collections.immutable.ImmutableList @Composable fun TypingNotificationView( @@ -57,7 +58,7 @@ fun TypingNotificationView( } @Composable -private fun computeTypingNotificationText(typingMembers: List): AnnotatedString { +private fun computeTypingNotificationText(typingMembers: ImmutableList): AnnotatedString { val names = when (typingMembers.size) { 0 -> "" // Cannot happen 1 -> typingMembers[0].disambiguatedDisplayName From 7831311d1463c540b1e0e7ecc781f24a71fdf0ce Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Feb 2024 10:12:50 +0100 Subject: [PATCH 09/11] Fix compilation warning. --- .../android/libraries/matrix/impl/room/RustMatrixRoom.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 65c648e5e2..a22811f893 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -120,9 +120,9 @@ class RustMatrixRoom( channel.trySend(initial) } innerRoom.subscribeToTypingNotifications(object : TypingNotificationsListener { - override fun call(typingUsers: List) { + override fun call(typingUserIds: List) { channel.trySend( - typingUsers + typingUserIds .filter { it != sessionData.userId } .map(::UserId)) } From 3ea448e6bb5d32ab0a991bc21a592f7a67ec30eb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Feb 2024 10:43:08 +0100 Subject: [PATCH 10/11] Fix formatting issue. --- .../android/libraries/matrix/impl/room/RustMatrixRoom.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index a22811f893..6ab29c1840 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -124,7 +124,8 @@ class RustMatrixRoom( channel.trySend( typingUserIds .filter { it != sessionData.userId } - .map(::UserId)) + .map(::UserId) + ) } }) } From 1df1e83efc8f78fc9665f90e6cccf24969d01299 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Feb 2024 12:47:23 +0100 Subject: [PATCH 11/11] Fix Konsist test by removing the explicit type. --- .../impl/typing/TypingNotificationPresenterTest.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt index a879c5a4bc..485c318e93 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationPresenterTest.kt @@ -178,12 +178,10 @@ class TypingNotificationPresenterTest { sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore( isRenderTypingNotificationsEnabled = true ), - ): TypingNotificationPresenter { - return TypingNotificationPresenter( - room = matrixRoom, - sessionPreferencesStore = sessionPreferencesStore, - ) - } + ) = TypingNotificationPresenter( + room = matrixRoom, + sessionPreferencesStore = sessionPreferencesStore, + ) private fun createDefaultRoomMember( userId: UserId,