From 06a903718b763aec85e2c1d85f8687dcc5fcb0d6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 24 Jun 2026 17:59:08 +0200 Subject: [PATCH] Display user status next to display names across rooms and profiles --- .../home/impl/components/RoomSummaryRow.kt | 26 ++++---- .../datasource/RoomListRoomSummaryFactory.kt | 1 + .../home/impl/model/RoomListRoomSummary.kt | 2 + .../impl/model/RoomListRoomSummaryProvider.kt | 5 +- .../messages/impl/MessagesPresenter.kt | 1 + .../features/messages/impl/MessagesState.kt | 2 + .../messages/impl/MessagesStateProvider.kt | 3 + .../features/messages/impl/MessagesView.kt | 1 + .../suggestions/SuggestionsPickerView.kt | 19 ++++-- .../impl/threads/list/ThreadsListView.kt | 7 ++- .../timeline/components/MessageShieldView.kt | 1 + .../components/TimelineItemEventRow.kt | 2 + .../impl/topbars/MessagesViewTopBar.kt | 29 ++++++--- .../impl/RoomDetailsStateProvider.kt | 6 +- .../roomdetails/impl/RoomDetailsView.kt | 61 ++++++++++-------- .../members/RoomMemberListStateProvider.kt | 3 + .../details/RoomMemberDetailsPresenter.kt | 1 + ...ternalRoomMemberModerationStateProvider.kt | 2 + .../impl/RoomMemberModerationView.kt | 17 ++++- .../userprofile/api/UserProfileState.kt | 2 + .../impl/root/UserProfilePresenter.kt | 1 + .../shared/UserProfileHeaderSection.kt | 17 ++++- .../shared/UserProfileStateProvider.kt | 3 + .../userprofile/shared/UserProfileView.kt | 1 + .../molecules/MatrixBadgeRowMolecule.kt | 2 +- .../libraries/matrix/api/room/RoomMember.kt | 2 + .../api/timeline/item/event/ProfileDetails.kt | 4 +- .../item/event/ProfileTimelineDetailsTest.kt | 3 + .../impl/room/member/RoomMemberMapper.kt | 2 + .../item/event/EventTimelineItemMapper.kt | 4 +- .../matrix/test/timeline/TimelineFixture.kt | 5 +- .../matrix/ui/components/CheckableUserRow.kt | 3 +- .../ui/components/DisplayNameWithStatus.kt | 62 +++++++++++++++++++ .../matrix/ui/components/MatrixUserHeader.kt | 13 ++-- .../ui/components/MatrixUserProvider.kt | 4 ++ .../matrix/ui/components/MatrixUserRow.kt | 11 +++- .../libraries/matrix/ui/components/UserRow.kt | 41 +++++++++--- .../ui/messages/RoomMemberProfilesCache.kt | 6 ++ .../reply/InReplyToDetailsProvider.kt | 1 + .../matrix/ui/messages/sender/SenderName.kt | 19 +++++- .../messages/sender/SenderNameDataProvider.kt | 5 +- .../ui/model/DisplayedStatusExtention.kt | 24 +++++++ .../src/main/res/values/temporary.xml | 10 +++ .../previewutils/room/RoomMemberFixture.kt | 3 + .../textcomposer/mentions/MentionSpan.kt | 30 ++++++--- .../mentions/MentionSpanUpdater.kt | 8 +++ 46 files changed, 386 insertions(+), 89 deletions(-) create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/DisplayNameWithStatus.kt create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/DisplayedStatusExtention.kt create mode 100644 libraries/matrixui/src/main/res/values/temporary.xml diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index ba9a204a64..a48b4b7469 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -63,8 +63,11 @@ import io.element.android.libraries.designsystem.theme.roomListRoomName import io.element.android.libraries.designsystem.theme.unreadIndicator import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.ui.components.InviteSenderView +import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus import io.element.android.libraries.matrix.ui.model.InviteSender +import io.element.android.libraries.matrix.ui.model.toEmojiText import io.element.android.libraries.ui.strings.CommonStrings import timber.log.Timber @@ -126,7 +129,8 @@ internal fun RoomSummaryRow( NameAndTimestampRow( name = room.name, timestamp = room.timestamp, - isHighlighted = room.isHighlighted + isHighlighted = room.isHighlighted, + dmUserStatus = room.dmUserStatus, ) MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount) } @@ -142,7 +146,8 @@ internal fun RoomSummaryRow( NameAndTimestampRow( name = room.name, timestamp = null, - isHighlighted = room.isHighlighted + isHighlighted = room.isHighlighted, + dmUserStatus = null, ) if (room.canonicalAlias != null) { Text( @@ -218,22 +223,21 @@ private fun NameAndTimestampRow( name: String?, timestamp: String?, isHighlighted: Boolean, + dmUserStatus: DisplayedStatus?, modifier: Modifier = Modifier ) { Row( modifier = modifier.fillMaxWidth(), horizontalArrangement = spacedBy(16.dp) ) { - Text( - modifier = Modifier - .weight(1f) - .clipToBounds(), + val displayName = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name) + DisplayNameWithStatus( + name = displayName, + status = dmUserStatus?.toEmojiText(), + modifier = Modifier.weight(1f), style = ElementTheme.typography.fontBodyLgMedium, - text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name), - fontStyle = FontStyle.Italic.takeIf { name == null }, - color = ElementTheme.colors.roomListRoomName, - maxLines = 1, - overflow = TextOverflow.Ellipsis + nameColor = ElementTheme.colors.roomListRoomName, + nameFontStyle = FontStyle.Italic.takeIf { name == null }, ) // Timestamp Text( diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt index e34f2845da..232a21e42e 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt @@ -76,6 +76,7 @@ class RoomListRoomSummaryFactory( }.toImmutableList(), isTombstoned = roomInfo.successorRoom != null, isSpace = roomInfo.isSpace, + dmUserStatus = if (roomInfo.isDm) roomInfo.heroes.firstOrNull()?.displayedStatus else null, ) } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt index 628d0d0a9b..3ad568abb6 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.ui.model.InviteSender import kotlinx.collections.immutable.ImmutableList @@ -42,6 +43,7 @@ data class RoomListRoomSummary( val isTombstoned: Boolean, val heroes: ImmutableList, val isSpace: Boolean, + val dmUserStatus: DisplayedStatus?, ) { val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE && (numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) || diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt index 09e6c2e6c9..2e75858e3e 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.CallIntent import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.ui.model.InviteSender import kotlinx.collections.immutable.toImmutableList @@ -182,6 +183,7 @@ internal fun aRoomListRoomSummary( heroes: List = emptyList(), isTombstoned: Boolean = false, isSpace: Boolean = false, + dmUserStatus: DisplayedStatus? = null, ) = RoomListRoomSummary( id = id, roomId = RoomId(id), @@ -204,5 +206,6 @@ internal fun aRoomListRoomSummary( canonicalAlias = canonicalAlias, heroes = heroes.toImmutableList(), isTombstoned = isTombstoned, - isSpace = isSpace + isSpace = isSpace, + dmUserStatus = dmUserStatus, ) 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 1109b2c70d..85c61f2131 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 @@ -319,6 +319,7 @@ class MessagesPresenter( appName = buildMeta.applicationName, pinnedMessagesBannerState = pinnedMessagesBannerState, dmUserVerificationState = dmUserVerificationState, + dmUserStatus = dmRoomMember?.displayedStatus, roomMemberModerationState = roomMemberModerationState, topBarSharedHistoryIcon = topBarSharedHistoryIcon, successorRoom = roomInfo.successorRoom, 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 a16485c6f7..d9ec875320 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 @@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom +import io.element.android.libraries.matrix.api.user.DisplayedStatus import kotlinx.collections.immutable.ImmutableList data class MessagesState( @@ -53,6 +54,7 @@ data class MessagesState( val appName: String, val pinnedMessagesBannerState: PinnedMessagesBannerState, val dmUserVerificationState: IdentityState?, + val dmUserStatus: DisplayedStatus?, val roomMemberModerationState: RoomMemberModerationState, /** Type of "shared history" icon to show in the top bar. */ val topBarSharedHistoryIcon: SharedHistoryIcon, 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 6389089e07..89d701afa9 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 @@ -50,6 +50,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown import io.element.android.libraries.textcomposer.model.aTextEditorStateRich @@ -129,6 +130,7 @@ fun aMessagesState( hasUnreadThreads = false, ), isCurrentlySharingLiveLocationInRoom: Boolean = false, + dmUserStatus: DisplayedStatus? = null, eventSink: (MessagesEvent) -> Unit = {}, ) = MessagesState( roomId = RoomId("!id:domain"), @@ -159,6 +161,7 @@ fun aMessagesState( successorRoom = successorRoom, threads = threads, showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom, + dmUserStatus = dmUserStatus, eventSink = 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 ec014a4ff6..5c478c01e6 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 @@ -244,6 +244,7 @@ fun MessagesView( heroes = state.heroes, dmUserIdentityState = state.dmUserVerificationState, sharedHistoryIcon = state.topBarSharedHistoryIcon, + dmUserStatus = state.dmUserStatus, onBackClick = { hidingKeyboard { onBackClick() } }, onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } }, menuActions = { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt index ef2362c794..9cc76b2458 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/suggestions/SuggestionsPickerView.kt @@ -41,7 +41,9 @@ import io.element.android.libraries.matrix.api.core.RoomId 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 io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.matrix.ui.model.toEmojiText import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import kotlinx.collections.immutable.ImmutableList @@ -112,12 +114,16 @@ private fun SuggestionItemView( is ResolvedSuggestion.Member -> AvatarType.User is ResolvedSuggestion.Command -> null } - val title = when (suggestion) { + val title: String? = when (suggestion) { is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title) - is ResolvedSuggestion.Member -> suggestion.roomMember.displayName + is ResolvedSuggestion.Member -> suggestion.roomMember.displayName ?: suggestion.roomMember.userId.value is ResolvedSuggestion.Alias -> suggestion.roomName is ResolvedSuggestion.Command -> suggestion.command.command } + val statusEmoji = when (suggestion) { + is ResolvedSuggestion.Member -> suggestion.roomMember.displayedStatus?.toEmojiText() + else -> null + } val details = when (suggestion) { is ResolvedSuggestion.AtRoom, is ResolvedSuggestion.Member, @@ -148,11 +154,11 @@ private fun SuggestionItemView( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { title?.let { - Text( - text = it, + DisplayNameWithStatus( + name = title, + status = statusEmoji, style = ElementTheme.typography.fontBodyLgRegular, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + nameColor = ElementTheme.colors.textPrimary, ) } details?.let { @@ -191,6 +197,7 @@ internal fun SuggestionsPickerViewPreview() { role = RoomMember.Role.User, membershipChangeReason = null, isServiceMember = false, + displayedStatus = null, ) val anAlias = remember { RoomAlias("#room:domain.org") } SuggestionsPickerView( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt index 3bcb8ab67a..dd44fe0af3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt @@ -383,7 +383,12 @@ fun aThreadListItem( fun aThreadListItemEvent( threadId: ThreadId = ThreadId("\$a-thread-id"), senderId: UserId = UserId("@a-user-id:server"), - senderProfile: ProfileDetails = ProfileDetails.Ready(displayName = USER_NAME_ALICE, displayNameAmbiguous = false, avatarUrl = null), + senderProfile: ProfileDetails = ProfileDetails.Ready( + displayName = USER_NAME_ALICE, + displayNameAmbiguous = false, + avatarUrl = null, + displayedStatus = null, + ), isOwn: Boolean = false, content: EventContent = MessageContent( body = "Hello world!", diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt index 1859e66750..a68d115e08 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt @@ -166,6 +166,7 @@ internal fun MessageShieldViewPreview() { displayName = USER_NAME_ALICE, displayNameAmbiguous = false, avatarUrl = null, + displayedStatus = null, ), ) ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 30053294da..fed2ab6577 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -962,6 +962,7 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview { displayName = USER_NAME_ALICE, avatarUrl = null, displayNameAmbiguous = false, + displayedStatus = null, ), timestamp = 0L, ) @@ -1140,6 +1141,7 @@ internal fun ThreadSummaryViewPreview() { displayName = USER_NAME_ALICE, avatarUrl = null, displayNameAmbiguous = true, + displayedStatus = null, ), timestamp = 0L, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt index 4f023ad2bd..67fb07e4d2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -46,11 +45,13 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ROOM_NAME import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.user.DisplayedStatus +import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.matrix.ui.model.toEmojiText import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -65,6 +66,7 @@ internal fun MessagesViewTopBar( heroes: ImmutableList, dmUserIdentityState: IdentityState?, sharedHistoryIcon: SharedHistoryIcon, + dmUserStatus: DisplayedStatus?, onRoomDetailsClick: () -> Unit, onBackClick: () -> Unit, modifier: Modifier = Modifier, @@ -91,6 +93,7 @@ internal fun MessagesViewTopBar( roomAvatar = roomAvatar, isTombstoned = isTombstoned, heroes = heroes, + dmUserStatus = dmUserStatus, modifier = titleModifier ) @@ -144,6 +147,7 @@ private fun RoomAvatarAndNameRow( roomAvatar: AvatarData, heroes: ImmutableList, isTombstoned: Boolean, + dmUserStatus: DisplayedStatus?, modifier: Modifier = Modifier ) { Row( @@ -157,14 +161,13 @@ private fun RoomAvatarAndNameRow( isTombstoned = isTombstoned, ), ) - Text( - modifier = Modifier - .padding(start = 8.dp), - text = roomName ?: stringResource(CommonStrings.common_no_room_name), + DisplayNameWithStatus( + name = roomName ?: stringResource(CommonStrings.common_no_room_name), + status = dmUserStatus?.toEmojiText(), + modifier = Modifier.padding(start = 8.dp), style = ElementTheme.typography.fontBodyLgMedium, - fontStyle = FontStyle.Italic.takeIf { roomName == null }, - maxLines = 1, - overflow = TextOverflow.Ellipsis + nameColor = ElementTheme.colors.textPrimary, + nameFontStyle = FontStyle.Italic.takeIf { roomName == null }, ) } } @@ -184,6 +187,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview { roomCallState: RoomCallState = RoomCallState.Unavailable, dmUserIdentityState: IdentityState? = null, sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE, + dmUserStatus: DisplayedStatus? = null, displayThreads: Boolean = false, ) = MessagesViewTopBar( roomName = roomName, @@ -192,6 +196,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview { heroes = heroes, dmUserIdentityState = dmUserIdentityState, sharedHistoryIcon = sharedHistoryIcon, + dmUserStatus = dmUserStatus, onRoomDetailsClick = {}, onBackClick = {}, menuActions = { @@ -246,5 +251,11 @@ internal fun MessagesViewTopBarPreview() = ElementPreview { AMessagesViewTopBar( displayThreads = true, ) + HorizontalDivider() + AMessagesViewTopBar( + roomName = "A DM with a very very very long name", + dmUserIdentityState = IdentityState.Verified, + dmUserStatus = DisplayedStatus.InCall(0L) + ) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt index 935b7293ae..6aa4f11a8e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt @@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationSettings import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import kotlinx.collections.immutable.toImmutableList @@ -75,6 +76,7 @@ fun aDmRoomMember( role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, isServiceMember: Boolean = false, + displayedStatus: DisplayedStatus? = null, ) = RoomMember( userId = userId, displayName = displayName, @@ -86,6 +88,7 @@ fun aDmRoomMember( role = role, membershipChangeReason = membershipChangeReason, isServiceMember = isServiceMember, + displayedStatus = displayedStatus, ) fun aRoomDetailsState( @@ -178,12 +181,13 @@ fun aDmRoomDetailsState( roomName: String = "Daniel", isEncrypted: Boolean = true, dmRoomMemberVerificationState: UserProfileVerificationState = UserProfileVerificationState.UNKNOWN, + displayedStatus: DisplayedStatus? = null, ) = aRoomDetailsState( roomName = roomName, isPublic = false, isEncrypted = isEncrypted, canInvite = true, - roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)), + roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored, displayedStatus = displayedStatus)), dmOtherMemberDetailsState = aUserProfileState( isBlocked = AsyncData.Success(isDmMemberIgnored), verificationState = dmRoomMemberVerificationState, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 591ff7ff1e..0b47464a22 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import im.vector.app.features.analytics.plan.Interaction @@ -88,6 +89,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.getBestName import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.matrix.ui.model.toText import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -489,11 +491,23 @@ private fun RoomHeaderSection( } .testTag(TestTags.roomDetailAvatar) ) - TitleAndSubtitle( - title = roomName, - subtitle = roomAlias?.value, - onSubtitleClick = onSubtitleClick, + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = roomName, + style = ElementTheme.typography.fontHeadingLgBold, + textAlign = TextAlign.Center, ) + if (roomAlias != null) { + Spacer(modifier = Modifier.height(12.dp)) + Text( + modifier = Modifier.niceClickable { onSubtitleClick(roomAlias.value) }, + text = roomAlias.value, + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + } + Spacer(modifier = Modifier.height(12.dp)) } } @@ -531,37 +545,34 @@ private fun DmHeaderSection( } .testTag(TestTags.roomDetailAvatar) ) - TitleAndSubtitle( - title = roomName, - subtitle = otherMember.userId.value, - onSubtitleClick = onSubtitleClick, - ) - } -} - -@Composable -private fun TitleAndSubtitle( - title: String, - subtitle: String?, - onSubtitleClick: (String) -> Unit, -) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { Spacer(modifier = Modifier.height(24.dp)) Text( - text = title, + text = roomName, style = ElementTheme.typography.fontHeadingLgBold, textAlign = TextAlign.Center, ) - if (subtitle != null) { - Spacer(modifier = Modifier.height(6.dp)) + val userStatus = otherMember.displayedStatus?.toText() + if(userStatus != null){ + Spacer(modifier = Modifier.height(12.dp)) Text( - modifier = Modifier.niceClickable { onSubtitleClick(subtitle) }, - text = subtitle, - style = ElementTheme.typography.fontBodyLgRegular, + text = userStatus, + style = ElementTheme.typography.fontBodyLgMedium, + maxLines = 1, color = ElementTheme.colors.textSecondary, + overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() ) } + Spacer(modifier = Modifier.height(12.dp)) + Text( + modifier = Modifier.niceClickable { onSubtitleClick(otherMember.userId.value) }, + text = otherMember.userId.value, + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(12.dp)) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt index 23c6292294..472a185076 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt @@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.identity.IdentityState 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.user.DisplayedStatus import kotlinx.collections.immutable.persistentListOf internal class RoomMemberListStateProvider : PreviewParameterProvider { @@ -129,6 +130,7 @@ fun aRoomMember( role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, isServiceMember: Boolean = false, + displayedStatus: DisplayedStatus? = null, ) = RoomMember( userId = userId, displayName = displayName, @@ -140,6 +142,7 @@ fun aRoomMember( role = role, membershipChangeReason = membershipChangeReason, isServiceMember = isServiceMember, + displayedStatus = displayedStatus, ) fun aRoomMemberList() = persistentListOf( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index f47ea0e8b3..9b18c88c93 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -128,6 +128,7 @@ class RoomMemberDetailsPresenter( return userProfileState.copy( userName = roomUserName ?: userProfileState.userName, avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl, + displayedStatus = roomMember?.displayedStatus ?: userProfileState.displayedStatus, verificationState = verificationState, snackbarMessage = snackbarMessage, eventSink = ::handleEvent, diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt index 2bb4db0c69..2c32f2c47a 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt @@ -16,6 +16,7 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.toImmutableList @@ -82,6 +83,7 @@ fun anAlice() = MatrixUser( userId = UserId(value = "@alice:server.org"), displayName = USER_NAME_ALICE, avatarUrl = null, + displayedStatus = DisplayedStatus.InCall(0L), ) fun aRoomMembersModerationState( diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt index 410c573e4d..508d5e5da7 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt @@ -57,6 +57,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getBestName +import io.element.android.libraries.matrix.ui.model.toText import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch @@ -264,9 +265,23 @@ private fun RoomMemberActionsBottomSheet( overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) .fillMaxWidth() ) + val userStatus = user.displayedStatus?.toText() + if(userStatus != null){ + Text( + text = userStatus, + style = ElementTheme.typography.fontBodyLgMedium, + maxLines = 1, + color = ElementTheme.colors.textSecondary, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .fillMaxWidth() + ) + } // Show user ID only if it's different from the display name if (bestName != user.userId.value) { Text( diff --git a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt index 0e0016ee14..4de0d50ca4 100644 --- a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt +++ b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt @@ -13,6 +13,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.DisplayedStatus data class UserProfileState( val userId: UserId, @@ -26,6 +27,7 @@ data class UserProfileState( val dmRoomId: RoomId?, val canCall: Boolean, val snackbarMessage: SnackbarMessage?, + val displayedStatus: DisplayedStatus?, val eventSink: (UserProfileEvents) -> Unit ) { enum class ConfirmationDialog { diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt index 7e09a03ec3..24977713c0 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt @@ -145,6 +145,7 @@ class UserProfilePresenter( userId = userId, userName = userProfile?.displayName, avatarUrl = userProfile?.avatarUrl, + displayedStatus = userProfile?.displayedStatus, isBlocked = isBlocked.value, verificationState = UserProfileVerificationState.UNKNOWN, startDmActionState = startDmActionState.value, diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt index 5ba0280b14..4f1c56e80e 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileHeaderSection.kt @@ -42,6 +42,8 @@ import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.DisplayedStatus +import io.element.android.libraries.matrix.ui.model.toText import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -56,6 +58,7 @@ fun UserProfileHeaderSection( openAvatarPreview: (url: String) -> Unit, onUserIdClick: () -> Unit, withdrawVerificationClick: () -> Unit, + displayedStatus: DisplayedStatus?, modifier: Modifier = Modifier ) { Column( @@ -90,7 +93,16 @@ fun UserProfileHeaderSection( style = ElementTheme.typography.fontHeadingLgBold, textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(12.dp)) + } + displayedStatus?.let { status -> + Text( + text = status.toText(), + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textSecondary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(12.dp)) } Text( modifier = Modifier.niceClickable { onUserIdClick() }, @@ -99,6 +111,7 @@ fun UserProfileHeaderSection( color = ElementTheme.colors.textSecondary, textAlign = TextAlign.Center, ) + Spacer(modifier = Modifier.height(12.dp)) when (verificationState) { UserProfileVerificationState.UNKNOWN, UserProfileVerificationState.UNVERIFIED -> Unit UserProfileVerificationState.VERIFIED -> { @@ -143,6 +156,7 @@ internal fun UserProfileHeaderSectionPreview() = ElementPreview { userId = UserId("@alice:example.com"), userName = USER_NAME_ALICE, verificationState = UserProfileVerificationState.VERIFIED, + displayedStatus = DisplayedStatus.InCall(0L), openAvatarPreview = {}, onUserIdClick = {}, withdrawVerificationClick = {}, @@ -157,6 +171,7 @@ internal fun UserProfileHeaderSectionWithVerificationViolationPreview() = Elemen userId = UserId("@alice:example.com"), userName = USER_NAME_ALICE, verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION, + displayedStatus = DisplayedStatus.InCall(0L), openAvatarPreview = {}, onUserIdClick = {}, withdrawVerificationClick = {}, diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt index fe318a8670..4e25284753 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt @@ -18,6 +18,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.ui.components.aMatrixUser open class UserProfileStateProvider : PreviewParameterProvider { @@ -48,6 +49,7 @@ fun aUserProfileState( dmRoomId: RoomId? = null, canCall: Boolean = false, snackbarMessage: SnackbarMessage? = null, + displayedStatus: DisplayedStatus? = null, eventSink: (UserProfileEvents) -> Unit = {}, ) = UserProfileState( userId = userId, @@ -61,5 +63,6 @@ fun aUserProfileState( dmRoomId = dmRoomId, canCall = canCall, snackbarMessage = snackbarMessage, + displayedStatus = displayedStatus, eventSink = eventSink, ) diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt index 5d541edf2b..a09a091834 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt @@ -78,6 +78,7 @@ fun UserProfileView( userId = state.userId, userName = state.userName, verificationState = state.verificationState, + displayedStatus = state.displayedStatus, openAvatarPreview = { avatarUrl -> openAvatarPreview(state.userName ?: state.userId.value, avatarUrl) }, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/MatrixBadgeRowMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/MatrixBadgeRowMolecule.kt index 16f4a40993..26ee9927b9 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/MatrixBadgeRowMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/MatrixBadgeRowMolecule.kt @@ -25,7 +25,7 @@ fun MatrixBadgeRowMolecule( ) { FlowRow( modifier = modifier - .padding(start = 16.dp, end = 16.dp, top = 8.dp), + .padding(start = 16.dp, end = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), verticalArrangement = Arrangement.spacedBy(8.dp) ) { 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 abf685a38b..1874986d70 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 @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.api.room import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.MatrixUser data class RoomMember( @@ -23,6 +24,7 @@ data class RoomMember( val role: Role, val membershipChangeReason: String?, val isServiceMember: Boolean, + val displayedStatus: DisplayedStatus?, ) { /** * Role of the RoomMember, based on its [powerLevel]. diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileDetails.kt index 6393a27bb2..73b0478d63 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileDetails.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileDetails.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.api.timeline.item.event import androidx.compose.runtime.Immutable import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.DisplayedStatus @Immutable sealed interface ProfileDetails { @@ -20,7 +21,8 @@ sealed interface ProfileDetails { data class Ready( val displayName: String?, val displayNameAmbiguous: Boolean, - val avatarUrl: String? + val avatarUrl: String?, + val displayedStatus: DisplayedStatus?, ) : ProfileDetails data class Error( diff --git a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetailsTest.kt b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetailsTest.kt index ffb59ba7d1..8021abd9c9 100644 --- a/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetailsTest.kt +++ b/libraries/matrix/api/src/test/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/ProfileTimelineDetailsTest.kt @@ -38,6 +38,7 @@ class ProfileTimelineDetailsTest { displayName = null, displayNameAmbiguous = false, avatarUrl = null, + displayedStatus = null, ).getDisambiguatedDisplayName(aUserId) ).isEqualTo(A_USER_ID) } @@ -49,6 +50,7 @@ class ProfileTimelineDetailsTest { displayName = "Alice", displayNameAmbiguous = false, avatarUrl = null, + displayedStatus = null, ).getDisambiguatedDisplayName(aUserId) ).isEqualTo("Alice") } @@ -60,6 +62,7 @@ class ProfileTimelineDetailsTest { displayName = "Alice", displayNameAmbiguous = true, avatarUrl = null, + displayedStatus = null, ).getDisambiguatedDisplayName(aUserId) ).isEqualTo("Alice ($A_USER_ID)") } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt index 33ecb74ff3..2a4ecf3fdc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberMapper.kt @@ -30,6 +30,8 @@ object RoomMemberMapper { role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel), membershipChangeReason = roomMember.membershipChangeReason, isServiceMember = roomMember.isServiceMember, + //TODO map this value when available + displayedStatus = null, ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index 297b0b31a4..ccb88038d4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -74,7 +74,9 @@ fun RustProfileDetails.map(): ProfileDetails { is RustProfileDetails.Ready -> ProfileDetails.Ready( displayName = displayName, displayNameAmbiguous = displayNameAmbiguous, - avatarUrl = avatarUrl + avatarUrl = avatarUrl, + //TODO map this value when available + displayedStatus = null, ) } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt index 2825e7c5be..b61009a5c6 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt @@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.SendHandlePro import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemDebugInfoProvider +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME @@ -84,11 +85,13 @@ fun anEventTimelineItem( fun aProfileDetails( displayName: String? = A_USER_NAME, displayNameAmbiguous: Boolean = false, - avatarUrl: String? = null + avatarUrl: String? = null, + displayedStatus: DisplayedStatus? = null, ): ProfileDetails = ProfileDetails.Ready( displayName = displayName, displayNameAmbiguous = displayNameAmbiguous, avatarUrl = avatarUrl, + displayedStatus = displayedStatus, ) fun aProfileChangeMessageContent( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableUserRow.kt index 917b03f309..ce516ecd54 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableUserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CheckableUserRow.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -50,7 +51,7 @@ fun CheckableUserRow( UserRow( modifier = rowModifier, avatarData = data.avatarData, - name = data.name, + name = AnnotatedString(data.name), subtext = data.subtext, enabled = enabled, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/DisplayNameWithStatus.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/DisplayNameWithStatus.kt new file mode 100644 index 0000000000..29ff5763ea --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/DisplayNameWithStatus.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.ui.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Text + +/** + * Component to display side by side a display name and an optional status. + */ +@Composable +fun DisplayNameWithStatus( + name: String, + status: String?, + style: TextStyle, + modifier: Modifier = Modifier, + nameColor: Color = Color.Unspecified, + nameFontStyle: FontStyle? = null, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = name, + modifier = Modifier + .weight(1f, fill = false) + .clipToBounds(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = style, + color = nameColor, + fontStyle = nameFontStyle, + ) + if (status != null) { + Spacer(Modifier.width(4.dp)) + Text( + text = status, + style = style, + color = ElementTheme.colors.textSecondary, + ) + } + } +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt index 0b9bde0fb9..c064b60a35 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getBestName +import io.element.android.libraries.matrix.ui.model.toEmojiText @Composable fun MatrixUserHeader( @@ -58,13 +59,11 @@ fun MatrixUserHeader( verticalArrangement = Arrangement.spacedBy(2.dp) ) { // Name - Text( - modifier = Modifier.clipToBounds(), - text = matrixUser.getBestName(), - maxLines = 1, - style = ElementTheme.typography.fontHeadingMdRegular, - overflow = TextOverflow.Ellipsis, - color = ElementTheme.colors.textPrimary, + DisplayNameWithStatus( + name = matrixUser.getBestName(), + status = matrixUser.displayedStatus?.toEmojiText(), + nameColor = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontHeadingMdRegular ) // Id if (matrixUser.displayName.isNullOrEmpty().not()) { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt index 639bafbcbf..c4af70e51d 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.designsystem.preview.USER_NAME_SUSIE import io.element.android.libraries.designsystem.preview.USER_NAME_VICTOR import io.element.android.libraries.designsystem.preview.USER_NAME_WALTER import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.MatrixUser open class MatrixUserProvider : PreviewParameterProvider { @@ -28,6 +29,7 @@ open class MatrixUserProvider : PreviewParameterProvider { get() = sequenceOf( aMatrixUser(), aMatrixUser(displayName = null), + aMatrixUser(displayedStatus = DisplayedStatus.InCall(0L) ), ) } @@ -43,10 +45,12 @@ fun aMatrixUser( id: String? = null, displayName: String? = USER_NAME_ALICE, avatarUrl: String? = null, + displayedStatus: DisplayedStatus? = null, ) = MatrixUser( userId = UserId(id ?: "@${displayName?.lowercase()?.replace(" ", "_") ?: "id"}:server.org"), displayName = displayName, avatarUrl = avatarUrl, + displayedStatus = displayedStatus, ) fun aMatrixUserList() = listOf( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt index ed7fd63435..7cd76ef5ee 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.ui.components import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import io.element.android.compound.theme.ElementTheme import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -19,6 +20,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getBestName +import io.element.android.libraries.matrix.ui.model.toEmojiText @Composable fun MatrixUserRow( @@ -29,7 +31,14 @@ fun MatrixUserRow( trailingContent: @Composable (() -> Unit)? = null, ) = UserRow( avatarData = matrixUser.getAvatarData(avatarSize), - name = matrixUser.getBestName(), + nameContent = { + DisplayNameWithStatus( + name = matrixUser.getBestName(), + status = matrixUser.displayedStatus?.toEmojiText(), + nameColor = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodyLgRegular, + ) + }, subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value, modifier = modifier, verticalSpaceWidth = verticalSpaceWidth, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt index 8d236d1a2a..c0a7e969ff 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UserRow.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -30,7 +31,36 @@ import io.element.android.libraries.designsystem.theme.components.Text @Composable internal fun UserRow( avatarData: AvatarData, - name: String, + name: AnnotatedString, + subtext: String?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + verticalSpaceWidth: Dp = 12.dp, + trailingContent: @Composable (() -> Unit)? = null, +) { + UserRow( + avatarData = avatarData, + nameContent = { + Text( + modifier = Modifier.clipToBounds(), + text = name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = ElementTheme.typography.fontBodyLgRegular, + ) + }, + subtext = subtext, + modifier = modifier, + enabled = enabled, + verticalSpaceWidth = verticalSpaceWidth, + trailingContent = trailingContent, + ) +} + +@Composable +internal fun UserRow( + avatarData: AvatarData, + nameContent: @Composable () -> Unit, subtext: String?, modifier: Modifier = Modifier, enabled: Boolean = true, @@ -52,14 +82,7 @@ internal fun UserRow( modifier = Modifier.weight(1f), ) { // Name - Text( - modifier = Modifier.clipToBounds(), - text = name, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = if (enabled) ElementTheme.colors.textPrimary else ElementTheme.colors.textDisabled, - style = ElementTheme.typography.fontBodyLgRegular, - ) + nameContent() // Id subtext?.let { Text( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/RoomMemberProfilesCache.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/RoomMemberProfilesCache.kt index 4a9f4f98a4..2d924ea3e2 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/RoomMemberProfilesCache.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/RoomMemberProfilesCache.kt @@ -13,6 +13,7 @@ import dev.zacsweers.metro.SingleIn import io.element.android.libraries.di.RoomScope 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.user.DisplayedStatus import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.drop @@ -31,4 +32,9 @@ class RoomMemberProfilesCache { fun getDisplayName(userId: UserId): String? { return cache.value[userId]?.disambiguatedDisplayName } + + fun getDisplayedStatus(userId: UserId): DisplayedStatus? { + return cache.value[userId]?.displayedStatus + } + } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt index ca118349e9..b335ef9609 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt @@ -167,4 +167,5 @@ fun aProfileDetailsReady( displayName = displayName, displayNameAmbiguous = displayNameAmbiguous, avatarUrl = avatarUrl, + displayedStatus = null, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt index cdca0937c1..42432c6950 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails +import io.element.android.libraries.matrix.ui.model.toText // https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0 @Composable @@ -54,6 +55,19 @@ fun SenderName( SecondaryText(text = senderId.value, mode = senderNameMode) } } + val userStatus = senderProfile.displayedStatus + if (senderNameMode is SenderNameMode.Timeline && userStatus != null) { + Text( + modifier = Modifier + .clipToBounds() + .alignByBaseline(), + text = userStatus.toText(), + style = ElementTheme.typography.fontBodyMdMedium, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } } } @@ -74,13 +88,16 @@ private fun RowScope.MainText( SenderNameMode.ActionList, SenderNameMode.Reply -> Modifier } + val color = when (mode) { is SenderNameMode.Timeline -> mode.mainColor SenderNameMode.ActionList, SenderNameMode.Reply -> ElementTheme.colors.textPrimary } Text( - modifier = modifier.clipToBounds(), + modifier = modifier + .clipToBounds() + .weight(1f, fill = false), text = text, style = style, color = color, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderNameDataProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderNameDataProvider.kt index 96ad91b869..e4ca73e69c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderNameDataProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderNameDataProvider.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails +import io.element.android.libraries.matrix.api.user.DisplayedStatus data class SenderNameData( val userId: UserId, @@ -47,12 +48,14 @@ open class SenderNameDataProvider : PreviewParameterProvider { private fun aSenderNameData( senderNameMode: SenderNameMode, displayNameAmbiguous: Boolean = false, + displayedStatus: DisplayedStatus? = null, ) = SenderNameData( userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"), profileDetails = ProfileDetails.Ready( displayName = "Alice ${senderNameMode.javaClass.simpleName}", displayNameAmbiguous = displayNameAmbiguous, - avatarUrl = null + avatarUrl = null, + displayedStatus = displayedStatus, ), senderNameMode = senderNameMode, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/DisplayedStatusExtention.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/DisplayedStatusExtention.kt new file mode 100644 index 0000000000..4d11599356 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/DisplayedStatusExtention.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.ui.model + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.matrix.api.user.DisplayedStatus +import io.element.android.libraries.matrix.ui.R + +fun DisplayedStatus.toEmojiText(): String = when (this) { + is DisplayedStatus.UserSet -> status.emoji + is DisplayedStatus.InCall -> "🎧" +} + +@Composable +fun DisplayedStatus.toText(): String = when (this) { + is DisplayedStatus.UserSet -> "${status.emoji} ${status.text}" + is DisplayedStatus.InCall -> "🎧 ${stringResource(R.string.common_user_status_on_a_call)}" +} diff --git a/libraries/matrixui/src/main/res/values/temporary.xml b/libraries/matrixui/src/main/res/values/temporary.xml new file mode 100644 index 0000000000..d31b5025fd --- /dev/null +++ b/libraries/matrixui/src/main/res/values/temporary.xml @@ -0,0 +1,10 @@ + + + + On a call + diff --git a/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt b/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt index 49f77ddefb..6fe6e82fa7 100644 --- a/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt +++ b/libraries/previewutils/src/main/kotlin/io/element/android/libraries/previewutils/room/RoomMemberFixture.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.designsystem.preview.USER_NAME_WALTER 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 io.element.android.libraries.matrix.api.user.DisplayedStatus import kotlinx.collections.immutable.persistentListOf fun aRoomMember( @@ -34,6 +35,7 @@ fun aRoomMember( role: RoomMember.Role = RoomMember.Role.User, membershipChangeReason: String? = null, isServiceMember: Boolean = false, + userStatus: DisplayedStatus? = null, ) = RoomMember( userId = userId, displayName = displayName, @@ -45,6 +47,7 @@ fun aRoomMember( role = role, membershipChangeReason = membershipChangeReason, isServiceMember = isServiceMember, + displayedStatus = userStatus, ) fun aRoomMemberList() = persistentListOf( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpan.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpan.kt index e2ae08c421..5c7615fde4 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpan.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpan.kt @@ -39,11 +39,17 @@ class MentionSpan( private var typeface: Typeface = Typeface.DEFAULT private var measuredTextWidth = 0 + private var measuredTrailingWidth = 0 // The formatted display text, will be set by the formatter var displayText: CharSequence = "" private set + var trailingText: String? = null + set(value) { + field = value?.let { " $it" } + } + /** * Updates the visual properties of this span. */ @@ -80,6 +86,10 @@ class MentionSpan( displayText = formatter.formatDisplayText(type) } + fun updateTrailingText(trailingText: String?){ + this.trailingText = trailingText + } + override fun getSize( paint: Paint, text: CharSequence?, @@ -91,7 +101,8 @@ class MentionSpan( textPaint.typeface = typeface // Measure the full text width without truncation measuredTextWidth = textPaint.measureText(displayText, 0, displayText.length).roundToInt() - return measuredTextWidth + startPadding + endPadding + measuredTrailingWidth = trailingText?.let { textPaint.measureText(it).roundToInt() } ?: 0 + return measuredTextWidth + measuredTrailingWidth + startPadding + endPadding } override fun draw( @@ -106,7 +117,8 @@ class MentionSpan( paint: Paint ) { val availableWidth = (canvas.width - x).coerceAtLeast(0f) - val measuredWidth = measuredTextWidth + startPadding + endPadding + + val measuredWidth = measuredTextWidth + measuredTrailingWidth + startPadding + endPadding val pillWidth = minOf(availableWidth, measuredWidth.toFloat()) backgroundPaint.color = backgroundColor @@ -118,18 +130,18 @@ class MentionSpan( textPaint.color = textColor textPaint.typeface = typeface - val availableWidthForText = availableWidth - startPadding - endPadding + val availableWidthForText = availableWidth - startPadding - endPadding - measuredTrailingWidth val textToDraw = if (measuredTextWidth > availableWidthForText) { - TextUtils.ellipsize( - displayText, - textPaint, - availableWidthForText, - TextUtils.TruncateAt.END - ) + TextUtils.ellipsize(displayText, textPaint, availableWidthForText, TextUtils.TruncateAt.END) } else { displayText } canvas.drawText(textToDraw, 0, textToDraw.length, x + startPadding, y.toFloat(), textPaint) + + trailingText?.let { + val drawnTextWidth = minOf(availableWidthForText, measuredTextWidth.toFloat()) + canvas.drawText(it, x + startPadding + drawnTextWidth, y.toFloat(), textPaint) + } } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanUpdater.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanUpdater.kt index a530d64437..2142220e24 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanUpdater.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanUpdater.kt @@ -18,6 +18,7 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache import io.element.android.libraries.matrix.ui.messages.RoomNamesCache +import io.element.android.libraries.matrix.ui.model.toEmojiText interface MentionSpanUpdater { fun updateMentionSpans(text: CharSequence): CharSequence @@ -48,6 +49,13 @@ class DefaultMentionSpanUpdater( for (mentionSpan in text.getMentionSpans()) { mentionSpan.updateTheme(theme) mentionSpan.updateDisplayText(formatter) + mentionSpan.updateTrailingText( + if (mentionSpan.type is MentionType.User) { + roomMemberProfilesCache.getDisplayedStatus(mentionSpan.type.userId)?.toEmojiText() + } else { + null + } + ) } return text }