mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-07 11:00:22 +00:00
Merge pull request #7232 from element-hq/feature/fga/display_user_status
WIP : display user status
This commit is contained in:
+14
-11
@@ -63,6 +63,8 @@ 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.DisplayNameWithStatus
|
||||
import io.element.android.libraries.matrix.ui.components.InviteSenderView
|
||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -126,7 +128,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 +145,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 +222,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,
|
||||
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(
|
||||
|
||||
+2
@@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.room.CallIntentConsensus
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.ui.model.dmUserStatus
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -76,6 +77,7 @@ class RoomListRoomSummaryFactory(
|
||||
}.toImmutableList(),
|
||||
isTombstoned = roomInfo.successorRoom != null,
|
||||
isSpace = roomInfo.isSpace,
|
||||
dmUserStatus = roomInfo.dmUserStatus(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+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,
|
||||
)
|
||||
|
||||
+4
-1
@@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -87,6 +88,7 @@ internal fun createRoomListRoomSummary(
|
||||
timestamp: String? = null,
|
||||
isTombstoned: Boolean = false,
|
||||
isSpace: Boolean = false,
|
||||
dmUserStatus: DisplayedStatus? = null,
|
||||
) = RoomListRoomSummary(
|
||||
id = A_ROOM_ID.value,
|
||||
roomId = A_ROOM_ID,
|
||||
@@ -109,5 +111,6 @@ internal fun createRoomListRoomSummary(
|
||||
isDm = false,
|
||||
heroes = heroes.toImmutableList(),
|
||||
isTombstoned = isTombstoned,
|
||||
isSpace = isSpace
|
||||
isSpace = isSpace,
|
||||
dmUserStatus = dmUserStatus,
|
||||
)
|
||||
|
||||
+2
@@ -84,6 +84,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsSta
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||
import io.element.android.libraries.matrix.ui.model.dmUserStatus
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||
@@ -319,6 +320,7 @@ class MessagesPresenter(
|
||||
appName = buildMeta.applicationName,
|
||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||
dmUserVerificationState = dmUserVerificationState,
|
||||
dmUserStatus = roomInfo.dmUserStatus(),
|
||||
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
|
||||
@@ -128,6 +129,7 @@ fun aMessagesState(
|
||||
hasUnreadThreads = false,
|
||||
),
|
||||
isCurrentlySharingLiveLocationInRoom: Boolean = false,
|
||||
dmUserStatus: DisplayedStatus? = null,
|
||||
eventSink: (MessagesEvent) -> Unit = {},
|
||||
) = MessagesState(
|
||||
roomId = RoomId("!id:domain"),
|
||||
@@ -158,6 +160,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,6 +41,8 @@ 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.api.room.getBestName
|
||||
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion
|
||||
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||
@@ -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.getBestName()
|
||||
is ResolvedSuggestion.Alias -> suggestion.roomName
|
||||
is ResolvedSuggestion.Command -> suggestion.command.command
|
||||
}
|
||||
val status = when (suggestion) {
|
||||
is ResolvedSuggestion.Member -> suggestion.roomMember.displayedStatus
|
||||
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 = status,
|
||||
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,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
+12
-2
@@ -315,7 +315,12 @@ private fun inReplyToInvalidContent(): InReplyToDetails.Ready = InReplyToDetails
|
||||
)
|
||||
),
|
||||
textContent = "A text content",
|
||||
senderProfile = ProfileDetails.Ready(displayName = "Sender", displayNameAmbiguous = false, avatarUrl = null)
|
||||
senderProfile = ProfileDetails.Ready(
|
||||
displayName = "Sender",
|
||||
displayNameAmbiguous = false,
|
||||
avatarUrl = null,
|
||||
displayedStatus = null
|
||||
)
|
||||
)
|
||||
|
||||
private fun inReplyToTextContent(): InReplyToDetails.Ready = InReplyToDetails.Ready(
|
||||
@@ -332,5 +337,10 @@ private fun inReplyToTextContent(): InReplyToDetails.Ready = InReplyToDetails.Re
|
||||
)
|
||||
),
|
||||
textContent = "A text content",
|
||||
senderProfile = ProfileDetails.Ready(displayName = "Sender", displayNameAmbiguous = false, avatarUrl = null)
|
||||
senderProfile = ProfileDetails.Ready(
|
||||
displayName = "Sender",
|
||||
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,
|
||||
)
|
||||
|
||||
+19
-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,9 +45,10 @@ 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.ui.strings.CommonStrings
|
||||
@@ -65,6 +65,7 @@ internal fun MessagesViewTopBar(
|
||||
heroes: ImmutableList<AvatarData>,
|
||||
dmUserIdentityState: IdentityState?,
|
||||
sharedHistoryIcon: SharedHistoryIcon,
|
||||
dmUserStatus: DisplayedStatus?,
|
||||
onRoomDetailsClick: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -91,6 +92,7 @@ internal fun MessagesViewTopBar(
|
||||
roomAvatar = roomAvatar,
|
||||
isTombstoned = isTombstoned,
|
||||
heroes = heroes,
|
||||
dmUserStatus = dmUserStatus,
|
||||
modifier = titleModifier
|
||||
)
|
||||
|
||||
@@ -144,6 +146,7 @@ private fun RoomAvatarAndNameRow(
|
||||
roomAvatar: AvatarData,
|
||||
heroes: ImmutableList<AvatarData>,
|
||||
isTombstoned: Boolean,
|
||||
dmUserStatus: DisplayedStatus?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
@@ -157,14 +160,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,
|
||||
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 +186,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 +195,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
|
||||
heroes = heroes,
|
||||
dmUserIdentityState = dmUserIdentityState,
|
||||
sharedHistoryIcon = sharedHistoryIcon,
|
||||
dmUserStatus = dmUserStatus,
|
||||
onRoomDetailsClick = {},
|
||||
onBackClick = {},
|
||||
menuActions = {
|
||||
@@ -246,5 +250,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
-5
@@ -41,9 +41,9 @@ sealed interface EmojiPickerSheetState {
|
||||
}
|
||||
|
||||
enum class PredefinedUserStatus(val emoji: String, @StringRes val labelRes: Int) {
|
||||
IN_A_MEETING("💬", R.string.common_user_status_in_a_meeting),
|
||||
FOCUS_TIME("💡", R.string.common_user_status_focus_time),
|
||||
ON_THE_ROAD("🚙", R.string.common_user_status_on_the_road),
|
||||
BE_RIGHT_BACK("☕", R.string.common_user_status_be_right_back),
|
||||
AWAY("🌴", R.string.common_user_status_away),
|
||||
IN_A_MEETING("💬", R.string.screen_settings_user_status_in_a_meeting),
|
||||
FOCUS_TIME("💡", R.string.screen_settings_user_status_focus_time),
|
||||
ON_THE_ROAD("🚙", R.string.screen_settings_user_status_on_the_road),
|
||||
BE_RIGHT_BACK("☕", R.string.screen_settings_user_status_be_right_back),
|
||||
AWAY("🌴", R.string.screen_settings_user_status_away),
|
||||
}
|
||||
|
||||
+4
-4
@@ -132,7 +132,7 @@ private fun EmptyStatusRow(
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_preferences_user_status_placeholder),
|
||||
text = stringResource(R.string.screen_settings_user_status_placeholder),
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
)
|
||||
},
|
||||
@@ -150,7 +150,7 @@ private fun CurrentStatusRow(
|
||||
) {
|
||||
val (emoji, text) = when (displayedStatus) {
|
||||
is DisplayedStatus.UserSet -> displayedStatus.status.emoji to displayedStatus.status.text
|
||||
is DisplayedStatus.InCall -> "🎧" to stringResource(R.string.common_user_status_on_a_call)
|
||||
is DisplayedStatus.InCall -> "🎧" to stringResource(CommonStrings.common_on_a_call)
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = { Text(text = text, modifier = Modifier.padding(vertical = 16.dp)) },
|
||||
@@ -225,7 +225,7 @@ private fun UserStatusPickerBottomSheet(
|
||||
}
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = stringResource(R.string.common_user_status_custom))
|
||||
Text(text = stringResource(R.string.screen_settings_user_status_custom))
|
||||
},
|
||||
leadingContent = ListItemContent.Custom { EmojiText("✏️") },
|
||||
onClick = {
|
||||
@@ -265,7 +265,7 @@ private fun CustomStatusInputRow(
|
||||
TextField(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
state = textFieldState,
|
||||
placeholder = stringResource(R.string.screen_preferences_user_status_custom_hint),
|
||||
placeholder = stringResource(R.string.screen_settings_user_status_custom_hint),
|
||||
inputTransformation = InputTransformation.maxLength(CUSTOM_STATUS_TEXT_MAX_LENGTH),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
onKeyboardAction = { if (hasChanges) onConfirm() },
|
||||
|
||||
@@ -101,6 +101,14 @@ If you proceed, some of your settings may change."</string>
|
||||
<string name="screen_notification_settings_system_notifications_action_required_content_link">"system settings"</string>
|
||||
<string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string>
|
||||
<string name="screen_notification_settings_title">"Notifications"</string>
|
||||
<string name="screen_settings_user_status_away">"Away"</string>
|
||||
<string name="screen_settings_user_status_be_right_back">"Be right back"</string>
|
||||
<string name="screen_settings_user_status_custom">"Custom…"</string>
|
||||
<string name="screen_settings_user_status_custom_hint">"Custom status…"</string>
|
||||
<string name="screen_settings_user_status_focus_time">"Focus time"</string>
|
||||
<string name="screen_settings_user_status_in_a_meeting">"In a meeting"</string>
|
||||
<string name="screen_settings_user_status_on_the_road">"On the road"</string>
|
||||
<string name="screen_settings_user_status_placeholder">"What\'s your status?"</string>
|
||||
<string name="theme_black">"Black"</string>
|
||||
<string name="theme_dark">"Dark"</string>
|
||||
<string name="theme_light">"Light"</string>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<?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_in_a_meeting">In a meeting</string>
|
||||
<string name="common_user_status_focus_time">Focus time</string>
|
||||
<string name="common_user_status_on_the_road">On the road</string>
|
||||
<string name="common_user_status_be_right_back">Be right back</string>
|
||||
<string name="common_user_status_away">Away</string>
|
||||
<string name="common_user_status_on_a_call">On a call</string>
|
||||
<string name="common_user_status_custom">Custom…</string>
|
||||
<string name="screen_preferences_user_status_placeholder">What\'s your status?</string>
|
||||
<string name="screen_preferences_user_status_custom_hint">Custom status…</string>
|
||||
</resources>
|
||||
+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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.test.room
|
||||
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(
|
||||
@@ -24,6 +25,7 @@ fun aRoomMember(
|
||||
role: RoomMember.Role = RoomMember.Role.User,
|
||||
membershipChangeReason: String? = null,
|
||||
isServiceMember: Boolean = false,
|
||||
displayedStatus: DisplayedStatus? = null,
|
||||
) = RoomMember(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
@@ -35,6 +37,7 @@ fun aRoomMember(
|
||||
role = role,
|
||||
membershipChangeReason = membershipChangeReason,
|
||||
isServiceMember = isServiceMember,
|
||||
displayedStatus = displayedStatus,
|
||||
)
|
||||
|
||||
fun aRoomMemberList() = persistentListOf(
|
||||
|
||||
+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,
|
||||
)
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||
import io.element.android.libraries.matrix.ui.model.toEmojiText
|
||||
|
||||
/**
|
||||
* Component to display side by side a display name and an optional status.
|
||||
*/
|
||||
@Composable
|
||||
fun DisplayNameWithStatus(
|
||||
name: String,
|
||||
status: DisplayedStatus?,
|
||||
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.toEmojiText(),
|
||||
style = style,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-7
@@ -18,7 +18,6 @@ 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.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -58,13 +57,11 @@ fun MatrixUserHeader(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
modifier = Modifier.clipToBounds(),
|
||||
text = matrixUser.getBestName(),
|
||||
maxLines = 1,
|
||||
DisplayNameWithStatus(
|
||||
name = matrixUser.getBestName(),
|
||||
status = matrixUser.displayedStatus,
|
||||
nameColor = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontHeadingMdRegular,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
// 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(
|
||||
|
||||
+9
-1
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -29,7 +30,14 @@ fun MatrixUserRow(
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
||||
) = UserRow(
|
||||
avatarData = matrixUser.getAvatarData(avatarSize),
|
||||
name = matrixUser.getBestName(),
|
||||
nameContent = {
|
||||
DisplayNameWithStatus(
|
||||
name = matrixUser.getBestName(),
|
||||
status = matrixUser.displayedStatus,
|
||||
nameColor = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
)
|
||||
},
|
||||
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||
modifier = modifier,
|
||||
verticalSpaceWidth = verticalSpaceWidth,
|
||||
|
||||
+33
-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,37 @@ 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,
|
||||
color = if (enabled) ElementTheme.colors.textPrimary else ElementTheme.colors.textDisabled,
|
||||
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 +83,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(
|
||||
|
||||
+5
@@ -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,8 @@ 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.ui.strings.CommonStrings
|
||||
|
||||
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(CommonStrings.common_on_a_call)}"
|
||||
}
|
||||
+12
@@ -13,6 +13,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||
|
||||
fun RoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
|
||||
id = id.value,
|
||||
@@ -45,3 +46,14 @@ fun RoomInfo.roleOf(userId: UserId): RoomMember.Role {
|
||||
val powerLevel = powerLevelOf(userId = userId)
|
||||
return RoomMember.Role.forPowerLevel(powerLevel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the [DisplayedStatus] of the other member in a DM.
|
||||
*/
|
||||
fun RoomInfo.dmUserStatus(): DisplayedStatus? {
|
||||
return if (isDm) {
|
||||
heroes.firstOrNull()?.displayedStatus
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
+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,
|
||||
displayedStatus: DisplayedStatus? = null,
|
||||
) = RoomMember(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
@@ -45,6 +47,7 @@ fun aRoomMember(
|
||||
role = role,
|
||||
membershipChangeReason = membershipChangeReason,
|
||||
isServiceMember = isServiceMember,
|
||||
displayedStatus = displayedStatus,
|
||||
)
|
||||
|
||||
fun aRoomMemberList() = persistentListOf(
|
||||
|
||||
+19
-9
@@ -39,11 +39,15 @@ 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
|
||||
private set
|
||||
|
||||
/**
|
||||
* Updates the visual properties of this span.
|
||||
*/
|
||||
@@ -80,6 +84,10 @@ class MentionSpan(
|
||||
displayText = formatter.formatDisplayText(type)
|
||||
}
|
||||
|
||||
fun updateTrailingText(trailingText: String?) {
|
||||
this.trailingText = trailingText
|
||||
}
|
||||
|
||||
override fun getSize(
|
||||
paint: Paint,
|
||||
text: CharSequence?,
|
||||
@@ -91,7 +99,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 +115,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 +128,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
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<string name="a11y_join_call">"Join call"</string>
|
||||
<string name="a11y_jump_to_bottom">"Jump to bottom"</string>
|
||||
<string name="a11y_jump_to_unread">"Jump to unread"</string>
|
||||
<string name="a11y_jump_to_unread_messages">"Jump to first unread message"</string>
|
||||
<string name="a11y_move_the_map_to_my_location">"Move the map to my location"</string>
|
||||
<string name="a11y_notifications_mentions_only">"Mentions only"</string>
|
||||
<string name="a11y_notifications_muted">"Muted"</string>
|
||||
@@ -143,6 +144,7 @@
|
||||
<string name="action_manage_account_and_devices">"Manage account & devices"</string>
|
||||
<string name="action_manage_devices">"Manage devices"</string>
|
||||
<string name="action_manage_rooms">"Manage rooms"</string>
|
||||
<string name="action_mark_as_read">"Mark as read"</string>
|
||||
<string name="action_message">"Message"</string>
|
||||
<string name="action_minimize">"Minimise"</string>
|
||||
<string name="action_next">"Next"</string>
|
||||
@@ -515,6 +517,8 @@ Are you sure you want to continue?"</string>
|
||||
<item quantity="other">"%1$d people"</item>
|
||||
</plurals>
|
||||
<string name="screen_live_location_sheet_title">"On the map"</string>
|
||||
<string name="screen_local_network_opt_in_subtitle">"Your homeserver is on your local network. To connect, Element needs permission to reach devices on this network."</string>
|
||||
<string name="screen_local_network_opt_in_title">"Allow access to local network"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_description">"Press on a message and choose “%1$s” to include here."</string>
|
||||
<string name="screen_pinned_timeline_empty_state_headline">"Pin important messages so that they can be easily discovered"</string>
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="action_mark_as_read">"Mark as read"</string>
|
||||
<string name="a11y_jump_to_unread_messages">"Jump to first unread message"</string>
|
||||
<string name="a11y_state_expanded">"Expanded"</string>
|
||||
<string name="a11y_state_collapsed">"Collapsed"</string>
|
||||
<string name="screen_local_network_opt_in_title">Allow access to local network</string>
|
||||
<string name="screen_local_network_opt_in_subtitle">Your homeserver is on your local network. To connect, Element needs permission to reach other devices on this network.</string>
|
||||
<string name="a11y_active">Active</string>
|
||||
<string name="a11y_inactive">Inactive</string>
|
||||
</resources>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user