diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt index c29e375a44..62491235ec 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeState.kt @@ -8,7 +8,6 @@ package io.element.android.features.messages.impl.crypto.identity import io.element.android.libraries.matrix.api.encryption.identity.IdentityState -import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.collections.immutable.ImmutableList data class IdentityChangeState( @@ -17,6 +16,6 @@ data class IdentityChangeState( ) data class RoomMemberIdentityStateChange( - val roomMember: RoomMember, + val identityRoomMember: IdentityRoomMember, val identityState: IdentityState, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt index 7c32770961..9b338b4833 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenter.kt @@ -13,12 +13,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import androidx.compose.runtime.rememberCoroutineScope import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.EncryptionService 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 io.element.android.libraries.matrix.ui.model.getAvatarData import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -59,9 +61,10 @@ class IdentityChangeStatePresenter @Inject constructor( identityStateChanges.map { identityStateChange -> val member = membersState.roomMembers() ?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId } + ?.toIdentityRoomMember() ?: createDefaultRoomMemberForIdentityChange(identityStateChange.userId) RoomMemberIdentityStateChange( - roomMember = member, + identityRoomMember = member, identityState = identityStateChange.identityState, ) } @@ -81,21 +84,19 @@ class IdentityChangeStatePresenter @Inject constructor( } } -/** - * Create a default [RoomMember] for identity change 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 createDefaultRoomMemberForIdentityChange(userId: UserId): RoomMember { - return RoomMember( - userId = userId, - displayName = null, - avatarUrl = null, - membership = RoomMembershipState.JOIN, - isNameAmbiguous = false, - powerLevel = 0, - normalizedPowerLevel = 0, - isIgnored = false, - role = RoomMember.Role.USER, - ) -} +private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember( + userId = userId, + disambiguatedDisplayName = disambiguatedDisplayName, + avatarData = getAvatarData(AvatarSize.ComposerAlert), +) + +private fun createDefaultRoomMemberForIdentityChange(userId: UserId) = IdentityRoomMember( + userId = userId, + disambiguatedDisplayName = userId.value, + avatarData = AvatarData( + id = userId.value, + name = null, + url = null, + size = AvatarSize.ComposerAlert, + ), +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt index 167ccc68ab..fa70bebe6a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt @@ -8,7 +8,9 @@ package io.element.android.features.messages.impl.crypto.identity import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.messages.impl.typing.aTypingRoomMember +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import kotlinx.collections.immutable.toImmutableList @@ -19,7 +21,7 @@ class IdentityChangeStateProvider : PreviewParameterProvider>.observeRoomTypingMembers() { + private fun ProduceStateScope>.observeRoomTypingMembers() { combine(room.roomTypingMembersFlow, room.membersStateFlow) { typingMembers, membersState -> typingMembers .map { userId -> membersState.roomMembers() ?.firstOrNull { roomMember -> roomMember.userId == userId } + ?.toTypingRoomMember() ?: createDefaultRoomMemberForTyping(userId) } } @@ -77,21 +77,14 @@ class TypingNotificationPresenter @Inject constructor( } } -/** - * 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, - role = RoomMember.Role.USER, +private fun RoomMember.toTypingRoomMember(): TypingRoomMember { + return TypingRoomMember( + disambiguatedDisplayName = disambiguatedDisplayName, + ) +} + +private fun createDefaultRoomMemberForTyping(userId: UserId): TypingRoomMember { + return TypingRoomMember( + disambiguatedDisplayName = userId.value, ) } 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 e4c239449c..c94cfd4cb9 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 @@ -7,7 +7,6 @@ package io.element.android.features.messages.impl.typing -import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.collections.immutable.ImmutableList /** @@ -17,7 +16,7 @@ data class TypingNotificationState( /** Whether to render the typing notifications based on the user's preferences. */ val renderTypingNotifications: Boolean, /** The room members currently typing. */ - val typingMembers: ImmutableList, + val typingMembers: ImmutableList, /** Whether to reserve space for the typing notifications at the bottom of the timeline. */ val reserveSpace: Boolean, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateForMessagesProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateForMessagesProvider.kt deleted file mode 100644 index b1757b5545..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateForMessagesProvider.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only - * Please see LICENSE in the repository root for full details. - */ - -package io.element.android.features.messages.impl.typing - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider - -class TypingNotificationStateForMessagesProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aTypingNotificationState( - typingMembers = listOf( - aTypingRoomMember(displayName = "Alice"), - aTypingRoomMember(displayName = "Bob"), - ), - ), - aTypingNotificationState( - typingMembers = listOf(aTypingRoomMember()), - reserveSpace = true - ), - aTypingNotificationState(reserveSpace = true), - ) -} 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 5baf868417..3722185d00 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 @@ -8,9 +8,6 @@ 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 { @@ -24,39 +21,39 @@ class TypingNotificationStateProvider : PreviewParameterProvider = emptyList(), + typingMembers: List = emptyList(), reserveSpace: Boolean = false, ) = TypingNotificationState( renderTypingNotifications = true, @@ -76,19 +73,7 @@ internal fun aTypingNotificationState( ) 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, - role = RoomMember.Role.USER, - ) -} + disambiguatedDisplayName: String = "@alice:example.com", +) = TypingRoomMember( + disambiguatedDisplayName = disambiguatedDisplayName, +) 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 01dcb6e141..1142341984 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 @@ -41,7 +41,6 @@ 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 import kotlinx.collections.immutable.ImmutableList @Suppress("MultipleEmitters") // False positive @@ -53,7 +52,8 @@ fun TypingNotificationView( val displayNotifications = state.typingMembers.isNotEmpty() && state.renderTypingNotifications @Suppress("ModifierNaming") - @Composable fun TypingText(text: AnnotatedString, textModifier: Modifier = Modifier) { + @Composable + fun TypingText(text: AnnotatedString, textModifier: Modifier = Modifier) { Text( modifier = textModifier, text = text, @@ -66,7 +66,9 @@ fun TypingNotificationView( // Display the typing notification space when either a typing notification needs to be displayed or a previous one already was AnimatedVisibility( - modifier = modifier.fillMaxWidth().padding(vertical = 2.dp), + modifier = modifier + .fillMaxWidth() + .padding(vertical = 2.dp), visible = displayNotifications || state.reserveSpace, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically(), @@ -95,7 +97,7 @@ fun TypingNotificationView( } @Composable -private fun computeTypingNotificationText(typingMembers: ImmutableList): AnnotatedString { +private fun computeTypingNotificationText(typingMembers: ImmutableList): AnnotatedString { // Remember the last value to avoid empty typing messages while animating var result by remember { mutableStateOf(AnnotatedString("")) } if (typingMembers.isNotEmpty()) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingRoomMember.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingRoomMember.kt new file mode 100644 index 0000000000..edd658763f --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingRoomMember.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.messages.impl.typing + +data class TypingRoomMember( + val disambiguatedDisplayName: String, +) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt index edf559a261..5235361870 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePresenterTest.kt @@ -8,7 +8,7 @@ package io.element.android.features.messages.impl.crypto.identity import com.google.common.truth.Truth.assertThat -import io.element.android.features.messages.impl.typing.aTypingRoomMember +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.identity.IdentityState @@ -19,6 +19,7 @@ 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.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -59,7 +60,7 @@ class IdentityChangeStatePresenterTest { val finalItem = awaitItem() assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1) val value = finalItem.roomMemberIdentityStateChanges.first() - assertThat(value.roomMember.userId).isEqualTo(A_USER_ID_2) + assertThat(value.identityRoomMember.userId).isEqualTo(A_USER_ID_2) assertThat(value.identityState).isEqualTo(IdentityState.PinViolation) } } @@ -71,7 +72,7 @@ class IdentityChangeStatePresenterTest { givenRoomMembersState( MatrixRoomMembersState.Ready( listOf( - aTypingRoomMember( + aRoomMember( A_USER_ID_2, displayName = "Alice", ), @@ -94,8 +95,9 @@ class IdentityChangeStatePresenterTest { val finalItem = awaitItem() assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1) val value = finalItem.roomMemberIdentityStateChanges.first() - assertThat(value.roomMember.userId).isEqualTo(A_USER_ID_2) - assertThat(value.roomMember.displayName).isEqualTo("Alice") + assertThat(value.identityRoomMember.userId).isEqualTo(A_USER_ID_2) + assertThat(value.identityRoomMember.disambiguatedDisplayName).isEqualTo("Alice") + assertThat(value.identityRoomMember.avatarData.size).isEqualTo(AvatarSize.ComposerAlert) assertThat(value.identityState).isEqualTo(IdentityState.PinViolation) } } 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 61c37dc449..a0e611ab5e 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 @@ -21,6 +21,7 @@ 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.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore import io.element.android.tests.testutils.WarmUpRule @@ -49,7 +50,6 @@ class TypingNotificationPresenterTest { @Test fun `present - typing notification disabled`() = runTest { - val aDefaultRoomMember = createDefaultRoomMember(A_USER_ID_2) val room = FakeMatrixRoom() val sessionPreferencesStore = InMemorySessionPreferencesStore( isRenderTypingNotificationsEnabled = false @@ -73,7 +73,11 @@ class TypingNotificationPresenterTest { val oneMemberTypingState = awaitItem() assertThat(oneMemberTypingState.renderTypingNotifications).isTrue() assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) - assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember) + assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo( + TypingRoomMember( + disambiguatedDisplayName = A_USER_ID_2.value, + ) + ) // Preferences changes again sessionPreferencesStore.setRenderTypingNotifications(false) skipItems(2) @@ -85,7 +89,6 @@ class TypingNotificationPresenterTest { @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) { @@ -96,7 +99,11 @@ class TypingNotificationPresenterTest { room.givenRoomTypingMembers(listOf(A_USER_ID_2)) val oneMemberTypingState = awaitItem() assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) - assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember) + assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo( + TypingRoomMember( + disambiguatedDisplayName = A_USER_ID_2.value, + ) + ) // User stops typing room.givenRoomTypingMembers(emptyList()) skipItems(1) @@ -129,7 +136,11 @@ class TypingNotificationPresenterTest { room.givenRoomTypingMembers(listOf(A_USER_ID_2)) val oneMemberTypingState = awaitItem() assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) - assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aKnownRoomMember) + assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo( + TypingRoomMember( + disambiguatedDisplayName = "Alice Doe (@bob:server.org)", + ) + ) // User stops typing room.givenRoomTypingMembers(emptyList()) skipItems(1) @@ -152,7 +163,11 @@ class TypingNotificationPresenterTest { room.givenRoomTypingMembers(listOf(A_USER_ID_2)) val oneMemberTypingState = awaitItem() assertThat(oneMemberTypingState.typingMembers.size).isEqualTo(1) - assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo(aDefaultRoomMember) + assertThat(oneMemberTypingState.typingMembers.first()).isEqualTo( + TypingRoomMember( + disambiguatedDisplayName = A_USER_ID_2.value, + ) + ) // User is getting known room.givenRoomMembersState( MatrixRoomMembersState.Ready( @@ -161,7 +176,11 @@ class TypingNotificationPresenterTest { ) skipItems(1) val finalState = awaitItem() - assertThat(finalState.typingMembers.first()).isEqualTo(aKnownRoomMember) + assertThat(finalState.typingMembers.first()).isEqualTo( + TypingRoomMember( + disambiguatedDisplayName = "Alice Doe (@bob:server.org)", + ) + ) } } @@ -204,7 +223,7 @@ class TypingNotificationPresenterTest { private fun createDefaultRoomMember( userId: UserId, - ) = aTypingRoomMember( + ) = aRoomMember( userId = userId, displayName = null, isNameAmbiguous = false, @@ -212,7 +231,7 @@ class TypingNotificationPresenterTest { private fun createKnownRoomMember( userId: UserId, - ) = aTypingRoomMember( + ) = aRoomMember( userId = userId, displayName = "Alice Doe", isNameAmbiguous = true,