Display user status next to display names across rooms and profiles

This commit is contained in:
ganfra
2026-06-24 17:59:08 +02:00
parent 18034e02a2
commit 06a903718b
46 changed files with 386 additions and 89 deletions
@@ -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.designsystem.theme.unreadIndicator
import io.element.android.libraries.matrix.api.notification.CallIntent 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.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.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.InviteSender
import io.element.android.libraries.matrix.ui.model.toEmojiText
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber import timber.log.Timber
@@ -126,7 +129,8 @@ internal fun RoomSummaryRow(
NameAndTimestampRow( NameAndTimestampRow(
name = room.name, name = room.name,
timestamp = room.timestamp, timestamp = room.timestamp,
isHighlighted = room.isHighlighted isHighlighted = room.isHighlighted,
dmUserStatus = room.dmUserStatus,
) )
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount) MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
} }
@@ -142,7 +146,8 @@ internal fun RoomSummaryRow(
NameAndTimestampRow( NameAndTimestampRow(
name = room.name, name = room.name,
timestamp = null, timestamp = null,
isHighlighted = room.isHighlighted isHighlighted = room.isHighlighted,
dmUserStatus = null,
) )
if (room.canonicalAlias != null) { if (room.canonicalAlias != null) {
Text( Text(
@@ -218,22 +223,21 @@ private fun NameAndTimestampRow(
name: String?, name: String?,
timestamp: String?, timestamp: String?,
isHighlighted: Boolean, isHighlighted: Boolean,
dmUserStatus: DisplayedStatus?,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalArrangement = spacedBy(16.dp) horizontalArrangement = spacedBy(16.dp)
) { ) {
Text( val displayName = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name)
modifier = Modifier DisplayNameWithStatus(
.weight(1f) name = displayName,
.clipToBounds(), status = dmUserStatus?.toEmojiText(),
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyLgMedium, style = ElementTheme.typography.fontBodyLgMedium,
text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name), nameColor = ElementTheme.colors.roomListRoomName,
fontStyle = FontStyle.Italic.takeIf { name == null }, nameFontStyle = FontStyle.Italic.takeIf { name == null },
color = ElementTheme.colors.roomListRoomName,
maxLines = 1,
overflow = TextOverflow.Ellipsis
) )
// Timestamp // Timestamp
Text( Text(
@@ -76,6 +76,7 @@ class RoomListRoomSummaryFactory(
}.toImmutableList(), }.toImmutableList(),
isTombstoned = roomInfo.successorRoom != null, isTombstoned = roomInfo.successorRoom != null,
isSpace = roomInfo.isSpace, isSpace = roomInfo.isSpace,
dmUserStatus = if (roomInfo.isDm) roomInfo.heroes.firstOrNull()?.displayedStatus else null,
) )
} }
@@ -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.core.RoomId
import io.element.android.libraries.matrix.api.notification.CallIntent 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.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -42,6 +43,7 @@ data class RoomListRoomSummary(
val isTombstoned: Boolean, val isTombstoned: Boolean,
val heroes: ImmutableList<AvatarData>, val heroes: ImmutableList<AvatarData>,
val isSpace: Boolean, val isSpace: Boolean,
val dmUserStatus: DisplayedStatus?,
) { ) {
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE && val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) || (numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
@@ -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.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent 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.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -182,6 +183,7 @@ internal fun aRoomListRoomSummary(
heroes: List<AvatarData> = emptyList(), heroes: List<AvatarData> = emptyList(),
isTombstoned: Boolean = false, isTombstoned: Boolean = false,
isSpace: Boolean = false, isSpace: Boolean = false,
dmUserStatus: DisplayedStatus? = null,
) = RoomListRoomSummary( ) = RoomListRoomSummary(
id = id, id = id,
roomId = RoomId(id), roomId = RoomId(id),
@@ -204,5 +206,6 @@ internal fun aRoomListRoomSummary(
canonicalAlias = canonicalAlias, canonicalAlias = canonicalAlias,
heroes = heroes.toImmutableList(), heroes = heroes.toImmutableList(),
isTombstoned = isTombstoned, isTombstoned = isTombstoned,
isSpace = isSpace isSpace = isSpace,
dmUserStatus = dmUserStatus,
) )
@@ -319,6 +319,7 @@ class MessagesPresenter(
appName = buildMeta.applicationName, appName = buildMeta.applicationName,
pinnedMessagesBannerState = pinnedMessagesBannerState, pinnedMessagesBannerState = pinnedMessagesBannerState,
dmUserVerificationState = dmUserVerificationState, dmUserVerificationState = dmUserVerificationState,
dmUserStatus = dmRoomMember?.displayedStatus,
roomMemberModerationState = roomMemberModerationState, roomMemberModerationState = roomMemberModerationState,
topBarSharedHistoryIcon = topBarSharedHistoryIcon, topBarSharedHistoryIcon = topBarSharedHistoryIcon,
successorRoom = roomInfo.successorRoom, successorRoom = roomInfo.successorRoom,
@@ -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.core.RoomId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState 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.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class MessagesState( data class MessagesState(
@@ -53,6 +54,7 @@ data class MessagesState(
val appName: String, val appName: String,
val pinnedMessagesBannerState: PinnedMessagesBannerState, val pinnedMessagesBannerState: PinnedMessagesBannerState,
val dmUserVerificationState: IdentityState?, val dmUserVerificationState: IdentityState?,
val dmUserStatus: DisplayedStatus?,
val roomMemberModerationState: RoomMemberModerationState, val roomMemberModerationState: RoomMemberModerationState,
/** Type of "shared history" icon to show in the top bar. */ /** Type of "shared history" icon to show in the top bar. */
val topBarSharedHistoryIcon: SharedHistoryIcon, val topBarSharedHistoryIcon: SharedHistoryIcon,
@@ -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.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom 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.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.MessageComposerMode
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
@@ -129,6 +130,7 @@ fun aMessagesState(
hasUnreadThreads = false, hasUnreadThreads = false,
), ),
isCurrentlySharingLiveLocationInRoom: Boolean = false, isCurrentlySharingLiveLocationInRoom: Boolean = false,
dmUserStatus: DisplayedStatus? = null,
eventSink: (MessagesEvent) -> Unit = {}, eventSink: (MessagesEvent) -> Unit = {},
) = MessagesState( ) = MessagesState(
roomId = RoomId("!id:domain"), roomId = RoomId("!id:domain"),
@@ -159,6 +161,7 @@ fun aMessagesState(
successorRoom = successorRoom, successorRoom = successorRoom,
threads = threads, threads = threads,
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom, showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom,
dmUserStatus = dmUserStatus,
eventSink = eventSink, eventSink = eventSink,
) )
@@ -244,6 +244,7 @@ fun MessagesView(
heroes = state.heroes, heroes = state.heroes,
dmUserIdentityState = state.dmUserVerificationState, dmUserIdentityState = state.dmUserVerificationState,
sharedHistoryIcon = state.topBarSharedHistoryIcon, sharedHistoryIcon = state.topBarSharedHistoryIcon,
dmUserStatus = state.dmUserStatus,
onBackClick = { hidingKeyboard { onBackClick() } }, onBackClick = { hidingKeyboard { onBackClick() } },
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } }, onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
menuActions = { menuActions = {
@@ -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.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
import io.element.android.libraries.matrix.ui.model.getAvatarData 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.slashcommands.api.SlashCommandSuggestion
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -112,12 +114,16 @@ private fun SuggestionItemView(
is ResolvedSuggestion.Member -> AvatarType.User is ResolvedSuggestion.Member -> AvatarType.User
is ResolvedSuggestion.Command -> null 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.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.Alias -> suggestion.roomName
is ResolvedSuggestion.Command -> suggestion.command.command is ResolvedSuggestion.Command -> suggestion.command.command
} }
val statusEmoji = when (suggestion) {
is ResolvedSuggestion.Member -> suggestion.roomMember.displayedStatus?.toEmojiText()
else -> null
}
val details = when (suggestion) { val details = when (suggestion) {
is ResolvedSuggestion.AtRoom, is ResolvedSuggestion.AtRoom,
is ResolvedSuggestion.Member, is ResolvedSuggestion.Member,
@@ -148,11 +154,11 @@ private fun SuggestionItemView(
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
title?.let { title?.let {
Text( DisplayNameWithStatus(
text = it, name = title,
status = statusEmoji,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
maxLines = 1, nameColor = ElementTheme.colors.textPrimary,
overflow = TextOverflow.Ellipsis,
) )
} }
details?.let { details?.let {
@@ -191,6 +197,7 @@ internal fun SuggestionsPickerViewPreview() {
role = RoomMember.Role.User, role = RoomMember.Role.User,
membershipChangeReason = null, membershipChangeReason = null,
isServiceMember = false, isServiceMember = false,
displayedStatus = null,
) )
val anAlias = remember { RoomAlias("#room:domain.org") } val anAlias = remember { RoomAlias("#room:domain.org") }
SuggestionsPickerView( SuggestionsPickerView(
@@ -383,7 +383,12 @@ fun aThreadListItem(
fun aThreadListItemEvent( fun aThreadListItemEvent(
threadId: ThreadId = ThreadId("\$a-thread-id"), threadId: ThreadId = ThreadId("\$a-thread-id"),
senderId: UserId = UserId("@a-user-id:server"), 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, isOwn: Boolean = false,
content: EventContent = MessageContent( content: EventContent = MessageContent(
body = "Hello world!", body = "Hello world!",
@@ -166,6 +166,7 @@ internal fun MessageShieldViewPreview() {
displayName = USER_NAME_ALICE, displayName = USER_NAME_ALICE,
displayNameAmbiguous = false, displayNameAmbiguous = false,
avatarUrl = null, avatarUrl = null,
displayedStatus = null,
), ),
) )
) )
@@ -962,6 +962,7 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
displayName = USER_NAME_ALICE, displayName = USER_NAME_ALICE,
avatarUrl = null, avatarUrl = null,
displayNameAmbiguous = false, displayNameAmbiguous = false,
displayedStatus = null,
), ),
timestamp = 0L, timestamp = 0L,
) )
@@ -1140,6 +1141,7 @@ internal fun ThreadSummaryViewPreview() {
displayName = USER_NAME_ALICE, displayName = USER_NAME_ALICE,
avatarUrl = null, avatarUrl = null,
displayNameAmbiguous = true, displayNameAmbiguous = true,
displayedStatus = null,
), ),
timestamp = 0L, timestamp = 0L,
) )
@@ -26,7 +26,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons 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.preview.ROOM_NAME
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider 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.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar 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.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.components.aMatrixUserList
import io.element.android.libraries.matrix.ui.model.getAvatarData 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 io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@@ -65,6 +66,7 @@ internal fun MessagesViewTopBar(
heroes: ImmutableList<AvatarData>, heroes: ImmutableList<AvatarData>,
dmUserIdentityState: IdentityState?, dmUserIdentityState: IdentityState?,
sharedHistoryIcon: SharedHistoryIcon, sharedHistoryIcon: SharedHistoryIcon,
dmUserStatus: DisplayedStatus?,
onRoomDetailsClick: () -> Unit, onRoomDetailsClick: () -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -91,6 +93,7 @@ internal fun MessagesViewTopBar(
roomAvatar = roomAvatar, roomAvatar = roomAvatar,
isTombstoned = isTombstoned, isTombstoned = isTombstoned,
heroes = heroes, heroes = heroes,
dmUserStatus = dmUserStatus,
modifier = titleModifier modifier = titleModifier
) )
@@ -144,6 +147,7 @@ private fun RoomAvatarAndNameRow(
roomAvatar: AvatarData, roomAvatar: AvatarData,
heroes: ImmutableList<AvatarData>, heroes: ImmutableList<AvatarData>,
isTombstoned: Boolean, isTombstoned: Boolean,
dmUserStatus: DisplayedStatus?,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(
@@ -157,14 +161,13 @@ private fun RoomAvatarAndNameRow(
isTombstoned = isTombstoned, isTombstoned = isTombstoned,
), ),
) )
Text( DisplayNameWithStatus(
modifier = Modifier name = roomName ?: stringResource(CommonStrings.common_no_room_name),
.padding(start = 8.dp), status = dmUserStatus?.toEmojiText(),
text = roomName ?: stringResource(CommonStrings.common_no_room_name), modifier = Modifier.padding(start = 8.dp),
style = ElementTheme.typography.fontBodyLgMedium, style = ElementTheme.typography.fontBodyLgMedium,
fontStyle = FontStyle.Italic.takeIf { roomName == null }, nameColor = ElementTheme.colors.textPrimary,
maxLines = 1, nameFontStyle = FontStyle.Italic.takeIf { roomName == null },
overflow = TextOverflow.Ellipsis
) )
} }
} }
@@ -184,6 +187,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
roomCallState: RoomCallState = RoomCallState.Unavailable, roomCallState: RoomCallState = RoomCallState.Unavailable,
dmUserIdentityState: IdentityState? = null, dmUserIdentityState: IdentityState? = null,
sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE, sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE,
dmUserStatus: DisplayedStatus? = null,
displayThreads: Boolean = false, displayThreads: Boolean = false,
) = MessagesViewTopBar( ) = MessagesViewTopBar(
roomName = roomName, roomName = roomName,
@@ -192,6 +196,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
heroes = heroes, heroes = heroes,
dmUserIdentityState = dmUserIdentityState, dmUserIdentityState = dmUserIdentityState,
sharedHistoryIcon = sharedHistoryIcon, sharedHistoryIcon = sharedHistoryIcon,
dmUserStatus = dmUserStatus,
onRoomDetailsClick = {}, onRoomDetailsClick = {},
onBackClick = {}, onBackClick = {},
menuActions = { menuActions = {
@@ -246,5 +251,11 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
AMessagesViewTopBar( AMessagesViewTopBar(
displayThreads = true, displayThreads = true,
) )
HorizontalDivider()
AMessagesViewTopBar(
roomName = "A DM with a very very very long name",
dmUserIdentityState = IdentityState.Verified,
dmUserStatus = DisplayedStatus.InCall(0L)
)
} }
} }
@@ -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.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings 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.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.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -75,6 +76,7 @@ fun aDmRoomMember(
role: RoomMember.Role = RoomMember.Role.User, role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null, membershipChangeReason: String? = null,
isServiceMember: Boolean = false, isServiceMember: Boolean = false,
displayedStatus: DisplayedStatus? = null,
) = RoomMember( ) = RoomMember(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
@@ -86,6 +88,7 @@ fun aDmRoomMember(
role = role, role = role,
membershipChangeReason = membershipChangeReason, membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember, isServiceMember = isServiceMember,
displayedStatus = displayedStatus,
) )
fun aRoomDetailsState( fun aRoomDetailsState(
@@ -178,12 +181,13 @@ fun aDmRoomDetailsState(
roomName: String = "Daniel", roomName: String = "Daniel",
isEncrypted: Boolean = true, isEncrypted: Boolean = true,
dmRoomMemberVerificationState: UserProfileVerificationState = UserProfileVerificationState.UNKNOWN, dmRoomMemberVerificationState: UserProfileVerificationState = UserProfileVerificationState.UNKNOWN,
displayedStatus: DisplayedStatus? = null,
) = aRoomDetailsState( ) = aRoomDetailsState(
roomName = roomName, roomName = roomName,
isPublic = false, isPublic = false,
isEncrypted = isEncrypted, isEncrypted = isEncrypted,
canInvite = true, canInvite = true,
roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)), roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored, displayedStatus = displayedStatus)),
dmOtherMemberDetailsState = aUserProfileState( dmOtherMemberDetailsState = aUserProfileState(
isBlocked = AsyncData.Success(isDmMemberIgnored), isBlocked = AsyncData.Success(isDmMemberIgnored),
verificationState = dmRoomMemberVerificationState, verificationState = dmRoomMemberVerificationState,
@@ -37,6 +37,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign 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.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import im.vector.app.features.analytics.plan.Interaction 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.room.getBestName
import io.element.android.libraries.matrix.api.user.MatrixUser 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.getAvatarData
import io.element.android.libraries.matrix.ui.model.toText
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@@ -489,11 +491,23 @@ private fun RoomHeaderSection(
} }
.testTag(TestTags.roomDetailAvatar) .testTag(TestTags.roomDetailAvatar)
) )
TitleAndSubtitle( Spacer(modifier = Modifier.height(24.dp))
title = roomName, Text(
subtitle = roomAlias?.value, text = roomName,
onSubtitleClick = onSubtitleClick, 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) .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)) Spacer(modifier = Modifier.height(24.dp))
Text( Text(
text = title, text = roomName,
style = ElementTheme.typography.fontHeadingLgBold, style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
if (subtitle != null) { val userStatus = otherMember.displayedStatus?.toText()
Spacer(modifier = Modifier.height(6.dp)) if(userStatus != null){
Spacer(modifier = Modifier.height(12.dp))
Text( Text(
modifier = Modifier.niceClickable { onSubtitleClick(subtitle) }, text = userStatus,
text = subtitle, 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, style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
} Spacer(modifier = Modifier.height(12.dp))
} }
} }
@@ -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.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> { internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
@@ -129,6 +130,7 @@ fun aRoomMember(
role: RoomMember.Role = RoomMember.Role.User, role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null, membershipChangeReason: String? = null,
isServiceMember: Boolean = false, isServiceMember: Boolean = false,
displayedStatus: DisplayedStatus? = null,
) = RoomMember( ) = RoomMember(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
@@ -140,6 +142,7 @@ fun aRoomMember(
role = role, role = role,
membershipChangeReason = membershipChangeReason, membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember, isServiceMember = isServiceMember,
displayedStatus = displayedStatus,
) )
fun aRoomMemberList() = persistentListOf( fun aRoomMemberList() = persistentListOf(
@@ -128,6 +128,7 @@ class RoomMemberDetailsPresenter(
return userProfileState.copy( return userProfileState.copy(
userName = roomUserName ?: userProfileState.userName, userName = roomUserName ?: userProfileState.userName,
avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl, avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl,
displayedStatus = roomMember?.displayedStatus ?: userProfileState.displayedStatus,
verificationState = verificationState, verificationState = verificationState,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
eventSink = ::handleEvent, eventSink = ::handleEvent,
@@ -16,6 +16,7 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE 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.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -82,6 +83,7 @@ fun anAlice() = MatrixUser(
userId = UserId(value = "@alice:server.org"), userId = UserId(value = "@alice:server.org"),
displayName = USER_NAME_ALICE, displayName = USER_NAME_ALICE,
avatarUrl = null, avatarUrl = null,
displayedStatus = DisplayedStatus.InCall(0L),
) )
fun aRoomMembersModerationState( fun aRoomMembersModerationState(
@@ -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.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData 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.getBestName
import io.element.android.libraries.matrix.ui.model.toText
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -264,9 +265,23 @@ private fun RoomMemberActionsBottomSheet(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp) .padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
.fillMaxWidth() .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 // Show user ID only if it's different from the display name
if (bestName != user.userId.value) { if (bestName != user.userId.value) {
Text( Text(
@@ -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.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId 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.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
data class UserProfileState( data class UserProfileState(
val userId: UserId, val userId: UserId,
@@ -26,6 +27,7 @@ data class UserProfileState(
val dmRoomId: RoomId?, val dmRoomId: RoomId?,
val canCall: Boolean, val canCall: Boolean,
val snackbarMessage: SnackbarMessage?, val snackbarMessage: SnackbarMessage?,
val displayedStatus: DisplayedStatus?,
val eventSink: (UserProfileEvents) -> Unit val eventSink: (UserProfileEvents) -> Unit
) { ) {
enum class ConfirmationDialog { enum class ConfirmationDialog {
@@ -145,6 +145,7 @@ class UserProfilePresenter(
userId = userId, userId = userId,
userName = userProfile?.displayName, userName = userProfile?.displayName,
avatarUrl = userProfile?.avatarUrl, avatarUrl = userProfile?.avatarUrl,
displayedStatus = userProfile?.displayedStatus,
isBlocked = isBlocked.value, isBlocked = isBlocked.value,
verificationState = UserProfileVerificationState.UNKNOWN, verificationState = UserProfileVerificationState.UNKNOWN,
startDmActionState = startDmActionState.value, startDmActionState = startDmActionState.value,
@@ -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.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text 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.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.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@@ -56,6 +58,7 @@ fun UserProfileHeaderSection(
openAvatarPreview: (url: String) -> Unit, openAvatarPreview: (url: String) -> Unit,
onUserIdClick: () -> Unit, onUserIdClick: () -> Unit,
withdrawVerificationClick: () -> Unit, withdrawVerificationClick: () -> Unit,
displayedStatus: DisplayedStatus?,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Column( Column(
@@ -90,7 +93,16 @@ fun UserProfileHeaderSection(
style = ElementTheme.typography.fontHeadingLgBold, style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center, 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( Text(
modifier = Modifier.niceClickable { onUserIdClick() }, modifier = Modifier.niceClickable { onUserIdClick() },
@@ -99,6 +111,7 @@ fun UserProfileHeaderSection(
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
Spacer(modifier = Modifier.height(12.dp))
when (verificationState) { when (verificationState) {
UserProfileVerificationState.UNKNOWN, UserProfileVerificationState.UNVERIFIED -> Unit UserProfileVerificationState.UNKNOWN, UserProfileVerificationState.UNVERIFIED -> Unit
UserProfileVerificationState.VERIFIED -> { UserProfileVerificationState.VERIFIED -> {
@@ -143,6 +156,7 @@ internal fun UserProfileHeaderSectionPreview() = ElementPreview {
userId = UserId("@alice:example.com"), userId = UserId("@alice:example.com"),
userName = USER_NAME_ALICE, userName = USER_NAME_ALICE,
verificationState = UserProfileVerificationState.VERIFIED, verificationState = UserProfileVerificationState.VERIFIED,
displayedStatus = DisplayedStatus.InCall(0L),
openAvatarPreview = {}, openAvatarPreview = {},
onUserIdClick = {}, onUserIdClick = {},
withdrawVerificationClick = {}, withdrawVerificationClick = {},
@@ -157,6 +171,7 @@ internal fun UserProfileHeaderSectionWithVerificationViolationPreview() = Elemen
userId = UserId("@alice:example.com"), userId = UserId("@alice:example.com"),
userName = USER_NAME_ALICE, userName = USER_NAME_ALICE,
verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION, verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION,
displayedStatus = DisplayedStatus.InCall(0L),
openAvatarPreview = {}, openAvatarPreview = {},
onUserIdClick = {}, onUserIdClick = {},
withdrawVerificationClick = {}, withdrawVerificationClick = {},
@@ -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.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId 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.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> { open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> {
@@ -48,6 +49,7 @@ fun aUserProfileState(
dmRoomId: RoomId? = null, dmRoomId: RoomId? = null,
canCall: Boolean = false, canCall: Boolean = false,
snackbarMessage: SnackbarMessage? = null, snackbarMessage: SnackbarMessage? = null,
displayedStatus: DisplayedStatus? = null,
eventSink: (UserProfileEvents) -> Unit = {}, eventSink: (UserProfileEvents) -> Unit = {},
) = UserProfileState( ) = UserProfileState(
userId = userId, userId = userId,
@@ -61,5 +63,6 @@ fun aUserProfileState(
dmRoomId = dmRoomId, dmRoomId = dmRoomId,
canCall = canCall, canCall = canCall,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
displayedStatus = displayedStatus,
eventSink = eventSink, eventSink = eventSink,
) )
@@ -78,6 +78,7 @@ fun UserProfileView(
userId = state.userId, userId = state.userId,
userName = state.userName, userName = state.userName,
verificationState = state.verificationState, verificationState = state.verificationState,
displayedStatus = state.displayedStatus,
openAvatarPreview = { avatarUrl -> openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.userName ?: state.userId.value, avatarUrl) openAvatarPreview(state.userName ?: state.userId.value, avatarUrl)
}, },
@@ -25,7 +25,7 @@ fun MatrixBadgeRowMolecule(
) { ) {
FlowRow( FlowRow(
modifier = modifier 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), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
@@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.api.room
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
data class RoomMember( data class RoomMember(
@@ -23,6 +24,7 @@ data class RoomMember(
val role: Role, val role: Role,
val membershipChangeReason: String?, val membershipChangeReason: String?,
val isServiceMember: Boolean, val isServiceMember: Boolean,
val displayedStatus: DisplayedStatus?,
) { ) {
/** /**
* Role of the RoomMember, based on its [powerLevel]. * Role of the RoomMember, based on its [powerLevel].
@@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.api.timeline.item.event
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
@Immutable @Immutable
sealed interface ProfileDetails { sealed interface ProfileDetails {
@@ -20,7 +21,8 @@ sealed interface ProfileDetails {
data class Ready( data class Ready(
val displayName: String?, val displayName: String?,
val displayNameAmbiguous: Boolean, val displayNameAmbiguous: Boolean,
val avatarUrl: String? val avatarUrl: String?,
val displayedStatus: DisplayedStatus?,
) : ProfileDetails ) : ProfileDetails
data class Error( data class Error(
@@ -38,6 +38,7 @@ class ProfileTimelineDetailsTest {
displayName = null, displayName = null,
displayNameAmbiguous = false, displayNameAmbiguous = false,
avatarUrl = null, avatarUrl = null,
displayedStatus = null,
).getDisambiguatedDisplayName(aUserId) ).getDisambiguatedDisplayName(aUserId)
).isEqualTo(A_USER_ID) ).isEqualTo(A_USER_ID)
} }
@@ -49,6 +50,7 @@ class ProfileTimelineDetailsTest {
displayName = "Alice", displayName = "Alice",
displayNameAmbiguous = false, displayNameAmbiguous = false,
avatarUrl = null, avatarUrl = null,
displayedStatus = null,
).getDisambiguatedDisplayName(aUserId) ).getDisambiguatedDisplayName(aUserId)
).isEqualTo("Alice") ).isEqualTo("Alice")
} }
@@ -60,6 +62,7 @@ class ProfileTimelineDetailsTest {
displayName = "Alice", displayName = "Alice",
displayNameAmbiguous = true, displayNameAmbiguous = true,
avatarUrl = null, avatarUrl = null,
displayedStatus = null,
).getDisambiguatedDisplayName(aUserId) ).getDisambiguatedDisplayName(aUserId)
).isEqualTo("Alice ($A_USER_ID)") ).isEqualTo("Alice ($A_USER_ID)")
} }
@@ -30,6 +30,8 @@ object RoomMemberMapper {
role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel), role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel),
membershipChangeReason = roomMember.membershipChangeReason, membershipChangeReason = roomMember.membershipChangeReason,
isServiceMember = roomMember.isServiceMember, isServiceMember = roomMember.isServiceMember,
//TODO map this value when available
displayedStatus = null,
) )
} }
@@ -74,7 +74,9 @@ fun RustProfileDetails.map(): ProfileDetails {
is RustProfileDetails.Ready -> ProfileDetails.Ready( is RustProfileDetails.Ready -> ProfileDetails.Ready(
displayName = displayName, displayName = displayName,
displayNameAmbiguous = displayNameAmbiguous, displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = avatarUrl avatarUrl = avatarUrl,
//TODO map this value when available
displayedStatus = null,
) )
} }
} }
@@ -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.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType 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.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.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
@@ -84,11 +85,13 @@ fun anEventTimelineItem(
fun aProfileDetails( fun aProfileDetails(
displayName: String? = A_USER_NAME, displayName: String? = A_USER_NAME,
displayNameAmbiguous: Boolean = false, displayNameAmbiguous: Boolean = false,
avatarUrl: String? = null avatarUrl: String? = null,
displayedStatus: DisplayedStatus? = null,
): ProfileDetails = ProfileDetails.Ready( ): ProfileDetails = ProfileDetails.Ready(
displayName = displayName, displayName = displayName,
displayNameAmbiguous = displayNameAmbiguous, displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,
displayedStatus = displayedStatus,
) )
fun aProfileChangeMessageContent( fun aProfileChangeMessageContent(
@@ -19,6 +19,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
@@ -50,7 +51,7 @@ fun CheckableUserRow(
UserRow( UserRow(
modifier = rowModifier, modifier = rowModifier,
avatarData = data.avatarData, avatarData = data.avatarData,
name = data.name, name = AnnotatedString(data.name),
subtext = data.subtext, subtext = data.subtext,
enabled = enabled, enabled = enabled,
) )
@@ -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,
)
}
}
}
@@ -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.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData 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.getBestName
import io.element.android.libraries.matrix.ui.model.toEmojiText
@Composable @Composable
fun MatrixUserHeader( fun MatrixUserHeader(
@@ -58,13 +59,11 @@ fun MatrixUserHeader(
verticalArrangement = Arrangement.spacedBy(2.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
// Name // Name
Text( DisplayNameWithStatus(
modifier = Modifier.clipToBounds(), name = matrixUser.getBestName(),
text = matrixUser.getBestName(), status = matrixUser.displayedStatus?.toEmojiText(),
maxLines = 1, nameColor = ElementTheme.colors.textPrimary,
style = ElementTheme.typography.fontHeadingMdRegular, style = ElementTheme.typography.fontHeadingMdRegular
overflow = TextOverflow.Ellipsis,
color = ElementTheme.colors.textPrimary,
) )
// Id // Id
if (matrixUser.displayName.isNullOrEmpty().not()) { if (matrixUser.displayName.isNullOrEmpty().not()) {
@@ -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_VICTOR
import io.element.android.libraries.designsystem.preview.USER_NAME_WALTER 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.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> { open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
@@ -28,6 +29,7 @@ open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
get() = sequenceOf( get() = sequenceOf(
aMatrixUser(), aMatrixUser(),
aMatrixUser(displayName = null), aMatrixUser(displayName = null),
aMatrixUser(displayedStatus = DisplayedStatus.InCall(0L) ),
) )
} }
@@ -43,10 +45,12 @@ fun aMatrixUser(
id: String? = null, id: String? = null,
displayName: String? = USER_NAME_ALICE, displayName: String? = USER_NAME_ALICE,
avatarUrl: String? = null, avatarUrl: String? = null,
displayedStatus: DisplayedStatus? = null,
) = MatrixUser( ) = MatrixUser(
userId = UserId(id ?: "@${displayName?.lowercase()?.replace(" ", "_") ?: "id"}:server.org"), userId = UserId(id ?: "@${displayName?.lowercase()?.replace(" ", "_") ?: "id"}:server.org"),
displayName = displayName, displayName = displayName,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,
displayedStatus = displayedStatus,
) )
fun aMatrixUserList() = listOf( fun aMatrixUserList() = listOf(
@@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.ui.components
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import io.element.android.compound.theme.ElementTheme
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData 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.getBestName
import io.element.android.libraries.matrix.ui.model.toEmojiText
@Composable @Composable
fun MatrixUserRow( fun MatrixUserRow(
@@ -29,7 +31,14 @@ fun MatrixUserRow(
trailingContent: @Composable (() -> Unit)? = null, trailingContent: @Composable (() -> Unit)? = null,
) = UserRow( ) = UserRow(
avatarData = matrixUser.getAvatarData(avatarSize), avatarData = matrixUser.getAvatarData(avatarSize),
nameContent = {
DisplayNameWithStatus(
name = matrixUser.getBestName(), 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, subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
modifier = modifier, modifier = modifier,
verticalSpaceWidth = verticalSpaceWidth, verticalSpaceWidth = verticalSpaceWidth,
@@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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 @Composable
internal fun UserRow( internal fun UserRow(
avatarData: AvatarData, 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?, subtext: String?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
@@ -52,14 +82,7 @@ internal fun UserRow(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) { ) {
// Name // Name
Text( nameContent()
modifier = Modifier.clipToBounds(),
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = if (enabled) ElementTheme.colors.textPrimary else ElementTheme.colors.textDisabled,
style = ElementTheme.typography.fontBodyLgRegular,
)
// Id // Id
subtext?.let { subtext?.let {
Text( Text(
@@ -13,6 +13,7 @@ import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember 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.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
@@ -31,4 +32,9 @@ class RoomMemberProfilesCache {
fun getDisplayName(userId: UserId): String? { fun getDisplayName(userId: UserId): String? {
return cache.value[userId]?.disambiguatedDisplayName return cache.value[userId]?.disambiguatedDisplayName
} }
fun getDisplayedStatus(userId: UserId): DisplayedStatus? {
return cache.value[userId]?.displayedStatus
}
} }
@@ -167,4 +167,5 @@ fun aProfileDetailsReady(
displayName = displayName, displayName = displayName,
displayNameAmbiguous = displayNameAmbiguous, displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,
displayedStatus = null,
) )
@@ -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.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails 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 // https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0
@Composable @Composable
@@ -54,6 +55,19 @@ fun SenderName(
SecondaryText(text = senderId.value, mode = senderNameMode) 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.ActionList,
SenderNameMode.Reply -> Modifier SenderNameMode.Reply -> Modifier
} }
val color = when (mode) { val color = when (mode) {
is SenderNameMode.Timeline -> mode.mainColor is SenderNameMode.Timeline -> mode.mainColor
SenderNameMode.ActionList, SenderNameMode.ActionList,
SenderNameMode.Reply -> ElementTheme.colors.textPrimary SenderNameMode.Reply -> ElementTheme.colors.textPrimary
} }
Text( Text(
modifier = modifier.clipToBounds(), modifier = modifier
.clipToBounds()
.weight(1f, fill = false),
text = text, text = text,
style = style, style = style,
color = color, color = color,
@@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.api.user.DisplayedStatus
data class SenderNameData( data class SenderNameData(
val userId: UserId, val userId: UserId,
@@ -47,12 +48,14 @@ open class SenderNameDataProvider : PreviewParameterProvider<SenderNameData> {
private fun aSenderNameData( private fun aSenderNameData(
senderNameMode: SenderNameMode, senderNameMode: SenderNameMode,
displayNameAmbiguous: Boolean = false, displayNameAmbiguous: Boolean = false,
displayedStatus: DisplayedStatus? = null,
) = SenderNameData( ) = SenderNameData(
userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"), userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"),
profileDetails = ProfileDetails.Ready( profileDetails = ProfileDetails.Ready(
displayName = "Alice ${senderNameMode.javaClass.simpleName}", displayName = "Alice ${senderNameMode.javaClass.simpleName}",
displayNameAmbiguous = displayNameAmbiguous, displayNameAmbiguous = displayNameAmbiguous,
avatarUrl = null avatarUrl = null,
displayedStatus = displayedStatus,
), ),
senderNameMode = senderNameMode, senderNameMode = senderNameMode,
) )
@@ -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)}"
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<resources>
<string name="common_user_status_on_a_call">On a call</string>
</resources>
@@ -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.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
fun aRoomMember( fun aRoomMember(
@@ -34,6 +35,7 @@ fun aRoomMember(
role: RoomMember.Role = RoomMember.Role.User, role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null, membershipChangeReason: String? = null,
isServiceMember: Boolean = false, isServiceMember: Boolean = false,
userStatus: DisplayedStatus? = null,
) = RoomMember( ) = RoomMember(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,
@@ -45,6 +47,7 @@ fun aRoomMember(
role = role, role = role,
membershipChangeReason = membershipChangeReason, membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember, isServiceMember = isServiceMember,
displayedStatus = userStatus,
) )
fun aRoomMemberList() = persistentListOf( fun aRoomMemberList() = persistentListOf(
@@ -39,11 +39,17 @@ class MentionSpan(
private var typeface: Typeface = Typeface.DEFAULT private var typeface: Typeface = Typeface.DEFAULT
private var measuredTextWidth = 0 private var measuredTextWidth = 0
private var measuredTrailingWidth = 0
// The formatted display text, will be set by the formatter // The formatted display text, will be set by the formatter
var displayText: CharSequence = "" var displayText: CharSequence = ""
private set private set
var trailingText: String? = null
set(value) {
field = value?.let { " $it" }
}
/** /**
* Updates the visual properties of this span. * Updates the visual properties of this span.
*/ */
@@ -80,6 +86,10 @@ class MentionSpan(
displayText = formatter.formatDisplayText(type) displayText = formatter.formatDisplayText(type)
} }
fun updateTrailingText(trailingText: String?){
this.trailingText = trailingText
}
override fun getSize( override fun getSize(
paint: Paint, paint: Paint,
text: CharSequence?, text: CharSequence?,
@@ -91,7 +101,8 @@ class MentionSpan(
textPaint.typeface = typeface textPaint.typeface = typeface
// Measure the full text width without truncation // Measure the full text width without truncation
measuredTextWidth = textPaint.measureText(displayText, 0, displayText.length).roundToInt() 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( override fun draw(
@@ -106,7 +117,8 @@ class MentionSpan(
paint: Paint paint: Paint
) { ) {
val availableWidth = (canvas.width - x).coerceAtLeast(0f) val availableWidth = (canvas.width - x).coerceAtLeast(0f)
val measuredWidth = measuredTextWidth + startPadding + endPadding
val measuredWidth = measuredTextWidth + measuredTrailingWidth + startPadding + endPadding
val pillWidth = minOf(availableWidth, measuredWidth.toFloat()) val pillWidth = minOf(availableWidth, measuredWidth.toFloat())
backgroundPaint.color = backgroundColor backgroundPaint.color = backgroundColor
@@ -118,18 +130,18 @@ class MentionSpan(
textPaint.color = textColor textPaint.color = textColor
textPaint.typeface = typeface textPaint.typeface = typeface
val availableWidthForText = availableWidth - startPadding - endPadding val availableWidthForText = availableWidth - startPadding - endPadding - measuredTrailingWidth
val textToDraw = if (measuredTextWidth > availableWidthForText) { val textToDraw = if (measuredTextWidth > availableWidthForText) {
TextUtils.ellipsize( TextUtils.ellipsize(displayText, textPaint, availableWidthForText, TextUtils.TruncateAt.END)
displayText,
textPaint,
availableWidthForText,
TextUtils.TruncateAt.END
)
} else { } else {
displayText displayText
} }
canvas.drawText(textToDraw, 0, textToDraw.length, x + startPadding, y.toFloat(), textPaint) 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)
}
} }
} }
@@ -18,6 +18,7 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache 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.messages.RoomNamesCache
import io.element.android.libraries.matrix.ui.model.toEmojiText
interface MentionSpanUpdater { interface MentionSpanUpdater {
fun updateMentionSpans(text: CharSequence): CharSequence fun updateMentionSpans(text: CharSequence): CharSequence
@@ -48,6 +49,13 @@ class DefaultMentionSpanUpdater(
for (mentionSpan in text.getMentionSpans()) { for (mentionSpan in text.getMentionSpans()) {
mentionSpan.updateTheme(theme) mentionSpan.updateTheme(theme)
mentionSpan.updateDisplayText(formatter) mentionSpan.updateDisplayText(formatter)
mentionSpan.updateTrailingText(
if (mentionSpan.type is MentionType.User) {
roomMemberProfilesCache.getDisplayedStatus(mentionSpan.type.userId)?.toEmojiText()
} else {
null
}
)
} }
return text return text
} }