Pinned messages : start branching ui to the timeline

This commit is contained in:
ganfra 2024-08-05 20:40:59 +02:00
parent 0832fb9f2f
commit 060b0350e0
6 changed files with 148 additions and 25 deletions

View file

@ -280,7 +280,20 @@ class MessagesFlowNode @AssistedInject constructor(
.build() .build()
} }
NavTarget.PinnedEvents -> { NavTarget.PinnedEvents -> {
createNode<PinnedMessagesListNode>(buildContext) val callback = object : PinnedMessagesListNode.Callback {
override fun onEventClick(event: TimelineItem.Event) {
processEventClick(event)
}
override fun onUserDataClick(userId: UserId) {
callbacks.forEach { it.onUserDataClick(userId) }
}
override fun onPermalinkClick(data: PermalinkData) {
callbacks.forEach { it.onPermalinkClick(data) }
}
}
createNode<PinnedMessagesListNode>(buildContext, plugins = listOf(callback))
} }
NavTarget.Empty -> { NavTarget.Empty -> {
node(buildContext) {} node(buildContext) {}

View file

@ -16,18 +16,27 @@
package io.element.android.features.messages.impl.pinned.list package io.element.android.features.messages.impl.pinned.list
import android.content.Context
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.messages.impl.MessagesNode.Callback
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
@ContributesNode(RoomScope::class) @ContributesNode(RoomScope::class)
class PinnedMessagesListNode @AssistedInject constructor( class PinnedMessagesListNode @AssistedInject constructor(
@ -35,16 +44,55 @@ class PinnedMessagesListNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: PinnedMessagesListPresenter, private val presenter: PinnedMessagesListPresenter,
private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
private val permalinkParser: PermalinkParser,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onEventClick(event: TimelineItem.Event)
fun onUserDataClick(userId: UserId)
fun onPermalinkClick(data: PermalinkData)
}
private val callbacks = plugins<Callback>()
private fun onEventClick(event: TimelineItem.Event) {
return callbacks.forEach { it.onEventClick(event) }
}
private fun onUserDataClick(userId: UserId) {
callbacks.forEach { it.onUserDataClick(userId) }
}
private fun onLinkClick(context: Context, url: String) {
when (val permalink = permalinkParser.parse(url)) {
is PermalinkData.UserLink -> {
// Open the room member profile, it will fallback to
// the user profile if the user is not in the room
callbacks.forEach { it.onUserDataClick(permalink.userId) }
}
is PermalinkData.RoomLink -> {
callbacks.forEach { it.onPermalinkClick(permalink) }
}
is PermalinkData.FallbackLink,
is PermalinkData.RoomEmailInviteLink -> {
context.openUrlInExternalApp(url)
}
}
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
CompositionLocalProvider( CompositionLocalProvider(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
) { ) {
val context = LocalContext.current
val state = presenter.present() val state = presenter.present()
PinnedMessagesListView( PinnedMessagesListView(
state = state, state = state,
onBackClick = ::navigateUp,
onEventClick = ::onEventClick,
onUserDataClick = ::onUserDataClick,
onLinkClick = { url -> onLinkClick(context, url) },
modifier = modifier modifier = modifier
) )
} }

View file

@ -19,9 +19,12 @@ package io.element.android.features.messages.impl.pinned.list
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -35,7 +38,15 @@ class PinnedMessagesListPresenter @Inject constructor(
@Composable @Composable
override fun present(): PinnedMessagesListState { override fun present(): PinnedMessagesListState {
val timelineItems by timelineItemsFactory.collectItemsAsState() val timelineItems by timelineItemsFactory.collectItemsAsState()
val timelineRoomInfo = remember {
TimelineRoomInfo(
isDm = room.isDm,
name = room.displayName,
userHasPermissionToSendMessage = false,
userHasPermissionToSendReaction = false,
isCallOngoing = false,
)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
val timeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect val timeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect
combine(timeline.timelineItems, room.membersStateFlow) { items, membersState -> combine(timeline.timelineItems, room.membersStateFlow) { items, membersState ->
@ -51,6 +62,7 @@ class PinnedMessagesListPresenter @Inject constructor(
} }
return PinnedMessagesListState( return PinnedMessagesListState(
timelineRoomInfo = timelineRoomInfo,
timelineItems = timelineItems, timelineItems = timelineItems,
eventSink = ::handleEvents eventSink = ::handleEvents
) )

View file

@ -16,10 +16,19 @@
package io.element.android.features.messages.impl.pinned.list package io.element.android.features.messages.impl.pinned.list
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class PinnedMessagesListState( data class PinnedMessagesListState(
val timelineRoomInfo: TimelineRoomInfo,
val timelineItems: ImmutableList<TimelineItem>, val timelineItems: ImmutableList<TimelineItem>,
val eventSink: (PinnedMessagesListEvents) -> Unit val eventSink: (PinnedMessagesListEvents) -> Unit,
val pinnedMessagesCount: String =
timelineItems
.count { timelineItem -> timelineItem is TimelineItem.Event }
.takeIf { it > 0 }
?.toString()
?: ""
) )

View file

@ -24,37 +24,77 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Scaffold 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.matrix.api.core.UserId
import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun PinnedMessagesListView( fun PinnedMessagesListView(
state: PinnedMessagesListState, state: PinnedMessagesListState,
onBackClick: () -> Unit,
onEventClick: (event: TimelineItem.Event) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
) { padding -> topBar = {
PinnedMessagesListTopBar(state, onBackClick)
},
content = { padding ->
PinnedMessagesListContent( PinnedMessagesListContent(
state = state, state = state,
onEventClick = onEventClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
modifier = Modifier modifier = Modifier
.padding(padding) .padding(padding)
.consumeWindowInsets(padding), .consumeWindowInsets(padding),
) )
} }
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PinnedMessagesListTopBar(
state: PinnedMessagesListState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
TopAppBar(
title = {
Text(
text = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator_description, state.pinnedMessagesCount),
style = ElementTheme.typography.fontBodyLgMedium
)
},
navigationIcon = { BackButton(onClick = onBackClick) },
modifier = modifier,
)
} }
@Composable @Composable
fun PinnedMessagesListContent( private fun PinnedMessagesListContent(
state: PinnedMessagesListState, state: PinnedMessagesListState,
onEventClick: (event: TimelineItem.Event) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box(modifier) { Box(modifier) {
@ -71,21 +111,14 @@ fun PinnedMessagesListContent(
) { timelineItem -> ) { timelineItem ->
TimelineItemRow( TimelineItemRow(
timelineItem = timelineItem, timelineItem = timelineItem,
timelineRoomInfo = TimelineRoomInfo( timelineRoomInfo = state.timelineRoomInfo,
isDm = false,
name = null,
userHasPermissionToSendMessage = false,
userHasPermissionToSendReaction = false,
isCallOngoing = false,
),
renderReadReceipts = false, renderReadReceipts = false,
isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true && isLastOutgoingMessage = false,
state.timelineItems.first().identifier() == timelineItem.identifier(),
focusedEventId = null, focusedEventId = null,
onClick = {}, onClick = onEventClick,
onLongClick = {}, onLongClick = {},
onUserDataClick = { }, onUserDataClick = onUserDataClick,
onLinkClick = {}, onLinkClick = onLinkClick,
inReplyToClick = {}, inReplyToClick = {},
onReactionClick = { _, _ -> }, onReactionClick = { _, _ -> },
onReactionLongClick = { _, _ -> }, onReactionLongClick = { _, _ -> },
@ -106,5 +139,9 @@ fun PinnedMessagesTimelineViewPreview(@PreviewParameter(PinnedMessagesTimelineSt
ElementPreview { ElementPreview {
PinnedMessagesListView( PinnedMessagesListView(
state = state, state = state,
onBackClick = {},
onEventClick = { },
onUserDataClick = {},
onLinkClick = {},
) )
} }

View file

@ -17,6 +17,8 @@
package io.element.android.features.messages.impl.pinned.list package io.element.android.features.messages.impl.pinned.list
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -28,8 +30,10 @@ open class PinnedMessagesTimelineStateProvider : PreviewParameterProvider<Pinned
} }
fun pinnedMessagesListState( fun pinnedMessagesListState(
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
timelineItems: List<TimelineItem> = emptyList(), timelineItems: List<TimelineItem> = emptyList(),
) = PinnedMessagesListState( ) = PinnedMessagesListState(
timelineRoomInfo = timelineRoomInfo,
timelineItems = timelineItems.toImmutableList(), timelineItems = timelineItems.toImmutableList(),
eventSink = {} eventSink = {}
) )