mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Display user status next to display names across rooms and profiles
This commit is contained in:
+15
-11
@@ -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(
|
||||
|
||||
+1
@@ -76,6 +76,7 @@ class RoomListRoomSummaryFactory(
|
||||
}.toImmutableList(),
|
||||
isTombstoned = roomInfo.successorRoom != null,
|
||||
isSpace = roomInfo.isSpace,
|
||||
dmUserStatus = if (roomInfo.isDm) roomInfo.heroes.firstOrNull()?.displayedStatus else null,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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<AvatarData>,
|
||||
val isSpace: Boolean,
|
||||
val dmUserStatus: DisplayedStatus?,
|
||||
) {
|
||||
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
||||
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
||||
|
||||
+4
-1
@@ -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<AvatarData> = 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,
|
||||
)
|
||||
|
||||
+1
@@ -319,6 +319,7 @@ class MessagesPresenter(
|
||||
appName = buildMeta.applicationName,
|
||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||
dmUserVerificationState = dmUserVerificationState,
|
||||
dmUserStatus = dmRoomMember?.displayedStatus,
|
||||
roomMemberModerationState = roomMemberModerationState,
|
||||
topBarSharedHistoryIcon = topBarSharedHistoryIcon,
|
||||
successorRoom = roomInfo.successorRoom,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+3
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
+1
@@ -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 = {
|
||||
|
||||
+13
-6
@@ -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(
|
||||
|
||||
+6
-1
@@ -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!",
|
||||
|
||||
+1
@@ -166,6 +166,7 @@ internal fun MessageShieldViewPreview() {
|
||||
displayName = USER_NAME_ALICE,
|
||||
displayNameAmbiguous = false,
|
||||
avatarUrl = null,
|
||||
displayedStatus = null,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
+2
@@ -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,
|
||||
)
|
||||
|
||||
+20
-9
@@ -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<AvatarData>,
|
||||
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<AvatarData>,
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -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,
|
||||
|
||||
+36
-25
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -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<RoomMemberListState> {
|
||||
@@ -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(
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+2
@@ -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(
|
||||
|
||||
+16
-1
@@ -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(
|
||||
|
||||
+2
@@ -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 {
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+16
-1
@@ -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 = {},
|
||||
|
||||
+3
@@ -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<UserProfileState> {
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
+1
@@ -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)
|
||||
},
|
||||
|
||||
+1
-1
@@ -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)
|
||||
) {
|
||||
|
||||
+2
@@ -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].
|
||||
|
||||
+3
-1
@@ -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(
|
||||
|
||||
+3
@@ -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)")
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -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(
|
||||
|
||||
+2
-1
@@ -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,
|
||||
)
|
||||
|
||||
+62
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-7
@@ -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()) {
|
||||
|
||||
+4
@@ -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<MatrixUser> {
|
||||
@@ -28,6 +29,7 @@ open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
|
||||
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(
|
||||
|
||||
+10
-1
@@ -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,
|
||||
|
||||
+32
-9
@@ -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(
|
||||
|
||||
+6
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
@@ -167,4 +167,5 @@ fun aProfileDetailsReady(
|
||||
displayName = displayName,
|
||||
displayNameAmbiguous = displayNameAmbiguous,
|
||||
avatarUrl = avatarUrl,
|
||||
displayedStatus = null,
|
||||
)
|
||||
|
||||
+18
-1
@@ -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,
|
||||
|
||||
+4
-1
@@ -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<SenderNameData> {
|
||||
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,
|
||||
)
|
||||
|
||||
+24
@@ -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>
|
||||
+3
@@ -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(
|
||||
|
||||
+21
-9
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user