Click on avatar in moderation bottom sheet opens avatar preview (#6991)

* Make the avatar in the room member moderation bottom sheet open the avatar in the media viewer (#6962)

* Make the avatar in the room member moderation bottom sheet open the avatar in the media viewer.

* Fix issue with avatar overlay not dismissing the bottom sheet: the bottom sheet would eat all the touch events until the first click, which would somehow cancel this behaviour

* Fix lint issues

* Update screenshots

---------

Co-authored-by: bxdxnn <267911624+bxdxnn@users.noreply.github.com>
Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa
2026-06-08 16:32:13 +02:00
committed by GitHub
parent 786c53ac06
commit bea2903093
22 changed files with 111 additions and 39 deletions
@@ -190,6 +190,9 @@ class MessagesFlowNode(
@Parcelize
data object ThreadsList : NavTarget
@Parcelize
data class AvatarPreview(val name: String, val avatarUrl: String) : NavTarget
}
private val callback: MessagesEntryPoint.Callback = callback()
@@ -327,6 +330,10 @@ class MessagesFlowNode(
override fun navigateToDeveloperSettings() {
callback.navigateToDeveloperSettings()
}
override fun navigateToAvatarPreview(username: String, avatarUrl: String) {
overlay.show(NavTarget.AvatarPreview(username, avatarUrl))
}
}
val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId)
createNode<MessagesNode>(buildContext, listOf(callback, inputs))
@@ -562,6 +569,10 @@ class MessagesFlowNode(
override fun navigateToDeveloperSettings() {
callback.navigateToDeveloperSettings()
}
override fun navigateToAvatarPreview(username: String, avatarUrl: String) {
overlay.show(NavTarget.AvatarPreview(username, avatarUrl))
}
}
createNode<ThreadedMessagesNode>(buildContext, listOf(inputs, callback))
}
@@ -573,6 +584,31 @@ class MessagesFlowNode(
}
createNode<ThreadsListNode>(buildContext, listOf(callback))
}
is NavTarget.AvatarPreview -> {
val callback = object : MediaViewerEntryPoint.Callback {
override fun onDone() {
overlay.hide()
}
override fun viewInTimeline(eventId: EventId) {
// Cannot happen
}
override fun forwardEvent(eventId: EventId, fromPinnedEvents: Boolean) {
// Cannot happen
}
}
val params = mediaViewerEntryPoint.createParamsForAvatar(
filename = navTarget.name,
avatarUrl = navTarget.avatarUrl,
)
mediaViewerEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = params,
callback = callback,
)
}
}
}
@@ -66,6 +66,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.alias.matches
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard
@@ -136,6 +137,8 @@ class MessagesNode(
fun navigateToDeveloperSettings()
fun navigateToThreadsList()
fun navigateToAvatarPreview(username: String, avatarUrl: String)
}
override fun onBuilt() {
@@ -319,6 +322,11 @@ class MessagesNode(
else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target))
}
},
onAvatarClick = { user ->
user.avatarUrl?.let { url ->
callback.navigateToAvatarPreview(user.getBestName(), url)
}
},
modifier = Modifier,
)
@@ -67,6 +67,7 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.alias.matches
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.libraries.ui.utils.a11y.hasExternalKeyboard
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
import io.element.android.services.analytics.api.AnalyticsService
@@ -138,6 +139,8 @@ class ThreadedMessagesNode(
fun navigateToRoomCall(roomId: RoomId, isAudioCall: Boolean)
fun navigateToThread(threadRootId: ThreadId, focusedEventId: EventId?)
fun navigateToDeveloperSettings()
fun navigateToAvatarPreview(username: String, avatarUrl: String)
}
override fun onBuilt() {
@@ -315,6 +318,11 @@ class ThreadedMessagesNode(
else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target))
}
},
onAvatarClick = { user ->
user.avatarUrl?.let { url ->
callback.navigateToAvatarPreview(user.getBestName(), url)
}
},
modifier = Modifier,
)
@@ -258,6 +258,10 @@ class RoomDetailsFlowNode(
override fun navigateToInviteMembers() {
backstack.push(NavTarget.InviteMembers)
}
override fun navigateToAvatarPreview(username: String, avatarUrl: String) {
overlay.show(NavTarget.AvatarPreview(username, avatarUrl))
}
}
createNode<RoomMemberListNode>(buildContext, listOf(roomMemberListCallback))
}
@@ -27,6 +27,7 @@ import io.element.android.libraries.architecture.appyx.launchMolecule
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.ui.model.getBestName
import io.element.android.services.analytics.api.AnalyticsService
@ContributesNode(RoomScope::class)
@@ -41,6 +42,7 @@ class RoomMemberListNode(
interface Callback : Plugin {
fun navigateToRoomMemberDetails(roomMemberId: UserId)
fun navigateToInviteMembers()
fun navigateToAvatarPreview(username: String, avatarUrl: String)
}
private val callback: Callback = callback()
@@ -82,6 +84,11 @@ class RoomMemberListNode(
else -> state.moderationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target))
}
},
onAvatarClick = { user ->
user.avatarUrl?.let { url ->
callback.navigateToAvatarPreview(user.getBestName(), url)
}
},
modifier = Modifier,
)
}
@@ -17,6 +17,7 @@ interface RoomMemberModerationRenderer {
fun Render(
state: RoomMemberModerationState,
onSelectAction: (ModerationAction, MatrixUser) -> Unit,
onAvatarClick: ((MatrixUser) -> Unit)?,
modifier: Modifier,
)
}
@@ -25,10 +25,11 @@ class DefaultRoomMemberModerationRenderer : RoomMemberModerationRenderer {
override fun Render(
state: RoomMemberModerationState,
onSelectAction: (ModerationAction, MatrixUser) -> Unit,
onAvatarClick: ((MatrixUser) -> Unit)?,
modifier: Modifier
) {
if (state is InternalRoomMemberModerationState) {
RoomMemberModerationView(state, onSelectAction, modifier)
RoomMemberModerationView(modifier = modifier, state = state, onSelectAction = onSelectAction, onAvatarClick = onAvatarClick)
} else {
SideEffect {
Timber.d("RoomMemberModerationRenderer: Render called with unsupported state: $state")
@@ -12,6 +12,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
@@ -64,6 +65,7 @@ import timber.log.Timber
fun RoomMemberModerationView(
state: InternalRoomMemberModerationState,
onSelectAction: (ModerationAction, MatrixUser) -> Unit,
onAvatarClick: ((MatrixUser) -> Unit)?,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
@@ -73,6 +75,7 @@ fun RoomMemberModerationView(
user = selectedUser,
actions = state.actions,
onSelectAction = onSelectAction,
onAvatarClick = onAvatarClick,
onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) },
)
}
@@ -214,6 +217,7 @@ private fun RoomMemberActionsBottomSheet(
user: MatrixUser,
actions: ImmutableList<ModerationActionState>,
onSelectAction: (ModerationAction, MatrixUser) -> Unit,
onAvatarClick: ((MatrixUser) -> Unit)? = null,
onDismiss: () -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
@@ -240,10 +244,11 @@ private fun RoomMemberActionsBottomSheet(
modifier = Modifier
.padding(bottom = 24.dp)
.align(Alignment.CenterHorizontally)
.clickable {
.clickable(enabled = user.avatarUrl != null && onAvatarClick != null) {
coroutineScope.launch {
onSelectAction(ModerationAction.DisplayProfile, user)
bottomSheetState.hide()
onAvatarClick?.invoke(user)
onDismiss()
}
}
)
@@ -282,8 +287,8 @@ private fun RoomMemberActionsBottomSheet(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.UserProfile())),
onClick = {
coroutineScope.launch {
onSelectAction(action, user)
bottomSheetState.hide()
onSelectAction(action, user)
}
},
enabled = actionState.isEnabled
@@ -340,16 +345,18 @@ private fun RoomMemberActionsBottomSheet(
@PreviewsDayNight
@Composable
internal fun RoomMemberModerationViewPreview(@PreviewParameter(InternalRoomMemberModerationStateProvider::class) state: InternalRoomMemberModerationState) {
val isDoingAction = listOf(state.kickUserAsyncAction, state.banUserAsyncAction, state.unbanUserAsyncAction).any { it is AsyncAction.Loading }
val modifier = if (isDoingAction) {
Modifier.fillMaxWidth().heightIn(min = 64.dp)
} else {
Modifier.fillMaxSize()
}
ElementPreview {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 64.dp)
) {
Box(modifier) {
RoomMemberModerationView(
state = state,
onSelectAction = { _, _ ->
},
onSelectAction = { _, _ -> },
onAvatarClick = {},
)
}
}
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8b15da251ba8ac26c61fd21079f09e3b614fd20e8ded4805cda0da180631afcc
size 17347
oid sha256:75880628a454ca00a186e5d1c5438117e7af4ab9b23eb6fbf9c9696210234169
size 17246
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc4a3ba905c3599ad8f6451787906ba61f8dc716464d81e8c1c1bc86023032c3
size 20345
oid sha256:1fe61c20e2c6a7bb72e82aefd546469dfc49bf736b6d12f4c1927628df1bdb3e
size 20211
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5704d02932451bc129ec92f3a9dab7fd66bb55bfdf3e142a4f3a436ad1d3b129
size 22722
oid sha256:ca3ee9a09bbbc69f2ba7e8b40cdc5196cc53745fe3222282d98bcf948dc16745
size 22596
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8755fa3568186a798047a7fbf05404c0f738b168ca77f16afecc815bbec084ab
size 22828
oid sha256:96971a8981aea8918c01827403933c9e75275a2fda92716f803e6f28ad284bae
size 22702
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e611504d154db3889c1a5900e6af392ca058462450d66f0dde762ab4a45e1b76
size 29532
oid sha256:06ca0aa653033b59313325ad508c5916e2fedf29ba6dd3a5f714e90d01f4bf7c
size 29644
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f6135b24d94d7732342ec9353cdefb115ce5e9a799dfaee5d57b2e8e0a7ed631
size 26505
oid sha256:ebfb75986cd385e9837a24a5f279e07f0744bab5613fd7cbaf0a493d021c7699
size 26606
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7bf31df95e781eae5530ed98ab9773f3e7d0f4fd4152226596b78328b88d69a8
size 27223
oid sha256:a58e6ed7f10d7014c9c3b55fbb769e04619313212449a8fc582501d2904d669f
size 27315
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba469dd7ffb3b27261cc4f227584b7cc03bf2ddcb974a4a1150c9515a075ac78
size 16562
oid sha256:bc96eec2d61713f01d43c92812bbf27bc44a202ce55f0fa1dc8e8659dabcfcf2
size 16347
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d21c293e519e998d8820171766f1651d2440b18dad6f002c6580ca3fe2f24d69
size 19397
oid sha256:56c4df7da32b71dca109a1db1f4a797a2930e3b42c00eb51d72f0e1f5b99bf48
size 19200
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:24a1a6004dc058573f264a5f02ffc85ca925e9f0c01f7ceacbaf972b3335d327
size 21819
oid sha256:592c6ec197b193803679eee5df7769fa4858fe883a2f3a1294f39ad87f9f4ff1
size 21610
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cf93cbb2ce344639162524714d254c0db21cab448dec38ee1d4820265678a7ec
size 21909
oid sha256:62e99b8070a394ea5b2d666f3a8abf0fe41ea4a34a382223903f2de4085195dc
size 21710
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:641f434040dfc9f040622fd0f17519d353f0bc45a17603ef837e53fad2b529c2
size 27570
oid sha256:793cff43740f6e477a255af566985d8bb7b0242d89484b3f0fc5cfec3b4a2af8
size 27608
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d9c0f0c69bd55b5c9a925cb89587d959998a833c5927873b4ef358959ce4ece
size 24944
oid sha256:ad842be0c519bfd7713eb93df2a7216d159970aff1ea44725641c5db26f3ad6b
size 24995
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f17c5df90399b6de382af8111ba36b0ec072596f42103f76477692a801493799
size 25515
oid sha256:541b9ad607838fc9d44d6d3759d61df54c178257fe646366304d96ebda5f7e77
size 25565