mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Merge branch 'develop' into feature/fga/timeline-multi-forward
This commit is contained in:
+5
-7
@@ -8,7 +8,6 @@
|
||||
|
||||
package io.element.android.features.messages.impl
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -650,8 +649,9 @@ class MessagesPresenter(
|
||||
event.eventId ?: return
|
||||
room.getPermalinkFor(event.eventId).fold(
|
||||
onSuccess = { permalink ->
|
||||
clipboardHelper.copyPlainText(permalink)
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_link_copied_to_clipboard))
|
||||
clipboardHelper.copyPlainText(permalink) {
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_link_copied_to_clipboard))
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to get permalink for event ${event.eventId}")
|
||||
@@ -666,16 +666,14 @@ class MessagesPresenter(
|
||||
is TimelineItemStateContent -> event.content.body
|
||||
else -> return
|
||||
}
|
||||
clipboardHelper.copyPlainText(content)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
clipboardHelper.copyPlainText(content) {
|
||||
snackbarDispatcher.post(SnackbarMessage(R.string.screen_room_timeline_message_copied))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyCaption(event: TimelineItem.Event) {
|
||||
val content = event.content.captionOrNull() ?: return
|
||||
clipboardHelper.copyPlainText(content)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
clipboardHelper.copyPlainText(content) {
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -15,7 +15,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
@@ -26,8 +25,9 @@ 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.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||
@@ -53,6 +53,8 @@ class PinnedMessagesListNode(
|
||||
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val emojiPickerRenderer: EmojiPickerRenderer,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val toastHelper: ToastHelper,
|
||||
) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator {
|
||||
interface Callback : Plugin {
|
||||
fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean)
|
||||
@@ -114,7 +116,6 @@ class PinnedMessagesListNode(
|
||||
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
|
||||
val view = LocalView.current
|
||||
val state = presenter.present()
|
||||
PinnedMessagesListView(
|
||||
@@ -132,10 +133,9 @@ class PinnedMessagesListNode(
|
||||
view.performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS
|
||||
)
|
||||
context.copyToClipboard(
|
||||
text = it.url,
|
||||
toastMessage = toastMessage,
|
||||
)
|
||||
clipboardHelper.copyPlainText(it.url) {
|
||||
toastHelper.show(CommonStrings.common_copied_to_clipboard)
|
||||
}
|
||||
},
|
||||
emojiPickerRenderer = emojiPickerRenderer,
|
||||
modifier = modifier
|
||||
|
||||
+1
@@ -267,6 +267,7 @@ private fun ThreadListItemRow(
|
||||
UnreadIndicatorAtom(
|
||||
size = 14.dp,
|
||||
isVisible = hasUnreadNotifications,
|
||||
count = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -28,10 +28,10 @@ sealed interface TimelineEvent {
|
||||
data object HideShieldDialog : TimelineEvent
|
||||
|
||||
data object MarkAllAsRead : TimelineEvent
|
||||
|
||||
data class EnterSelectionMode(val action: SelectionAction, val eventId: EventId) : TimelineEvent
|
||||
data class ToggleSelection(val eventId: EventId) : TimelineEvent
|
||||
data object ExitSelectionMode : TimelineEvent
|
||||
data class CopyToClipboard(val text: String) : TimelineEvent
|
||||
|
||||
/**
|
||||
* Events coming from a timeline item.
|
||||
|
||||
+9
@@ -44,6 +44,8 @@ import io.element.android.features.messages.impl.voicemessages.timeline.Redacted
|
||||
import io.element.android.features.poll.api.actions.EndPollAction
|
||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||
import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
@@ -112,6 +114,8 @@ class TimelinePresenter(
|
||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||
private val markAsFullyRead: MarkAsFullyRead,
|
||||
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val toastHelper: ToastHelper,
|
||||
) : Presenter<TimelineState> {
|
||||
private val tag = "TimelinePresenter"
|
||||
|
||||
@@ -355,6 +359,11 @@ class TimelinePresenter(
|
||||
is TimelineEvent.ValidateMedia -> {
|
||||
timelineProtectionState.eventSink(TimelineProtectionEvent.ValidateContent(event.mediaSources, event.validationState))
|
||||
}
|
||||
is TimelineEvent.CopyToClipboard -> {
|
||||
clipboardHelper.copyPlainText(event.text) {
|
||||
toastHelper.show(CommonStrings.common_copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-8
@@ -64,7 +64,6 @@ import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
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.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
@@ -96,7 +95,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||
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.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.components.dialogs.AlertDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
@@ -171,8 +169,6 @@ fun TimelineView(
|
||||
state.eventSink(TimelineEvent.FocusOnEvent(eventId))
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
|
||||
val view = LocalView.current
|
||||
fun inReplyToClick(eventId: EventId) {
|
||||
state.eventSink(TimelineEvent.FocusOnEvent(eventId))
|
||||
@@ -182,10 +178,7 @@ fun TimelineView(
|
||||
view.performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS
|
||||
)
|
||||
context.copyToClipboard(
|
||||
text = link.url,
|
||||
toastMessage = toastMessage,
|
||||
)
|
||||
state.eventSink(TimelineEvent.CopyToClipboard(link.url))
|
||||
}
|
||||
|
||||
fun prefetchMoreItems() {
|
||||
@@ -606,6 +599,7 @@ private fun JumpToPositionButton(
|
||||
.offset { IntOffset(x = 0, y = dotYOffset.roundToPx()) },
|
||||
color = ElementTheme.colors.iconSuccessPrimary,
|
||||
border = BorderStroke(2.dp, ElementTheme.colors.bgCanvasDefault),
|
||||
count = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-1
@@ -16,17 +16,22 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
@AssistedInject
|
||||
class EventDebugInfoNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val toastHelper: ToastHelper,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
data class Inputs(
|
||||
val eventId: EventId?,
|
||||
@@ -39,6 +44,12 @@ class EventDebugInfoNode(
|
||||
navigateUp()
|
||||
}
|
||||
|
||||
private fun onCopyText(text: String) {
|
||||
clipboardHelper.copyPlainText(text) {
|
||||
toastHelper.show(CommonStrings.common_copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) = with(inputs) {
|
||||
EventDebugInfoView(
|
||||
@@ -46,7 +57,8 @@ class EventDebugInfoNode(
|
||||
model = timelineItemDebugInfo.model,
|
||||
originalJson = timelineItemDebugInfo.originalJson,
|
||||
latestEditedJson = timelineItemDebugInfo.latestEditedJson,
|
||||
onBackClick = ::onBackClick
|
||||
onBackClick = ::onBackClick,
|
||||
onCopyText = ::onCopyText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+21
-13
@@ -8,8 +8,6 @@
|
||||
|
||||
package io.element.android.features.messages.impl.timeline.debug
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
@@ -34,11 +32,9 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.getSystemService
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
@@ -63,6 +59,7 @@ fun EventDebugInfoView(
|
||||
originalJson: String?,
|
||||
latestEditedJson: String?,
|
||||
onBackClick: () -> Unit,
|
||||
onCopyText: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isTest: Boolean = false,
|
||||
) {
|
||||
@@ -87,20 +84,30 @@ fun EventDebugInfoView(
|
||||
item {
|
||||
Column(Modifier.padding(vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(text = "Event ID:")
|
||||
CopyableText(text = eventId?.value ?: "-", modifier = Modifier.fillMaxWidth())
|
||||
CopyableText(text = eventId?.value ?: "-", onClick = onCopyText, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
item {
|
||||
CollapsibleSection(title = "Model:", text = model, initiallyExpanded = sectionsInitiallyExpanded)
|
||||
CollapsibleSection(title = "Model:", text = model, onCopyText = onCopyText, initiallyExpanded = sectionsInitiallyExpanded)
|
||||
}
|
||||
if (originalJson != null) {
|
||||
item {
|
||||
CollapsibleSection(title = "Original JSON:", text = prettyJSON(originalJson), initiallyExpanded = sectionsInitiallyExpanded)
|
||||
CollapsibleSection(
|
||||
title = "Original JSON:",
|
||||
text = prettyJSON(originalJson),
|
||||
onCopyText = onCopyText,
|
||||
initiallyExpanded = sectionsInitiallyExpanded,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (latestEditedJson != null) {
|
||||
item {
|
||||
CollapsibleSection(title = "Latest edited JSON:", text = prettyJSON(latestEditedJson), initiallyExpanded = sectionsInitiallyExpanded)
|
||||
CollapsibleSection(
|
||||
title = "Latest edited JSON:",
|
||||
text = prettyJSON(latestEditedJson),
|
||||
onCopyText = onCopyText,
|
||||
initiallyExpanded = sectionsInitiallyExpanded,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +127,7 @@ private fun prettyJSON(maybeJSON: String): String {
|
||||
private fun CollapsibleSection(
|
||||
title: String,
|
||||
text: String,
|
||||
onCopyText: (String) -> Unit,
|
||||
initiallyExpanded: Boolean = false,
|
||||
) {
|
||||
var isExpanded by remember { mutableStateOf(initiallyExpanded) }
|
||||
@@ -139,7 +147,7 @@ private fun CollapsibleSection(
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = isExpanded, enter = expandVertically(), exit = shrinkVertically()) {
|
||||
CopyableText(text = text, modifier = Modifier.fillMaxWidth())
|
||||
CopyableText(text = text, onClick = onCopyText, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,16 +155,15 @@ private fun CollapsibleSection(
|
||||
@Composable
|
||||
private fun CopyableText(
|
||||
text: String,
|
||||
onClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = remember { requireNotNull(context.getSystemService<ClipboardManager>()) }
|
||||
Box(
|
||||
modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(6.dp)
|
||||
.clickable { clipboardManager.setPrimaryClip(ClipData.newPlainText("JSON", text)) }
|
||||
.clickable { onClick(text) }
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
@@ -174,6 +181,7 @@ internal fun EventDebugInfoViewPreview() = ElementPreview {
|
||||
model = "Rust(\n\tModel()\n)",
|
||||
originalJson = "{\"name\": \"original\"}",
|
||||
latestEditedJson = "{\"name\": \"edited\"}",
|
||||
onBackClick = { }
|
||||
onBackClick = { },
|
||||
onCopyText = { },
|
||||
)
|
||||
}
|
||||
|
||||
+41
@@ -33,6 +33,10 @@ import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||
import io.element.android.features.roomcall.api.aStandByCallState
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||
import io.element.android.libraries.androidutils.toast.FakeToastHelper
|
||||
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
@@ -74,6 +78,7 @@ import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
@@ -1543,6 +1548,38 @@ class TimelinePresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy to clipboard on old device shows a toast`() = runTest {
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = true)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createTimelinePresenter(
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem().eventSink(TimelineEvent.CopyToClipboard("https://element.io"))
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo("https://element.io")
|
||||
assertThat(toastHelper.shownToasts).containsExactly(CommonStrings.common_copied_to_clipboard)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy to clipboard on new device does not show a toast`() = runTest {
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = false)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createTimelinePresenter(
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
presenter.test {
|
||||
awaitFirstItem().eventSink(TimelineEvent.CopyToClipboard("https://element.io"))
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo("https://element.io")
|
||||
assertThat(toastHelper.shownToasts).isEmpty()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show shield hide shield`() = runTest {
|
||||
val presenter = createTimelinePresenter()
|
||||
@@ -2045,6 +2082,8 @@ class TimelinePresenterTest {
|
||||
markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead { _, _ -> },
|
||||
timelineProtectionPresenter: Presenter<TimelineProtectionState> = { aTimelineProtectionState() },
|
||||
resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState> = { aResolveVerifiedUserSendFailureState() },
|
||||
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||
toastHelper: ToastHelper = FakeToastHelper(),
|
||||
): TimelinePresenter {
|
||||
return TimelinePresenter(
|
||||
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
|
||||
@@ -2072,6 +2111,8 @@ class TimelinePresenterTest {
|
||||
liveLocationShareManager = liveLocationShareManager,
|
||||
markAsFullyRead = markAsFullyRead,
|
||||
timelineProtectionPresenter = timelineProtectionPresenter,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user