mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Merge pull request #6676 from bxdxnn/misc/reactions-pinned-messages
Show reactions in the pinned messages list
This commit is contained in:
+2
@@ -11,8 +11,10 @@ package io.element.android.features.messages.impl.pinned.list
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
|
||||
sealed interface PinnedMessagesListEvent {
|
||||
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : PinnedMessagesListEvent
|
||||
data class OpenThread(val threadRootId: ThreadId) : PinnedMessagesListEvent
|
||||
data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : PinnedMessagesListEvent
|
||||
}
|
||||
|
||||
+3
@@ -30,6 +30,7 @@ import io.element.android.libraries.androidutils.system.copyToClipboard
|
||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
@@ -50,6 +51,7 @@ class PinnedMessagesListNode(
|
||||
actionListPresenterFactory: ActionListPresenter.Factory,
|
||||
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val emojiPickerRenderer: EmojiPickerRenderer,
|
||||
) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator {
|
||||
interface Callback : Plugin {
|
||||
fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean)
|
||||
@@ -134,6 +136,7 @@ class PinnedMessagesListNode(
|
||||
toastMessage = toastMessage,
|
||||
)
|
||||
},
|
||||
emojiPickerRenderer = emojiPickerRenderer,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
+27
-5
@@ -30,6 +30,8 @@ import io.element.android.features.messages.impl.link.LinkState
|
||||
import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider
|
||||
import io.element.android.features.messages.impl.pinned.keepDisplayablePinnedEvents
|
||||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
@@ -47,6 +49,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
||||
@@ -71,6 +74,8 @@ class PinnedMessagesListPresenter(
|
||||
private val linkPresenter: Presenter<LinkState>,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
@Assisted private val actionListPresenter: Presenter<ActionListState>,
|
||||
private val customReactionPresenter: Presenter<CustomReactionState>,
|
||||
private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
|
||||
@SessionCoroutineScope
|
||||
private val sessionCoroutineScope: CoroutineScope,
|
||||
private val analyticsService: AnalyticsService,
|
||||
@@ -88,7 +93,7 @@ class PinnedMessagesListPresenter(
|
||||
private val timelineItemsFactory: TimelineItemsFactory = timelineItemsFactoryCreator.create(
|
||||
config = TimelineItemsFactoryConfig(
|
||||
computeReadReceipts = false,
|
||||
computeReactions = false,
|
||||
computeReactions = true,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -96,6 +101,9 @@ class PinnedMessagesListPresenter(
|
||||
override fun present(): PinnedMessagesListState {
|
||||
htmlConverterProvider.Update()
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms ->
|
||||
perms.userEventPermissions()
|
||||
}
|
||||
val timelineRoomInfo by remember {
|
||||
derivedStateOf {
|
||||
TimelineRoomInfo(
|
||||
@@ -103,7 +111,7 @@ class PinnedMessagesListPresenter(
|
||||
name = roomInfo.name,
|
||||
// We don't need to compute those values
|
||||
userHasPermissionToSendMessage = false,
|
||||
userHasPermissionToSendReaction = false,
|
||||
userHasPermissionToSendReaction = userEventPermissions.canSendReaction,
|
||||
// We do not care about the call state here.
|
||||
roomCallState = aStandByCallState(),
|
||||
// don't compute this value or the pin icon will be shown
|
||||
@@ -119,9 +127,8 @@ class PinnedMessagesListPresenter(
|
||||
}
|
||||
val timelineProtectionState = timelineProtectionPresenter.present()
|
||||
val linkState = linkPresenter.present()
|
||||
val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms ->
|
||||
perms.userEventPermissions()
|
||||
}
|
||||
val customReactionState = customReactionPresenter.present()
|
||||
val reactionSummaryState = reactionSummaryPresenter.present()
|
||||
|
||||
val displayThreadSummaries by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false)
|
||||
|
||||
@@ -138,6 +145,7 @@ class PinnedMessagesListPresenter(
|
||||
when (event) {
|
||||
is PinnedMessagesListEvent.HandleAction -> sessionCoroutineScope.handleTimelineAction(event.action, event.event)
|
||||
is PinnedMessagesListEvent.OpenThread -> navigator.navigateToThread(event.threadRootId)
|
||||
is PinnedMessagesListEvent.ToggleReaction -> sessionCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,10 +156,20 @@ class PinnedMessagesListPresenter(
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
userEventPermissions = userEventPermissions,
|
||||
timelineItems = pinnedMessageItems,
|
||||
customReactionState = customReactionState,
|
||||
reactionSummaryState = reactionSummaryState,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId) {
|
||||
launch {
|
||||
timelineProvider.invokeOnTimeline {
|
||||
toggleReaction(emoji, eventOrTransactionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.handleTimelineAction(
|
||||
action: TimelineItemAction,
|
||||
targetEvent: TimelineItem.Event,
|
||||
@@ -234,6 +252,8 @@ class PinnedMessagesListPresenter(
|
||||
linkState: LinkState,
|
||||
userEventPermissions: UserEventPermissions,
|
||||
timelineItems: AsyncData<ImmutableList<TimelineItem>>,
|
||||
customReactionState: CustomReactionState,
|
||||
reactionSummaryState: ReactionSummaryState,
|
||||
eventSink: (PinnedMessagesListEvent) -> Unit
|
||||
): PinnedMessagesListState {
|
||||
return when (timelineItems) {
|
||||
@@ -252,6 +272,8 @@ class PinnedMessagesListPresenter(
|
||||
userEventPermissions = userEventPermissions,
|
||||
timelineItems = timelineItems.data,
|
||||
actionListState = actionListState,
|
||||
customReactionState = customReactionState,
|
||||
reactionSummaryState = reactionSummaryState,
|
||||
eventSink = eventSink
|
||||
)
|
||||
}
|
||||
|
||||
+4
@@ -16,6 +16,8 @@ import io.element.android.features.messages.impl.UserEventPermissions
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.link.LinkState
|
||||
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||
@@ -34,6 +36,8 @@ sealed interface PinnedMessagesListState {
|
||||
val timelineItems: ImmutableList<TimelineItem>,
|
||||
val actionListState: ActionListState,
|
||||
val linkState: LinkState,
|
||||
val customReactionState: CustomReactionState,
|
||||
val reactionSummaryState: ReactionSummaryState,
|
||||
val displayThreadSummaries: Boolean,
|
||||
val eventSink: (PinnedMessagesListEvent) -> Unit,
|
||||
) : PinnedMessagesListState {
|
||||
|
||||
+8
@@ -10,6 +10,8 @@ package io.element.android.features.messages.impl.pinned.list
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.messages.impl.UserEventPermissions
|
||||
import io.element.android.features.messages.impl.aCustomReactionState
|
||||
import io.element.android.features.messages.impl.aReactionSummaryState
|
||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||
import io.element.android.features.messages.impl.link.LinkState
|
||||
@@ -19,6 +21,8 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemDaySepara
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemReactions
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAudioContent
|
||||
@@ -92,6 +96,8 @@ fun aLoadedPinnedMessagesListState(
|
||||
linkState: LinkState = aLinkState(),
|
||||
timelineItems: List<TimelineItem> = emptyList(),
|
||||
actionListState: ActionListState = anActionListState(),
|
||||
customReactionState: CustomReactionState = aCustomReactionState(),
|
||||
reactionSummaryState: ReactionSummaryState = aReactionSummaryState(),
|
||||
aUserEventPermissions: UserEventPermissions = UserEventPermissions.DEFAULT,
|
||||
displayThreadSummaries: Boolean = false,
|
||||
eventSink: (PinnedMessagesListEvent) -> Unit = {}
|
||||
@@ -101,6 +107,8 @@ fun aLoadedPinnedMessagesListState(
|
||||
linkState = linkState,
|
||||
timelineItems = timelineItems.toImmutableList(),
|
||||
actionListState = actionListState,
|
||||
customReactionState = customReactionState,
|
||||
reactionSummaryState = reactionSummaryState,
|
||||
userEventPermissions = aUserEventPermissions,
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
eventSink = eventSink,
|
||||
|
||||
+39
-5
@@ -33,8 +33,12 @@ import io.element.android.features.messages.impl.link.LinkEvent
|
||||
import io.element.android.features.messages.impl.link.LinkView
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent
|
||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||
@@ -50,6 +54,8 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
|
||||
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
|
||||
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -66,6 +72,7 @@ fun PinnedMessagesListView(
|
||||
onUserDataClick: (MatrixUser) -> Unit,
|
||||
onLinkClick: (Link) -> Unit,
|
||||
onLinkLongClick: (Link) -> Unit,
|
||||
emojiPickerRenderer: EmojiPickerRenderer,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
@@ -89,6 +96,7 @@ fun PinnedMessagesListView(
|
||||
onLinkClick = onLinkClick,
|
||||
onLinkLongClick = onLinkLongClick,
|
||||
onErrorDismiss = onBackClick,
|
||||
emojiPickerRenderer = emojiPickerRenderer,
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
@@ -121,6 +129,7 @@ private fun PinnedMessagesListContent(
|
||||
onLinkClick: (Link) -> Unit,
|
||||
onLinkLongClick: (Link) -> Unit,
|
||||
onErrorDismiss: () -> Unit,
|
||||
emojiPickerRenderer: EmojiPickerRenderer,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier.fillMaxSize()) {
|
||||
@@ -141,6 +150,7 @@ private fun PinnedMessagesListContent(
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onLinkLongClick = onLinkLongClick,
|
||||
emojiPickerRenderer = emojiPickerRenderer,
|
||||
)
|
||||
PinnedMessagesListState.Loading -> {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
@@ -180,6 +190,7 @@ private fun PinnedMessagesListLoaded(
|
||||
onUserDataClick: (MatrixUser) -> Unit,
|
||||
onLinkClick: (Link) -> Unit,
|
||||
onLinkLongClick: (Link) -> Unit,
|
||||
emojiPickerRenderer: EmojiPickerRenderer,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onActionSelected(timelineItemAction: TimelineItemAction, event: TimelineItem.Event) {
|
||||
@@ -203,11 +214,25 @@ private fun PinnedMessagesListLoaded(
|
||||
)
|
||||
}
|
||||
|
||||
fun onReactionClick(emoji: String, event: TimelineItem.Event) {
|
||||
state.eventSink(PinnedMessagesListEvent.ToggleReaction(emoji, event.eventOrTransactionId))
|
||||
}
|
||||
|
||||
fun onReactionLongClick(emoji: String, event: TimelineItem.Event) {
|
||||
event.eventId?.let { eventId ->
|
||||
state.reactionSummaryState.eventSink(ReactionSummaryEvent.ShowReactionSummary(eventId, event.reactionsState.reactions, emoji))
|
||||
}
|
||||
}
|
||||
|
||||
fun onMoreReactionsClick(event: TimelineItem.Event) {
|
||||
state.customReactionState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event))
|
||||
}
|
||||
|
||||
ActionListView(
|
||||
state = state.actionListState,
|
||||
onSelectAction = ::onActionSelected,
|
||||
onCustomReactionClick = {},
|
||||
onEmojiReactionClick = { _, _ -> },
|
||||
onCustomReactionClick = ::onMoreReactionsClick,
|
||||
onEmojiReactionClick = ::onReactionClick,
|
||||
onVerifiedUserSendFailureClick = {}
|
||||
)
|
||||
|
||||
@@ -239,9 +264,9 @@ private fun PinnedMessagesListLoaded(
|
||||
onLongClick = ::onMessageLongClick,
|
||||
displayThreadSummaries = displayThreadSummaries,
|
||||
inReplyToClick = {},
|
||||
onReactionClick = { _, _ -> },
|
||||
onReactionLongClick = { _, _ -> },
|
||||
onMoreReactionsClick = {},
|
||||
onReactionClick = ::onReactionClick,
|
||||
onReactionLongClick = ::onReactionLongClick,
|
||||
onMoreReactionsClick = ::onMoreReactionsClick,
|
||||
onReadReceiptClick = {},
|
||||
onSwipeToReply = {},
|
||||
onJoinCallClick = {},
|
||||
@@ -273,6 +298,14 @@ private fun PinnedMessagesListLoaded(
|
||||
state.linkState,
|
||||
onLinkValid = onLinkClick,
|
||||
)
|
||||
CustomReactionBottomSheet(
|
||||
state = state.customReactionState,
|
||||
onSelectEmoji = { eventOrTransactionId, emoji ->
|
||||
state.eventSink(PinnedMessagesListEvent.ToggleReaction(emoji.unicode, eventOrTransactionId))
|
||||
},
|
||||
emojiPickerRenderer = emojiPickerRenderer,
|
||||
)
|
||||
ReactionSummaryView(state = state.reactionSummaryState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -322,5 +355,6 @@ internal fun PinnedMessagesListViewPreview(@PreviewParameter(PinnedMessagesListS
|
||||
onUserDataClick = {},
|
||||
onLinkClick = {},
|
||||
onLinkLongClick = {},
|
||||
emojiPickerRenderer = NoOpEmojiPickerRenderer,
|
||||
)
|
||||
}
|
||||
|
||||
+4
@@ -10,6 +10,8 @@ package io.element.android.features.messages.impl.pinned.list
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.PinUnpinAction
|
||||
import io.element.android.features.messages.impl.aCustomReactionState
|
||||
import io.element.android.features.messages.impl.aReactionSummaryState
|
||||
import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
|
||||
@@ -400,6 +402,8 @@ class PinnedMessagesListPresenterTest {
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
actionListPresenter = { anActionListState() },
|
||||
linkPresenter = { aLinkState() },
|
||||
customReactionPresenter = { aCustomReactionState() },
|
||||
reactionSummaryPresenter = { aReactionSummaryState() },
|
||||
analyticsService = analyticsService,
|
||||
featureFlagService = featureFlagService,
|
||||
sessionCoroutineScope = this,
|
||||
|
||||
+2
@@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState
|
||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent
|
||||
import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
@@ -111,6 +112,7 @@ private fun AndroidComposeUiTest<ComponentActivity>.setPinnedMessagesListView(
|
||||
onUserDataClick = onUserDataClick,
|
||||
onLinkClick = onLinkClick,
|
||||
onLinkLongClick = onLinkLongClick,
|
||||
emojiPickerRenderer = NoOpEmojiPickerRenderer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user