diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index ca4bf907ac..3b30d11a7f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -280,7 +280,20 @@ class MessagesFlowNode @AssistedInject constructor( .build() } NavTarget.PinnedEvents -> { - createNode(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(buildContext, plugins = listOf(callback)) } NavTarget.Empty -> { node(buildContext) {} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt index 5aee2bc3bb..ff53cf0781 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -16,18 +16,27 @@ package io.element.android.features.messages.impl.pinned.list +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject 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.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.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) class PinnedMessagesListNode @AssistedInject constructor( @@ -35,16 +44,55 @@ class PinnedMessagesListNode @AssistedInject constructor( @Assisted plugins: List, private val presenter: PinnedMessagesListPresenter, private val timelineItemPresenterFactories: TimelineItemPresenterFactories, + private val permalinkParser: PermalinkParser, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onEventClick(event: TimelineItem.Event) + fun onUserDataClick(userId: UserId) + fun onPermalinkClick(data: PermalinkData) + } + + private val callbacks = plugins() + + 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 override fun View(modifier: Modifier) { CompositionLocalProvider( LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, ) { + val context = LocalContext.current val state = presenter.present() PinnedMessagesListView( state = state, + onBackClick = ::navigateUp, + onEventClick = ::onEventClick, + onUserDataClick = ::onUserDataClick, + onLinkClick = { url -> onLinkClick(context, url) }, modifier = modifier ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index cde419f463..35d6c984f4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -19,9 +19,12 @@ package io.element.android.features.messages.impl.pinned.list import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.libraries.architecture.Presenter 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 kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -35,7 +38,15 @@ class PinnedMessagesListPresenter @Inject constructor( @Composable override fun present(): PinnedMessagesListState { val timelineItems by timelineItemsFactory.collectItemsAsState() - + val timelineRoomInfo = remember { + TimelineRoomInfo( + isDm = room.isDm, + name = room.displayName, + userHasPermissionToSendMessage = false, + userHasPermissionToSendReaction = false, + isCallOngoing = false, + ) + } LaunchedEffect(Unit) { val timeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect combine(timeline.timelineItems, room.membersStateFlow) { items, membersState -> @@ -51,6 +62,7 @@ class PinnedMessagesListPresenter @Inject constructor( } return PinnedMessagesListState( + timelineRoomInfo = timelineRoomInfo, timelineItems = timelineItems, eventSink = ::handleEvents ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt index 4c1d779b62..80a3578e74 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt @@ -16,10 +16,19 @@ 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 kotlinx.collections.immutable.ImmutableList data class PinnedMessagesListState( + val timelineRoomInfo: TimelineRoomInfo, val timelineItems: ImmutableList, - val eventSink: (PinnedMessagesListEvents) -> Unit + val eventSink: (PinnedMessagesListEvents) -> Unit, + + val pinnedMessagesCount: String = + timelineItems + .count { timelineItem -> timelineItem is TimelineItem.Event } + .takeIf { it > 0 } + ?.toString() + ?: "" ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index f5e1d6a173..c86b14a042 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -24,37 +24,77 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter 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.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.PreviewsDayNight 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 fun PinnedMessagesListView( state: PinnedMessagesListState, + onBackClick: () -> Unit, + onEventClick: (event: TimelineItem.Event) -> Unit, + onUserDataClick: (UserId) -> Unit, + onLinkClick: (String) -> Unit, modifier: Modifier = Modifier, ) { Scaffold( modifier = modifier, - ) { padding -> - PinnedMessagesListContent( - state = state, - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding), - ) - } + topBar = { + PinnedMessagesListTopBar(state, onBackClick) + }, + content = { padding -> + PinnedMessagesListContent( + state = state, + onEventClick = onEventClick, + onUserDataClick = onUserDataClick, + onLinkClick = onLinkClick, + modifier = Modifier + .padding(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 -fun PinnedMessagesListContent( +private fun PinnedMessagesListContent( state: PinnedMessagesListState, + onEventClick: (event: TimelineItem.Event) -> Unit, + onUserDataClick: (UserId) -> Unit, + onLinkClick: (String) -> Unit, modifier: Modifier = Modifier, ) { Box(modifier) { @@ -71,21 +111,14 @@ fun PinnedMessagesListContent( ) { timelineItem -> TimelineItemRow( timelineItem = timelineItem, - timelineRoomInfo = TimelineRoomInfo( - isDm = false, - name = null, - userHasPermissionToSendMessage = false, - userHasPermissionToSendReaction = false, - isCallOngoing = false, - ), + timelineRoomInfo = state.timelineRoomInfo, renderReadReceipts = false, - isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true && - state.timelineItems.first().identifier() == timelineItem.identifier(), + isLastOutgoingMessage = false, focusedEventId = null, - onClick = {}, + onClick = onEventClick, onLongClick = {}, - onUserDataClick = { }, - onLinkClick = {}, + onUserDataClick = onUserDataClick, + onLinkClick = onLinkClick, inReplyToClick = {}, onReactionClick = { _, _ -> }, onReactionLongClick = { _, _ -> }, @@ -106,5 +139,9 @@ fun PinnedMessagesTimelineViewPreview(@PreviewParameter(PinnedMessagesTimelineSt ElementPreview { PinnedMessagesListView( state = state, + onBackClick = {}, + onEventClick = { }, + onUserDataClick = {}, + onLinkClick = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt index e70e893909..b7f3124406 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesTimelineListProvider.kt @@ -17,6 +17,8 @@ package io.element.android.features.messages.impl.pinned.list 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 kotlinx.collections.immutable.toImmutableList @@ -28,8 +30,10 @@ open class PinnedMessagesTimelineStateProvider : PreviewParameterProvider = emptyList(), ) = PinnedMessagesListState( + timelineRoomInfo = timelineRoomInfo, timelineItems = timelineItems.toImmutableList(), eventSink = {} )