mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-06 16:43:43 +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.designsystem.theme.unreadIndicator
|
||||||
import io.element.android.libraries.matrix.api.notification.CallIntent
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
|
||||||
import io.element.android.libraries.matrix.ui.components.InviteSenderView
|
import io.element.android.libraries.matrix.ui.components.InviteSenderView
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -126,7 +128,8 @@ internal fun RoomSummaryRow(
|
|||||||
NameAndTimestampRow(
|
NameAndTimestampRow(
|
||||||
name = room.name,
|
name = room.name,
|
||||||
timestamp = room.timestamp,
|
timestamp = room.timestamp,
|
||||||
isHighlighted = room.isHighlighted
|
isHighlighted = room.isHighlighted,
|
||||||
|
dmUserStatus = room.dmUserStatus,
|
||||||
)
|
)
|
||||||
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
|
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
|
||||||
}
|
}
|
||||||
@@ -142,7 +145,8 @@ internal fun RoomSummaryRow(
|
|||||||
NameAndTimestampRow(
|
NameAndTimestampRow(
|
||||||
name = room.name,
|
name = room.name,
|
||||||
timestamp = null,
|
timestamp = null,
|
||||||
isHighlighted = room.isHighlighted
|
isHighlighted = room.isHighlighted,
|
||||||
|
dmUserStatus = null,
|
||||||
)
|
)
|
||||||
if (room.canonicalAlias != null) {
|
if (room.canonicalAlias != null) {
|
||||||
Text(
|
Text(
|
||||||
@@ -218,22 +222,21 @@ private fun NameAndTimestampRow(
|
|||||||
name: String?,
|
name: String?,
|
||||||
timestamp: String?,
|
timestamp: String?,
|
||||||
isHighlighted: Boolean,
|
isHighlighted: Boolean,
|
||||||
|
dmUserStatus: DisplayedStatus?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = spacedBy(16.dp)
|
horizontalArrangement = spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
val displayName = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name)
|
||||||
modifier = Modifier
|
DisplayNameWithStatus(
|
||||||
.weight(1f)
|
name = displayName,
|
||||||
.clipToBounds(),
|
status = dmUserStatus,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name),
|
nameColor = ElementTheme.colors.roomListRoomName,
|
||||||
fontStyle = FontStyle.Italic.takeIf { name == null },
|
nameFontStyle = FontStyle.Italic.takeIf { name == null },
|
||||||
color = ElementTheme.colors.roomListRoomName,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
// Timestamp
|
// Timestamp
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
+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.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
|
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
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.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -76,6 +77,7 @@ class RoomListRoomSummaryFactory(
|
|||||||
}.toImmutableList(),
|
}.toImmutableList(),
|
||||||
isTombstoned = roomInfo.successorRoom != null,
|
isTombstoned = roomInfo.successorRoom != null,
|
||||||
isSpace = roomInfo.isSpace,
|
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.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.notification.CallIntent
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ data class RoomListRoomSummary(
|
|||||||
val isTombstoned: Boolean,
|
val isTombstoned: Boolean,
|
||||||
val heroes: ImmutableList<AvatarData>,
|
val heroes: ImmutableList<AvatarData>,
|
||||||
val isSpace: Boolean,
|
val isSpace: Boolean,
|
||||||
|
val dmUserStatus: DisplayedStatus?,
|
||||||
) {
|
) {
|
||||||
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
||||||
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
||||||
|
|||||||
+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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.notification.CallIntent
|
import io.element.android.libraries.matrix.api.notification.CallIntent
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.ui.model.InviteSender
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@@ -182,6 +183,7 @@ internal fun aRoomListRoomSummary(
|
|||||||
heroes: List<AvatarData> = emptyList(),
|
heroes: List<AvatarData> = emptyList(),
|
||||||
isTombstoned: Boolean = false,
|
isTombstoned: Boolean = false,
|
||||||
isSpace: Boolean = false,
|
isSpace: Boolean = false,
|
||||||
|
dmUserStatus: DisplayedStatus? = null,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = id,
|
id = id,
|
||||||
roomId = RoomId(id),
|
roomId = RoomId(id),
|
||||||
@@ -204,5 +206,6 @@ internal fun aRoomListRoomSummary(
|
|||||||
canonicalAlias = canonicalAlias,
|
canonicalAlias = canonicalAlias,
|
||||||
heroes = heroes.toImmutableList(),
|
heroes = heroes.toImmutableList(),
|
||||||
isTombstoned = isTombstoned,
|
isTombstoned = isTombstoned,
|
||||||
isSpace = isSpace
|
isSpace = isSpace,
|
||||||
|
dmUserStatus = dmUserStatus,
|
||||||
)
|
)
|
||||||
|
|||||||
+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.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
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.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_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -87,6 +88,7 @@ internal fun createRoomListRoomSummary(
|
|||||||
timestamp: String? = null,
|
timestamp: String? = null,
|
||||||
isTombstoned: Boolean = false,
|
isTombstoned: Boolean = false,
|
||||||
isSpace: Boolean = false,
|
isSpace: Boolean = false,
|
||||||
|
dmUserStatus: DisplayedStatus? = null,
|
||||||
) = RoomListRoomSummary(
|
) = RoomListRoomSummary(
|
||||||
id = A_ROOM_ID.value,
|
id = A_ROOM_ID.value,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
@@ -109,5 +111,6 @@ internal fun createRoomListRoomSummary(
|
|||||||
isDm = false,
|
isDm = false,
|
||||||
heroes = heroes.toImmutableList(),
|
heroes = heroes.toImmutableList(),
|
||||||
isTombstoned = isTombstoned,
|
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.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
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.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.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
@@ -319,6 +320,7 @@ class MessagesPresenter(
|
|||||||
appName = buildMeta.applicationName,
|
appName = buildMeta.applicationName,
|
||||||
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
pinnedMessagesBannerState = pinnedMessagesBannerState,
|
||||||
dmUserVerificationState = dmUserVerificationState,
|
dmUserVerificationState = dmUserVerificationState,
|
||||||
|
dmUserStatus = roomInfo.dmUserStatus(),
|
||||||
roomMemberModerationState = roomMemberModerationState,
|
roomMemberModerationState = roomMemberModerationState,
|
||||||
topBarSharedHistoryIcon = topBarSharedHistoryIcon,
|
topBarSharedHistoryIcon = topBarSharedHistoryIcon,
|
||||||
successorRoom = roomInfo.successorRoom,
|
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.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||||
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class MessagesState(
|
data class MessagesState(
|
||||||
@@ -53,6 +54,7 @@ data class MessagesState(
|
|||||||
val appName: String,
|
val appName: String,
|
||||||
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
val pinnedMessagesBannerState: PinnedMessagesBannerState,
|
||||||
val dmUserVerificationState: IdentityState?,
|
val dmUserVerificationState: IdentityState?,
|
||||||
|
val dmUserStatus: DisplayedStatus?,
|
||||||
val roomMemberModerationState: RoomMemberModerationState,
|
val roomMemberModerationState: RoomMemberModerationState,
|
||||||
/** Type of "shared history" icon to show in the top bar. */
|
/** Type of "shared history" icon to show in the top bar. */
|
||||||
val topBarSharedHistoryIcon: SharedHistoryIcon,
|
val topBarSharedHistoryIcon: SharedHistoryIcon,
|
||||||
|
|||||||
+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.encryption.identity.IdentityState
|
||||||
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
|
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
|
||||||
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
|
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
|
||||||
@@ -128,6 +129,7 @@ fun aMessagesState(
|
|||||||
hasUnreadThreads = false,
|
hasUnreadThreads = false,
|
||||||
),
|
),
|
||||||
isCurrentlySharingLiveLocationInRoom: Boolean = false,
|
isCurrentlySharingLiveLocationInRoom: Boolean = false,
|
||||||
|
dmUserStatus: DisplayedStatus? = null,
|
||||||
eventSink: (MessagesEvent) -> Unit = {},
|
eventSink: (MessagesEvent) -> Unit = {},
|
||||||
) = MessagesState(
|
) = MessagesState(
|
||||||
roomId = RoomId("!id:domain"),
|
roomId = RoomId("!id:domain"),
|
||||||
@@ -158,6 +160,7 @@ fun aMessagesState(
|
|||||||
successorRoom = successorRoom,
|
successorRoom = successorRoom,
|
||||||
threads = threads,
|
threads = threads,
|
||||||
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom,
|
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom,
|
||||||
|
dmUserStatus = dmUserStatus,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+1
@@ -244,6 +244,7 @@ fun MessagesView(
|
|||||||
heroes = state.heroes,
|
heroes = state.heroes,
|
||||||
dmUserIdentityState = state.dmUserVerificationState,
|
dmUserIdentityState = state.dmUserVerificationState,
|
||||||
sharedHistoryIcon = state.topBarSharedHistoryIcon,
|
sharedHistoryIcon = state.topBarSharedHistoryIcon,
|
||||||
|
dmUserStatus = state.dmUserStatus,
|
||||||
onBackClick = { hidingKeyboard { onBackClick() } },
|
onBackClick = { hidingKeyboard { onBackClick() } },
|
||||||
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
|
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
|
||||||
menuActions = {
|
menuActions = {
|
||||||
|
|||||||
+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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
|
import io.element.android.libraries.matrix.api.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.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion
|
import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||||
@@ -112,12 +114,16 @@ private fun SuggestionItemView(
|
|||||||
is ResolvedSuggestion.Member -> AvatarType.User
|
is ResolvedSuggestion.Member -> AvatarType.User
|
||||||
is ResolvedSuggestion.Command -> null
|
is ResolvedSuggestion.Command -> null
|
||||||
}
|
}
|
||||||
val title = when (suggestion) {
|
val title: String? = when (suggestion) {
|
||||||
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
|
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
|
||||||
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName
|
is ResolvedSuggestion.Member -> suggestion.roomMember.getBestName()
|
||||||
is ResolvedSuggestion.Alias -> suggestion.roomName
|
is ResolvedSuggestion.Alias -> suggestion.roomName
|
||||||
is ResolvedSuggestion.Command -> suggestion.command.command
|
is ResolvedSuggestion.Command -> suggestion.command.command
|
||||||
}
|
}
|
||||||
|
val status = when (suggestion) {
|
||||||
|
is ResolvedSuggestion.Member -> suggestion.roomMember.displayedStatus
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
val details = when (suggestion) {
|
val details = when (suggestion) {
|
||||||
is ResolvedSuggestion.AtRoom,
|
is ResolvedSuggestion.AtRoom,
|
||||||
is ResolvedSuggestion.Member,
|
is ResolvedSuggestion.Member,
|
||||||
@@ -148,11 +154,11 @@ private fun SuggestionItemView(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
title?.let {
|
title?.let {
|
||||||
Text(
|
DisplayNameWithStatus(
|
||||||
text = it,
|
name = title,
|
||||||
|
status = status,
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
maxLines = 1,
|
nameColor = ElementTheme.colors.textPrimary,
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
details?.let {
|
details?.let {
|
||||||
@@ -191,6 +197,7 @@ internal fun SuggestionsPickerViewPreview() {
|
|||||||
role = RoomMember.Role.User,
|
role = RoomMember.Role.User,
|
||||||
membershipChangeReason = null,
|
membershipChangeReason = null,
|
||||||
isServiceMember = false,
|
isServiceMember = false,
|
||||||
|
displayedStatus = null,
|
||||||
)
|
)
|
||||||
val anAlias = remember { RoomAlias("#room:domain.org") }
|
val anAlias = remember { RoomAlias("#room:domain.org") }
|
||||||
SuggestionsPickerView(
|
SuggestionsPickerView(
|
||||||
|
|||||||
+6
-1
@@ -383,7 +383,12 @@ fun aThreadListItem(
|
|||||||
fun aThreadListItemEvent(
|
fun aThreadListItemEvent(
|
||||||
threadId: ThreadId = ThreadId("\$a-thread-id"),
|
threadId: ThreadId = ThreadId("\$a-thread-id"),
|
||||||
senderId: UserId = UserId("@a-user-id:server"),
|
senderId: UserId = UserId("@a-user-id:server"),
|
||||||
senderProfile: ProfileDetails = ProfileDetails.Ready(displayName = USER_NAME_ALICE, displayNameAmbiguous = false, avatarUrl = null),
|
senderProfile: ProfileDetails = ProfileDetails.Ready(
|
||||||
|
displayName = USER_NAME_ALICE,
|
||||||
|
displayNameAmbiguous = false,
|
||||||
|
avatarUrl = null,
|
||||||
|
displayedStatus = null,
|
||||||
|
),
|
||||||
isOwn: Boolean = false,
|
isOwn: Boolean = false,
|
||||||
content: EventContent = MessageContent(
|
content: EventContent = MessageContent(
|
||||||
body = "Hello world!",
|
body = "Hello world!",
|
||||||
|
|||||||
+1
@@ -166,6 +166,7 @@ internal fun MessageShieldViewPreview() {
|
|||||||
displayName = USER_NAME_ALICE,
|
displayName = USER_NAME_ALICE,
|
||||||
displayNameAmbiguous = false,
|
displayNameAmbiguous = false,
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
|
displayedStatus = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+12
-2
@@ -315,7 +315,12 @@ private fun inReplyToInvalidContent(): InReplyToDetails.Ready = InReplyToDetails
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
textContent = "A text content",
|
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(
|
private fun inReplyToTextContent(): InReplyToDetails.Ready = InReplyToDetails.Ready(
|
||||||
@@ -332,5 +337,10 @@ private fun inReplyToTextContent(): InReplyToDetails.Ready = InReplyToDetails.Re
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
textContent = "A text content",
|
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,
|
displayName = USER_NAME_ALICE,
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
displayNameAmbiguous = false,
|
displayNameAmbiguous = false,
|
||||||
|
displayedStatus = null,
|
||||||
),
|
),
|
||||||
timestamp = 0L,
|
timestamp = 0L,
|
||||||
)
|
)
|
||||||
@@ -1140,6 +1141,7 @@ internal fun ThreadSummaryViewPreview() {
|
|||||||
displayName = USER_NAME_ALICE,
|
displayName = USER_NAME_ALICE,
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
displayNameAmbiguous = true,
|
displayNameAmbiguous = true,
|
||||||
|
displayedStatus = null,
|
||||||
),
|
),
|
||||||
timestamp = 0L,
|
timestamp = 0L,
|
||||||
)
|
)
|
||||||
|
|||||||
+19
-9
@@ -26,7 +26,6 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.semantics.heading
|
import androidx.compose.ui.semantics.heading
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
@@ -46,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.preview.ROOM_NAME
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -65,6 +65,7 @@ internal fun MessagesViewTopBar(
|
|||||||
heroes: ImmutableList<AvatarData>,
|
heroes: ImmutableList<AvatarData>,
|
||||||
dmUserIdentityState: IdentityState?,
|
dmUserIdentityState: IdentityState?,
|
||||||
sharedHistoryIcon: SharedHistoryIcon,
|
sharedHistoryIcon: SharedHistoryIcon,
|
||||||
|
dmUserStatus: DisplayedStatus?,
|
||||||
onRoomDetailsClick: () -> Unit,
|
onRoomDetailsClick: () -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -91,6 +92,7 @@ internal fun MessagesViewTopBar(
|
|||||||
roomAvatar = roomAvatar,
|
roomAvatar = roomAvatar,
|
||||||
isTombstoned = isTombstoned,
|
isTombstoned = isTombstoned,
|
||||||
heroes = heroes,
|
heroes = heroes,
|
||||||
|
dmUserStatus = dmUserStatus,
|
||||||
modifier = titleModifier
|
modifier = titleModifier
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -144,6 +146,7 @@ private fun RoomAvatarAndNameRow(
|
|||||||
roomAvatar: AvatarData,
|
roomAvatar: AvatarData,
|
||||||
heroes: ImmutableList<AvatarData>,
|
heroes: ImmutableList<AvatarData>,
|
||||||
isTombstoned: Boolean,
|
isTombstoned: Boolean,
|
||||||
|
dmUserStatus: DisplayedStatus?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -157,14 +160,13 @@ private fun RoomAvatarAndNameRow(
|
|||||||
isTombstoned = isTombstoned,
|
isTombstoned = isTombstoned,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
Text(
|
DisplayNameWithStatus(
|
||||||
modifier = Modifier
|
name = roomName ?: stringResource(CommonStrings.common_no_room_name),
|
||||||
.padding(start = 8.dp),
|
status = dmUserStatus,
|
||||||
text = roomName ?: stringResource(CommonStrings.common_no_room_name),
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
fontStyle = FontStyle.Italic.takeIf { roomName == null },
|
nameColor = ElementTheme.colors.textPrimary,
|
||||||
maxLines = 1,
|
nameFontStyle = FontStyle.Italic.takeIf { roomName == null },
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,6 +186,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
|
|||||||
roomCallState: RoomCallState = RoomCallState.Unavailable,
|
roomCallState: RoomCallState = RoomCallState.Unavailable,
|
||||||
dmUserIdentityState: IdentityState? = null,
|
dmUserIdentityState: IdentityState? = null,
|
||||||
sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE,
|
sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE,
|
||||||
|
dmUserStatus: DisplayedStatus? = null,
|
||||||
displayThreads: Boolean = false,
|
displayThreads: Boolean = false,
|
||||||
) = MessagesViewTopBar(
|
) = MessagesViewTopBar(
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
@@ -192,6 +195,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
|
|||||||
heroes = heroes,
|
heroes = heroes,
|
||||||
dmUserIdentityState = dmUserIdentityState,
|
dmUserIdentityState = dmUserIdentityState,
|
||||||
sharedHistoryIcon = sharedHistoryIcon,
|
sharedHistoryIcon = sharedHistoryIcon,
|
||||||
|
dmUserStatus = dmUserStatus,
|
||||||
onRoomDetailsClick = {},
|
onRoomDetailsClick = {},
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
menuActions = {
|
menuActions = {
|
||||||
@@ -246,5 +250,11 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
|
|||||||
AMessagesViewTopBar(
|
AMessagesViewTopBar(
|
||||||
displayThreads = true,
|
displayThreads = true,
|
||||||
)
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
AMessagesViewTopBar(
|
||||||
|
roomName = "A DM with a very very very long name",
|
||||||
|
dmUserIdentityState = IdentityState.Verified,
|
||||||
|
dmUserStatus = DisplayedStatus.InCall(0L)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -41,9 +41,9 @@ sealed interface EmojiPickerSheetState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class PredefinedUserStatus(val emoji: String, @StringRes val labelRes: Int) {
|
enum class PredefinedUserStatus(val emoji: String, @StringRes val labelRes: Int) {
|
||||||
IN_A_MEETING("💬", R.string.common_user_status_in_a_meeting),
|
IN_A_MEETING("💬", R.string.screen_settings_user_status_in_a_meeting),
|
||||||
FOCUS_TIME("💡", R.string.common_user_status_focus_time),
|
FOCUS_TIME("💡", R.string.screen_settings_user_status_focus_time),
|
||||||
ON_THE_ROAD("🚙", R.string.common_user_status_on_the_road),
|
ON_THE_ROAD("🚙", R.string.screen_settings_user_status_on_the_road),
|
||||||
BE_RIGHT_BACK("☕", R.string.common_user_status_be_right_back),
|
BE_RIGHT_BACK("☕", R.string.screen_settings_user_status_be_right_back),
|
||||||
AWAY("🌴", R.string.common_user_status_away),
|
AWAY("🌴", R.string.screen_settings_user_status_away),
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -132,7 +132,7 @@ private fun EmptyStatusRow(
|
|||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
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),
|
modifier = Modifier.padding(vertical = 16.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -150,7 +150,7 @@ private fun CurrentStatusRow(
|
|||||||
) {
|
) {
|
||||||
val (emoji, text) = when (displayedStatus) {
|
val (emoji, text) = when (displayedStatus) {
|
||||||
is DisplayedStatus.UserSet -> displayedStatus.status.emoji to displayedStatus.status.text
|
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(
|
ListItem(
|
||||||
headlineContent = { Text(text = text, modifier = Modifier.padding(vertical = 16.dp)) },
|
headlineContent = { Text(text = text, modifier = Modifier.padding(vertical = 16.dp)) },
|
||||||
@@ -225,7 +225,7 @@ private fun UserStatusPickerBottomSheet(
|
|||||||
}
|
}
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(text = stringResource(R.string.common_user_status_custom))
|
Text(text = stringResource(R.string.screen_settings_user_status_custom))
|
||||||
},
|
},
|
||||||
leadingContent = ListItemContent.Custom { EmojiText("✏️") },
|
leadingContent = ListItemContent.Custom { EmojiText("✏️") },
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -265,7 +265,7 @@ private fun CustomStatusInputRow(
|
|||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
state = textFieldState,
|
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),
|
inputTransformation = InputTransformation.maxLength(CUSTOM_STATUS_TEXT_MAX_LENGTH),
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
onKeyboardAction = { if (hasChanges) onConfirm() },
|
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_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_system_notifications_turned_off">"System notifications turned off"</string>
|
||||||
<string name="screen_notification_settings_title">"Notifications"</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_black">"Black"</string>
|
||||||
<string name="theme_dark">"Dark"</string>
|
<string name="theme_dark">"Dark"</string>
|
||||||
<string name="theme_light">"Light"</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.RoomNotificationMode
|
||||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -75,6 +76,7 @@ fun aDmRoomMember(
|
|||||||
role: RoomMember.Role = RoomMember.Role.User,
|
role: RoomMember.Role = RoomMember.Role.User,
|
||||||
membershipChangeReason: String? = null,
|
membershipChangeReason: String? = null,
|
||||||
isServiceMember: Boolean = false,
|
isServiceMember: Boolean = false,
|
||||||
|
displayedStatus: DisplayedStatus? = null,
|
||||||
) = RoomMember(
|
) = RoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
@@ -86,6 +88,7 @@ fun aDmRoomMember(
|
|||||||
role = role,
|
role = role,
|
||||||
membershipChangeReason = membershipChangeReason,
|
membershipChangeReason = membershipChangeReason,
|
||||||
isServiceMember = isServiceMember,
|
isServiceMember = isServiceMember,
|
||||||
|
displayedStatus = displayedStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomDetailsState(
|
fun aRoomDetailsState(
|
||||||
@@ -178,12 +181,13 @@ fun aDmRoomDetailsState(
|
|||||||
roomName: String = "Daniel",
|
roomName: String = "Daniel",
|
||||||
isEncrypted: Boolean = true,
|
isEncrypted: Boolean = true,
|
||||||
dmRoomMemberVerificationState: UserProfileVerificationState = UserProfileVerificationState.UNKNOWN,
|
dmRoomMemberVerificationState: UserProfileVerificationState = UserProfileVerificationState.UNKNOWN,
|
||||||
|
displayedStatus: DisplayedStatus? = null,
|
||||||
) = aRoomDetailsState(
|
) = aRoomDetailsState(
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
isPublic = false,
|
isPublic = false,
|
||||||
isEncrypted = isEncrypted,
|
isEncrypted = isEncrypted,
|
||||||
canInvite = true,
|
canInvite = true,
|
||||||
roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)),
|
roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored, displayedStatus = displayedStatus)),
|
||||||
dmOtherMemberDetailsState = aUserProfileState(
|
dmOtherMemberDetailsState = aUserProfileState(
|
||||||
isBlocked = AsyncData.Success(isDmMemberIgnored),
|
isBlocked = AsyncData.Success(isDmMemberIgnored),
|
||||||
verificationState = dmRoomMemberVerificationState,
|
verificationState = dmRoomMemberVerificationState,
|
||||||
|
|||||||
+36
-25
@@ -37,6 +37,7 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import im.vector.app.features.analytics.plan.Interaction
|
import im.vector.app.features.analytics.plan.Interaction
|
||||||
@@ -88,6 +89,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
|||||||
import io.element.android.libraries.matrix.api.room.getBestName
|
import io.element.android.libraries.matrix.api.room.getBestName
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toText
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -489,11 +491,23 @@ private fun RoomHeaderSection(
|
|||||||
}
|
}
|
||||||
.testTag(TestTags.roomDetailAvatar)
|
.testTag(TestTags.roomDetailAvatar)
|
||||||
)
|
)
|
||||||
TitleAndSubtitle(
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
title = roomName,
|
Text(
|
||||||
subtitle = roomAlias?.value,
|
text = roomName,
|
||||||
onSubtitleClick = onSubtitleClick,
|
style = ElementTheme.typography.fontHeadingLgBold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
if (roomAlias != null) {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.niceClickable { onSubtitleClick(roomAlias.value) },
|
||||||
|
text = roomAlias.value,
|
||||||
|
style = ElementTheme.typography.fontBodyLgRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,37 +545,34 @@ private fun DmHeaderSection(
|
|||||||
}
|
}
|
||||||
.testTag(TestTags.roomDetailAvatar)
|
.testTag(TestTags.roomDetailAvatar)
|
||||||
)
|
)
|
||||||
TitleAndSubtitle(
|
|
||||||
title = roomName,
|
|
||||||
subtitle = otherMember.userId.value,
|
|
||||||
onSubtitleClick = onSubtitleClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun TitleAndSubtitle(
|
|
||||||
title: String,
|
|
||||||
subtitle: String?,
|
|
||||||
onSubtitleClick: (String) -> Unit,
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = roomName,
|
||||||
style = ElementTheme.typography.fontHeadingLgBold,
|
style = ElementTheme.typography.fontHeadingLgBold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
if (subtitle != null) {
|
val userStatus = otherMember.displayedStatus?.toText()
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
if (userStatus != null) {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.niceClickable { onSubtitleClick(subtitle) },
|
text = userStatus,
|
||||||
text = subtitle,
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
maxLines = 1,
|
||||||
color = ElementTheme.colors.textSecondary,
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
textAlign = TextAlign.Center,
|
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.encryption.identity.IdentityState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
||||||
@@ -129,6 +130,7 @@ fun aRoomMember(
|
|||||||
role: RoomMember.Role = RoomMember.Role.User,
|
role: RoomMember.Role = RoomMember.Role.User,
|
||||||
membershipChangeReason: String? = null,
|
membershipChangeReason: String? = null,
|
||||||
isServiceMember: Boolean = false,
|
isServiceMember: Boolean = false,
|
||||||
|
displayedStatus: DisplayedStatus? = null,
|
||||||
) = RoomMember(
|
) = RoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
@@ -140,6 +142,7 @@ fun aRoomMember(
|
|||||||
role = role,
|
role = role,
|
||||||
membershipChangeReason = membershipChangeReason,
|
membershipChangeReason = membershipChangeReason,
|
||||||
isServiceMember = isServiceMember,
|
isServiceMember = isServiceMember,
|
||||||
|
displayedStatus = displayedStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomMemberList() = persistentListOf(
|
fun aRoomMemberList() = persistentListOf(
|
||||||
|
|||||||
+1
@@ -128,6 +128,7 @@ class RoomMemberDetailsPresenter(
|
|||||||
return userProfileState.copy(
|
return userProfileState.copy(
|
||||||
userName = roomUserName ?: userProfileState.userName,
|
userName = roomUserName ?: userProfileState.userName,
|
||||||
avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl,
|
avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl,
|
||||||
|
displayedStatus = roomMember?.displayedStatus ?: userProfileState.displayedStatus,
|
||||||
verificationState = verificationState,
|
verificationState = verificationState,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
|
|||||||
+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.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ fun anAlice() = MatrixUser(
|
|||||||
userId = UserId(value = "@alice:server.org"),
|
userId = UserId(value = "@alice:server.org"),
|
||||||
displayName = USER_NAME_ALICE,
|
displayName = USER_NAME_ALICE,
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
|
displayedStatus = DisplayedStatus.InCall(0L),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomMembersModerationState(
|
fun aRoomMembersModerationState(
|
||||||
|
|||||||
+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.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toText
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -264,9 +265,23 @@ private fun RoomMemberActionsBottomSheet(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
|
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
val userStatus = user.displayedStatus?.toText()
|
||||||
|
if (userStatus != null) {
|
||||||
|
Text(
|
||||||
|
text = userStatus,
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
// Show user ID only if it's different from the display name
|
// Show user ID only if it's different from the display name
|
||||||
if (bestName != user.userId.value) {
|
if (bestName != user.userId.value) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
+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.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
|
||||||
data class UserProfileState(
|
data class UserProfileState(
|
||||||
val userId: UserId,
|
val userId: UserId,
|
||||||
@@ -26,6 +27,7 @@ data class UserProfileState(
|
|||||||
val dmRoomId: RoomId?,
|
val dmRoomId: RoomId?,
|
||||||
val canCall: Boolean,
|
val canCall: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
|
val displayedStatus: DisplayedStatus?,
|
||||||
val eventSink: (UserProfileEvents) -> Unit
|
val eventSink: (UserProfileEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
enum class ConfirmationDialog {
|
enum class ConfirmationDialog {
|
||||||
|
|||||||
+1
@@ -145,6 +145,7 @@ class UserProfilePresenter(
|
|||||||
userId = userId,
|
userId = userId,
|
||||||
userName = userProfile?.displayName,
|
userName = userProfile?.displayName,
|
||||||
avatarUrl = userProfile?.avatarUrl,
|
avatarUrl = userProfile?.avatarUrl,
|
||||||
|
displayedStatus = userProfile?.displayedStatus,
|
||||||
isBlocked = isBlocked.value,
|
isBlocked = isBlocked.value,
|
||||||
verificationState = UserProfileVerificationState.UNKNOWN,
|
verificationState = UserProfileVerificationState.UNKNOWN,
|
||||||
startDmActionState = startDmActionState.value,
|
startDmActionState = startDmActionState.value,
|
||||||
|
|||||||
+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.OutlinedButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toText
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -56,6 +58,7 @@ fun UserProfileHeaderSection(
|
|||||||
openAvatarPreview: (url: String) -> Unit,
|
openAvatarPreview: (url: String) -> Unit,
|
||||||
onUserIdClick: () -> Unit,
|
onUserIdClick: () -> Unit,
|
||||||
withdrawVerificationClick: () -> Unit,
|
withdrawVerificationClick: () -> Unit,
|
||||||
|
displayedStatus: DisplayedStatus?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -90,7 +93,16 @@ fun UserProfileHeaderSection(
|
|||||||
style = ElementTheme.typography.fontHeadingLgBold,
|
style = ElementTheme.typography.fontHeadingLgBold,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
displayedStatus?.let { status ->
|
||||||
|
Text(
|
||||||
|
text = status.toText(),
|
||||||
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.niceClickable { onUserIdClick() },
|
modifier = Modifier.niceClickable { onUserIdClick() },
|
||||||
@@ -99,6 +111,7 @@ fun UserProfileHeaderSection(
|
|||||||
color = ElementTheme.colors.textSecondary,
|
color = ElementTheme.colors.textSecondary,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
when (verificationState) {
|
when (verificationState) {
|
||||||
UserProfileVerificationState.UNKNOWN, UserProfileVerificationState.UNVERIFIED -> Unit
|
UserProfileVerificationState.UNKNOWN, UserProfileVerificationState.UNVERIFIED -> Unit
|
||||||
UserProfileVerificationState.VERIFIED -> {
|
UserProfileVerificationState.VERIFIED -> {
|
||||||
@@ -143,6 +156,7 @@ internal fun UserProfileHeaderSectionPreview() = ElementPreview {
|
|||||||
userId = UserId("@alice:example.com"),
|
userId = UserId("@alice:example.com"),
|
||||||
userName = USER_NAME_ALICE,
|
userName = USER_NAME_ALICE,
|
||||||
verificationState = UserProfileVerificationState.VERIFIED,
|
verificationState = UserProfileVerificationState.VERIFIED,
|
||||||
|
displayedStatus = DisplayedStatus.InCall(0L),
|
||||||
openAvatarPreview = {},
|
openAvatarPreview = {},
|
||||||
onUserIdClick = {},
|
onUserIdClick = {},
|
||||||
withdrawVerificationClick = {},
|
withdrawVerificationClick = {},
|
||||||
@@ -157,6 +171,7 @@ internal fun UserProfileHeaderSectionWithVerificationViolationPreview() = Elemen
|
|||||||
userId = UserId("@alice:example.com"),
|
userId = UserId("@alice:example.com"),
|
||||||
userName = USER_NAME_ALICE,
|
userName = USER_NAME_ALICE,
|
||||||
verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION,
|
verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION,
|
||||||
|
displayedStatus = DisplayedStatus.InCall(0L),
|
||||||
openAvatarPreview = {},
|
openAvatarPreview = {},
|
||||||
onUserIdClick = {},
|
onUserIdClick = {},
|
||||||
withdrawVerificationClick = {},
|
withdrawVerificationClick = {},
|
||||||
|
|||||||
+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.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
|
|
||||||
open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> {
|
open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> {
|
||||||
@@ -48,6 +49,7 @@ fun aUserProfileState(
|
|||||||
dmRoomId: RoomId? = null,
|
dmRoomId: RoomId? = null,
|
||||||
canCall: Boolean = false,
|
canCall: Boolean = false,
|
||||||
snackbarMessage: SnackbarMessage? = null,
|
snackbarMessage: SnackbarMessage? = null,
|
||||||
|
displayedStatus: DisplayedStatus? = null,
|
||||||
eventSink: (UserProfileEvents) -> Unit = {},
|
eventSink: (UserProfileEvents) -> Unit = {},
|
||||||
) = UserProfileState(
|
) = UserProfileState(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
@@ -61,5 +63,6 @@ fun aUserProfileState(
|
|||||||
dmRoomId = dmRoomId,
|
dmRoomId = dmRoomId,
|
||||||
canCall = canCall,
|
canCall = canCall,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
|
displayedStatus = displayedStatus,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
@@ -78,6 +78,7 @@ fun UserProfileView(
|
|||||||
userId = state.userId,
|
userId = state.userId,
|
||||||
userName = state.userName,
|
userName = state.userName,
|
||||||
verificationState = state.verificationState,
|
verificationState = state.verificationState,
|
||||||
|
displayedStatus = state.displayedStatus,
|
||||||
openAvatarPreview = { avatarUrl ->
|
openAvatarPreview = { avatarUrl ->
|
||||||
openAvatarPreview(state.userName ?: state.userId.value, avatarUrl)
|
openAvatarPreview(state.userName ?: state.userId.value, avatarUrl)
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ fun MatrixBadgeRowMolecule(
|
|||||||
) {
|
) {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(start = 16.dp, end = 16.dp, top = 8.dp),
|
.padding(start = 16.dp, end = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
+2
@@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.api.room
|
|||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
data class RoomMember(
|
data class RoomMember(
|
||||||
@@ -23,6 +24,7 @@ data class RoomMember(
|
|||||||
val role: Role,
|
val role: Role,
|
||||||
val membershipChangeReason: String?,
|
val membershipChangeReason: String?,
|
||||||
val isServiceMember: Boolean,
|
val isServiceMember: Boolean,
|
||||||
|
val displayedStatus: DisplayedStatus?,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Role of the RoomMember, based on its [powerLevel].
|
* Role of the RoomMember, based on its [powerLevel].
|
||||||
|
|||||||
+3
-1
@@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.api.timeline.item.event
|
|||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface ProfileDetails {
|
sealed interface ProfileDetails {
|
||||||
@@ -20,7 +21,8 @@ sealed interface ProfileDetails {
|
|||||||
data class Ready(
|
data class Ready(
|
||||||
val displayName: String?,
|
val displayName: String?,
|
||||||
val displayNameAmbiguous: Boolean,
|
val displayNameAmbiguous: Boolean,
|
||||||
val avatarUrl: String?
|
val avatarUrl: String?,
|
||||||
|
val displayedStatus: DisplayedStatus?,
|
||||||
) : ProfileDetails
|
) : ProfileDetails
|
||||||
|
|
||||||
data class Error(
|
data class Error(
|
||||||
|
|||||||
+3
@@ -38,6 +38,7 @@ class ProfileTimelineDetailsTest {
|
|||||||
displayName = null,
|
displayName = null,
|
||||||
displayNameAmbiguous = false,
|
displayNameAmbiguous = false,
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
|
displayedStatus = null,
|
||||||
).getDisambiguatedDisplayName(aUserId)
|
).getDisambiguatedDisplayName(aUserId)
|
||||||
).isEqualTo(A_USER_ID)
|
).isEqualTo(A_USER_ID)
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,7 @@ class ProfileTimelineDetailsTest {
|
|||||||
displayName = "Alice",
|
displayName = "Alice",
|
||||||
displayNameAmbiguous = false,
|
displayNameAmbiguous = false,
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
|
displayedStatus = null,
|
||||||
).getDisambiguatedDisplayName(aUserId)
|
).getDisambiguatedDisplayName(aUserId)
|
||||||
).isEqualTo("Alice")
|
).isEqualTo("Alice")
|
||||||
}
|
}
|
||||||
@@ -60,6 +62,7 @@ class ProfileTimelineDetailsTest {
|
|||||||
displayName = "Alice",
|
displayName = "Alice",
|
||||||
displayNameAmbiguous = true,
|
displayNameAmbiguous = true,
|
||||||
avatarUrl = null,
|
avatarUrl = null,
|
||||||
|
displayedStatus = null,
|
||||||
).getDisambiguatedDisplayName(aUserId)
|
).getDisambiguatedDisplayName(aUserId)
|
||||||
).isEqualTo("Alice ($A_USER_ID)")
|
).isEqualTo("Alice ($A_USER_ID)")
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -30,6 +30,8 @@ object RoomMemberMapper {
|
|||||||
role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel),
|
role = mapRole(roomMember.suggestedRoleForPowerLevel, powerLevel),
|
||||||
membershipChangeReason = roomMember.membershipChangeReason,
|
membershipChangeReason = roomMember.membershipChangeReason,
|
||||||
isServiceMember = roomMember.isServiceMember,
|
isServiceMember = roomMember.isServiceMember,
|
||||||
|
// TODO map this value when available
|
||||||
|
displayedStatus = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -74,7 +74,9 @@ fun RustProfileDetails.map(): ProfileDetails {
|
|||||||
is RustProfileDetails.Ready -> ProfileDetails.Ready(
|
is RustProfileDetails.Ready -> ProfileDetails.Ready(
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
displayNameAmbiguous = displayNameAmbiguous,
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
avatarUrl = avatarUrl
|
avatarUrl = avatarUrl,
|
||||||
|
// TODO map this value when available
|
||||||
|
displayedStatus = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
fun aRoomMember(
|
fun aRoomMember(
|
||||||
@@ -24,6 +25,7 @@ fun aRoomMember(
|
|||||||
role: RoomMember.Role = RoomMember.Role.User,
|
role: RoomMember.Role = RoomMember.Role.User,
|
||||||
membershipChangeReason: String? = null,
|
membershipChangeReason: String? = null,
|
||||||
isServiceMember: Boolean = false,
|
isServiceMember: Boolean = false,
|
||||||
|
displayedStatus: DisplayedStatus? = null,
|
||||||
) = RoomMember(
|
) = RoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
@@ -35,6 +37,7 @@ fun aRoomMember(
|
|||||||
role = role,
|
role = role,
|
||||||
membershipChangeReason = membershipChangeReason,
|
membershipChangeReason = membershipChangeReason,
|
||||||
isServiceMember = isServiceMember,
|
isServiceMember = isServiceMember,
|
||||||
|
displayedStatus = displayedStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomMemberList() = persistentListOf(
|
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.StickerContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemDebugInfoProvider
|
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemDebugInfoProvider
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
@@ -84,11 +85,13 @@ fun anEventTimelineItem(
|
|||||||
fun aProfileDetails(
|
fun aProfileDetails(
|
||||||
displayName: String? = A_USER_NAME,
|
displayName: String? = A_USER_NAME,
|
||||||
displayNameAmbiguous: Boolean = false,
|
displayNameAmbiguous: Boolean = false,
|
||||||
avatarUrl: String? = null
|
avatarUrl: String? = null,
|
||||||
|
displayedStatus: DisplayedStatus? = null,
|
||||||
): ProfileDetails = ProfileDetails.Ready(
|
): ProfileDetails = ProfileDetails.Ready(
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
displayNameAmbiguous = displayNameAmbiguous,
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
|
displayedStatus = displayedStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aProfileChangeMessageContent(
|
fun aProfileChangeMessageContent(
|
||||||
|
|||||||
+2
-1
@@ -19,6 +19,7 @@ import androidx.compose.runtime.Immutable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
@@ -50,7 +51,7 @@ fun CheckableUserRow(
|
|||||||
UserRow(
|
UserRow(
|
||||||
modifier = rowModifier,
|
modifier = rowModifier,
|
||||||
avatarData = data.avatarData,
|
avatarData = data.avatarData,
|
||||||
name = data.name,
|
name = AnnotatedString(data.name),
|
||||||
subtext = data.subtext,
|
subtext = data.subtext,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
)
|
)
|
||||||
|
|||||||
+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.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -58,13 +57,11 @@ fun MatrixUserHeader(
|
|||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
// Name
|
// Name
|
||||||
Text(
|
DisplayNameWithStatus(
|
||||||
modifier = Modifier.clipToBounds(),
|
name = matrixUser.getBestName(),
|
||||||
text = matrixUser.getBestName(),
|
status = matrixUser.displayedStatus,
|
||||||
maxLines = 1,
|
nameColor = ElementTheme.colors.textPrimary,
|
||||||
style = ElementTheme.typography.fontHeadingMdRegular,
|
style = ElementTheme.typography.fontHeadingMdRegular,
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
color = ElementTheme.colors.textPrimary,
|
|
||||||
)
|
)
|
||||||
// Id
|
// Id
|
||||||
if (matrixUser.displayName.isNullOrEmpty().not()) {
|
if (matrixUser.displayName.isNullOrEmpty().not()) {
|
||||||
|
|||||||
+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_VICTOR
|
||||||
import io.element.android.libraries.designsystem.preview.USER_NAME_WALTER
|
import io.element.android.libraries.designsystem.preview.USER_NAME_WALTER
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
|
open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
|
||||||
@@ -28,6 +29,7 @@ open class MatrixUserProvider : PreviewParameterProvider<MatrixUser> {
|
|||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aMatrixUser(),
|
aMatrixUser(),
|
||||||
aMatrixUser(displayName = null),
|
aMatrixUser(displayName = null),
|
||||||
|
aMatrixUser(displayedStatus = DisplayedStatus.InCall(0L)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +45,12 @@ fun aMatrixUser(
|
|||||||
id: String? = null,
|
id: String? = null,
|
||||||
displayName: String? = USER_NAME_ALICE,
|
displayName: String? = USER_NAME_ALICE,
|
||||||
avatarUrl: String? = null,
|
avatarUrl: String? = null,
|
||||||
|
displayedStatus: DisplayedStatus? = null,
|
||||||
) = MatrixUser(
|
) = MatrixUser(
|
||||||
userId = UserId(id ?: "@${displayName?.lowercase()?.replace(" ", "_") ?: "id"}:server.org"),
|
userId = UserId(id ?: "@${displayName?.lowercase()?.replace(" ", "_") ?: "id"}:server.org"),
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
|
displayedStatus = displayedStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aMatrixUserList() = listOf(
|
fun aMatrixUserList() = listOf(
|
||||||
|
|||||||
+9
-1
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
@@ -29,7 +30,14 @@ fun MatrixUserRow(
|
|||||||
trailingContent: @Composable (() -> Unit)? = null,
|
trailingContent: @Composable (() -> Unit)? = null,
|
||||||
) = UserRow(
|
) = UserRow(
|
||||||
avatarData = matrixUser.getAvatarData(avatarSize),
|
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,
|
subtext = if (matrixUser.displayName.isNullOrEmpty()) null else matrixUser.userId.value,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
verticalSpaceWidth = verticalSpaceWidth,
|
verticalSpaceWidth = verticalSpaceWidth,
|
||||||
|
|||||||
+33
-9
@@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -30,7 +31,37 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun UserRow(
|
internal fun UserRow(
|
||||||
avatarData: AvatarData,
|
avatarData: AvatarData,
|
||||||
name: String,
|
name: AnnotatedString,
|
||||||
|
subtext: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
verticalSpaceWidth: Dp = 12.dp,
|
||||||
|
trailingContent: @Composable (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
UserRow(
|
||||||
|
avatarData = avatarData,
|
||||||
|
nameContent = {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.clipToBounds(),
|
||||||
|
text = name,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
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?,
|
subtext: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
@@ -52,14 +83,7 @@ internal fun UserRow(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
// Name
|
// Name
|
||||||
Text(
|
nameContent()
|
||||||
modifier = Modifier.clipToBounds(),
|
|
||||||
text = name,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
color = if (enabled) ElementTheme.colors.textPrimary else ElementTheme.colors.textDisabled,
|
|
||||||
style = ElementTheme.typography.fontBodyLgRegular,
|
|
||||||
)
|
|
||||||
// Id
|
// Id
|
||||||
subtext?.let {
|
subtext?.let {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
+5
@@ -13,6 +13,7 @@ import dev.zacsweers.metro.SingleIn
|
|||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
@@ -31,4 +32,8 @@ class RoomMemberProfilesCache {
|
|||||||
fun getDisplayName(userId: UserId): String? {
|
fun getDisplayName(userId: UserId): String? {
|
||||||
return cache.value[userId]?.disambiguatedDisplayName
|
return cache.value[userId]?.disambiguatedDisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDisplayedStatus(userId: UserId): DisplayedStatus? {
|
||||||
|
return cache.value[userId]?.displayedStatus
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -167,4 +167,5 @@ fun aProfileDetailsReady(
|
|||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
displayNameAmbiguous = displayNameAmbiguous,
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
avatarUrl = avatarUrl,
|
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.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toText
|
||||||
|
|
||||||
// https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0
|
// https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0
|
||||||
@Composable
|
@Composable
|
||||||
@@ -54,6 +55,19 @@ fun SenderName(
|
|||||||
SecondaryText(text = senderId.value, mode = senderNameMode)
|
SecondaryText(text = senderId.value, mode = senderNameMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val userStatus = senderProfile.displayedStatus
|
||||||
|
if (senderNameMode is SenderNameMode.Timeline && userStatus != null) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.clipToBounds()
|
||||||
|
.alignByBaseline(),
|
||||||
|
text = userStatus.toText(),
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,13 +88,16 @@ private fun RowScope.MainText(
|
|||||||
SenderNameMode.ActionList,
|
SenderNameMode.ActionList,
|
||||||
SenderNameMode.Reply -> Modifier
|
SenderNameMode.Reply -> Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
val color = when (mode) {
|
val color = when (mode) {
|
||||||
is SenderNameMode.Timeline -> mode.mainColor
|
is SenderNameMode.Timeline -> mode.mainColor
|
||||||
SenderNameMode.ActionList,
|
SenderNameMode.ActionList,
|
||||||
SenderNameMode.Reply -> ElementTheme.colors.textPrimary
|
SenderNameMode.Reply -> ElementTheme.colors.textPrimary
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
modifier = modifier.clipToBounds(),
|
modifier = modifier
|
||||||
|
.clipToBounds()
|
||||||
|
.weight(1f, fill = false),
|
||||||
text = text,
|
text = text,
|
||||||
style = style,
|
style = style,
|
||||||
color = color,
|
color = color,
|
||||||
|
|||||||
+4
-1
@@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
|
||||||
data class SenderNameData(
|
data class SenderNameData(
|
||||||
val userId: UserId,
|
val userId: UserId,
|
||||||
@@ -47,12 +48,14 @@ open class SenderNameDataProvider : PreviewParameterProvider<SenderNameData> {
|
|||||||
private fun aSenderNameData(
|
private fun aSenderNameData(
|
||||||
senderNameMode: SenderNameMode,
|
senderNameMode: SenderNameMode,
|
||||||
displayNameAmbiguous: Boolean = false,
|
displayNameAmbiguous: Boolean = false,
|
||||||
|
displayedStatus: DisplayedStatus? = null,
|
||||||
) = SenderNameData(
|
) = SenderNameData(
|
||||||
userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"),
|
userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"),
|
||||||
profileDetails = ProfileDetails.Ready(
|
profileDetails = ProfileDetails.Ready(
|
||||||
displayName = "Alice ${senderNameMode.javaClass.simpleName}",
|
displayName = "Alice ${senderNameMode.javaClass.simpleName}",
|
||||||
displayNameAmbiguous = displayNameAmbiguous,
|
displayNameAmbiguous = displayNameAmbiguous,
|
||||||
avatarUrl = null
|
avatarUrl = null,
|
||||||
|
displayedStatus = displayedStatus,
|
||||||
),
|
),
|
||||||
senderNameMode = senderNameMode,
|
senderNameMode = senderNameMode,
|
||||||
)
|
)
|
||||||
|
|||||||
+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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
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.room.RoomMember
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
|
|
||||||
fun RoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
|
fun RoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
|
||||||
id = id.value,
|
id = id.value,
|
||||||
@@ -45,3 +46,14 @@ fun RoomInfo.roleOf(userId: UserId): RoomMember.Role {
|
|||||||
val powerLevel = powerLevelOf(userId = userId)
|
val powerLevel = powerLevelOf(userId = userId)
|
||||||
return RoomMember.Role.forPowerLevel(powerLevel)
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
|
import io.element.android.libraries.matrix.api.user.DisplayedStatus
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
fun aRoomMember(
|
fun aRoomMember(
|
||||||
@@ -34,6 +35,7 @@ fun aRoomMember(
|
|||||||
role: RoomMember.Role = RoomMember.Role.User,
|
role: RoomMember.Role = RoomMember.Role.User,
|
||||||
membershipChangeReason: String? = null,
|
membershipChangeReason: String? = null,
|
||||||
isServiceMember: Boolean = false,
|
isServiceMember: Boolean = false,
|
||||||
|
displayedStatus: DisplayedStatus? = null,
|
||||||
) = RoomMember(
|
) = RoomMember(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
@@ -45,6 +47,7 @@ fun aRoomMember(
|
|||||||
role = role,
|
role = role,
|
||||||
membershipChangeReason = membershipChangeReason,
|
membershipChangeReason = membershipChangeReason,
|
||||||
isServiceMember = isServiceMember,
|
isServiceMember = isServiceMember,
|
||||||
|
displayedStatus = displayedStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomMemberList() = persistentListOf(
|
fun aRoomMemberList() = persistentListOf(
|
||||||
|
|||||||
+19
-9
@@ -39,11 +39,15 @@ class MentionSpan(
|
|||||||
private var typeface: Typeface = Typeface.DEFAULT
|
private var typeface: Typeface = Typeface.DEFAULT
|
||||||
|
|
||||||
private var measuredTextWidth = 0
|
private var measuredTextWidth = 0
|
||||||
|
private var measuredTrailingWidth = 0
|
||||||
|
|
||||||
// The formatted display text, will be set by the formatter
|
// The formatted display text, will be set by the formatter
|
||||||
var displayText: CharSequence = ""
|
var displayText: CharSequence = ""
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var trailingText: String? = null
|
||||||
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the visual properties of this span.
|
* Updates the visual properties of this span.
|
||||||
*/
|
*/
|
||||||
@@ -80,6 +84,10 @@ class MentionSpan(
|
|||||||
displayText = formatter.formatDisplayText(type)
|
displayText = formatter.formatDisplayText(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateTrailingText(trailingText: String?) {
|
||||||
|
this.trailingText = trailingText
|
||||||
|
}
|
||||||
|
|
||||||
override fun getSize(
|
override fun getSize(
|
||||||
paint: Paint,
|
paint: Paint,
|
||||||
text: CharSequence?,
|
text: CharSequence?,
|
||||||
@@ -91,7 +99,8 @@ class MentionSpan(
|
|||||||
textPaint.typeface = typeface
|
textPaint.typeface = typeface
|
||||||
// Measure the full text width without truncation
|
// Measure the full text width without truncation
|
||||||
measuredTextWidth = textPaint.measureText(displayText, 0, displayText.length).roundToInt()
|
measuredTextWidth = textPaint.measureText(displayText, 0, displayText.length).roundToInt()
|
||||||
return measuredTextWidth + startPadding + endPadding
|
measuredTrailingWidth = trailingText?.let { textPaint.measureText(" $it").roundToInt() } ?: 0
|
||||||
|
return measuredTextWidth + measuredTrailingWidth + startPadding + endPadding
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun draw(
|
override fun draw(
|
||||||
@@ -106,7 +115,8 @@ class MentionSpan(
|
|||||||
paint: Paint
|
paint: Paint
|
||||||
) {
|
) {
|
||||||
val availableWidth = (canvas.width - x).coerceAtLeast(0f)
|
val availableWidth = (canvas.width - x).coerceAtLeast(0f)
|
||||||
val measuredWidth = measuredTextWidth + startPadding + endPadding
|
|
||||||
|
val measuredWidth = measuredTextWidth + measuredTrailingWidth + startPadding + endPadding
|
||||||
val pillWidth = minOf(availableWidth, measuredWidth.toFloat())
|
val pillWidth = minOf(availableWidth, measuredWidth.toFloat())
|
||||||
|
|
||||||
backgroundPaint.color = backgroundColor
|
backgroundPaint.color = backgroundColor
|
||||||
@@ -118,18 +128,18 @@ class MentionSpan(
|
|||||||
textPaint.color = textColor
|
textPaint.color = textColor
|
||||||
textPaint.typeface = typeface
|
textPaint.typeface = typeface
|
||||||
|
|
||||||
val availableWidthForText = availableWidth - startPadding - endPadding
|
val availableWidthForText = availableWidth - startPadding - endPadding - measuredTrailingWidth
|
||||||
val textToDraw = if (measuredTextWidth > availableWidthForText) {
|
val textToDraw = if (measuredTextWidth > availableWidthForText) {
|
||||||
TextUtils.ellipsize(
|
TextUtils.ellipsize(displayText, textPaint, availableWidthForText, TextUtils.TruncateAt.END)
|
||||||
displayText,
|
|
||||||
textPaint,
|
|
||||||
availableWidthForText,
|
|
||||||
TextUtils.TruncateAt.END
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
displayText
|
displayText
|
||||||
}
|
}
|
||||||
canvas.drawText(textToDraw, 0, textToDraw.length, x + startPadding, y.toFloat(), textPaint)
|
canvas.drawText(textToDraw, 0, textToDraw.length, x + startPadding, y.toFloat(), textPaint)
|
||||||
|
|
||||||
|
trailingText?.let {
|
||||||
|
val drawnTextWidth = minOf(availableWidthForText, measuredTextWidth.toFloat())
|
||||||
|
canvas.drawText(" $it", x + startPadding + drawnTextWidth, y.toFloat(), textPaint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
@@ -18,6 +18,7 @@ import io.element.android.compound.theme.ElementTheme
|
|||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
|
||||||
import io.element.android.libraries.matrix.ui.messages.RoomNamesCache
|
import io.element.android.libraries.matrix.ui.messages.RoomNamesCache
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toEmojiText
|
||||||
|
|
||||||
interface MentionSpanUpdater {
|
interface MentionSpanUpdater {
|
||||||
fun updateMentionSpans(text: CharSequence): CharSequence
|
fun updateMentionSpans(text: CharSequence): CharSequence
|
||||||
@@ -48,6 +49,13 @@ class DefaultMentionSpanUpdater(
|
|||||||
for (mentionSpan in text.getMentionSpans()) {
|
for (mentionSpan in text.getMentionSpans()) {
|
||||||
mentionSpan.updateTheme(theme)
|
mentionSpan.updateTheme(theme)
|
||||||
mentionSpan.updateDisplayText(formatter)
|
mentionSpan.updateDisplayText(formatter)
|
||||||
|
mentionSpan.updateTrailingText(
|
||||||
|
if (mentionSpan.type is MentionType.User) {
|
||||||
|
roomMemberProfilesCache.getDisplayedStatus(mentionSpan.type.userId)?.toEmojiText()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<string name="a11y_join_call">"Join call"</string>
|
<string name="a11y_join_call">"Join call"</string>
|
||||||
<string name="a11y_jump_to_bottom">"Jump to bottom"</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">"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_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_mentions_only">"Mentions only"</string>
|
||||||
<string name="a11y_notifications_muted">"Muted"</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_account_and_devices">"Manage account & devices"</string>
|
||||||
<string name="action_manage_devices">"Manage devices"</string>
|
<string name="action_manage_devices">"Manage devices"</string>
|
||||||
<string name="action_manage_rooms">"Manage rooms"</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_message">"Message"</string>
|
||||||
<string name="action_minimize">"Minimise"</string>
|
<string name="action_minimize">"Minimise"</string>
|
||||||
<string name="action_next">"Next"</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>
|
<item quantity="other">"%1$d people"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_live_location_sheet_title">"On the map"</string>
|
<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_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_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>
|
<string name="screen_pinned_timeline_empty_state_headline">"Pin important messages so that they can be easily discovered"</string>
|
||||||
|
|||||||
@@ -6,12 +6,8 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<resources>
|
<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_expanded">"Expanded"</string>
|
||||||
<string name="a11y_state_collapsed">"Collapsed"</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_active">Active</string>
|
||||||
<string name="a11y_inactive">Inactive</string>
|
<string name="a11y_inactive">Inactive</string>
|
||||||
</resources>
|
</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