Open room member avatar in a media viewer (#1911)

* Open room member avatar in viewer.

The `MediaViewer` was extracted to its own library module.

* Update screenshots

* Restore KSP processor in `:libraries:mediaviewer:api`, this should generate Showkase components again.

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2023-11-28 18:39:41 +01:00 committed by GitHub
parent d0cc8c0b2f
commit bbc4d18a9d
68 changed files with 439 additions and 122 deletions

View file

@ -34,12 +34,17 @@ import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
import io.element.android.features.roomdetails.impl.members.details.avatar.RoomMemberAvatarPreviewNode
import io.element.android.features.roomdetails.impl.notificationsettings.RoomNotificationSettingsNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.mediaviewer.api.local.MediaInfo
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
@ -79,6 +84,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
@Parcelize
data class RoomMemberDetails(val roomMemberId: UserId) : NavTarget
@Parcelize
data class MemberAvatarPreview(val userName: String, val avatarUrl: String) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -100,6 +108,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
override fun openRoomNotificationSettings() {
backstack.push(NavTarget.RoomNotificationSettings(showUserDefinedSettingStyle = false))
}
override fun openAvatarPreview(username: String, url: String) {
backstack.push(NavTarget.MemberAvatarPreview(username, url))
}
}
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
}
@ -136,9 +148,31 @@ class RoomDetailsFlowNode @AssistedInject constructor(
}
is NavTarget.RoomMemberDetails -> {
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId))
val callback = object : RoomMemberDetailsNode.Callback {
override fun openAvatarPreview(username: String, avatarUrl: String) {
backstack.push(NavTarget.MemberAvatarPreview(username, avatarUrl))
}
}
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId), callback)
createNode<RoomMemberDetailsNode>(buildContext, plugins)
}
is NavTarget.MemberAvatarPreview -> {
// We need to fake the MimeType here for the viewer to work.
val mimeType = MimeTypes.Images
val input = MediaViewerNode.Inputs(
mediaInfo = MediaInfo(
name = navTarget.userName,
mimeType = mimeType,
formattedFileSize = "",
fileExtension = ""
),
mediaSource = MediaSource(url = navTarget.avatarUrl),
thumbnailSource = MediaSource(url = navTarget.avatarUrl),
canDownload = false,
canShare = false,
)
createNode<RoomMemberAvatarPreviewNode>(buildContext, listOf(input))
}
}
}

View file

@ -52,6 +52,7 @@ class RoomDetailsNode @AssistedInject constructor(
fun openInviteMembers()
fun editRoomDetails()
fun openRoomNotificationSettings()
fun openAvatarPreview(username: String, url: String)
}
private val callbacks = plugins<Callback>()
@ -110,6 +111,10 @@ class RoomDetailsNode @AssistedInject constructor(
callbacks.forEach { it.editRoomDetails() }
}
private fun openAvatarPreview(username: String, url: String) {
callbacks.forEach { it.openAvatarPreview(username, url) }
}
@Composable
override fun View(modifier: Modifier) {
val context = LocalContext.current
@ -140,6 +145,7 @@ class RoomDetailsNode @AssistedInject constructor(
openRoomMemberList = ::openRoomMemberList,
openRoomNotificationSettings = ::openRoomNotificationSettings,
invitePeople = ::invitePeople,
openAvatarPreview = ::openAvatarPreview,
)
}
}

View file

@ -89,6 +89,7 @@ fun RoomDetailsView(
openRoomMemberList: () -> Unit,
openRoomNotificationSettings: () -> Unit,
invitePeople: () -> Unit,
openAvatarPreview: (username: String, url: String) -> Unit,
modifier: Modifier = Modifier,
) {
fun onShareMember() {
@ -132,7 +133,12 @@ fun RoomDetailsView(
RoomMemberHeaderSection(
avatarUrl = state.roomAvatarUrl ?: member.avatarUrl,
userId = member.userId.value,
userName = state.roomName
userName = state.roomName,
openAvatarPreview = {
if (member.avatarUrl != null) {
openAvatarPreview(member.displayName ?: member.userId.value, member.avatarUrl!!)
}
},
)
RoomMemberMainActionsSection(onShareUser = ::onShareMember)
}
@ -411,5 +417,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
openRoomMemberList = {},
openRoomNotificationSettings = {},
invitePeople = {},
openAvatarPreview = { _, _ -> },
)
}

View file

@ -46,11 +46,16 @@ class RoomMemberDetailsNode @AssistedInject constructor(
presenterFactory: RoomMemberDetailsPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
interface Callback: NodeInputs {
fun openAvatarPreview(username: String, avatarUrl: String)
}
data class RoomMemberDetailsInput(
val roomMemberId: UserId
) : NodeInputs
private val inputs = inputs<RoomMemberDetailsInput>()
private val callback = inputs<Callback>()
private val presenter = presenterFactory.create(inputs.roomMemberId)
init {
@ -84,7 +89,8 @@ class RoomMemberDetailsNode @AssistedInject constructor(
state = state,
modifier = modifier,
goBack = this::navigateUp,
onShareUser = ::onShareUser
onShareUser = ::onShareUser,
openAvatarPreview = callback::openAvatarPreview,
)
}
}

View file

@ -17,7 +17,6 @@
package io.element.android.features.roomdetails.impl.members.details
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.height
@ -38,12 +37,13 @@ import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.TopAppBar
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RoomMemberDetailsView(
state: RoomMemberDetailsState,
onShareUser: () -> Unit,
goBack: () -> Unit,
openAvatarPreview: (username: String, url: String) -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
@ -62,6 +62,9 @@ fun RoomMemberDetailsView(
avatarUrl = state.avatarUrl,
userId = state.userId,
userName = state.userName,
openAvatarPreview = { avatarUrl ->
openAvatarPreview(state.userName ?: state.userId, avatarUrl)
},
)
RoomMemberMainActionsSection(onShareUser = onShareUser)
@ -110,5 +113,6 @@ private fun ContentToPreview(state: RoomMemberDetailsState) {
state = state,
onShareUser = {},
goBack = {},
openAvatarPreview = { _, _ -> }
)
}

View file

@ -16,6 +16,7 @@
package io.element.android.features.roomdetails.impl.members.details
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -41,13 +42,16 @@ fun RoomMemberHeaderSection(
avatarUrl: String?,
userId: String,
userName: String?,
openAvatarPreview: (url: String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Box(modifier = Modifier.size(70.dp)) {
Avatar(
avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.UserHeader),
modifier = Modifier.fillMaxSize()
modifier = Modifier
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
.fillMaxSize()
)
}
Spacer(modifier = Modifier.height(24.dp))

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomdetails.impl.members.details.avatar
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerNode
import io.element.android.libraries.mediaviewer.api.viewer.MediaViewerPresenter
@ContributesNode(RoomScope::class)
class RoomMemberAvatarPreviewNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: MediaViewerPresenter.Factory,
) : MediaViewerNode(buildContext, plugins, presenterFactory)