Merge pull request #7232 from element-hq/feature/fga/display_user_status

WIP : display user status
This commit is contained in:
ganfra
2026-07-23 15:10:19 +02:00
committed by GitHub
153 changed files with 625 additions and 305 deletions
@@ -63,6 +63,8 @@ import io.element.android.libraries.designsystem.theme.roomListRoomName
import io.element.android.libraries.designsystem.theme.unreadIndicator
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
import io.element.android.libraries.matrix.ui.components.InviteSenderView
import io.element.android.libraries.matrix.ui.model.InviteSender
import io.element.android.libraries.ui.strings.CommonStrings
@@ -126,7 +128,8 @@ internal fun RoomSummaryRow(
NameAndTimestampRow(
name = room.name,
timestamp = room.timestamp,
isHighlighted = room.isHighlighted
isHighlighted = room.isHighlighted,
dmUserStatus = room.dmUserStatus,
)
MessagePreviewAndIndicatorRow(room = room, showUnreadCount = showUnreadCount)
}
@@ -142,7 +145,8 @@ internal fun RoomSummaryRow(
NameAndTimestampRow(
name = room.name,
timestamp = null,
isHighlighted = room.isHighlighted
isHighlighted = room.isHighlighted,
dmUserStatus = null,
)
if (room.canonicalAlias != null) {
Text(
@@ -218,22 +222,21 @@ private fun NameAndTimestampRow(
name: String?,
timestamp: String?,
isHighlighted: Boolean,
dmUserStatus: DisplayedStatus?,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = spacedBy(16.dp)
) {
Text(
modifier = Modifier
.weight(1f)
.clipToBounds(),
val displayName = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name)
DisplayNameWithStatus(
name = displayName,
status = dmUserStatus,
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyLgMedium,
text = name?.toSafeLength(ellipsize = true) ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { name == null },
color = ElementTheme.colors.roomListRoomName,
maxLines = 1,
overflow = TextOverflow.Ellipsis
nameColor = ElementTheme.colors.roomListRoomName,
nameFontStyle = FontStyle.Italic.takeIf { name == null },
)
// Timestamp
Text(
@@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.room.CallIntentConsensus
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.dmUserStatus
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.toInviteSender
import kotlinx.collections.immutable.toImmutableList
@@ -76,6 +77,7 @@ class RoomListRoomSummaryFactory(
}.toImmutableList(),
isTombstoned = roomInfo.successorRoom != null,
isSpace = roomInfo.isSpace,
dmUserStatus = roomInfo.dmUserStatus(),
)
}
@@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.ImmutableList
@@ -42,6 +43,7 @@ data class RoomListRoomSummary(
val isTombstoned: Boolean,
val heroes: ImmutableList<AvatarData>,
val isSpace: Boolean,
val dmUserStatus: DisplayedStatus?,
) {
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallIntent
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.toImmutableList
@@ -182,6 +183,7 @@ internal fun aRoomListRoomSummary(
heroes: List<AvatarData> = emptyList(),
isTombstoned: Boolean = false,
isSpace: Boolean = false,
dmUserStatus: DisplayedStatus? = null,
) = RoomListRoomSummary(
id = id,
roomId = RoomId(id),
@@ -204,5 +206,6 @@ internal fun aRoomListRoomSummary(
canonicalAlias = canonicalAlias,
heroes = heroes.toImmutableList(),
isTombstoned = isTombstoned,
isSpace = isSpace
isSpace = isSpace,
dmUserStatus = dmUserStatus,
)
@@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import kotlinx.collections.immutable.toImmutableList
@@ -87,6 +88,7 @@ internal fun createRoomListRoomSummary(
timestamp: String? = null,
isTombstoned: Boolean = false,
isSpace: Boolean = false,
dmUserStatus: DisplayedStatus? = null,
) = RoomListRoomSummary(
id = A_ROOM_ID.value,
roomId = A_ROOM_ID,
@@ -109,5 +111,6 @@ internal fun createRoomListRoomSummary(
isDm = false,
heroes = heroes.toImmutableList(),
isTombstoned = isTombstoned,
isSpace = isSpace
isSpace = isSpace,
dmUserStatus = dmUserStatus,
)
@@ -84,6 +84,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsSta
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.reply.map
import io.element.android.libraries.matrix.ui.model.dmUserStatus
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import io.element.android.libraries.textcomposer.model.MessageComposerMode
@@ -319,6 +320,7 @@ class MessagesPresenter(
appName = buildMeta.applicationName,
pinnedMessagesBannerState = pinnedMessagesBannerState,
dmUserVerificationState = dmUserVerificationState,
dmUserStatus = roomInfo.dmUserStatus(),
roomMemberModerationState = roomMemberModerationState,
topBarSharedHistoryIcon = topBarSharedHistoryIcon,
successorRoom = roomInfo.successorRoom,
@@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import kotlinx.collections.immutable.ImmutableList
data class MessagesState(
@@ -53,6 +54,7 @@ data class MessagesState(
val appName: String,
val pinnedMessagesBannerState: PinnedMessagesBannerState,
val dmUserVerificationState: IdentityState?,
val dmUserStatus: DisplayedStatus?,
val roomMemberModerationState: RoomMemberModerationState,
/** Type of "shared history" icon to show in the top bar. */
val topBarSharedHistoryIcon: SharedHistoryIcon,
@@ -50,6 +50,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
import io.element.android.libraries.textcomposer.model.aTextEditorStateRich
@@ -128,6 +129,7 @@ fun aMessagesState(
hasUnreadThreads = false,
),
isCurrentlySharingLiveLocationInRoom: Boolean = false,
dmUserStatus: DisplayedStatus? = null,
eventSink: (MessagesEvent) -> Unit = {},
) = MessagesState(
roomId = RoomId("!id:domain"),
@@ -158,6 +160,7 @@ fun aMessagesState(
successorRoom = successorRoom,
threads = threads,
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom,
dmUserStatus = dmUserStatus,
eventSink = eventSink,
)
@@ -244,6 +244,7 @@ fun MessagesView(
heroes = state.heroes,
dmUserIdentityState = state.dmUserVerificationState,
sharedHistoryIcon = state.topBarSharedHistoryIcon,
dmUserStatus = state.dmUserStatus,
onBackClick = { hidingKeyboard { onBackClick() } },
onRoomDetailsClick = { hidingKeyboard { onRoomDetailsClick() } },
menuActions = {
@@ -41,6 +41,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.getBestName
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.slashcommands.api.SlashCommandSuggestion
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
@@ -112,12 +114,16 @@ private fun SuggestionItemView(
is ResolvedSuggestion.Member -> AvatarType.User
is ResolvedSuggestion.Command -> null
}
val title = when (suggestion) {
val title: String? = when (suggestion) {
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName
is ResolvedSuggestion.Member -> suggestion.roomMember.getBestName()
is ResolvedSuggestion.Alias -> suggestion.roomName
is ResolvedSuggestion.Command -> suggestion.command.command
}
val status = when (suggestion) {
is ResolvedSuggestion.Member -> suggestion.roomMember.displayedStatus
else -> null
}
val details = when (suggestion) {
is ResolvedSuggestion.AtRoom,
is ResolvedSuggestion.Member,
@@ -148,11 +154,11 @@ private fun SuggestionItemView(
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
title?.let {
Text(
text = it,
DisplayNameWithStatus(
name = title,
status = status,
style = ElementTheme.typography.fontBodyLgRegular,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
nameColor = ElementTheme.colors.textPrimary,
)
}
details?.let {
@@ -191,6 +197,7 @@ internal fun SuggestionsPickerViewPreview() {
role = RoomMember.Role.User,
membershipChangeReason = null,
isServiceMember = false,
displayedStatus = null,
)
val anAlias = remember { RoomAlias("#room:domain.org") }
SuggestionsPickerView(
@@ -383,7 +383,12 @@ fun aThreadListItem(
fun aThreadListItemEvent(
threadId: ThreadId = ThreadId("\$a-thread-id"),
senderId: UserId = UserId("@a-user-id:server"),
senderProfile: ProfileDetails = ProfileDetails.Ready(displayName = USER_NAME_ALICE, displayNameAmbiguous = false, avatarUrl = null),
senderProfile: ProfileDetails = ProfileDetails.Ready(
displayName = USER_NAME_ALICE,
displayNameAmbiguous = false,
avatarUrl = null,
displayedStatus = null,
),
isOwn: Boolean = false,
content: EventContent = MessageContent(
body = "Hello world!",
@@ -166,6 +166,7 @@ internal fun MessageShieldViewPreview() {
displayName = USER_NAME_ALICE,
displayNameAmbiguous = false,
avatarUrl = null,
displayedStatus = null,
),
)
)
@@ -315,7 +315,12 @@ private fun inReplyToInvalidContent(): InReplyToDetails.Ready = InReplyToDetails
)
),
textContent = "A text content",
senderProfile = ProfileDetails.Ready(displayName = "Sender", displayNameAmbiguous = false, avatarUrl = null)
senderProfile = ProfileDetails.Ready(
displayName = "Sender",
displayNameAmbiguous = false,
avatarUrl = null,
displayedStatus = null
)
)
private fun inReplyToTextContent(): InReplyToDetails.Ready = InReplyToDetails.Ready(
@@ -332,5 +337,10 @@ private fun inReplyToTextContent(): InReplyToDetails.Ready = InReplyToDetails.Re
)
),
textContent = "A text content",
senderProfile = ProfileDetails.Ready(displayName = "Sender", displayNameAmbiguous = false, avatarUrl = null)
senderProfile = ProfileDetails.Ready(
displayName = "Sender",
displayNameAmbiguous = false,
avatarUrl = null,
displayedStatus = null
)
)
@@ -962,6 +962,7 @@ internal fun TimelineItemEventRowWithThreadSummaryPreview() = ElementPreview {
displayName = USER_NAME_ALICE,
avatarUrl = null,
displayNameAmbiguous = false,
displayedStatus = null,
),
timestamp = 0L,
)
@@ -1140,6 +1141,7 @@ internal fun ThreadSummaryViewPreview() {
displayName = USER_NAME_ALICE,
avatarUrl = null,
displayNameAmbiguous = true,
displayedStatus = null,
),
timestamp = 0L,
)
@@ -26,7 +26,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
@@ -46,9 +45,10 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ROOM_NAME
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.components.DisplayNameWithStatus
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings
@@ -65,6 +65,7 @@ internal fun MessagesViewTopBar(
heroes: ImmutableList<AvatarData>,
dmUserIdentityState: IdentityState?,
sharedHistoryIcon: SharedHistoryIcon,
dmUserStatus: DisplayedStatus?,
onRoomDetailsClick: () -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
@@ -91,6 +92,7 @@ internal fun MessagesViewTopBar(
roomAvatar = roomAvatar,
isTombstoned = isTombstoned,
heroes = heroes,
dmUserStatus = dmUserStatus,
modifier = titleModifier
)
@@ -144,6 +146,7 @@ private fun RoomAvatarAndNameRow(
roomAvatar: AvatarData,
heroes: ImmutableList<AvatarData>,
isTombstoned: Boolean,
dmUserStatus: DisplayedStatus?,
modifier: Modifier = Modifier
) {
Row(
@@ -157,14 +160,13 @@ private fun RoomAvatarAndNameRow(
isTombstoned = isTombstoned,
),
)
Text(
modifier = Modifier
.padding(start = 8.dp),
text = roomName ?: stringResource(CommonStrings.common_no_room_name),
DisplayNameWithStatus(
name = roomName ?: stringResource(CommonStrings.common_no_room_name),
status = dmUserStatus,
modifier = Modifier.padding(start = 8.dp),
style = ElementTheme.typography.fontBodyLgMedium,
fontStyle = FontStyle.Italic.takeIf { roomName == null },
maxLines = 1,
overflow = TextOverflow.Ellipsis
nameColor = ElementTheme.colors.textPrimary,
nameFontStyle = FontStyle.Italic.takeIf { roomName == null },
)
}
}
@@ -184,6 +186,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
roomCallState: RoomCallState = RoomCallState.Unavailable,
dmUserIdentityState: IdentityState? = null,
sharedHistoryIcon: SharedHistoryIcon = SharedHistoryIcon.NONE,
dmUserStatus: DisplayedStatus? = null,
displayThreads: Boolean = false,
) = MessagesViewTopBar(
roomName = roomName,
@@ -192,6 +195,7 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
heroes = heroes,
dmUserIdentityState = dmUserIdentityState,
sharedHistoryIcon = sharedHistoryIcon,
dmUserStatus = dmUserStatus,
onRoomDetailsClick = {},
onBackClick = {},
menuActions = {
@@ -246,5 +250,11 @@ internal fun MessagesViewTopBarPreview() = ElementPreview {
AMessagesViewTopBar(
displayThreads = true,
)
HorizontalDivider()
AMessagesViewTopBar(
roomName = "A DM with a very very very long name",
dmUserIdentityState = IdentityState.Verified,
dmUserStatus = DisplayedStatus.InCall(0L)
)
}
}
@@ -41,9 +41,9 @@ sealed interface EmojiPickerSheetState {
}
enum class PredefinedUserStatus(val emoji: String, @StringRes val labelRes: Int) {
IN_A_MEETING("💬", R.string.common_user_status_in_a_meeting),
FOCUS_TIME("💡", R.string.common_user_status_focus_time),
ON_THE_ROAD("🚙", R.string.common_user_status_on_the_road),
BE_RIGHT_BACK("☕", R.string.common_user_status_be_right_back),
AWAY("🌴", R.string.common_user_status_away),
IN_A_MEETING("💬", R.string.screen_settings_user_status_in_a_meeting),
FOCUS_TIME("💡", R.string.screen_settings_user_status_focus_time),
ON_THE_ROAD("🚙", R.string.screen_settings_user_status_on_the_road),
BE_RIGHT_BACK("☕", R.string.screen_settings_user_status_be_right_back),
AWAY("🌴", R.string.screen_settings_user_status_away),
}
@@ -132,7 +132,7 @@ private fun EmptyStatusRow(
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.screen_preferences_user_status_placeholder),
text = stringResource(R.string.screen_settings_user_status_placeholder),
modifier = Modifier.padding(vertical = 16.dp),
)
},
@@ -150,7 +150,7 @@ private fun CurrentStatusRow(
) {
val (emoji, text) = when (displayedStatus) {
is DisplayedStatus.UserSet -> displayedStatus.status.emoji to displayedStatus.status.text
is DisplayedStatus.InCall -> "🎧" to stringResource(R.string.common_user_status_on_a_call)
is DisplayedStatus.InCall -> "🎧" to stringResource(CommonStrings.common_on_a_call)
}
ListItem(
headlineContent = { Text(text = text, modifier = Modifier.padding(vertical = 16.dp)) },
@@ -225,7 +225,7 @@ private fun UserStatusPickerBottomSheet(
}
ListItem(
headlineContent = {
Text(text = stringResource(R.string.common_user_status_custom))
Text(text = stringResource(R.string.screen_settings_user_status_custom))
},
leadingContent = ListItemContent.Custom { EmojiText("✏️") },
onClick = {
@@ -265,7 +265,7 @@ private fun CustomStatusInputRow(
TextField(
modifier = Modifier.focusRequester(focusRequester),
state = textFieldState,
placeholder = stringResource(R.string.screen_preferences_user_status_custom_hint),
placeholder = stringResource(R.string.screen_settings_user_status_custom_hint),
inputTransformation = InputTransformation.maxLength(CUSTOM_STATUS_TEXT_MAX_LENGTH),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
onKeyboardAction = { if (hasChanges) onConfirm() },
@@ -101,6 +101,14 @@ If you proceed, some of your settings may change."</string>
<string name="screen_notification_settings_system_notifications_action_required_content_link">"system settings"</string>
<string name="screen_notification_settings_system_notifications_turned_off">"System notifications turned off"</string>
<string name="screen_notification_settings_title">"Notifications"</string>
<string name="screen_settings_user_status_away">"Away"</string>
<string name="screen_settings_user_status_be_right_back">"Be right back"</string>
<string name="screen_settings_user_status_custom">"Custom…"</string>
<string name="screen_settings_user_status_custom_hint">"Custom status…"</string>
<string name="screen_settings_user_status_focus_time">"Focus time"</string>
<string name="screen_settings_user_status_in_a_meeting">"In a meeting"</string>
<string name="screen_settings_user_status_on_the_road">"On the road"</string>
<string name="screen_settings_user_status_placeholder">"What\'s your status?"</string>
<string name="theme_black">"Black"</string>
<string name="theme_dark">"Dark"</string>
<string name="theme_light">"Light"</string>
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2026 Element Creations Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<string name="common_user_status_in_a_meeting">In a meeting</string>
<string name="common_user_status_focus_time">Focus time</string>
<string name="common_user_status_on_the_road">On the road</string>
<string name="common_user_status_be_right_back">Be right back</string>
<string name="common_user_status_away">Away</string>
<string name="common_user_status_on_a_call">On a call</string>
<string name="common_user_status_custom">Custom…</string>
<string name="screen_preferences_user_status_placeholder">What\'s your status?</string>
<string name="screen_preferences_user_status_custom_hint">Custom status…</string>
</resources>
@@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import kotlinx.collections.immutable.toImmutableList
@@ -75,6 +76,7 @@ fun aDmRoomMember(
role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null,
isServiceMember: Boolean = false,
displayedStatus: DisplayedStatus? = null,
) = RoomMember(
userId = userId,
displayName = displayName,
@@ -86,6 +88,7 @@ fun aDmRoomMember(
role = role,
membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember,
displayedStatus = displayedStatus,
)
fun aRoomDetailsState(
@@ -178,12 +181,13 @@ fun aDmRoomDetailsState(
roomName: String = "Daniel",
isEncrypted: Boolean = true,
dmRoomMemberVerificationState: UserProfileVerificationState = UserProfileVerificationState.UNKNOWN,
displayedStatus: DisplayedStatus? = null,
) = aRoomDetailsState(
roomName = roomName,
isPublic = false,
isEncrypted = isEncrypted,
canInvite = true,
roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored)),
roomType = RoomDetailsType.Dm(otherMember = aDmRoomMember(isIgnored = isDmMemberIgnored, displayedStatus = displayedStatus)),
dmOtherMemberDetailsState = aUserProfileState(
isBlocked = AsyncData.Success(isDmMemberIgnored),
verificationState = dmRoomMemberVerificationState,
@@ -37,6 +37,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import im.vector.app.features.analytics.plan.Interaction
@@ -88,6 +89,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.getBestName
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.toText
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
@@ -489,11 +491,23 @@ private fun RoomHeaderSection(
}
.testTag(TestTags.roomDetailAvatar)
)
TitleAndSubtitle(
title = roomName,
subtitle = roomAlias?.value,
onSubtitleClick = onSubtitleClick,
Spacer(modifier = Modifier.height(24.dp))
Text(
text = roomName,
style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center,
)
if (roomAlias != null) {
Spacer(modifier = Modifier.height(12.dp))
Text(
modifier = Modifier.niceClickable { onSubtitleClick(roomAlias.value) },
text = roomAlias.value,
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
Spacer(modifier = Modifier.height(12.dp))
}
}
@@ -531,37 +545,34 @@ private fun DmHeaderSection(
}
.testTag(TestTags.roomDetailAvatar)
)
TitleAndSubtitle(
title = roomName,
subtitle = otherMember.userId.value,
onSubtitleClick = onSubtitleClick,
)
}
}
@Composable
private fun TitleAndSubtitle(
title: String,
subtitle: String?,
onSubtitleClick: (String) -> Unit,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Spacer(modifier = Modifier.height(24.dp))
Text(
text = title,
text = roomName,
style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center,
)
if (subtitle != null) {
Spacer(modifier = Modifier.height(6.dp))
val userStatus = otherMember.displayedStatus?.toText()
if (userStatus != null) {
Spacer(modifier = Modifier.height(12.dp))
Text(
modifier = Modifier.niceClickable { onSubtitleClick(subtitle) },
text = subtitle,
style = ElementTheme.typography.fontBodyLgRegular,
text = userStatus,
style = ElementTheme.typography.fontBodyLgMedium,
maxLines = 1,
color = ElementTheme.colors.textSecondary,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(
modifier = Modifier.niceClickable { onSubtitleClick(otherMember.userId.value) },
text = otherMember.userId.value,
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(12.dp))
}
}
@@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import kotlinx.collections.immutable.persistentListOf
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
@@ -129,6 +130,7 @@ fun aRoomMember(
role: RoomMember.Role = RoomMember.Role.User,
membershipChangeReason: String? = null,
isServiceMember: Boolean = false,
displayedStatus: DisplayedStatus? = null,
) = RoomMember(
userId = userId,
displayName = displayName,
@@ -140,6 +142,7 @@ fun aRoomMember(
role = role,
membershipChangeReason = membershipChangeReason,
isServiceMember = isServiceMember,
displayedStatus = displayedStatus,
)
fun aRoomMemberList() = persistentListOf(
@@ -128,6 +128,7 @@ class RoomMemberDetailsPresenter(
return userProfileState.copy(
userName = roomUserName ?: userProfileState.userName,
avatarUrl = roomUserAvatar ?: userProfileState.avatarUrl,
displayedStatus = roomMember?.displayedStatus ?: userProfileState.displayedStatus,
verificationState = verificationState,
snackbarMessage = snackbarMessage,
eventSink = ::handleEvent,
@@ -16,6 +16,7 @@ import io.element.android.features.roommembermoderation.api.RoomMemberModeration
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.toImmutableList
@@ -82,6 +83,7 @@ fun anAlice() = MatrixUser(
userId = UserId(value = "@alice:server.org"),
displayName = USER_NAME_ALICE,
avatarUrl = null,
displayedStatus = DisplayedStatus.InCall(0L),
)
fun aRoomMembersModerationState(
@@ -57,6 +57,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.matrix.ui.model.toText
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch
@@ -264,9 +265,23 @@ private fun RoomMemberActionsBottomSheet(
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
.fillMaxWidth()
)
val userStatus = user.displayedStatus?.toText()
if (userStatus != null) {
Text(
text = userStatus,
style = ElementTheme.typography.fontBodyLgMedium,
maxLines = 1,
color = ElementTheme.colors.textSecondary,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
.fillMaxWidth()
)
}
// Show user ID only if it's different from the display name
if (bestName != user.userId.value) {
Text(
@@ -13,6 +13,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
data class UserProfileState(
val userId: UserId,
@@ -26,6 +27,7 @@ data class UserProfileState(
val dmRoomId: RoomId?,
val canCall: Boolean,
val snackbarMessage: SnackbarMessage?,
val displayedStatus: DisplayedStatus?,
val eventSink: (UserProfileEvents) -> Unit
) {
enum class ConfirmationDialog {
@@ -145,6 +145,7 @@ class UserProfilePresenter(
userId = userId,
userName = userProfile?.displayName,
avatarUrl = userProfile?.avatarUrl,
displayedStatus = userProfile?.displayedStatus,
isBlocked = isBlocked.value,
verificationState = UserProfileVerificationState.UNKNOWN,
startDmActionState = startDmActionState.value,
@@ -42,6 +42,8 @@ import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.model.toText
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings
@@ -56,6 +58,7 @@ fun UserProfileHeaderSection(
openAvatarPreview: (url: String) -> Unit,
onUserIdClick: () -> Unit,
withdrawVerificationClick: () -> Unit,
displayedStatus: DisplayedStatus?,
modifier: Modifier = Modifier
) {
Column(
@@ -90,7 +93,16 @@ fun UserProfileHeaderSection(
style = ElementTheme.typography.fontHeadingLgBold,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(6.dp))
Spacer(modifier = Modifier.height(12.dp))
}
displayedStatus?.let { status ->
Text(
text = status.toText(),
style = ElementTheme.typography.fontBodyLgMedium,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(12.dp))
}
Text(
modifier = Modifier.niceClickable { onUserIdClick() },
@@ -99,6 +111,7 @@ fun UserProfileHeaderSection(
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(12.dp))
when (verificationState) {
UserProfileVerificationState.UNKNOWN, UserProfileVerificationState.UNVERIFIED -> Unit
UserProfileVerificationState.VERIFIED -> {
@@ -143,6 +156,7 @@ internal fun UserProfileHeaderSectionPreview() = ElementPreview {
userId = UserId("@alice:example.com"),
userName = USER_NAME_ALICE,
verificationState = UserProfileVerificationState.VERIFIED,
displayedStatus = DisplayedStatus.InCall(0L),
openAvatarPreview = {},
onUserIdClick = {},
withdrawVerificationClick = {},
@@ -157,6 +171,7 @@ internal fun UserProfileHeaderSectionWithVerificationViolationPreview() = Elemen
userId = UserId("@alice:example.com"),
userName = USER_NAME_ALICE,
verificationState = UserProfileVerificationState.VERIFICATION_VIOLATION,
displayedStatus = DisplayedStatus.InCall(0L),
openAvatarPreview = {},
onUserIdClick = {},
withdrawVerificationClick = {},
@@ -18,6 +18,7 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.components.aMatrixUser
open class UserProfileStateProvider : PreviewParameterProvider<UserProfileState> {
@@ -48,6 +49,7 @@ fun aUserProfileState(
dmRoomId: RoomId? = null,
canCall: Boolean = false,
snackbarMessage: SnackbarMessage? = null,
displayedStatus: DisplayedStatus? = null,
eventSink: (UserProfileEvents) -> Unit = {},
) = UserProfileState(
userId = userId,
@@ -61,5 +63,6 @@ fun aUserProfileState(
dmRoomId = dmRoomId,
canCall = canCall,
snackbarMessage = snackbarMessage,
displayedStatus = displayedStatus,
eventSink = eventSink,
)
@@ -78,6 +78,7 @@ fun UserProfileView(
userId = state.userId,
userName = state.userName,
verificationState = state.verificationState,
displayedStatus = state.displayedStatus,
openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.userName ?: state.userId.value, avatarUrl)
},