[Room details] Open room member details when clicking on user data in timeline (#482)
This commit is contained in:
committed by
GitHub
parent
d7448c7acc
commit
b50350aaa0
@@ -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()
|
||||
|
||||
@@ -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
1
changelog.d/480.feature
Normal file
@@ -0,0 +1 @@
|
||||
Open room member details when tapping on a user in the timeline
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user