mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 06:55:41 +00:00
Display user status next to display names across rooms and profiles
This commit is contained in:
+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