diff --git a/changelog.d/2650.feature b/changelog.d/2650.feature new file mode 100644 index 0000000000..4287d42ac7 --- /dev/null +++ b/changelog.d/2650.feature @@ -0,0 +1 @@ +Add action to copy permalink diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 4c92585b0d..4edc943a73 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -89,6 +89,7 @@ import io.element.android.libraries.matrix.ui.room.canRedactOtherAsState import io.element.android.libraries.matrix.ui.room.canRedactOwnAsState import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.libraries.textcomposer.model.MessageComposerMode +import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -273,6 +274,7 @@ class MessagesPresenter @AssistedInject constructor( ) = launch { when (action) { TimelineItemAction.Copy -> handleCopyContents(targetEvent) + TimelineItemAction.CopyLink -> handleCopyLink(targetEvent) TimelineItemAction.Redact -> handleActionRedact(targetEvent) TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting) TimelineItemAction.Reply, @@ -435,6 +437,20 @@ class MessagesPresenter @AssistedInject constructor( event.eventId?.let { timelineState.eventSink(TimelineEvents.PollEndClicked(it)) } } + private suspend fun handleCopyLink(event: TimelineItem.Event) { + event.eventId ?: return + room.getPermalinkFor(event.eventId).fold( + onSuccess = { permalink -> + clipboardHelper.copyPlainText(permalink) + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_link_copied_to_clipboard)) + }, + onFailure = { + Timber.e(it, "Failed to get permalink for event ${event.eventId}") + snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_error)) + } + ) + } + private suspend fun handleCopyContents(event: TimelineItem.Event) { val content = when (event.content) { is TimelineItemTextBasedContent -> event.content.body diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index f836e2f8a0..5ebedeb06f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -96,6 +96,7 @@ class ActionListPresenter @Inject constructor( is TimelineItemStateContent -> { buildList { add(TimelineItemAction.Copy) + add(TimelineItemAction.CopyLink) if (isDeveloperModeEnabled) { add(TimelineItemAction.ViewSource) } @@ -119,6 +120,7 @@ class ActionListPresenter @Inject constructor( if (timelineItem.content.canBeCopied()) { add(TimelineItemAction.Copy) } + add(TimelineItemAction.CopyLink) if (isDeveloperModeEnabled) { add(TimelineItemAction.ViewSource) } @@ -136,6 +138,7 @@ class ActionListPresenter @Inject constructor( add(TimelineItemAction.Reply) add(TimelineItemAction.Forward) } + add(TimelineItemAction.CopyLink) if (isDeveloperModeEnabled) { add(TimelineItemAction.ViewSource) } @@ -176,6 +179,7 @@ class ActionListPresenter @Inject constructor( if (timelineItem.content.canBeCopied()) { add(TimelineItemAction.Copy) } + add(TimelineItemAction.CopyLink) if (isDeveloperModeEnabled) { add(TimelineItemAction.ViewSource) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index 10952058b5..be78037d76 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -135,6 +135,7 @@ fun aTimelineItemActionList(): ImmutableList { TimelineItemAction.Reply, TimelineItemAction.Forward, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.Edit, TimelineItemAction.Redact, TimelineItemAction.ReportContent, @@ -146,6 +147,7 @@ fun aTimelineItemPollActionList(): ImmutableList { TimelineItemAction.EndPoll, TimelineItemAction.Reply, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index f244f515f3..f61e6197c2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -31,6 +31,7 @@ sealed class TimelineItemAction( ) { data object Forward : TimelineItemAction(CommonStrings.action_forward, CompoundDrawables.ic_compound_forward) data object Copy : TimelineItemAction(CommonStrings.action_copy, 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) data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 6cceb6b5b5..bce97f4a6e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -233,6 +233,27 @@ class MessagesPresenterTest { } } + @Test + fun `present - handle action copy link`() = runTest { + val clipboardHelper = FakeClipboardHelper() + val event = aMessageEvent() + val matrixRoom = FakeMatrixRoom( + permalinkResult = { Result.success("a link") }, + ) + val presenter = createMessagesPresenter( + clipboardHelper = clipboardHelper, + matrixRoom = matrixRoom, + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.CopyLink, event)) + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + assertThat(clipboardHelper.clipboardContents).isEqualTo("a link") + } + } + @Test fun `present - handle action reply`() = runTest { val presenter = createMessagesPresenter() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index 981e8fc8ac..c1062625f7 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -153,6 +153,7 @@ class ActionListPresenterTest { TimelineItemAction.Reply, TimelineItemAction.Forward, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, ) @@ -193,6 +194,7 @@ class ActionListPresenterTest { actions = persistentListOf( TimelineItemAction.Forward, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, ) @@ -232,6 +234,7 @@ class ActionListPresenterTest { TimelineItemAction.Reply, TimelineItemAction.Forward, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, @@ -272,6 +275,7 @@ class ActionListPresenterTest { TimelineItemAction.Reply, TimelineItemAction.Forward, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, @@ -315,6 +319,7 @@ class ActionListPresenterTest { TimelineItemAction.Forward, TimelineItemAction.Edit, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.Redact, ) @@ -357,6 +362,7 @@ class ActionListPresenterTest { TimelineItemAction.Forward, TimelineItemAction.Edit, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, ) ) @@ -396,6 +402,7 @@ class ActionListPresenterTest { actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, TimelineItemAction.Redact, ) @@ -435,6 +442,7 @@ class ActionListPresenterTest { displayEmojiReactions = false, actions = persistentListOf( TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.ViewSource, ) ) @@ -473,6 +481,7 @@ class ActionListPresenterTest { displayEmojiReactions = false, actions = persistentListOf( TimelineItemAction.Copy, + TimelineItemAction.CopyLink, ) ) ) @@ -513,6 +522,7 @@ class ActionListPresenterTest { TimelineItemAction.Forward, TimelineItemAction.Edit, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) @@ -595,6 +605,7 @@ class ActionListPresenterTest { actions = persistentListOf( TimelineItemAction.Edit, TimelineItemAction.Copy, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) @@ -632,6 +643,7 @@ class ActionListPresenterTest { TimelineItemAction.Reply, TimelineItemAction.Edit, TimelineItemAction.EndPoll, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) @@ -668,6 +680,7 @@ class ActionListPresenterTest { actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.EndPoll, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) @@ -703,6 +716,7 @@ class ActionListPresenterTest { displayEmojiReactions = true, actions = persistentListOf( TimelineItemAction.Reply, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) @@ -738,6 +752,7 @@ class ActionListPresenterTest { actions = persistentListOf( TimelineItemAction.Reply, TimelineItemAction.Forward, + TimelineItemAction.CopyLink, TimelineItemAction.Redact, ) ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 3e01155a86..aa1f3fa025 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -328,5 +328,12 @@ interface MatrixRoom : Closeable { */ fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result + /** + * Get the permalink for the provided [eventId]. + * @param eventId The event id to get the permalink for. + * @return The permalink, or a failure. + */ + suspend fun getPermalinkFor(eventId: EventId): Result + override fun close() = destroy() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 7ab6e3640b..be61b06310 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.impl.room +import io.element.android.appconfig.MatrixConfiguration import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.matrix.api.core.EventId @@ -711,6 +712,19 @@ class RustMatrixRoom( ) } + override suspend fun getPermalinkFor(eventId: EventId): Result { + // FIXME Use the SDK API once https://github.com/matrix-org/matrix-rust-sdk/issues/3259 has been done + // Now use a simple builder + return runCatching { + buildString { + append(MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL) + append(roomId.value) + append("/") + append(eventId.value) + } + } + } + private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { return runCatching { MediaUploadHandlerImpl(files, handle()) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 32575e5924..f2395241b5 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -84,6 +84,7 @@ class FakeMatrixRoom( override val activeMemberCount: Long = 234L, val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), + private var permalinkResult: () -> Result = { Result.success("link") }, canRedactOwn: Boolean = false, canRedactOther: Boolean = false, ) : MatrixRoom { @@ -273,6 +274,10 @@ class FakeMatrixRoom( return cancelSendResult } + override suspend fun getPermalinkFor(eventId: EventId): Result { + return permalinkResult() + } + override suspend fun editMessage( originalEventId: EventId?, transactionId: TransactionId?,