Merge pull request #4376 from ShadowRZ/features/shadowrz/long-press-link-copy

Long press link to copy URL to clipboard
This commit is contained in:
Benoit Marty
2025-03-10 09:17:16 +01:00
committed by GitHub
13 changed files with 74 additions and 0 deletions

View File

@@ -8,10 +8,12 @@
package io.element.android.features.messages.impl.pinned.list
import android.content.Context
import android.view.HapticFeedbackConstants
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@@ -23,6 +25,7 @@ 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
import io.element.android.libraries.androidutils.system.copyToClipboard
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
@@ -30,6 +33,7 @@ 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
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.ui.strings.CommonStrings
@ContributesNode(RoomScope::class)
class PinnedMessagesListNode @AssistedInject constructor(
@@ -98,6 +102,7 @@ class PinnedMessagesListNode @AssistedInject constructor(
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
) {
val context = LocalContext.current
val view = LocalView.current
val state = presenter.present()
PinnedMessagesListView(
state = state,
@@ -105,6 +110,15 @@ class PinnedMessagesListNode @AssistedInject constructor(
onEventClick = ::onEventClick,
onUserDataClick = ::onUserDataClick,
onLinkClick = { url -> onLinkClick(context, url) },
onLinkLongClick = {
view.performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS
)
context.copyToClipboard(
it,
context.getString(CommonStrings.common_copied_to_clipboard)
)
},
modifier = modifier
)
}

View File

@@ -58,6 +58,7 @@ fun PinnedMessagesListView(
onEventClick: (event: TimelineItem.Event) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onLinkLongClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
@@ -78,6 +79,7 @@ fun PinnedMessagesListView(
onEventClick = onEventClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
onErrorDismiss = onBackClick,
modifier = Modifier
.padding(padding)
@@ -112,6 +114,7 @@ private fun PinnedMessagesListContent(
onEventClick: (event: TimelineItem.Event) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onLinkLongClick: (String) -> Unit,
onErrorDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
@@ -130,6 +133,7 @@ private fun PinnedMessagesListContent(
onEventClick = onEventClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
)
PinnedMessagesListState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -166,6 +170,7 @@ private fun PinnedMessagesListLoaded(
onEventClick: (event: TimelineItem.Event) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onLinkLongClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
fun onActionSelected(timelineItemAction: TimelineItemAction, event: TimelineItem.Event) {
@@ -216,6 +221,7 @@ private fun PinnedMessagesListLoaded(
focusedEventId = null,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
onContentClick = onEventClick,
onLongClick = ::onMessageLongClick,
inReplyToClick = {},
@@ -233,6 +239,7 @@ private fun PinnedMessagesListLoaded(
onContentClick = { onEventClick(event) },
onLongClick = { onMessageLongClick(event) },
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
modifier = contentModifier,
onContentLayoutChange = onContentLayoutChange
)
@@ -248,6 +255,7 @@ private fun TimelineItemEventContentViewWrapper(
timelineProtectionState: TimelineProtectionState,
onContentClick: () -> Unit,
onLinkClick: (String) -> Unit,
onLinkLongClick: (String) -> Unit,
onLongClick: (() -> Unit)?,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier,
@@ -264,6 +272,7 @@ private fun TimelineItemEventContentViewWrapper(
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
eventSink = { },
modifier = modifier,
onContentClick = onContentClick,
@@ -283,5 +292,6 @@ internal fun PinnedMessagesListViewPreview(@PreviewParameter(PinnedMessagesListS
onEventClick = { },
onUserDataClick = {},
onLinkClick = {},
onLinkLongClick = {},
)
}

View File

@@ -7,6 +7,7 @@
package io.element.android.features.messages.impl.timeline
import android.view.HapticFeedbackConstants
import android.view.accessibility.AccessibilityManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
@@ -41,6 +42,7 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -59,6 +61,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
import io.element.android.libraries.androidutils.system.copyToClipboard
import io.element.android.libraries.designsystem.components.dialogs.AlertDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -106,6 +109,7 @@ fun TimelineView(
}
val context = LocalContext.current
val view = LocalView.current
// Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version
val useReverseLayout = remember {
val accessibilityManager = context.getSystemService(AccessibilityManager::class.java)
@@ -116,6 +120,16 @@ fun TimelineView(
state.eventSink(TimelineEvents.FocusOnEvent(eventId))
}
fun onLinkLongClick(link: String) {
view.performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS
)
context.copyToClipboard(
link,
context.getString(CommonStrings.common_copied_to_clipboard)
)
}
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
AnimatedVisibility(visible = true, enter = fadeIn()) {
Box(modifier) {
@@ -141,6 +155,7 @@ fun TimelineView(
focusedEventId = state.focusedEventId,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onLinkLongClick = ::onLinkLongClick,
onContentClick = onContentClick,
onLongClick = onMessageLongClick,
inReplyToClick = ::inReplyToClick,

View File

@@ -33,6 +33,7 @@ internal fun ATimelineItemEventRow(
onEventClick = {},
onLongClick = {},
onLinkClick = {},
onLinkLongClick = {},
onUserDataClick = {},
inReplyToClick = {},
onReactionClick = { _, _ -> },

View File

@@ -118,6 +118,7 @@ fun TimelineItemEventRow(
onEventClick: () -> Unit,
onLongClick: () -> Unit,
onLinkClick: (String) -> Unit,
onLinkLongClick: (String) -> Unit,
onUserDataClick: (UserId) -> Unit,
inReplyToClick: (EventId) -> Unit,
onReactionClick: (emoji: String, eventId: TimelineItem.Event) -> Unit,
@@ -138,6 +139,7 @@ fun TimelineItemEventRow(
onLongClick = onLongClick,
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
eventSink = eventSink,
modifier = contentModifier,
onContentLayoutChange = onContentLayoutChange

View File

@@ -46,6 +46,7 @@ fun TimelineItemGroupedEventsRow(
inReplyToClick: (EventId) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onLinkLongClick: (String) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
@@ -59,6 +60,7 @@ fun TimelineItemGroupedEventsRow(
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
eventSink = eventSink,
modifier = contentModifier,
onContentClick = null,
@@ -87,6 +89,7 @@ fun TimelineItemGroupedEventsRow(
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
@@ -112,6 +115,7 @@ private fun TimelineItemGroupedEventsRowContent(
inReplyToClick: (EventId) -> Unit,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onLinkLongClick: (String) -> Unit,
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
@@ -125,6 +129,7 @@ private fun TimelineItemGroupedEventsRowContent(
hideMediaContent = timelineProtectionState.hideMediaContent(event.eventId),
onShowContentClick = { timelineProtectionState.eventSink(TimelineProtectionEvent.ShowContent(event.eventId)) },
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
eventSink = eventSink,
modifier = contentModifier,
onContentClick = null,
@@ -156,6 +161,7 @@ private fun TimelineItemGroupedEventsRowContent(
focusedEventId = focusedEventId,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
onContentClick = onClick,
onLongClick = onLongClick,
inReplyToClick = inReplyToClick,
@@ -199,6 +205,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
isLastOutgoingMessage = false,
onClick = {},
onLongClick = {},
onLinkLongClick = {},
inReplyToClick = {},
onUserDataClick = {},
onLinkClick = {},
@@ -224,6 +231,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
isLastOutgoingMessage = false,
onClick = {},
onLongClick = {},
onLinkLongClick = {},
inReplyToClick = {},
onUserDataClick = {},
onLinkClick = {},

View File

@@ -43,6 +43,7 @@ internal fun TimelineItemRow(
focusedEventId: EventId?,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String) -> Unit,
onLinkLongClick: (String) -> Unit,
onContentClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit,
inReplyToClick: (EventId) -> Unit,
@@ -63,6 +64,7 @@ internal fun TimelineItemRow(
onContentClick = { onContentClick(event) },
onLongClick = { onLongClick(event) },
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
eventSink = eventSink,
modifier = contentModifier,
onContentLayoutChange = onContentLayoutChange
@@ -122,6 +124,7 @@ internal fun TimelineItemRow(
onEventClick = { onContentClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) },
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
onUserDataClick = onUserDataClick,
inReplyToClick = inReplyToClick,
onReactionClick = onReactionClick,
@@ -150,6 +153,7 @@ internal fun TimelineItemRow(
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,

View File

@@ -71,6 +71,7 @@ fun TimelineItemStateEventRow(
TimelineItemEventContentView(
content = event.content,
onLinkClick = {},
onLinkLongClick = {},
hideMediaContent = false,
onShowContentClick = {},
eventSink = eventSink,

View File

@@ -40,6 +40,7 @@ fun TimelineItemEventContentView(
onLongClick: (() -> Unit)?,
onShowContentClick: () -> Unit,
onLinkClick: (url: String) -> Unit,
onLinkLongClick: (String) -> Unit,
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
modifier: Modifier = Modifier,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit = {},
@@ -60,6 +61,7 @@ fun TimelineItemEventContentView(
content = content,
modifier = modifier,
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
onContentLayoutChange = onContentLayoutChange
)
is TimelineItemUnknownContent -> TimelineItemUnknownView(
@@ -78,6 +80,7 @@ fun TimelineItemEventContentView(
onLongClick = onLongClick,
onShowContentClick = onShowContentClick,
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
onContentLayoutChange = onContentLayoutChange,
modifier = modifier,
)
@@ -96,6 +99,7 @@ fun TimelineItemEventContentView(
onLongClick = onLongClick,
onShowContentClick = onShowContentClick,
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
onContentLayoutChange = onContentLayoutChange,
modifier = modifier
)

View File

@@ -65,6 +65,7 @@ fun TimelineItemImageView(
onContentClick: (() -> Unit)?,
onLongClick: (() -> Unit)?,
onLinkClick: (String) -> Unit,
onLinkLongClick: (String) -> Unit,
onShowContentClick: () -> Unit,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier,
@@ -120,6 +121,7 @@ fun TimelineItemImageView(
text = caption,
style = ElementRichTextEditorStyle.textStyle(),
onLinkClickedListener = onLinkClick,
onLinkLongClickedListener = onLinkLongClick,
releaseOnDetach = false,
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange),
)
@@ -138,6 +140,7 @@ internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageCon
onContentClick = {},
onLongClick = {},
onLinkClick = {},
onLinkLongClick = {},
onContentLayoutChange = {},
)
}
@@ -152,6 +155,7 @@ internal fun TimelineItemImageViewHideMediaContentPreview() = ElementPreview {
onContentClick = {},
onLongClick = {},
onLinkClick = {},
onLinkLongClick = {},
onContentLayoutChange = {},
)
}

View File

@@ -46,6 +46,7 @@ import io.element.android.wysiwyg.compose.EditorStyledText
fun TimelineItemTextView(
content: TimelineItemTextBasedContent,
onLinkClick: (String) -> Unit,
onLinkLongClick: (String) -> Unit,
modifier: Modifier = Modifier,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit = {},
) {
@@ -64,6 +65,7 @@ fun TimelineItemTextView(
EditorStyledText(
text = body,
onLinkClickedListener = onLinkClick,
onLinkLongClickedListener = onLinkLongClick,
style = ElementRichTextEditorStyle.textStyle(),
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange),
releaseOnDetach = false,
@@ -115,6 +117,7 @@ internal fun TimelineItemTextViewPreview(
TimelineItemTextView(
content = content,
onLinkClick = {},
onLinkLongClick = {},
)
}
@@ -127,6 +130,7 @@ internal fun TimelineItemTextViewWithLinkifiedUrlPreview() = ElementPreview {
TimelineItemTextView(
content = content,
onLinkClick = {},
onLinkLongClick = {},
)
}
@@ -139,5 +143,6 @@ internal fun TimelineItemTextViewWithLinkifiedUrlAndNestedParenthesisPreview() =
TimelineItemTextView(
content = content,
onLinkClick = {},
onLinkLongClick = {},
)
}

View File

@@ -74,6 +74,7 @@ fun TimelineItemVideoView(
onLongClick: (() -> Unit)?,
onShowContentClick: () -> Unit,
onLinkClick: (String) -> Unit,
onLinkLongClick: (String) -> Unit,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -147,6 +148,7 @@ fun TimelineItemVideoView(
.widthIn(min = MIN_HEIGHT_IN_DP.dp * aspectRatio, max = MAX_HEIGHT_IN_DP.dp * aspectRatio),
text = caption,
onLinkClickedListener = onLinkClick,
onLinkLongClickedListener = onLinkLongClick,
style = ElementRichTextEditorStyle.textStyle(),
releaseOnDetach = false,
onTextLayout = ContentAvoidingLayout.measureLegacyLastTextLine(onContentLayoutChange = onContentLayoutChange),
@@ -166,6 +168,7 @@ internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoCon
onContentClick = {},
onLongClick = {},
onLinkClick = {},
onLinkLongClick = {},
onContentLayoutChange = {},
)
}
@@ -180,6 +183,7 @@ internal fun TimelineItemVideoViewHideMediaContentPreview() = ElementPreview {
onContentClick = {},
onLongClick = {},
onLinkClick = {},
onLinkLongClick = {},
onContentLayoutChange = {},
)
}

View File

@@ -100,6 +100,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinne
onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),
onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
onLinkClick: (String) -> Unit = EnsureNeverCalledWithParam(),
onLinkLongClick: (String) -> Unit = EnsureNeverCalledWithParam(),
) {
setSafeContent {
PinnedMessagesListView(
@@ -108,6 +109,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinne
onEventClick = onEventClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,
onLinkLongClick = onLinkLongClick,
)
}
}