Merge pull request #3963 from element-hq/feature/bma/copyCaption

Add timeline action item to copy caption
This commit is contained in:
Benoit Marty 2024-11-29 15:36:58 +01:00 committed by GitHub
commit 30897d3a3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 291 additions and 129 deletions

View file

@ -28,6 +28,8 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
@ -65,6 +67,7 @@ class MessagesNode @AssistedInject constructor(
messageComposerPresenterFactory: MessageComposerPresenter.Factory,
timelinePresenterFactory: TimelinePresenter.Factory,
presenterFactory: MessagesPresenter.Factory,
actionListPresenterFactory: ActionListPresenter.Factory,
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
private val mediaPlayer: MediaPlayer,
private val permalinkParser: PermalinkParser,
@ -73,6 +76,7 @@ class MessagesNode @AssistedInject constructor(
navigator = this,
composerPresenter = messageComposerPresenterFactory.create(this),
timelinePresenter = timelinePresenterFactory.create(this),
actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
)
private val callbacks = plugins<Callback>()

View file

@ -28,9 +28,8 @@ import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.appconfig.MessageComposerConfig
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
@ -93,7 +92,7 @@ class MessagesPresenter @AssistedInject constructor(
@Assisted private val timelinePresenter: Presenter<TimelineState>,
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
actionListPresenterFactory: ActionListPresenter.Factory,
@Assisted private val actionListPresenter: Presenter<ActionListState>,
private val customReactionPresenter: Presenter<CustomReactionState>,
private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
private val readReceiptBottomSheetPresenter: Presenter<ReadReceiptBottomSheetState>,
@ -110,14 +109,13 @@ class MessagesPresenter @AssistedInject constructor(
private val permalinkParser: PermalinkParser,
private val analyticsService: AnalyticsService,
) : Presenter<MessagesState> {
private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
@AssistedFactory
interface Factory {
fun create(
navigator: MessagesNavigator,
composerPresenter: Presenter<MessageComposerState>,
timelinePresenter: Presenter<TimelineState>,
actionListPresenter: Presenter<ActionListState>,
): MessagesPresenter
}
@ -272,7 +270,8 @@ class MessagesPresenter @AssistedInject constructor(
timelineState: TimelineState,
) = launch {
when (action) {
TimelineItemAction.Copy -> handleCopyContents(targetEvent)
TimelineItemAction.CopyText -> handleCopyContents(targetEvent)
TimelineItemAction.CopyCaption -> handleCopyCaption(targetEvent)
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
@ -488,11 +487,17 @@ class MessagesPresenter @AssistedInject constructor(
is TimelineItemStateContent -> event.content.body
else -> return
}
clipboardHelper.copyPlainText(content)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
snackbarDispatcher.post(SnackbarMessage(R.string.screen_room_timeline_message_copied))
}
}
private suspend fun handleCopyCaption(event: TimelineItem.Event) {
val content = (event.content as? TimelineItemEventContentWithAttachment)?.caption ?: return
clipboardHelper.copyPlainText(content)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
}
}
}

View file

@ -21,6 +21,7 @@ import dagger.assisted.AssistedInject
import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled
import io.element.android.features.messages.impl.UserEventPermissions
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionComparator
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory
@ -70,6 +71,8 @@ class DefaultActionListPresenter @AssistedInject constructor(
override fun create(postProcessor: TimelineItemActionPostProcessor): DefaultActionListPresenter
}
private val comparator = TimelineItemActionComparator()
@Composable
override fun present(): ActionListState {
val localCoroutineScope = rememberCoroutineScope()
@ -145,7 +148,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
isEventPinned: Boolean,
): List<TimelineItemAction> {
val canRedact = timelineItem.isMine && usersEventPermissions.canRedactOwn || !timelineItem.isMine && usersEventPermissions.canRedactOther
return buildList {
return buildSet {
if (timelineItem.canBeRepliedTo && usersEventPermissions.canSendMessage) {
if (timelineItem.isThreaded) {
add(TimelineItemAction.ReplyInThread)
@ -183,7 +186,9 @@ class DefaultActionListPresenter @AssistedInject constructor(
}
}
if (timelineItem.content.canBeCopied()) {
add(TimelineItemAction.Copy)
add(TimelineItemAction.CopyText)
} else if ((timelineItem.content as? TimelineItemEventContentWithAttachment)?.caption.isNullOrBlank().not()) {
add(TimelineItemAction.CopyCaption)
}
if (timelineItem.isRemote) {
add(TimelineItemAction.CopyLink)
@ -199,6 +204,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
}
}
.postFilter(timelineItem.content)
.sortedWith(comparator)
.let(postProcessor::process)
}
}
@ -206,7 +212,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
/**
* Post filter the actions based on the content of the event.
*/
private fun List<TimelineItemAction>.postFilter(content: TimelineItemEventContent): List<TimelineItemAction> {
private fun Iterable<TimelineItemAction>.postFilter(content: TimelineItemEventContent): Iterable<TimelineItemAction> {
return filter { action ->
when (content) {
is TimelineItemCallNotifyContent,

View file

@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.actionlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionComparator
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.anUnsignedDeviceSendFailure
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
@ -22,7 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
override val values: Sequence<ActionListState>
@ -50,7 +51,9 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
),
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
actions = aTimelineItemActionList(
copyAction = TimelineItemAction.CopyCaption,
),
)
),
anActionListState(
@ -61,7 +64,9 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
),
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
actions = aTimelineItemActionList(
copyAction = TimelineItemAction.CopyCaption,
),
)
),
anActionListState(
@ -72,7 +77,9 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
),
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
actions = aTimelineItemActionList(
copyAction = null,
),
)
),
anActionListState(
@ -83,18 +90,22 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
),
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
actions = aTimelineItemActionList(
copyAction = TimelineItemAction.CopyCaption,
),
)
),
anActionListState(
target = ActionListState.Target.Success(
event = aTimelineItemEvent(
content = aTimelineItemVoiceContent(),
content = aTimelineItemVoiceContent(caption = null),
timelineItemReactions = reactionsState
),
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(),
actions = aTimelineItemActionList(
copyAction = null,
),
)
),
anActionListState(
@ -161,27 +172,31 @@ fun anActionListState(
eventSink = eventSink
)
fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {
return persistentListOf(
fun aTimelineItemActionList(
copyAction: TimelineItemAction? = TimelineItemAction.CopyText
): ImmutableList<TimelineItemAction> {
return setOfNotNull(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Copy,
copyAction,
TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
TimelineItemAction.Redact,
TimelineItemAction.ReportContent,
TimelineItemAction.ViewSource,
)
.sortedWith(TimelineItemActionComparator())
.toPersistentList()
}
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
return persistentListOf(
return setOf(
TimelineItemAction.EndPoll,
TimelineItemAction.Reply,
TimelineItemAction.Copy,
TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
TimelineItemAction.Redact,
)
.sortedWith(TimelineItemActionComparator())
.toPersistentList()
}

View file

@ -22,7 +22,8 @@ sealed class TimelineItemAction(
) {
data object ViewInTimeline : TimelineItemAction(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on)
data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward)
data object Copy : TimelineItemAction(CommonStrings.action_copy, CompoundDrawables.ic_compound_copy)
data object CopyText : TimelineItemAction(CommonStrings.action_copy_text, CompoundDrawables.ic_compound_copy)
data object CopyCaption : TimelineItemAction(CommonStrings.action_copy_caption, CompoundDrawables.ic_compound_copy)
data object CopyLink : TimelineItemAction(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link)
data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true)
data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)

View file

@ -0,0 +1,37 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.messages.impl.actionlist.model
class TimelineItemActionComparator : Comparator<TimelineItemAction> {
// See order in https://www.figma.com/design/ux3tYoZV9WghC7hHT9Fhk0/Compound-iOS-Components?node-id=2946-2392
private val orderedList = listOf(
TimelineItemAction.EndPoll,
TimelineItemAction.ViewInTimeline,
TimelineItemAction.Reply,
TimelineItemAction.ReplyInThread,
TimelineItemAction.Forward,
TimelineItemAction.Pin,
TimelineItemAction.Unpin,
TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
TimelineItemAction.CopyText,
TimelineItemAction.AddCaption,
TimelineItemAction.EditCaption,
TimelineItemAction.CopyCaption,
TimelineItemAction.RemoveCaption,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
TimelineItemAction.Redact,
)
override fun compare(o1: TimelineItemAction, o2: TimelineItemAction): Int {
val index1 = orderedList.indexOf(o1)
val index2 = orderedList.indexOf(o2)
return index1.compareTo(index2)
}
}

View file

@ -19,6 +19,7 @@ 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.actionlist.ActionListPresenter
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
@ -35,6 +36,7 @@ class PinnedMessagesListNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: PinnedMessagesListPresenter.Factory,
actionListPresenterFactory: ActionListPresenter.Factory,
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
private val permalinkParser: PermalinkParser,
) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator {
@ -47,7 +49,10 @@ class PinnedMessagesListNode @AssistedInject constructor(
fun onForwardEventClick(eventId: EventId)
}
private val presenter = presenterFactory.create(this)
private val presenter = presenterFactory.create(
navigator = this,
actionListPresenter = actionListPresenterFactory.create(PinnedMessagesListTimelineActionPostProcessor())
)
private val callbacks = plugins<Callback>()
private fun onEventClick(event: TimelineItem.Event) {

View file

@ -23,7 +23,7 @@ import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.Interaction
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.features.messages.impl.UserEventPermissions
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
@ -64,13 +64,16 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
private val timelineProvider: PinnedEventsTimelineProvider,
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
private val snackbarDispatcher: SnackbarDispatcher,
actionListPresenterFactory: ActionListPresenter.Factory,
@Assisted private val actionListPresenter: Presenter<ActionListState>,
private val appCoroutineScope: CoroutineScope,
private val analyticsService: AnalyticsService,
) : Presenter<PinnedMessagesListState> {
@AssistedFactory
interface Factory {
fun create(navigator: PinnedMessagesListNavigator): PinnedMessagesListPresenter
fun create(
navigator: PinnedMessagesListNavigator,
actionListPresenter: Presenter<ActionListState>,
): PinnedMessagesListPresenter
}
private val timelineItemsFactory: TimelineItemsFactory = timelineItemsFactoryCreator.create(
@ -79,7 +82,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
computeReactions = false,
)
)
private val actionListPresenter = actionListPresenterFactory.create(PinnedMessagesListTimelineActionPostProcessor())
@Composable
override fun present(): PinnedMessagesListState {

View file

@ -14,7 +14,7 @@ import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.PinUnpinAction
import io.element.android.features.messages.impl.actionlist.ActionListEvents
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.FakeActionListPresenter
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.crypto.identity.anIdentityChangeState
import io.element.android.features.messages.impl.fixtures.aMessageEvent
@ -242,7 +242,7 @@ class MessagesPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Copy, event))
initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.CopyText, event))
skipItems(2)
assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body)
}
@ -1116,7 +1116,7 @@ class MessagesPresenterTest {
voiceMessageComposerPresenter = { aVoiceMessageComposerState() },
timelinePresenter = { aTimelineState(eventSink = timelineEventSink) },
timelineProtectionPresenter = { aTimelineProtectionState() },
actionListPresenterFactory = FakeActionListPresenter.Factory(actionListEventSink),
actionListPresenter = { anActionListState(eventSink = actionListEventSink) },
customReactionPresenter = { aCustomReactionState() },
reactionSummaryPresenter = { aReactionSummaryState() },
readReceiptBottomSheetPresenter = { aReadReceiptBottomSheetState() },

View file

@ -176,8 +176,8 @@ class ActionListPresenterTest {
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Pin,
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
)
@ -221,8 +221,8 @@ class ActionListPresenterTest {
TimelineItemAction.ReplyInThread,
TimelineItemAction.Forward,
TimelineItemAction.Pin,
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
)
@ -268,8 +268,8 @@ class ActionListPresenterTest {
actions = persistentListOf(
TimelineItemAction.Forward,
TimelineItemAction.Pin,
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
)
@ -314,8 +314,8 @@ class ActionListPresenterTest {
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Pin,
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
TimelineItemAction.Redact,
@ -361,8 +361,8 @@ class ActionListPresenterTest {
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Pin,
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
TimelineItemAction.Redact,
@ -408,10 +408,10 @@ class ActionListPresenterTest {
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Edit,
TimelineItemAction.Pin,
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
)
@ -453,10 +453,10 @@ class ActionListPresenterTest {
actions = persistentListOf(
TimelineItemAction.ReplyInThread,
TimelineItemAction.Forward,
TimelineItemAction.Edit,
TimelineItemAction.Pin,
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
)
@ -501,10 +501,10 @@ class ActionListPresenterTest {
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Edit,
TimelineItemAction.Pin,
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
)
)
@ -547,9 +547,9 @@ class ActionListPresenterTest {
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.AddCaption,
TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
TimelineItemAction.AddCaption,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
)
@ -646,10 +646,11 @@ class ActionListPresenterTest {
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.EditCaption,
TimelineItemAction.RemoveCaption,
TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
TimelineItemAction.EditCaption,
TimelineItemAction.CopyCaption,
TimelineItemAction.RemoveCaption,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
)
@ -660,6 +661,54 @@ class ActionListPresenterTest {
}
}
@Test
fun `present - compute for a media with caption item - other user event`() = runTest {
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
val messageEvent = aMessageEvent(
isMine = false,
isEditable = false,
content = aTimelineItemImageContent(
caption = A_CAPTION,
),
)
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(
event = messageEvent,
userEventPermissions = aUserEventPermissions(
canRedactOwn = true,
canRedactOther = false,
canSendMessage = true,
canSendReaction = true,
canPinUnpin = true,
),
)
)
val successState = awaitItem()
assertThat(successState.target).isEqualTo(
ActionListState.Target.Success(
event = messageEvent,
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
TimelineItemAction.CopyCaption,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
)
)
)
initialState.eventSink.invoke(ActionListEvents.Clear)
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
}
}
@Test
fun `present - compute for a state item in debug build`() = runTest {
val presenter = createActionListPresenter(isDeveloperModeEnabled = true, isPinFeatureEnabled = true)
@ -764,10 +813,10 @@ class ActionListPresenterTest {
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Edit,
TimelineItemAction.Pin,
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
TimelineItemAction.CopyText,
TimelineItemAction.Redact,
)
)
@ -811,9 +860,9 @@ class ActionListPresenterTest {
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Edit,
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
)
@ -865,10 +914,10 @@ class ActionListPresenterTest {
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Forward,
TimelineItemAction.Edit,
TimelineItemAction.Unpin,
TimelineItemAction.Copy,
TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
TimelineItemAction.CopyText,
TimelineItemAction.ViewSource,
TimelineItemAction.Redact,
)
@ -961,7 +1010,7 @@ class ActionListPresenterTest {
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Edit,
TimelineItemAction.Copy,
TimelineItemAction.CopyText,
TimelineItemAction.Redact,
)
)
@ -1000,11 +1049,11 @@ class ActionListPresenterTest {
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.Edit,
TimelineItemAction.EndPoll,
TimelineItemAction.Reply,
TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
TimelineItemAction.Edit,
TimelineItemAction.Redact,
)
)
@ -1043,8 +1092,8 @@ class ActionListPresenterTest {
displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(
TimelineItemAction.Reply,
TimelineItemAction.EndPoll,
TimelineItemAction.Reply,
TimelineItemAction.Pin,
TimelineItemAction.CopyLink,
TimelineItemAction.Redact,
@ -1105,7 +1154,9 @@ class ActionListPresenterTest {
val messageEvent = aMessageEvent(
isMine = true,
isEditable = false,
content = aTimelineItemVoiceContent(),
content = aTimelineItemVoiceContent(
caption = null,
),
)
initialState.eventSink.invoke(
ActionListEvents.ComputeForMessage(

View file

@ -1,24 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.messages.impl.actionlist
import androidx.compose.runtime.Composable
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
class FakeActionListPresenter(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter {
class Factory(private val eventSink: (ActionListEvents) -> Unit = {}) : ActionListPresenter.Factory {
override fun create(postProcessor: TimelineItemActionPostProcessor): ActionListPresenter {
return FakeActionListPresenter(eventSink)
}
}
@Composable
override fun present(): ActionListState {
return anActionListState(eventSink = eventSink)
}
}

View file

@ -9,7 +9,7 @@ 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.actionlist.FakeActionListPresenter
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
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
@ -312,7 +312,7 @@ class PinnedMessagesListPresenterTest {
timelineProvider = timelineProvider,
timelineProtectionPresenter = { aTimelineProtectionState() },
snackbarDispatcher = SnackbarDispatcher(),
actionListPresenterFactory = FakeActionListPresenter.Factory(),
actionListPresenter = { anActionListState() },
analyticsService = analyticsService,
appCoroutineScope = this,
)

View file

@ -0,0 +1,58 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.messages.impl.pinned.list
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import org.junit.Test
class PinnedMessagesListTimelineActionPostProcessorTest {
@Test
fun `ensure that ViewInTimeline is added`() {
val sut = PinnedMessagesListTimelineActionPostProcessor()
val result = sut.process(
listOf()
)
assertThat(result).isEqualTo(
listOf(TimelineItemAction.ViewInTimeline)
)
}
@Test
fun `ensure that some actions are kept and some other are filtered out`() {
val sut = PinnedMessagesListTimelineActionPostProcessor()
val result = sut.process(
listOf(
TimelineItemAction.Forward,
TimelineItemAction.CopyText,
TimelineItemAction.CopyCaption,
TimelineItemAction.CopyLink,
TimelineItemAction.Redact,
TimelineItemAction.Reply,
TimelineItemAction.ReplyInThread,
TimelineItemAction.Edit,
TimelineItemAction.EditCaption,
TimelineItemAction.AddCaption,
TimelineItemAction.RemoveCaption,
TimelineItemAction.ViewSource,
TimelineItemAction.ReportContent,
TimelineItemAction.EndPoll,
TimelineItemAction.Pin,
TimelineItemAction.Unpin,
)
)
assertThat(result).isEqualTo(
listOf(
TimelineItemAction.ViewInTimeline,
TimelineItemAction.Unpin,
TimelineItemAction.Forward,
TimelineItemAction.ViewSource,
)
)
}
}

View file

@ -46,8 +46,10 @@
<string name="action_confirm_password">"Confirm password"</string>
<string name="action_continue">"Continue"</string>
<string name="action_copy">"Copy"</string>
<string name="action_copy_caption">"Copy caption"</string>
<string name="action_copy_link">"Copy link"</string>
<string name="action_copy_link_to_message">"Copy link to message"</string>
<string name="action_copy_text">"Copy text"</string>
<string name="action_create">"Create"</string>
<string name="action_create_a_room">"Create a room"</string>
<string name="action_deactivate">"Deactivate"</string>

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ab41e6b8fe6c235a4915d88a3d42e29cc84678a2b41bafbe38f55111632d1eec
size 31740
oid sha256:e67a540966100272311381e87011149cdb15c8191a6f2bbc40d1febff999c431
size 24067

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2fb57605dad290919daa4c331442d69651e26db88235f77e4912ffb9631a36c0
size 45151
oid sha256:65023f7233f112547ccd0850c321b2d6610cfb6a1371c494f68722e75874f871
size 45915

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:baadc64d3b4f9464bc6063cc9cc10adfa256d25308c83f647e508f42fd45ef1c
size 46852
oid sha256:e2c2e01bd133c7b84e381141b6baf22783cb585cb3af9f790785dbf8aeaeeed6
size 47644

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1009af703a11348dac63dc071333e077f4bba76c73bd64bf9e532381c7607c1d
size 39624
oid sha256:9bd6fb63059cebc7dc7b850b5aa73549ab9846eda68b1b6d9a4f8e0716d42c3c
size 40419

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0f5adfb435286a84587a66d6970b1a7f03a8cb53eac86956f61092d97ea16951
size 43121
oid sha256:6b4e4a075bcb4b95455c27ce2e5f9f1bdac4a1aca22ad944dd53fdbaeb6ca970
size 44550

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d3843655f4b2f55b5eb2cdb014e1ca03f9570e312e725fe074a5191e0350c079
size 40677
oid sha256:1a2bc7e9098301d17e72a61a7baf01e52aa11566e1da4ffe3b43a66fa37652b2
size 42103

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce1c3d6c04316fd9f43b86edb04ba209eb6df450f40438f98b8503fb116d4ea4
size 40271
oid sha256:f097af2773ff2ecf70a69e4292dc118b4c8616f1c8f979e7d121e86a16ef3072
size 38506

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4839112543c6ab57848c1dd30440b12703da1a35a6457bf9fa5f5aabd1dd08b9
size 40963
oid sha256:8f49870a5333cbaf1c6c3ac5ddcb85290d04efc79ffec93d8dc39b59f9712820
size 42391

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:41528c85820cbe666c719536022fb52ce510055c8c8ef3f1ce36da32731f0a0b
size 41615
oid sha256:883f160afa3fc3d2c0be5098205ea616875084ee0f122ebf994d0fee53a8ecd1
size 39847

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ac14f7921bedaf7135655775b524fde596120d6ea9b708ba5653b3805f567067
size 41469
oid sha256:7790c892daeee8910ce17caac2c957d09fffec0d41a51b3adc1d5bef8dcee1a1
size 42261

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce9244eb3b531a4c2816a32c8f806476cd0776f6394f32cebcb82f8a48cd64d7
size 30837
oid sha256:bc7b733680a0d86ee1231345e58641fb3a208da21de62a50f624d9ae04c6e140
size 31661

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4aa886aa0f70c7c85fc25e9366792bd6e2c9e1a9b6c8864282834d9f72b3fcb2
size 30809
oid sha256:20504466241e36817557812f6b378aceab9c2e271a596bd3d037c6be41af7c54
size 23624

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:756255463c3747888878fef57ec4c3204075993d3fb5bdaf3d1bbe436648862c
size 44345
oid sha256:48028e4c3ca7e9b871683165f029430bcd4e0fc2411e4ffc83a93abb641d96a2
size 45132

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2bdc999259dabe20157611f27925d6f0c8f9f77eebc1914fa8f793b62aa8c299
size 45864
oid sha256:b040fe47521f5d9d76d7892412bb2b5cf7e2b93b951f5de5f772503797fb6b24
size 46548

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ffb565c4d790e24a2f395b12d8dbbfa1ba4abadd7cf9951deae049696e04b206
size 38843
oid sha256:6db046a97f14f2df3db2415d71a02ffd6e706fe73def67c21f0d844be279da59
size 39617

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:87d68af6f69c67de710145b3fabbff40bc6c9a767542e5a62be80d084972cb28
size 42221
oid sha256:7ee53243923d7b5adcc2094d3fe6bb87be00a179cae11966dcb230f2c3e8246e
size 43681

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08c6685bec4751c852ce2440c5b3dd006d46b80dfd0f5986a3392278f486e181
size 39863
oid sha256:b88e54ba4743e1e245b5a4c7d6b4045f816241f6cfc8716b8cede0b62666222b
size 41286

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:04a1cbb1d1e98f87446ad911b61b002e34ceb2725ec24dfd884561d3585b9d83
size 39518
oid sha256:d88b4d82bb0d416aff7b32647479181ac9702c36c08370e7671d6e5ef80687c7
size 37825

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d5ba01dbb6e8d1d238fcfdecbd67ee74bf833a3592087ec5ffc24b8ddeb85178
size 40124
oid sha256:b3d4dd1766a3f46acd86bdd5ec920c56a5e5f897c81c1477df10ba59dcd3d5b7
size 41554

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dd84e6907dc39e1ad7e6db95d8453aa51e18b14c881499c05a233678fa0e7094
size 40762
oid sha256:842fbea74e53c8804fd0a3e9fe96dcb21b5c91a099e1de33d8ec983fd9a8a80a
size 39112

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5c35af3e85d906c6ed24fbb454cf32c7b25f87471e631af8b6ccc1d8465de00
size 40631
oid sha256:1311ec5e008b44d81e6556164f780c57dea3e4721cc47caccdbf337dd4027eb7
size 41402

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:486f9e142212d3864dfb936905feb37421fd4e8c9b5734c5f9ee4f29ec1166f6
size 29964
oid sha256:d2cf27d2f053f33c67d944a8f45e0206165b2db2f3b959eb9e0bb943f84fe6a1
size 30657