[Room details] Open room member details when clicking on user data in timeline (#482)

This commit is contained in:
Jorge Martin Espinosa
2023-06-01 12:03:27 +02:00
committed by GitHub
parent d7448c7acc
commit b50350aaa0
15 changed files with 99 additions and 16 deletions

View File

@@ -39,6 +39,7 @@ import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.services.appnavstate.api.AppNavigationStateService
@@ -119,11 +120,20 @@ class RoomFlowNode @AssistedInject constructor(
override fun onRoomDetailsClicked() {
backstack.push(NavTarget.RoomDetails)
}
override fun onUserDataClicked(userId: UserId) {
backstack.push(NavTarget.RoomMemberDetails(userId))
}
}
messagesEntryPoint.createNode(this, buildContext, callback)
}
NavTarget.RoomDetails -> {
roomDetailsEntryPoint.createNode(this, buildContext, emptyList())
val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomDetails)
roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList())
}
is NavTarget.RoomMemberDetails -> {
val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomMemberDetails(navTarget.userId))
roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList())
}
}
}
@@ -134,6 +144,9 @@ class RoomFlowNode @AssistedInject constructor(
@Parcelize
object RoomDetails : NavTarget
@Parcelize
data class RoomMemberDetails(val userId: UserId) : NavTarget
}
private val timeline = inputs.room.timeline()

View File

@@ -60,7 +60,12 @@ class RoomFlowNodeTest {
var nodeId: String? = null
override fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List<Plugin>): Node {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
inputs: RoomDetailsEntryPoint.Inputs,
plugins: List<Plugin>
): Node {
return node(buildContext) {}.also {
nodeId = it.id
}

1
changelog.d/480.feature Normal file
View File

@@ -0,0 +1 @@
Open room member details when tapping on a user in the timeline

View File

@@ -20,6 +20,7 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.UserId
interface MessagesEntryPoint : FeatureEntryPoint {
fun createNode(
@@ -30,5 +31,6 @@ interface MessagesEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onRoomDetailsClicked()
fun onUserDataClicked(userId: UserId)
}
}

View File

@@ -39,6 +39,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.createNode
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.parcelize.Parcelize
@@ -89,6 +90,10 @@ class MessagesFlowNode @AssistedInject constructor(
override fun onPreviewAttachments(attachments: ImmutableList<Attachment>) {
backstack.push(NavTarget.AttachmentPreview(attachments.first()))
}
override fun onUserDataClicked(userId: UserId) {
callback?.onUserDataClicked(userId)
}
}
createNode<MessagesNode>(buildContext, listOf(callback))
}

View File

@@ -28,6 +28,7 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
@ContributesNode(RoomScope::class)
@@ -43,6 +44,7 @@ class MessagesNode @AssistedInject constructor(
fun onRoomDetailsClicked()
fun onEventClicked(event: TimelineItem.Event)
fun onPreviewAttachments(attachments: ImmutableList<Attachment>)
fun onUserDataClicked(userId: UserId)
}
private fun onRoomDetailsClicked() {
@@ -57,6 +59,10 @@ class MessagesNode @AssistedInject constructor(
callback?.onPreviewAttachments(attachments)
}
private fun onUserDataClicked(userId: UserId) {
callback?.onUserDataClicked(userId)
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@@ -66,6 +72,7 @@ class MessagesNode @AssistedInject constructor(
onRoomDetailsClicked = this::onRoomDetailsClicked,
onEventClicked = this::onEventClicked,
onPreviewAttachments = this::onPreviewAttachments,
onUserDataClicked = this::onUserDataClicked,
modifier = modifier,
)
}

View File

@@ -85,6 +85,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -97,6 +98,7 @@ fun MessagesView(
onBackPressed: () -> Unit,
onRoomDetailsClicked: () -> Unit,
onEventClicked: (event: TimelineItem.Event) -> Unit,
onUserDataClicked: (UserId) -> Unit,
onPreviewAttachments: (ImmutableList<Attachment>) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -203,6 +205,7 @@ fun MessagesView(
.consumeWindowInsets(padding),
onMessageClicked = ::onMessageClicked,
onMessageLongClicked = ::onMessageLongClicked,
onUserDataClicked = onUserDataClicked,
)
},
snackbarHost = {
@@ -240,6 +243,7 @@ fun MessagesViewContent(
state: MessagesState,
modifier: Modifier = Modifier,
onMessageClicked: (TimelineItem.Event) -> Unit = {},
onUserDataClicked: (UserId) -> Unit = {},
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
) {
Column(
@@ -255,6 +259,7 @@ fun MessagesViewContent(
modifier = Modifier.weight(1f),
onMessageClicked = onMessageClicked,
onMessageLongClicked = onMessageLongClicked,
onUserDataClicked = onUserDataClicked,
)
}
MessageComposerView(
@@ -354,6 +359,7 @@ private fun ContentToPreview(state: MessagesState) {
onBackPressed = {},
onRoomDetailsClicked = {},
onEventClicked = {},
onPreviewAttachments = {}
onPreviewAttachments = {},
onUserDataClicked = {},
)
}

View File

@@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -87,6 +88,7 @@ import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@@ -95,6 +97,7 @@ import kotlinx.coroutines.launch
fun TimelineView(
state: TimelineState,
modifier: Modifier = Modifier,
onUserDataClicked: (UserId) -> Unit = {},
onMessageClicked: (TimelineItem.Event) -> Unit = {},
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
) {
@@ -120,6 +123,7 @@ fun TimelineView(
highlightedItem = state.highlightedEventId?.value,
onClick = onMessageClicked,
onLongClick = onMessageLongClicked,
onUserDataClick = onUserDataClicked,
)
if (index == state.timelineItems.lastIndex) {
onReachedLoadMore()
@@ -139,6 +143,7 @@ fun TimelineView(
fun TimelineItemRow(
timelineItem: TimelineItem,
highlightedItem: String?,
onUserDataClick: (UserId) -> Unit,
onClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier
@@ -173,6 +178,7 @@ fun TimelineItemRow(
isHighlighted = highlightedItem == timelineItem.identifier(),
onClick = ::onClick,
onLongClick = ::onLongClick,
onUserDataClick = onUserDataClick,
modifier = modifier,
)
}
@@ -203,6 +209,7 @@ fun TimelineItemRow(
highlightedItem = highlightedItem,
onClick = onClick,
onLongClick = onLongClick,
onUserDataClick = onUserDataClick,
)
}
}
@@ -230,15 +237,21 @@ fun TimelineItemEventRow(
isHighlighted: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
onUserDataClick: (UserId) -> Unit,
modifier: Modifier = Modifier
) {
val interactionSource = remember { MutableInteractionSource() }
fun onUserDataClicked() {
onUserDataClick(event.senderId)
}
val (parentAlignment, contentAlignment) = if (event.isMine) {
Pair(Alignment.CenterEnd, Alignment.End)
} else {
Pair(Alignment.CenterStart, Alignment.Start)
}
Box(
modifier = modifier
.fillMaxWidth()
@@ -257,6 +270,7 @@ fun TimelineItemEventRow(
Modifier
.zIndex(1f)
.offset(y = 12.dp)
.clickable(onClick = ::onUserDataClicked)
)
}
val bubbleState = BubbleState(

View File

@@ -16,6 +16,7 @@
plugins {
id("io.element.android-library")
id("kotlin-parcelize")
}
android {

View File

@@ -16,11 +16,26 @@
package io.element.android.features.roomdetails.api
import android.os.Parcelable
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.parcelize.Parcelize
interface RoomDetailsEntryPoint : FeatureEntryPoint {
fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List<Plugin>): Node
sealed interface InitialTarget : Parcelable {
@Parcelize
object RoomDetails : InitialTarget
@Parcelize
data class RoomMemberDetails(val roomMemberId: UserId) : InitialTarget
}
data class Inputs(val initialElement: InitialTarget) : NodeInputs
fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs, plugins: List<Plugin>): Node
}

View File

@@ -21,13 +21,25 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint.InitialTarget
import io.element.android.features.roomdetails.impl.RoomDetailsFlowNode.NavTarget
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List<Plugin>): Node {
return parentNode.createNode<RoomDetailsFlowNode>(buildContext, plugins)
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
inputs: RoomDetailsEntryPoint.Inputs,
plugins: List<Plugin>
): Node {
return parentNode.createNode<RoomDetailsFlowNode>(buildContext, plugins + inputs)
}
}
internal fun InitialTarget.toNavTarget() = when (this) {
is InitialTarget.RoomDetails -> NavTarget.RoomDetails
is InitialTarget.RoomMemberDetails -> NavTarget.RoomMemberDetails(roomMemberId)
}

View File

@@ -28,6 +28,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
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
@@ -44,7 +45,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>,
) : BackstackNode<RoomDetailsFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.RoomDetails,
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Inputs>().first().initialElement.toNavTarget(),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
@@ -95,7 +96,8 @@ class RoomDetailsFlowNode @AssistedInject constructor(
createNode<RoomInviteMembersNode>(buildContext)
}
is NavTarget.RoomMemberDetails -> {
createNode<RoomMemberDetailsNode>(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMemberId)))
val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId))
createNode<RoomMemberDetailsNode>(buildContext, plugins)
}
}
}

View File

@@ -42,11 +42,11 @@ class RoomMemberDetailsNode @AssistedInject constructor(
presenterFactory: RoomMemberDetailsPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
data class Inputs(
val roomMemberId: UserId,
data class RoomMemberDetailsInput(
val roomMemberId: UserId
) : NodeInputs
private val inputs = inputs<Inputs>()
private val inputs = inputs<RoomMemberDetailsInput>()
private val presenter = presenterFactory.create(inputs.roomMemberId)
@Composable

View File

@@ -33,7 +33,7 @@ import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.ui.room.getRoomMember
import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -51,7 +51,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
override fun present(): RoomMemberDetailsState {
val coroutineScope = rememberCoroutineScope()
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
val roomMember by room.getRoomMember(roomMemberId)
val roomMember by room.getRoomMemberAsState(roomMemberId)
// the room member is not really live...
val isBlocked = remember {
mutableStateOf(roomMember?.isIgnored.orFalse())

View File

@@ -29,13 +29,13 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.roomMembers
@Composable
fun MatrixRoom.getRoomMember(userId: UserId): State<RoomMember?> {
fun MatrixRoom.getRoomMemberAsState(userId: UserId): State<RoomMember?> {
val roomMembersState by membersStateFlow.collectAsState()
return getRoomMember(roomMembersState = roomMembersState, userId = userId)
return getRoomMemberAsState(roomMembersState = roomMembersState, userId = userId)
}
@Composable
fun getRoomMember(roomMembersState: MatrixRoomMembersState, userId: UserId): State<RoomMember?> {
fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserId): State<RoomMember?> {
val roomMembers = roomMembersState.roomMembers()
return remember(roomMembers) {
derivedStateOf {