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 = {},
)
}
}