Skip to content

Commit 30897d3

Browse files
authored
Merge pull request #3963 from element-hq/feature/bma/copyCaption
Add timeline action item to copy caption
2 parents 3677c43 + 13ae189 commit 30897d3

File tree

36 files changed

+290
-128
lines changed

36 files changed

+290
-128
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import dagger.assisted.Assisted
2828
import dagger.assisted.AssistedInject
2929
import io.element.android.anvilannotations.ContributesNode
3030
import io.element.android.compound.theme.ElementTheme
31+
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
32+
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
3133
import io.element.android.features.messages.impl.attachments.Attachment
3234
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
3335
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
@@ -65,6 +67,7 @@ class MessagesNode @AssistedInject constructor(
6567
messageComposerPresenterFactory: MessageComposerPresenter.Factory,
6668
timelinePresenterFactory: TimelinePresenter.Factory,
6769
presenterFactory: MessagesPresenter.Factory,
70+
actionListPresenterFactory: ActionListPresenter.Factory,
6871
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
6972
private val mediaPlayer: MediaPlayer,
7073
private val permalinkParser: PermalinkParser,
@@ -73,6 +76,7 @@ class MessagesNode @AssistedInject constructor(
7376
navigator = this,
7477
composerPresenter = messageComposerPresenterFactory.create(this),
7578
timelinePresenter = timelinePresenterFactory.create(this),
79+
actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
7680
)
7781
private val callbacks = plugins<Callback>()
7882

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ import im.vector.app.features.analytics.plan.PinUnpinAction
2828
import io.element.android.appconfig.MessageComposerConfig
2929
import io.element.android.features.messages.api.timeline.HtmlConverterProvider
3030
import io.element.android.features.messages.impl.actionlist.ActionListEvents
31-
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
31+
import io.element.android.features.messages.impl.actionlist.ActionListState
3232
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
33-
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
3433
import io.element.android.features.messages.impl.crypto.identity.IdentityChangeState
3534
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
3635
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
@@ -93,7 +92,7 @@ class MessagesPresenter @AssistedInject constructor(
9392
@Assisted private val timelinePresenter: Presenter<TimelineState>,
9493
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
9594
private val identityChangeStatePresenter: Presenter<IdentityChangeState>,
96-
actionListPresenterFactory: ActionListPresenter.Factory,
95+
@Assisted private val actionListPresenter: Presenter<ActionListState>,
9796
private val customReactionPresenter: Presenter<CustomReactionState>,
9897
private val reactionSummaryPresenter: Presenter<ReactionSummaryState>,
9998
private val readReceiptBottomSheetPresenter: Presenter<ReadReceiptBottomSheetState>,
@@ -110,14 +109,13 @@ class MessagesPresenter @AssistedInject constructor(
110109
private val permalinkParser: PermalinkParser,
111110
private val analyticsService: AnalyticsService,
112111
) : Presenter<MessagesState> {
113-
private val actionListPresenter = actionListPresenterFactory.create(TimelineItemActionPostProcessor.Default)
114-
115112
@AssistedFactory
116113
interface Factory {
117114
fun create(
118115
navigator: MessagesNavigator,
119116
composerPresenter: Presenter<MessageComposerState>,
120117
timelinePresenter: Presenter<TimelineState>,
118+
actionListPresenter: Presenter<ActionListState>,
121119
): MessagesPresenter
122120
}
123121

@@ -272,7 +270,8 @@ class MessagesPresenter @AssistedInject constructor(
272270
timelineState: TimelineState,
273271
) = launch {
274272
when (action) {
275-
TimelineItemAction.Copy -> handleCopyContents(targetEvent)
273+
TimelineItemAction.CopyText -> handleCopyContents(targetEvent)
274+
TimelineItemAction.CopyCaption -> handleCopyCaption(targetEvent)
276275
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
277276
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
278277
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
@@ -488,11 +487,17 @@ class MessagesPresenter @AssistedInject constructor(
488487
is TimelineItemStateContent -> event.content.body
489488
else -> return
490489
}
491-
492490
clipboardHelper.copyPlainText(content)
493-
494491
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
495492
snackbarDispatcher.post(SnackbarMessage(R.string.screen_room_timeline_message_copied))
496493
}
497494
}
495+
496+
private suspend fun handleCopyCaption(event: TimelineItem.Event) {
497+
val content = (event.content as? TimelineItemEventContentWithAttachment)?.caption ?: return
498+
clipboardHelper.copyPlainText(content)
499+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
500+
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
501+
}
502+
}
498503
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import dagger.assisted.AssistedInject
2121
import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled
2222
import io.element.android.features.messages.impl.UserEventPermissions
2323
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
24+
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionComparator
2425
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
2526
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
2627
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailureFactory
@@ -70,6 +71,8 @@ class DefaultActionListPresenter @AssistedInject constructor(
7071
override fun create(postProcessor: TimelineItemActionPostProcessor): DefaultActionListPresenter
7172
}
7273

74+
private val comparator = TimelineItemActionComparator()
75+
7376
@Composable
7477
override fun present(): ActionListState {
7578
val localCoroutineScope = rememberCoroutineScope()
@@ -145,7 +148,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
145148
isEventPinned: Boolean,
146149
): List<TimelineItemAction> {
147150
val canRedact = timelineItem.isMine && usersEventPermissions.canRedactOwn || !timelineItem.isMine && usersEventPermissions.canRedactOther
148-
return buildList {
151+
return buildSet {
149152
if (timelineItem.canBeRepliedTo && usersEventPermissions.canSendMessage) {
150153
if (timelineItem.isThreaded) {
151154
add(TimelineItemAction.ReplyInThread)
@@ -183,7 +186,9 @@ class DefaultActionListPresenter @AssistedInject constructor(
183186
}
184187
}
185188
if (timelineItem.content.canBeCopied()) {
186-
add(TimelineItemAction.Copy)
189+
add(TimelineItemAction.CopyText)
190+
} else if ((timelineItem.content as? TimelineItemEventContentWithAttachment)?.caption.isNullOrBlank().not()) {
191+
add(TimelineItemAction.CopyCaption)
187192
}
188193
if (timelineItem.isRemote) {
189194
add(TimelineItemAction.CopyLink)
@@ -199,14 +204,15 @@ class DefaultActionListPresenter @AssistedInject constructor(
199204
}
200205
}
201206
.postFilter(timelineItem.content)
207+
.sortedWith(comparator)
202208
.let(postProcessor::process)
203209
}
204210
}
205211

206212
/**
207213
* Post filter the actions based on the content of the event.
208214
*/
209-
private fun List<TimelineItemAction>.postFilter(content: TimelineItemEventContent): List<TimelineItemAction> {
215+
private fun Iterable<TimelineItemAction>.postFilter(content: TimelineItemEventContent): Iterable<TimelineItemAction> {
210216
return filter { action ->
211217
when (content) {
212218
is TimelineItemCallNotifyContent,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.actionlist
99

1010
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
1111
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
12+
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionComparator
1213
import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUserSendFailure
1314
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.anUnsignedDeviceSendFailure
1415
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
@@ -22,7 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI
2223
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
2324
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
2425
import kotlinx.collections.immutable.ImmutableList
25-
import kotlinx.collections.immutable.persistentListOf
26+
import kotlinx.collections.immutable.toPersistentList
2627

2728
open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
2829
override val values: Sequence<ActionListState>
@@ -50,7 +51,9 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
5051
),
5152
displayEmojiReactions = true,
5253
verifiedUserSendFailure = VerifiedUserSendFailure.None,
53-
actions = aTimelineItemActionList(),
54+
actions = aTimelineItemActionList(
55+
copyAction = TimelineItemAction.CopyCaption,
56+
),
5457
)
5558
),
5659
anActionListState(
@@ -61,7 +64,9 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
6164
),
6265
displayEmojiReactions = true,
6366
verifiedUserSendFailure = VerifiedUserSendFailure.None,
64-
actions = aTimelineItemActionList(),
67+
actions = aTimelineItemActionList(
68+
copyAction = TimelineItemAction.CopyCaption,
69+
),
6570
)
6671
),
6772
anActionListState(
@@ -72,7 +77,9 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
7277
),
7378
displayEmojiReactions = true,
7479
verifiedUserSendFailure = VerifiedUserSendFailure.None,
75-
actions = aTimelineItemActionList(),
80+
actions = aTimelineItemActionList(
81+
copyAction = null,
82+
),
7683
)
7784
),
7885
anActionListState(
@@ -83,18 +90,22 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
8390
),
8491
displayEmojiReactions = true,
8592
verifiedUserSendFailure = VerifiedUserSendFailure.None,
86-
actions = aTimelineItemActionList(),
93+
actions = aTimelineItemActionList(
94+
copyAction = TimelineItemAction.CopyCaption,
95+
),
8796
)
8897
),
8998
anActionListState(
9099
target = ActionListState.Target.Success(
91100
event = aTimelineItemEvent(
92-
content = aTimelineItemVoiceContent(),
101+
content = aTimelineItemVoiceContent(caption = null),
93102
timelineItemReactions = reactionsState
94103
),
95104
displayEmojiReactions = true,
96105
verifiedUserSendFailure = VerifiedUserSendFailure.None,
97-
actions = aTimelineItemActionList(),
106+
actions = aTimelineItemActionList(
107+
copyAction = null,
108+
),
98109
)
99110
),
100111
anActionListState(
@@ -161,27 +172,31 @@ fun anActionListState(
161172
eventSink = eventSink
162173
)
163174

164-
fun aTimelineItemActionList(): ImmutableList<TimelineItemAction> {
165-
return persistentListOf(
175+
fun aTimelineItemActionList(
176+
copyAction: TimelineItemAction? = TimelineItemAction.CopyText
177+
): ImmutableList<TimelineItemAction> {
178+
return setOfNotNull(
166179
TimelineItemAction.Reply,
167180
TimelineItemAction.Forward,
168-
TimelineItemAction.Copy,
181+
copyAction,
169182
TimelineItemAction.CopyLink,
170183
TimelineItemAction.Edit,
171184
TimelineItemAction.Redact,
172185
TimelineItemAction.ReportContent,
173186
TimelineItemAction.ViewSource,
174187
)
188+
.sortedWith(TimelineItemActionComparator())
189+
.toPersistentList()
175190
}
176191

177192
fun aTimelineItemPollActionList(): ImmutableList<TimelineItemAction> {
178-
return persistentListOf(
193+
return setOf(
179194
TimelineItemAction.EndPoll,
180195
TimelineItemAction.Reply,
181-
TimelineItemAction.Copy,
196+
TimelineItemAction.Pin,
182197
TimelineItemAction.CopyLink,
183-
TimelineItemAction.ViewSource,
184-
TimelineItemAction.ReportContent,
185198
TimelineItemAction.Redact,
186199
)
200+
.sortedWith(TimelineItemActionComparator())
201+
.toPersistentList()
187202
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ sealed class TimelineItemAction(
2222
) {
2323
data object ViewInTimeline : TimelineItemAction(CommonStrings.action_view_in_timeline, CompoundDrawables.ic_compound_visibility_on)
2424
data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward)
25-
data object Copy : TimelineItemAction(CommonStrings.action_copy, CompoundDrawables.ic_compound_copy)
25+
data object CopyText : TimelineItemAction(CommonStrings.action_copy_text, CompoundDrawables.ic_compound_copy)
26+
data object CopyCaption : TimelineItemAction(CommonStrings.action_copy_caption, CompoundDrawables.ic_compound_copy)
2627
data object CopyLink : TimelineItemAction(CommonStrings.action_copy_link_to_message, CompoundDrawables.ic_compound_link)
2728
data object Redact : TimelineItemAction(CommonStrings.action_remove, CompoundDrawables.ic_compound_delete, destructive = true)
2829
data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.messages.impl.actionlist.model
9+
10+
class TimelineItemActionComparator : Comparator<TimelineItemAction> {
11+
// See order in https://www.figma.com/design/ux3tYoZV9WghC7hHT9Fhk0/Compound-iOS-Components?node-id=2946-2392
12+
private val orderedList = listOf(
13+
TimelineItemAction.EndPoll,
14+
TimelineItemAction.ViewInTimeline,
15+
TimelineItemAction.Reply,
16+
TimelineItemAction.ReplyInThread,
17+
TimelineItemAction.Forward,
18+
TimelineItemAction.Pin,
19+
TimelineItemAction.Unpin,
20+
TimelineItemAction.CopyLink,
21+
TimelineItemAction.Edit,
22+
TimelineItemAction.CopyText,
23+
TimelineItemAction.AddCaption,
24+
TimelineItemAction.EditCaption,
25+
TimelineItemAction.CopyCaption,
26+
TimelineItemAction.RemoveCaption,
27+
TimelineItemAction.ViewSource,
28+
TimelineItemAction.ReportContent,
29+
TimelineItemAction.Redact,
30+
)
31+
32+
override fun compare(o1: TimelineItemAction, o2: TimelineItemAction): Int {
33+
val index1 = orderedList.indexOf(o1)
34+
val index2 = orderedList.indexOf(o2)
35+
return index1.compareTo(index2)
36+
}
37+
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.bumble.appyx.core.plugin.plugins
1919
import dagger.assisted.Assisted
2020
import dagger.assisted.AssistedInject
2121
import io.element.android.anvilannotations.ContributesNode
22+
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
2223
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
2324
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
2425
import io.element.android.features.messages.impl.timeline.model.TimelineItem
@@ -35,6 +36,7 @@ class PinnedMessagesListNode @AssistedInject constructor(
3536
@Assisted buildContext: BuildContext,
3637
@Assisted plugins: List<Plugin>,
3738
presenterFactory: PinnedMessagesListPresenter.Factory,
39+
actionListPresenterFactory: ActionListPresenter.Factory,
3840
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
3941
private val permalinkParser: PermalinkParser,
4042
) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator {
@@ -47,7 +49,10 @@ class PinnedMessagesListNode @AssistedInject constructor(
4749
fun onForwardEventClick(eventId: EventId)
4850
}
4951

50-
private val presenter = presenterFactory.create(this)
52+
private val presenter = presenterFactory.create(
53+
navigator = this,
54+
actionListPresenter = actionListPresenterFactory.create(PinnedMessagesListTimelineActionPostProcessor())
55+
)
5156
private val callbacks = plugins<Callback>()
5257

5358
private fun onEventClick(event: TimelineItem.Event) {

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import dagger.assisted.AssistedInject
2323
import im.vector.app.features.analytics.plan.Interaction
2424
import im.vector.app.features.analytics.plan.PinUnpinAction
2525
import io.element.android.features.messages.impl.UserEventPermissions
26-
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
26+
import io.element.android.features.messages.impl.actionlist.ActionListState
2727
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
2828
import io.element.android.features.messages.impl.pinned.PinnedEventsTimelineProvider
2929
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
@@ -64,13 +64,16 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
6464
private val timelineProvider: PinnedEventsTimelineProvider,
6565
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
6666
private val snackbarDispatcher: SnackbarDispatcher,
67-
actionListPresenterFactory: ActionListPresenter.Factory,
67+
@Assisted private val actionListPresenter: Presenter<ActionListState>,
6868
private val appCoroutineScope: CoroutineScope,
6969
private val analyticsService: AnalyticsService,
7070
) : Presenter<PinnedMessagesListState> {
7171
@AssistedFactory
7272
interface Factory {
73-
fun create(navigator: PinnedMessagesListNavigator): PinnedMessagesListPresenter
73+
fun create(
74+
navigator: PinnedMessagesListNavigator,
75+
actionListPresenter: Presenter<ActionListState>,
76+
): PinnedMessagesListPresenter
7477
}
7578

7679
private val timelineItemsFactory: TimelineItemsFactory = timelineItemsFactoryCreator.create(
@@ -79,7 +82,6 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
7982
computeReactions = false,
8083
)
8184
)
82-
private val actionListPresenter = actionListPresenterFactory.create(PinnedMessagesListTimelineActionPostProcessor())
8385

8486
@Composable
8587
override fun present(): PinnedMessagesListState {

0 commit comments

Comments
 (0)