mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +00:00
Merge pull request #7857 from element-hq/feature/bma/copyToClipboardNoDoubleToast
Mark the recovery key as sensitive when copying it, and do not show a copy confirmation on Android 13+
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
|
||||
@@ -634,8 +633,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}")
|
||||
@@ -650,16 +650,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
|
||||
|
||||
+2
@@ -29,6 +29,8 @@ sealed interface TimelineEvent {
|
||||
|
||||
data object MarkAllAsRead : TimelineEvent
|
||||
|
||||
data class CopyToClipboard(val text: String) : TimelineEvent
|
||||
|
||||
/**
|
||||
* Events coming from a timeline item.
|
||||
*/
|
||||
|
||||
+10
@@ -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.di.annotations.SessionCoroutineScope
|
||||
@@ -60,6 +62,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.DisplayFirstTimelineItems
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationToMessage
|
||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
||||
@@ -104,6 +107,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"
|
||||
|
||||
@@ -315,6 +320,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-8
@@ -63,7 +63,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
|
||||
@@ -93,7 +92,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentPreviewParam
|
||||
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
|
||||
@@ -167,8 +165,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))
|
||||
@@ -178,10 +174,7 @@ fun TimelineView(
|
||||
view.performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS
|
||||
)
|
||||
context.copyToClipboard(
|
||||
text = link.url,
|
||||
toastMessage = toastMessage,
|
||||
)
|
||||
state.eventSink(TimelineEvent.CopyToClipboard(link.url))
|
||||
}
|
||||
|
||||
fun prefetchMoreItems() {
|
||||
|
||||
+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.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
@@ -73,6 +77,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
|
||||
@@ -1541,6 +1546,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()
|
||||
@@ -1897,6 +1934,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(),
|
||||
@@ -1923,6 +1962,8 @@ class TimelinePresenterTest {
|
||||
liveLocationShareManager = liveLocationShareManager,
|
||||
markAsFullyRead = markAsFullyRead,
|
||||
timelineProtectionPresenter = timelineProtectionPresenter,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -81,8 +81,9 @@ class BlockedUsersPresenter(
|
||||
unblockUserAction.value = AsyncAction.ConfirmingNoParams
|
||||
}
|
||||
is BlockedUsersEvent.CopyToClipboard -> {
|
||||
clipboardHelper.copyPlainText(event.userId.value)
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||
clipboardHelper.copyPlainText(event.userId.value) {
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||
}
|
||||
}
|
||||
BlockedUsersEvent.ConfirmUnblock -> {
|
||||
pendingUserToUnblock?.let {
|
||||
|
||||
+1
@@ -19,4 +19,5 @@ sealed interface DeveloperSettingsEvent {
|
||||
data object DismissMarkAllRoomsAsReadConfirmation : DeveloperSettingsEvent
|
||||
data object OpenPushRules : DeveloperSettingsEvent
|
||||
data object DismissPushRulesError : DeveloperSettingsEvent
|
||||
data class CopyToClipboard(val text: String) : DeveloperSettingsEvent
|
||||
}
|
||||
|
||||
+10
@@ -26,7 +26,9 @@ import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
|
||||
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
|
||||
import io.element.android.features.preferences.impl.tasks.MarkAllRoomsAsRead
|
||||
import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
|
||||
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
@@ -38,6 +40,7 @@ import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
|
||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -61,6 +64,8 @@ class DeveloperSettingsPresenter(
|
||||
private val markAllRoomsAsRead: MarkAllRoomsAsRead,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val notificationSettingsService: NotificationSettingsService,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val toastHelper: ToastHelper,
|
||||
) : Presenter<DeveloperSettingsState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory {
|
||||
@@ -130,6 +135,11 @@ class DeveloperSettingsPresenter(
|
||||
DeveloperSettingsEvent.DismissPushRulesError -> {
|
||||
pushRulesAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is DeveloperSettingsEvent.CopyToClipboard -> {
|
||||
clipboardHelper.copyPlainText(event.text) {
|
||||
toastHelper.show(CommonStrings.common_copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-12
@@ -15,14 +15,12 @@ import androidx.compose.foundation.progressSemantics
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsView
|
||||
import io.element.android.libraries.androidutils.system.copyToClipboard
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
@@ -84,7 +82,10 @@ fun DeveloperSettingsView(
|
||||
AppDeveloperSettingsView(
|
||||
state = state.appDeveloperSettingsState,
|
||||
)
|
||||
SessionCategory(deviceId = state.deviceId)
|
||||
SessionCategory(
|
||||
deviceId = state.deviceId,
|
||||
onCopyToClipboard = { state.eventSink(DeveloperSettingsEvent.CopyToClipboard(it)) },
|
||||
)
|
||||
NotificationCategory(
|
||||
onPushRulesClick = { state.eventSink(DeveloperSettingsEvent.OpenPushRules) },
|
||||
onPushHistoryClick = onPushHistoryClick,
|
||||
@@ -182,19 +183,15 @@ fun DeveloperSettingsView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionCategory(deviceId: DeviceId) {
|
||||
private fun SessionCategory(
|
||||
deviceId: DeviceId,
|
||||
onCopyToClipboard: (String) -> Unit,
|
||||
) {
|
||||
PreferenceCategory(title = "Session") {
|
||||
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
|
||||
val context = LocalContext.current
|
||||
ListItem(
|
||||
content = { Text("DeviceId") },
|
||||
supportingContent = { Text(text = deviceId.value) },
|
||||
onClick = {
|
||||
context.copyToClipboard(
|
||||
text = deviceId.value,
|
||||
toastMessage = toastMessage,
|
||||
)
|
||||
}
|
||||
onClick = { onCopyToClipboard(deviceId.value) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -17,4 +17,5 @@ sealed interface AppDeveloperSettingsEvent {
|
||||
data class SetCustomElementCallBaseUrl(val baseUrl: String?) : AppDeveloperSettingsEvent
|
||||
data class SetTracingLogLevel(val logLevel: LogLevelItem) : AppDeveloperSettingsEvent
|
||||
data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : AppDeveloperSettingsEvent
|
||||
data class CopyToClipboard(val text: String) : AppDeveloperSettingsEvent
|
||||
}
|
||||
|
||||
+10
@@ -22,6 +22,8 @@ import io.element.android.features.preferences.impl.developer.tracing.toLogLevel
|
||||
import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem
|
||||
import io.element.android.features.preferences.impl.model.EnabledFeature
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
@@ -29,6 +31,7 @@ import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -44,6 +47,8 @@ class AppDeveloperSettingsPresenter(
|
||||
private val rageshakePresenter: Presenter<RageshakePreferencesState>,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val toastHelper: ToastHelper,
|
||||
) : Presenter<AppDeveloperSettingsState> {
|
||||
@Composable
|
||||
override fun present(): AppDeveloperSettingsState {
|
||||
@@ -106,6 +111,11 @@ class AppDeveloperSettingsPresenter(
|
||||
}
|
||||
appPreferencesStore.setTracingLogPacks(currentPacks)
|
||||
}
|
||||
is AppDeveloperSettingsEvent.CopyToClipboard -> {
|
||||
clipboardHelper.copyPlainText(event.text) {
|
||||
toastHelper.show(CommonStrings.common_copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-16
@@ -14,7 +14,6 @@ import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
@@ -23,7 +22,6 @@ import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.preferences.impl.R
|
||||
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
|
||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
|
||||
import io.element.android.libraries.androidutils.system.copyToClipboard
|
||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown
|
||||
@@ -108,6 +106,7 @@ fun AppDeveloperSettingsView(
|
||||
GitCategory(
|
||||
gitBranch = state.gitBranch,
|
||||
gitSha = state.gitSha,
|
||||
onCopyToClipboard = { state.eventSink(AppDeveloperSettingsEvent.CopyToClipboard(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -116,29 +115,18 @@ fun AppDeveloperSettingsView(
|
||||
private fun GitCategory(
|
||||
gitBranch: String,
|
||||
gitSha: String,
|
||||
onCopyToClipboard: (String) -> Unit,
|
||||
) {
|
||||
PreferenceCategory(title = "Git") {
|
||||
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
|
||||
val context = LocalContext.current
|
||||
ListItem(
|
||||
content = { Text("Git branch") },
|
||||
supportingContent = { Text(text = gitBranch) },
|
||||
onClick = {
|
||||
context.copyToClipboard(
|
||||
text = gitBranch,
|
||||
toastMessage = toastMessage,
|
||||
)
|
||||
}
|
||||
onClick = { onCopyToClipboard(gitBranch) }
|
||||
)
|
||||
ListItem(
|
||||
content = { Text("Git SHA") },
|
||||
supportingContent = { Text(text = gitSha) },
|
||||
onClick = {
|
||||
context.copyToClipboard(
|
||||
text = gitSha,
|
||||
toastMessage = toastMessage,
|
||||
)
|
||||
}
|
||||
onClick = { onCopyToClipboard(gitSha) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -19,7 +19,11 @@ import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
|
||||
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
|
||||
import io.element.android.features.preferences.impl.tasks.FakeMarkAllRoomsAsRead
|
||||
import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
||||
import io.element.android.libraries.androidutils.toast.FakeToastHelper
|
||||
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.data.megaBytes
|
||||
@@ -33,6 +37,7 @@ import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
@@ -250,6 +255,38 @@ class DeveloperSettingsPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy to clipboard on old device shows a toast`() = runTest {
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = true)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createDeveloperSettingsPresenter(
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
presenter.test {
|
||||
awaitItem().eventSink(DeveloperSettingsEvent.CopyToClipboard("text"))
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo("text")
|
||||
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 = createDeveloperSettingsPresenter(
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
presenter.test {
|
||||
awaitItem().eventSink(DeveloperSettingsEvent.CopyToClipboard("text"))
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo("text")
|
||||
assertThat(toastHelper.shownToasts).isEmpty()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDeveloperSettingsPresenter(
|
||||
navigator: DeveloperSettingsNavigator = DeveloperSettingsNavigator { _, _ -> lambdaError() },
|
||||
sessionId: SessionId = A_SESSION_ID,
|
||||
@@ -262,6 +299,8 @@ class DeveloperSettingsPresenterTest {
|
||||
markAllRoomsAsRead: FakeMarkAllRoomsAsRead = FakeMarkAllRoomsAsRead(),
|
||||
buildMeta: BuildMeta = aBuildMeta(),
|
||||
notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
||||
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||
toastHelper: ToastHelper = FakeToastHelper(),
|
||||
): DeveloperSettingsPresenter {
|
||||
return DeveloperSettingsPresenter(
|
||||
navigator = navigator,
|
||||
@@ -277,6 +316,8 @@ class DeveloperSettingsPresenterTest {
|
||||
markAllRoomsAsRead = markAllRoomsAsRead,
|
||||
buildMeta = buildMeta,
|
||||
notificationSettingsService = notificationSettingsService,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+42
-1
@@ -12,12 +12,17 @@ package io.element.android.features.preferences.impl.developer.appsettings
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
|
||||
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
|
||||
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.AsyncData
|
||||
import io.element.android.libraries.featureflag.api.Feature
|
||||
import io.element.android.libraries.featureflag.test.FakeFeature
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
@@ -151,6 +156,38 @@ class AppDeveloperSettingsPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy to clipboard on old device shows a toast`() = runTest {
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = true)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createAppDeveloperSettingsPresenter(
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
presenter.test {
|
||||
awaitItem().eventSink(AppDeveloperSettingsEvent.CopyToClipboard("text"))
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo("text")
|
||||
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 = createAppDeveloperSettingsPresenter(
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
presenter.test {
|
||||
awaitItem().eventSink(AppDeveloperSettingsEvent.CopyToClipboard("text"))
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo("text")
|
||||
assertThat(toastHelper.shownToasts).isEmpty()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAppDeveloperSettingsPresenter(
|
||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(
|
||||
getAvailableFeaturesResult = { _, _ ->
|
||||
@@ -164,6 +201,8 @@ class AppDeveloperSettingsPresenterTest {
|
||||
}
|
||||
),
|
||||
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||
toastHelper: ToastHelper = FakeToastHelper(),
|
||||
): AppDeveloperSettingsPresenter {
|
||||
return AppDeveloperSettingsPresenter(
|
||||
featureFlagService = featureFlagService,
|
||||
@@ -172,7 +211,9 @@ class AppDeveloperSettingsPresenterTest {
|
||||
buildMeta = aBuildMeta(
|
||||
gitRevision = "1234567890",
|
||||
gitBranchName = "feature/awesome-feature"
|
||||
)
|
||||
),
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ sealed interface RoomDetailsEvent {
|
||||
data object MuteNotification : RoomDetailsEvent
|
||||
data object UnmuteNotification : RoomDetailsEvent
|
||||
data class CopyToClipboard(val text: String) : RoomDetailsEvent
|
||||
data object CopyRoomId : RoomDetailsEvent
|
||||
data class SetFavorite(val isFavorite: Boolean) : RoomDetailsEvent
|
||||
data object MarkAsRead : RoomDetailsEvent
|
||||
data object MarkAsUnread : RoomDetailsEvent
|
||||
|
||||
+10
-2
@@ -32,6 +32,7 @@ import io.element.android.features.roomdetailsedit.api.roomDetailsEditPermission
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyPermissions
|
||||
import io.element.android.features.securityandprivacy.api.securityAndPrivacyPermissions
|
||||
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.LocalSnackbarDispatcher
|
||||
@@ -74,6 +75,7 @@ class RoomDetailsPresenter(
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val toastHelper: ToastHelper,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
private val notificationCleaner: NotificationCleaner,
|
||||
@@ -167,8 +169,14 @@ class RoomDetailsPresenter(
|
||||
}
|
||||
is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite)
|
||||
is RoomDetailsEvent.CopyToClipboard -> {
|
||||
clipboardHelper.copyPlainText(event.text)
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||
clipboardHelper.copyPlainText(event.text) {
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||
}
|
||||
}
|
||||
is RoomDetailsEvent.CopyRoomId -> {
|
||||
clipboardHelper.copyPlainText(room.roomId.value) {
|
||||
toastHelper.show(CommonStrings.common_copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
is RoomDetailsEvent.MarkAsRead -> scope.markAsRead()
|
||||
is RoomDetailsEvent.MarkAsUnread -> scope.markAsUnread()
|
||||
|
||||
+3
-10
@@ -34,7 +34,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -48,7 +47,6 @@ import io.element.android.features.userprofile.api.UserProfileState
|
||||
import io.element.android.features.userprofile.api.UserProfileVerificationState
|
||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
|
||||
import io.element.android.libraries.androidutils.system.copyToClipboard
|
||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.MatrixBadgeRowMolecule
|
||||
@@ -339,6 +337,7 @@ fun RoomDetailsView(
|
||||
DebugInfoSection(
|
||||
roomId = state.roomId,
|
||||
roomVersion = state.roomVersion,
|
||||
onCopyRoomIdClick = { state.eventSink(RoomDetailsEvent.CopyRoomId) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -865,10 +864,9 @@ private fun OtherActionsSection(
|
||||
private fun DebugInfoSection(
|
||||
roomId: RoomId,
|
||||
roomVersion: String?,
|
||||
onCopyRoomIdClick: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
PreferenceCategory {
|
||||
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
|
||||
ListItem(
|
||||
content = {
|
||||
Text("Internal room ID")
|
||||
@@ -882,12 +880,7 @@ private fun DebugInfoSection(
|
||||
},
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Code())),
|
||||
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Copy())),
|
||||
onClick = {
|
||||
context.copyToClipboard(
|
||||
text = roomId.value,
|
||||
toastMessage = toastMessage,
|
||||
)
|
||||
},
|
||||
onClick = onCopyRoomIdClick,
|
||||
)
|
||||
ListItem(
|
||||
content = {
|
||||
|
||||
+3
-2
@@ -118,8 +118,9 @@ class RoomMemberDetailsPresenter(
|
||||
encryptionService.withdrawVerification(roomMemberId)
|
||||
}
|
||||
is UserProfileEvent.CopyToClipboard -> {
|
||||
clipboardHelper.copyPlainText(event.text)
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||
clipboardHelper.copyPlainText(event.text) {
|
||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||
}
|
||||
}
|
||||
else -> userProfileState.eventSink(event)
|
||||
}
|
||||
|
||||
+45
@@ -19,6 +19,8 @@ import io.element.android.features.roomdetails.impl.members.details.RoomMemberDe
|
||||
import io.element.android.features.userprofile.shared.aUserProfileState
|
||||
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.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
@@ -49,6 +51,7 @@ import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
@@ -85,6 +88,7 @@ class RoomDetailsPresenterTest {
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
||||
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||
toastHelper: ToastHelper = FakeToastHelper(),
|
||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||
navigator: RoomDetailsNavigator = FakeRoomDetailsNavigator(),
|
||||
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
|
||||
@@ -115,12 +119,53 @@ class RoomDetailsPresenterTest {
|
||||
dispatchers = dispatchers,
|
||||
analyticsService = analyticsService,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
notificationCleaner = notificationCleaner,
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy room id on old device shows a toast`() = runTest {
|
||||
val room = aJoinedRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
)
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = true)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createRoomDetailsPresenter(
|
||||
room = room,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) {
|
||||
awaitItem().eventSink(RoomDetailsEvent.CopyRoomId)
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo(room.roomId.value)
|
||||
assertThat(toastHelper.shownToasts).containsExactly(CommonStrings.common_copied_to_clipboard)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy room id on new device does not show a toast`() = runTest {
|
||||
val room = aJoinedRoom(
|
||||
roomPermissions = roomPermissions(),
|
||||
)
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = false)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createRoomDetailsPresenter(
|
||||
room = room,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) {
|
||||
awaitItem().eventSink(RoomDetailsEvent.CopyRoomId)
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo(room.roomId.value)
|
||||
assertThat(toastHelper.shownToasts).isEmpty()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - initial state is created from initial room info`() = runTest {
|
||||
val room = aJoinedRoom(
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ package io.element.android.features.securebackup.impl.setup
|
||||
sealed interface SecureBackupSetupEvent {
|
||||
data object CreateRecoveryKey : SecureBackupSetupEvent
|
||||
data object RecoveryKeyHasBeenSaved : SecureBackupSetupEvent
|
||||
data object CopyRecoveryKey : SecureBackupSetupEvent
|
||||
data object Done : SecureBackupSetupEvent
|
||||
data object DismissDialog : SecureBackupSetupEvent
|
||||
}
|
||||
|
||||
+12
@@ -22,9 +22,12 @@ import com.freeletics.flowredux.compose.rememberStateAndDispatch
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.features.securebackup.impl.loggerTagSetup
|
||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory
|
||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
|
||||
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.matrix.api.encryption.EnableRecoveryProgress
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
@@ -38,6 +41,8 @@ class SecureBackupSetupPresenter(
|
||||
@Assisted private val isChangeRecoveryKeyUserStory: Boolean,
|
||||
private val stateMachine: SecureBackupSetupStateMachine,
|
||||
private val encryptionService: EncryptionService,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val toastHelper: ToastHelper,
|
||||
) : Presenter<SecureBackupSetupState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
@@ -60,6 +65,13 @@ class SecureBackupSetupPresenter(
|
||||
}
|
||||
SecureBackupSetupEvent.RecoveryKeyHasBeenSaved ->
|
||||
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.UserSavedKey)
|
||||
SecureBackupSetupEvent.CopyRecoveryKey -> {
|
||||
val recoveryKey = setupState.recoveryKey() ?: return
|
||||
clipboardHelper.copyPlainText(recoveryKey, isSensitive = true) {
|
||||
toastHelper.show(R.string.screen_recovery_key_copied_to_clipboard)
|
||||
}
|
||||
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.UserSavedKey)
|
||||
}
|
||||
SecureBackupSetupEvent.DismissDialog -> {
|
||||
showSaveConfirmationDialog = false
|
||||
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.ClearError)
|
||||
|
||||
+1
-8
@@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView
|
||||
import io.element.android.libraries.androidutils.system.copyToClipboard
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
@@ -114,16 +113,10 @@ private fun subtitle(state: SecureBackupSetupState): String {
|
||||
private fun Content(
|
||||
state: SecureBackupSetupState,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val formattedRecoveryKey = state.recoveryKeyViewState.formattedRecoveryKey
|
||||
val toastMessage = stringResource(R.string.screen_recovery_key_copied_to_clipboard)
|
||||
val clickLambda = if (formattedRecoveryKey != null) {
|
||||
{
|
||||
context.copyToClipboard(
|
||||
text = formattedRecoveryKey,
|
||||
toastMessage = toastMessage,
|
||||
)
|
||||
state.eventSink.invoke(SecureBackupSetupEvent.RecoveryKeyHasBeenSaved)
|
||||
state.eventSink.invoke(SecureBackupSetupEvent.CopyRecoveryKey)
|
||||
}
|
||||
} else {
|
||||
if (!state.recoveryKeyViewState.inProgress) {
|
||||
|
||||
+4
@@ -15,6 +15,8 @@ import io.element.android.features.securebackup.api.SecureBackupSetupEntryPoint
|
||||
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode
|
||||
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupPresenter
|
||||
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupStateMachine
|
||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||
import io.element.android.libraries.androidutils.toast.FakeToastHelper
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
@@ -40,6 +42,8 @@ class DefaultSecureBackupSetupEntryPointTest {
|
||||
isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory,
|
||||
stateMachine = SecureBackupSetupStateMachine(),
|
||||
encryptionService = FakeEncryptionService(),
|
||||
clipboardHelper = FakeClipboardHelper(),
|
||||
toastHelper = FakeToastHelper(),
|
||||
)
|
||||
},
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
|
||||
+59
@@ -12,8 +12,13 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory
|
||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
|
||||
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.matrix.api.encryption.EnableRecoveryProgress
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import io.element.android.libraries.matrix.test.A_RECOVERY_KEY
|
||||
@@ -186,16 +191,70 @@ class SecureBackupSetupPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy recovery key on old device shows a toast`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = true)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createSecureBackupSetupPresenter(
|
||||
encryptionService = encryptionService,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink.invoke(SecureBackupSetupEvent.CreateRecoveryKey)
|
||||
skipItems(1)
|
||||
encryptionService.emitEnableRecoveryProgress(EnableRecoveryProgress.Done(A_RECOVERY_KEY))
|
||||
val createdState = awaitItem()
|
||||
createdState.eventSink.invoke(SecureBackupSetupEvent.CopyRecoveryKey)
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo(A_RECOVERY_KEY)
|
||||
assertThat(clipboardHelper.isSensitive).isTrue()
|
||||
assertThat(toastHelper.shownToasts).containsExactly(R.string.screen_recovery_key_copied_to_clipboard)
|
||||
assertThat(awaitItem().setupState).isInstanceOf(SetupState.CreatedAndSaved::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy recovery key on new device does not show a toast`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = false)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createSecureBackupSetupPresenter(
|
||||
encryptionService = encryptionService,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink.invoke(SecureBackupSetupEvent.CreateRecoveryKey)
|
||||
skipItems(1)
|
||||
encryptionService.emitEnableRecoveryProgress(EnableRecoveryProgress.Done(A_RECOVERY_KEY))
|
||||
val createdState = awaitItem()
|
||||
createdState.eventSink.invoke(SecureBackupSetupEvent.CopyRecoveryKey)
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo(A_RECOVERY_KEY)
|
||||
assertThat(clipboardHelper.isSensitive).isTrue()
|
||||
assertThat(toastHelper.shownToasts).isEmpty()
|
||||
assertThat(awaitItem().setupState).isInstanceOf(SetupState.CreatedAndSaved::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSecureBackupSetupPresenter(
|
||||
isChangeRecoveryKeyUserStory: Boolean = false,
|
||||
encryptionService: EncryptionService = FakeEncryptionService(
|
||||
enableRecoveryLambda = { _, _ -> Result.success("") },
|
||||
),
|
||||
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||
toastHelper: ToastHelper = FakeToastHelper(),
|
||||
): SecureBackupSetupPresenter {
|
||||
return SecureBackupSetupPresenter(
|
||||
isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory,
|
||||
stateMachine = SecureBackupSetupStateMachine(),
|
||||
encryptionService = encryptionService,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -15,10 +15,16 @@ import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.viewfolder.api.TextFileViewer
|
||||
import io.element.android.features.viewfolder.impl.file.ColorationMode
|
||||
import io.element.android.features.viewfolder.impl.file.FileContent
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultTextFileViewer : TextFileViewer {
|
||||
class DefaultTextFileViewer(
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val toastHelper: ToastHelper,
|
||||
) : TextFileViewer {
|
||||
@Composable
|
||||
override fun Render(
|
||||
lines: ImmutableList<String>,
|
||||
@@ -27,6 +33,11 @@ class DefaultTextFileViewer : TextFileViewer {
|
||||
FileContent(
|
||||
lines = lines,
|
||||
colorationMode = ColorationMode.None,
|
||||
onLineClick = { line ->
|
||||
clipboardHelper.copyPlainText(line) {
|
||||
toastHelper.show(CommonStrings.common_line_copied_to_clipboard)
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
+4
-10
@@ -23,12 +23,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.androidutils.system.copyToClipboard
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -37,6 +35,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
internal fun FileContent(
|
||||
lines: ImmutableList<String>,
|
||||
colorationMode: ColorationMode,
|
||||
onLineClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
@@ -60,6 +59,7 @@ internal fun FileContent(
|
||||
lineNumber = index + 1,
|
||||
line = line,
|
||||
colorationMode = colorationMode,
|
||||
onClick = { onLineClick(line) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -71,18 +71,12 @@ private fun LineRow(
|
||||
lineNumber: Int,
|
||||
line: String,
|
||||
colorationMode: ColorationMode,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val toastMessage = stringResource(CommonStrings.common_line_copied_to_clipboard)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = {
|
||||
context.copyToClipboard(
|
||||
text = line,
|
||||
toastMessage = toastMessage,
|
||||
)
|
||||
})
|
||||
.clickable(onClick = onClick)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
|
||||
+1
@@ -11,4 +11,5 @@ package io.element.android.features.viewfolder.impl.file
|
||||
sealed interface ViewFileEvent {
|
||||
data object SaveOnDisk : ViewFileEvent
|
||||
data object Share : ViewFileEvent
|
||||
data class CopyToClipboard(val text: String) : ViewFileEvent
|
||||
}
|
||||
|
||||
+8
@@ -18,8 +18,11 @@ import androidx.compose.runtime.setValue
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -30,6 +33,8 @@ class ViewFilePresenter(
|
||||
private val fileContentReader: FileContentReader,
|
||||
private val fileShare: FileShare,
|
||||
private val fileSave: FileSave,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val toastHelper: ToastHelper,
|
||||
) : Presenter<ViewFileState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
@@ -48,6 +53,9 @@ class ViewFilePresenter(
|
||||
when (event) {
|
||||
ViewFileEvent.Share -> coroutineScope.share(path)
|
||||
ViewFileEvent.SaveOnDisk -> coroutineScope.save(path)
|
||||
is ViewFileEvent.CopyToClipboard -> clipboardHelper.copyPlainText(event.text) {
|
||||
toastHelper.show(CommonStrings.common_line_copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -80,6 +80,7 @@ fun ViewFileView(
|
||||
modifier = Modifier.weight(1f),
|
||||
lines = state.lines.data.toImmutableList(),
|
||||
colorationMode = state.colorationMode,
|
||||
onLineClick = { state.eventSink(ViewFileEvent.CopyToClipboard(it)) },
|
||||
)
|
||||
is AsyncData.Failure -> AsyncFailure(throwable = state.lines.error, onRetry = null)
|
||||
}
|
||||
|
||||
+39
@@ -18,8 +18,13 @@ import io.element.android.features.viewfolder.impl.file.FileSave
|
||||
import io.element.android.features.viewfolder.impl.file.FileShare
|
||||
import io.element.android.features.viewfolder.impl.file.ViewFileEvent
|
||||
import io.element.android.features.viewfolder.impl.file.ViewFilePresenter
|
||||
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.AsyncData
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
@@ -126,17 +131,51 @@ class ViewFilePresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy line on old device shows a toast`() = runTest {
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = true)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createPresenter(clipboardHelper = clipboardHelper, toastHelper = toastHelper)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(ViewFileEvent.CopyToClipboard("aLine"))
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo("aLine")
|
||||
assertThat(toastHelper.shownToasts).containsExactly(CommonStrings.common_line_copied_to_clipboard)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy line on new device does not show a toast`() = runTest {
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = false)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createPresenter(clipboardHelper = clipboardHelper, toastHelper = toastHelper)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink(ViewFileEvent.CopyToClipboard("aLine"))
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo("aLine")
|
||||
assertThat(toastHelper.shownToasts).isEmpty()
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPresenter(
|
||||
path: String = "aPath",
|
||||
name: String = "aName",
|
||||
fileContentReader: FileContentReader = FakeFileContentReader(),
|
||||
fileShare: FileShare = FakeFileShare(),
|
||||
fileSave: FileSave = FakeFileSave(),
|
||||
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||
toastHelper: ToastHelper = FakeToastHelper(),
|
||||
) = ViewFilePresenter(
|
||||
path = path,
|
||||
name = name,
|
||||
fileContentReader = fileContentReader,
|
||||
fileShare = fileShare,
|
||||
fileSave = fileSave,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
}
|
||||
|
||||
+28
-2
@@ -9,8 +9,11 @@
|
||||
package io.element.android.libraries.androidutils.clipboard
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PersistableBundle
|
||||
import androidx.core.content.getSystemService
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
@@ -24,7 +27,30 @@ class AndroidClipboardHelper(
|
||||
) : ClipboardHelper {
|
||||
private val clipboardManager = requireNotNull(context.getSystemService<ClipboardManager>())
|
||||
|
||||
override fun copyPlainText(text: String) {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("", text))
|
||||
override fun copyPlainText(
|
||||
text: String,
|
||||
isSensitive: Boolean,
|
||||
onSuccessOnOldDevice: () -> Unit,
|
||||
) {
|
||||
val clipData = ClipData.newPlainText("", text)
|
||||
if (isSensitive) {
|
||||
clipData.description.extras = PersistableBundle().apply {
|
||||
putBoolean(EXTRA_IS_SENSITIVE, true)
|
||||
}
|
||||
}
|
||||
clipboardManager.setPrimaryClip(clipData)
|
||||
// On Android 13+, the system already displays a confirmation when content is copied to the clipboard.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
onSuccessOnOldDevice()
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// The constant only exists from API 33, but the documentation recommends using its value on older versions too.
|
||||
val EXTRA_IS_SENSITIVE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ClipDescription.EXTRA_IS_SENSITIVE
|
||||
} else {
|
||||
"android.content.extra.IS_SENSITIVE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -8,9 +8,18 @@
|
||||
|
||||
package io.element.android.libraries.androidutils.clipboard
|
||||
|
||||
/**
|
||||
* Wrapper class for handling clipboard operations so it can be used in JVM environments.
|
||||
*/
|
||||
interface ClipboardHelper {
|
||||
fun copyPlainText(text: String)
|
||||
/**
|
||||
* Copy a text to the clipboard.
|
||||
*
|
||||
* @param text the text to copy
|
||||
* @param isSensitive if true, the clip is flagged so the system clipboard preview (Android 13+) and keyboards hide its content.
|
||||
* @param onSuccessOnOldDevice invoked after the copy only on Android versions older than 13, since newer versions already
|
||||
* display a confirmation. Can be used to render a Toast or a Snackbar.
|
||||
*/
|
||||
fun copyPlainText(
|
||||
text: String,
|
||||
isSensitive: Boolean = false,
|
||||
onSuccessOnOldDevice: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
+13
-2
@@ -8,10 +8,21 @@
|
||||
|
||||
package io.element.android.libraries.androidutils.clipboard
|
||||
|
||||
class FakeClipboardHelper : ClipboardHelper {
|
||||
class FakeClipboardHelper(
|
||||
private val isOldDevice: Boolean = true,
|
||||
) : ClipboardHelper {
|
||||
var clipboardContents: Any? = null
|
||||
var isSensitive: Boolean? = null
|
||||
|
||||
override fun copyPlainText(text: String) {
|
||||
override fun copyPlainText(
|
||||
text: String,
|
||||
isSensitive: Boolean,
|
||||
onSuccessOnOldDevice: () -> Unit,
|
||||
) {
|
||||
clipboardContents = text
|
||||
this.isSensitive = isSensitive
|
||||
if (isOldDevice) {
|
||||
onSuccessOnOldDevice()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2022-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.system
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
class CopyToClipboardUseCase(
|
||||
private val context: Context,
|
||||
) {
|
||||
fun execute(text: CharSequence) {
|
||||
context.getSystemService<ClipboardManager>()
|
||||
?.setPrimaryClip(ClipData.newPlainText("", text))
|
||||
}
|
||||
}
|
||||
-19
@@ -50,25 +50,6 @@ fun Context.getVersionCodeFromManifest(): Long {
|
||||
)
|
||||
}
|
||||
|
||||
// ==============================================================================================================
|
||||
// Clipboard helper
|
||||
// ==============================================================================================================
|
||||
|
||||
/**
|
||||
* Copy a text to the clipboard, and display a Toast when done.
|
||||
*
|
||||
* @receiver the context
|
||||
* @param text the text to copy
|
||||
* @param toastMessage content of the toast message as a String resource. Null for no toast
|
||||
*/
|
||||
fun Context.copyToClipboard(
|
||||
text: CharSequence,
|
||||
toastMessage: String? = null
|
||||
) {
|
||||
CopyToClipboardUseCase(this).execute(text)
|
||||
toastMessage?.let { toast(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notification settings for the current app.
|
||||
* In android O will directly opens the notification settings, in lower version it will show the App settings
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.toast
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.system.toast
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AndroidToastHelper(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : ToastHelper {
|
||||
override fun show(resId: Int) {
|
||||
context.toast(resId)
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.toast
|
||||
|
||||
class FakeToastHelper : ToastHelper {
|
||||
val shownToasts = mutableListOf<Int>()
|
||||
|
||||
override fun show(resId: Int) {
|
||||
shownToasts.add(resId)
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.toast
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
interface ToastHelper {
|
||||
fun show(@StringRes resId: Int)
|
||||
}
|
||||
Reference in New Issue
Block a user