mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Use only ClipboardHelper to copy to the clipboard, and do not render feedback on Android 13+
Since Android 13, the system displays its own confirmation when content is copied to the clipboard, so the application does not render its own Toast or Snackbar anymore on those versions. Context.copyToClipboard, CopyToClipboardUseCase and the direct ClipboardManager usage in EventDebugInfoView are removed. ClipboardHelper.copyPlainText(text, isSensitive, onSuccessOnOldDevice) is now the only entry point, and AndroidClipboardHelper is the only class using ClipboardManager. Copy actions from the views are moved to their Presenter through new events, or to the Node when there is no Presenter. A ToastHelper is introduced so that Toasts are kept where they were used before. Snackbars are kept where they were already used. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-7
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
package io.element.android.features.messages.impl
|
package io.element.android.features.messages.impl
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
@@ -634,8 +633,9 @@ class MessagesPresenter(
|
|||||||
event.eventId ?: return
|
event.eventId ?: return
|
||||||
room.getPermalinkFor(event.eventId).fold(
|
room.getPermalinkFor(event.eventId).fold(
|
||||||
onSuccess = { permalink ->
|
onSuccess = { permalink ->
|
||||||
clipboardHelper.copyPlainText(permalink)
|
clipboardHelper.copyPlainText(permalink) {
|
||||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_link_copied_to_clipboard))
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_link_copied_to_clipboard))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
Timber.e(it, "Failed to get permalink for event ${event.eventId}")
|
Timber.e(it, "Failed to get permalink for event ${event.eventId}")
|
||||||
@@ -650,16 +650,14 @@ class MessagesPresenter(
|
|||||||
is TimelineItemStateContent -> event.content.body
|
is TimelineItemStateContent -> event.content.body
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
clipboardHelper.copyPlainText(content)
|
clipboardHelper.copyPlainText(content) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
snackbarDispatcher.post(SnackbarMessage(R.string.screen_room_timeline_message_copied))
|
snackbarDispatcher.post(SnackbarMessage(R.string.screen_room_timeline_message_copied))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCopyCaption(event: TimelineItem.Event) {
|
private fun handleCopyCaption(event: TimelineItem.Event) {
|
||||||
val content = event.content.captionOrNull() ?: return
|
val content = event.content.captionOrNull() ?: return
|
||||||
clipboardHelper.copyPlainText(content)
|
clipboardHelper.copyPlainText(content) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
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.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
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.LocalTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.system.openUrlInExternalApp
|
||||||
|
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||||
import io.element.android.libraries.architecture.callback
|
import io.element.android.libraries.architecture.callback
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer
|
||||||
@@ -53,6 +53,8 @@ class PinnedMessagesListNode(
|
|||||||
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
private val emojiPickerRenderer: EmojiPickerRenderer,
|
private val emojiPickerRenderer: EmojiPickerRenderer,
|
||||||
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val toastHelper: ToastHelper,
|
||||||
) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator {
|
) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator {
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean)
|
fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean)
|
||||||
@@ -114,7 +116,6 @@ class PinnedMessagesListNode(
|
|||||||
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
|
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
PinnedMessagesListView(
|
PinnedMessagesListView(
|
||||||
@@ -132,10 +133,9 @@ class PinnedMessagesListNode(
|
|||||||
view.performHapticFeedback(
|
view.performHapticFeedback(
|
||||||
HapticFeedbackConstants.LONG_PRESS
|
HapticFeedbackConstants.LONG_PRESS
|
||||||
)
|
)
|
||||||
context.copyToClipboard(
|
clipboardHelper.copyPlainText(it.url) {
|
||||||
text = it.url,
|
toastHelper.show(CommonStrings.common_copied_to_clipboard)
|
||||||
toastMessage = toastMessage,
|
}
|
||||||
)
|
|
||||||
},
|
},
|
||||||
emojiPickerRenderer = emojiPickerRenderer,
|
emojiPickerRenderer = emojiPickerRenderer,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|||||||
+2
@@ -29,6 +29,8 @@ sealed interface TimelineEvent {
|
|||||||
|
|
||||||
data object MarkAllAsRead : TimelineEvent
|
data object MarkAllAsRead : TimelineEvent
|
||||||
|
|
||||||
|
data class CopyToClipboard(val text: String) : TimelineEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events coming from a timeline item.
|
* 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.EndPollAction
|
||||||
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
import io.element.android.features.poll.api.actions.SendPollResponseAction
|
||||||
import io.element.android.features.roomcall.api.RoomCallState
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
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.LocalEventSendState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
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.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.DisplayFirstTimelineItems
|
||||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationToMessage
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.NotificationToMessage
|
||||||
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.OpenRoom
|
||||||
@@ -104,6 +107,8 @@ class TimelinePresenter(
|
|||||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||||
private val markAsFullyRead: MarkAsFullyRead,
|
private val markAsFullyRead: MarkAsFullyRead,
|
||||||
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
private val timelineProtectionPresenter: Presenter<TimelineProtectionState>,
|
||||||
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val toastHelper: ToastHelper,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
private val tag = "TimelinePresenter"
|
private val tag = "TimelinePresenter"
|
||||||
|
|
||||||
@@ -315,6 +320,11 @@ class TimelinePresenter(
|
|||||||
is TimelineEvent.ValidateMedia -> {
|
is TimelineEvent.ValidateMedia -> {
|
||||||
timelineProtectionState.eventSink(TimelineProtectionEvent.ValidateContent(event.mediaSources, event.validationState))
|
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.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
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.model.event.TimelineItemEventContentPreviewParam
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||||
import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState
|
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.atomic.atoms.UnreadIndicatorAtom
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.AlertDialog
|
import io.element.android.libraries.designsystem.components.dialogs.AlertDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
@@ -167,8 +165,6 @@ fun TimelineView(
|
|||||||
state.eventSink(TimelineEvent.FocusOnEvent(eventId))
|
state.eventSink(TimelineEvent.FocusOnEvent(eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
|
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
fun inReplyToClick(eventId: EventId) {
|
fun inReplyToClick(eventId: EventId) {
|
||||||
state.eventSink(TimelineEvent.FocusOnEvent(eventId))
|
state.eventSink(TimelineEvent.FocusOnEvent(eventId))
|
||||||
@@ -178,10 +174,7 @@ fun TimelineView(
|
|||||||
view.performHapticFeedback(
|
view.performHapticFeedback(
|
||||||
HapticFeedbackConstants.LONG_PRESS
|
HapticFeedbackConstants.LONG_PRESS
|
||||||
)
|
)
|
||||||
context.copyToClipboard(
|
state.eventSink(TimelineEvent.CopyToClipboard(link.url))
|
||||||
text = link.url,
|
|
||||||
toastMessage = toastMessage,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prefetchMoreItems() {
|
fun prefetchMoreItems() {
|
||||||
|
|||||||
+13
-1
@@ -16,17 +16,22 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.annotations.ContributesNode
|
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.NodeInputs
|
||||||
import io.element.android.libraries.architecture.inputs
|
import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class EventDebugInfoNode(
|
class EventDebugInfoNode(
|
||||||
@Assisted buildContext: BuildContext,
|
@Assisted buildContext: BuildContext,
|
||||||
@Assisted plugins: List<Plugin>,
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val toastHelper: ToastHelper,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val eventId: EventId?,
|
val eventId: EventId?,
|
||||||
@@ -39,6 +44,12 @@ class EventDebugInfoNode(
|
|||||||
navigateUp()
|
navigateUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onCopyText(text: String) {
|
||||||
|
clipboardHelper.copyPlainText(text) {
|
||||||
|
toastHelper.show(CommonStrings.common_copied_to_clipboard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) = with(inputs) {
|
override fun View(modifier: Modifier) = with(inputs) {
|
||||||
EventDebugInfoView(
|
EventDebugInfoView(
|
||||||
@@ -46,7 +57,8 @@ class EventDebugInfoNode(
|
|||||||
model = timelineItemDebugInfo.model,
|
model = timelineItemDebugInfo.model,
|
||||||
originalJson = timelineItemDebugInfo.originalJson,
|
originalJson = timelineItemDebugInfo.originalJson,
|
||||||
latestEditedJson = timelineItemDebugInfo.latestEditedJson,
|
latestEditedJson = timelineItemDebugInfo.latestEditedJson,
|
||||||
onBackClick = ::onBackClick
|
onBackClick = ::onBackClick,
|
||||||
|
onCopyText = ::onCopyText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-13
@@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.debug
|
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.AnimatedVisibility
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
@@ -34,11 +32,9 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
@@ -63,6 +59,7 @@ fun EventDebugInfoView(
|
|||||||
originalJson: String?,
|
originalJson: String?,
|
||||||
latestEditedJson: String?,
|
latestEditedJson: String?,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
|
onCopyText: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
isTest: Boolean = false,
|
isTest: Boolean = false,
|
||||||
) {
|
) {
|
||||||
@@ -87,20 +84,30 @@ fun EventDebugInfoView(
|
|||||||
item {
|
item {
|
||||||
Column(Modifier.padding(vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
Column(Modifier.padding(vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
Text(text = "Event ID:")
|
Text(text = "Event ID:")
|
||||||
CopyableText(text = eventId?.value ?: "-", modifier = Modifier.fillMaxWidth())
|
CopyableText(text = eventId?.value ?: "-", onClick = onCopyText, modifier = Modifier.fillMaxWidth())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
CollapsibleSection(title = "Model:", text = model, initiallyExpanded = sectionsInitiallyExpanded)
|
CollapsibleSection(title = "Model:", text = model, onCopyText = onCopyText, initiallyExpanded = sectionsInitiallyExpanded)
|
||||||
}
|
}
|
||||||
if (originalJson != null) {
|
if (originalJson != null) {
|
||||||
item {
|
item {
|
||||||
CollapsibleSection(title = "Original JSON:", text = prettyJSON(originalJson), initiallyExpanded = sectionsInitiallyExpanded)
|
CollapsibleSection(
|
||||||
|
title = "Original JSON:",
|
||||||
|
text = prettyJSON(originalJson),
|
||||||
|
onCopyText = onCopyText,
|
||||||
|
initiallyExpanded = sectionsInitiallyExpanded,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (latestEditedJson != null) {
|
if (latestEditedJson != null) {
|
||||||
item {
|
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(
|
private fun CollapsibleSection(
|
||||||
title: String,
|
title: String,
|
||||||
text: String,
|
text: String,
|
||||||
|
onCopyText: (String) -> Unit,
|
||||||
initiallyExpanded: Boolean = false,
|
initiallyExpanded: Boolean = false,
|
||||||
) {
|
) {
|
||||||
var isExpanded by remember { mutableStateOf(initiallyExpanded) }
|
var isExpanded by remember { mutableStateOf(initiallyExpanded) }
|
||||||
@@ -139,7 +147,7 @@ private fun CollapsibleSection(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
AnimatedVisibility(visible = isExpanded, enter = expandVertically(), exit = shrinkVertically()) {
|
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
|
@Composable
|
||||||
private fun CopyableText(
|
private fun CopyableText(
|
||||||
text: String,
|
text: String,
|
||||||
|
onClick: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val clipboardManager = remember { requireNotNull(context.getSystemService<ClipboardManager>()) }
|
|
||||||
Box(
|
Box(
|
||||||
modifier
|
modifier
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.clip(RoundedCornerShape(4.dp))
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
.padding(6.dp)
|
.padding(6.dp)
|
||||||
.clickable { clipboardManager.setPrimaryClip(ClipData.newPlainText("JSON", text)) }
|
.clickable { onClick(text) }
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
@@ -174,6 +181,7 @@ internal fun EventDebugInfoViewPreview() = ElementPreview {
|
|||||||
model = "Rust(\n\tModel()\n)",
|
model = "Rust(\n\tModel()\n)",
|
||||||
originalJson = "{\"name\": \"original\"}",
|
originalJson = "{\"name\": \"original\"}",
|
||||||
latestEditedJson = "{\"name\": \"edited\"}",
|
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.FakeEndPollAction
|
||||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||||
import io.element.android.features.roomcall.api.aStandByCallState
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
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.test.timeline.item.event.aRoomMembershipContent
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
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.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
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
|
@Test
|
||||||
fun `present - show shield hide shield`() = runTest {
|
fun `present - show shield hide shield`() = runTest {
|
||||||
val presenter = createTimelinePresenter()
|
val presenter = createTimelinePresenter()
|
||||||
@@ -1897,6 +1934,8 @@ class TimelinePresenterTest {
|
|||||||
markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead { _, _ -> },
|
markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead { _, _ -> },
|
||||||
timelineProtectionPresenter: Presenter<TimelineProtectionState> = { aTimelineProtectionState() },
|
timelineProtectionPresenter: Presenter<TimelineProtectionState> = { aTimelineProtectionState() },
|
||||||
resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState> = { aResolveVerifiedUserSendFailureState() },
|
resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState> = { aResolveVerifiedUserSendFailureState() },
|
||||||
|
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||||
|
toastHelper: ToastHelper = FakeToastHelper(),
|
||||||
): TimelinePresenter {
|
): TimelinePresenter {
|
||||||
return TimelinePresenter(
|
return TimelinePresenter(
|
||||||
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
|
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
|
||||||
@@ -1923,6 +1962,8 @@ class TimelinePresenterTest {
|
|||||||
liveLocationShareManager = liveLocationShareManager,
|
liveLocationShareManager = liveLocationShareManager,
|
||||||
markAsFullyRead = markAsFullyRead,
|
markAsFullyRead = markAsFullyRead,
|
||||||
timelineProtectionPresenter = timelineProtectionPresenter,
|
timelineProtectionPresenter = timelineProtectionPresenter,
|
||||||
|
clipboardHelper = clipboardHelper,
|
||||||
|
toastHelper = toastHelper,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -81,8 +81,9 @@ class BlockedUsersPresenter(
|
|||||||
unblockUserAction.value = AsyncAction.ConfirmingNoParams
|
unblockUserAction.value = AsyncAction.ConfirmingNoParams
|
||||||
}
|
}
|
||||||
is BlockedUsersEvent.CopyToClipboard -> {
|
is BlockedUsersEvent.CopyToClipboard -> {
|
||||||
clipboardHelper.copyPlainText(event.userId.value)
|
clipboardHelper.copyPlainText(event.userId.value) {
|
||||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BlockedUsersEvent.ConfirmUnblock -> {
|
BlockedUsersEvent.ConfirmUnblock -> {
|
||||||
pendingUserToUnblock?.let {
|
pendingUserToUnblock?.let {
|
||||||
|
|||||||
+1
@@ -19,4 +19,5 @@ sealed interface DeveloperSettingsEvent {
|
|||||||
data object DismissMarkAllRoomsAsReadConfirmation : DeveloperSettingsEvent
|
data object DismissMarkAllRoomsAsReadConfirmation : DeveloperSettingsEvent
|
||||||
data object OpenPushRules : DeveloperSettingsEvent
|
data object OpenPushRules : DeveloperSettingsEvent
|
||||||
data object DismissPushRulesError : 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.ComputeCacheSizeUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.MarkAllRoomsAsRead
|
import io.element.android.features.preferences.impl.tasks.MarkAllRoomsAsRead
|
||||||
import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase
|
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.filesize.FileSizeFormatter
|
||||||
|
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
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.DeviceId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
|
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.ImmutableMap
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -61,6 +64,8 @@ class DeveloperSettingsPresenter(
|
|||||||
private val markAllRoomsAsRead: MarkAllRoomsAsRead,
|
private val markAllRoomsAsRead: MarkAllRoomsAsRead,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val notificationSettingsService: NotificationSettingsService,
|
private val notificationSettingsService: NotificationSettingsService,
|
||||||
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val toastHelper: ToastHelper,
|
||||||
) : Presenter<DeveloperSettingsState> {
|
) : Presenter<DeveloperSettingsState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
fun interface Factory {
|
fun interface Factory {
|
||||||
@@ -130,6 +135,11 @@ class DeveloperSettingsPresenter(
|
|||||||
DeveloperSettingsEvent.DismissPushRulesError -> {
|
DeveloperSettingsEvent.DismissPushRulesError -> {
|
||||||
pushRulesAction.value = AsyncAction.Uninitialized
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.preferences.impl.R
|
import io.element.android.features.preferences.impl.R
|
||||||
import io.element.android.features.preferences.impl.developer.appsettings.AppDeveloperSettingsView
|
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.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
@@ -84,7 +82,10 @@ fun DeveloperSettingsView(
|
|||||||
AppDeveloperSettingsView(
|
AppDeveloperSettingsView(
|
||||||
state = state.appDeveloperSettingsState,
|
state = state.appDeveloperSettingsState,
|
||||||
)
|
)
|
||||||
SessionCategory(deviceId = state.deviceId)
|
SessionCategory(
|
||||||
|
deviceId = state.deviceId,
|
||||||
|
onCopyToClipboard = { state.eventSink(DeveloperSettingsEvent.CopyToClipboard(it)) },
|
||||||
|
)
|
||||||
NotificationCategory(
|
NotificationCategory(
|
||||||
onPushRulesClick = { state.eventSink(DeveloperSettingsEvent.OpenPushRules) },
|
onPushRulesClick = { state.eventSink(DeveloperSettingsEvent.OpenPushRules) },
|
||||||
onPushHistoryClick = onPushHistoryClick,
|
onPushHistoryClick = onPushHistoryClick,
|
||||||
@@ -182,19 +183,15 @@ fun DeveloperSettingsView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SessionCategory(deviceId: DeviceId) {
|
private fun SessionCategory(
|
||||||
|
deviceId: DeviceId,
|
||||||
|
onCopyToClipboard: (String) -> Unit,
|
||||||
|
) {
|
||||||
PreferenceCategory(title = "Session") {
|
PreferenceCategory(title = "Session") {
|
||||||
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
|
|
||||||
val context = LocalContext.current
|
|
||||||
ListItem(
|
ListItem(
|
||||||
content = { Text("DeviceId") },
|
content = { Text("DeviceId") },
|
||||||
supportingContent = { Text(text = deviceId.value) },
|
supportingContent = { Text(text = deviceId.value) },
|
||||||
onClick = {
|
onClick = { onCopyToClipboard(deviceId.value) }
|
||||||
context.copyToClipboard(
|
|
||||||
text = deviceId.value,
|
|
||||||
toastMessage = toastMessage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -17,4 +17,5 @@ sealed interface AppDeveloperSettingsEvent {
|
|||||||
data class SetCustomElementCallBaseUrl(val baseUrl: String?) : AppDeveloperSettingsEvent
|
data class SetCustomElementCallBaseUrl(val baseUrl: String?) : AppDeveloperSettingsEvent
|
||||||
data class SetTracingLogLevel(val logLevel: LogLevelItem) : AppDeveloperSettingsEvent
|
data class SetTracingLogLevel(val logLevel: LogLevelItem) : AppDeveloperSettingsEvent
|
||||||
data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : 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.developer.tracing.toLogLevelItem
|
||||||
import io.element.android.features.preferences.impl.model.EnabledFeature
|
import io.element.android.features.preferences.impl.model.EnabledFeature
|
||||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
|
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.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
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.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
|
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
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.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -44,6 +47,8 @@ class AppDeveloperSettingsPresenter(
|
|||||||
private val rageshakePresenter: Presenter<RageshakePreferencesState>,
|
private val rageshakePresenter: Presenter<RageshakePreferencesState>,
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val toastHelper: ToastHelper,
|
||||||
) : Presenter<AppDeveloperSettingsState> {
|
) : Presenter<AppDeveloperSettingsState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): AppDeveloperSettingsState {
|
override fun present(): AppDeveloperSettingsState {
|
||||||
@@ -106,6 +111,11 @@ class AppDeveloperSettingsPresenter(
|
|||||||
}
|
}
|
||||||
appPreferencesStore.setTracingLogPacks(currentPacks)
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
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.R
|
||||||
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
|
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
|
||||||
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
|
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.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown
|
||||||
@@ -108,6 +106,7 @@ fun AppDeveloperSettingsView(
|
|||||||
GitCategory(
|
GitCategory(
|
||||||
gitBranch = state.gitBranch,
|
gitBranch = state.gitBranch,
|
||||||
gitSha = state.gitSha,
|
gitSha = state.gitSha,
|
||||||
|
onCopyToClipboard = { state.eventSink(AppDeveloperSettingsEvent.CopyToClipboard(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,29 +115,18 @@ fun AppDeveloperSettingsView(
|
|||||||
private fun GitCategory(
|
private fun GitCategory(
|
||||||
gitBranch: String,
|
gitBranch: String,
|
||||||
gitSha: String,
|
gitSha: String,
|
||||||
|
onCopyToClipboard: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
PreferenceCategory(title = "Git") {
|
PreferenceCategory(title = "Git") {
|
||||||
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
|
|
||||||
val context = LocalContext.current
|
|
||||||
ListItem(
|
ListItem(
|
||||||
content = { Text("Git branch") },
|
content = { Text("Git branch") },
|
||||||
supportingContent = { Text(text = gitBranch) },
|
supportingContent = { Text(text = gitBranch) },
|
||||||
onClick = {
|
onClick = { onCopyToClipboard(gitBranch) }
|
||||||
context.copyToClipboard(
|
|
||||||
text = gitBranch,
|
|
||||||
toastMessage = toastMessage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
content = { Text("Git SHA") },
|
content = { Text("Git SHA") },
|
||||||
supportingContent = { Text(text = gitSha) },
|
supportingContent = { Text(text = gitSha) },
|
||||||
onClick = {
|
onClick = { onCopyToClipboard(gitSha) }
|
||||||
context.copyToClipboard(
|
|
||||||
text = gitSha,
|
|
||||||
toastMessage = toastMessage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+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.FakeComputeCacheSizeUseCase
|
||||||
import io.element.android.features.preferences.impl.tasks.FakeMarkAllRoomsAsRead
|
import io.element.android.features.preferences.impl.tasks.FakeMarkAllRoomsAsRead
|
||||||
import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase
|
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.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.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.data.megaBytes
|
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.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
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.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
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(
|
private fun createDeveloperSettingsPresenter(
|
||||||
navigator: DeveloperSettingsNavigator = DeveloperSettingsNavigator { _, _ -> lambdaError() },
|
navigator: DeveloperSettingsNavigator = DeveloperSettingsNavigator { _, _ -> lambdaError() },
|
||||||
sessionId: SessionId = A_SESSION_ID,
|
sessionId: SessionId = A_SESSION_ID,
|
||||||
@@ -262,6 +299,8 @@ class DeveloperSettingsPresenterTest {
|
|||||||
markAllRoomsAsRead: FakeMarkAllRoomsAsRead = FakeMarkAllRoomsAsRead(),
|
markAllRoomsAsRead: FakeMarkAllRoomsAsRead = FakeMarkAllRoomsAsRead(),
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
||||||
|
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||||
|
toastHelper: ToastHelper = FakeToastHelper(),
|
||||||
): DeveloperSettingsPresenter {
|
): DeveloperSettingsPresenter {
|
||||||
return DeveloperSettingsPresenter(
|
return DeveloperSettingsPresenter(
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
@@ -277,6 +316,8 @@ class DeveloperSettingsPresenterTest {
|
|||||||
markAllRoomsAsRead = markAllRoomsAsRead,
|
markAllRoomsAsRead = markAllRoomsAsRead,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
notificationSettingsService = notificationSettingsService,
|
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 com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
|
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
|
||||||
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
|
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.architecture.AsyncData
|
||||||
import io.element.android.libraries.featureflag.api.Feature
|
import io.element.android.libraries.featureflag.api.Feature
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeature
|
import io.element.android.libraries.featureflag.test.FakeFeature
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
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.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
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(
|
private fun createAppDeveloperSettingsPresenter(
|
||||||
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(
|
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(
|
||||||
getAvailableFeaturesResult = { _, _ ->
|
getAvailableFeaturesResult = { _, _ ->
|
||||||
@@ -164,6 +201,8 @@ class AppDeveloperSettingsPresenterTest {
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
|
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||||
|
toastHelper: ToastHelper = FakeToastHelper(),
|
||||||
): AppDeveloperSettingsPresenter {
|
): AppDeveloperSettingsPresenter {
|
||||||
return AppDeveloperSettingsPresenter(
|
return AppDeveloperSettingsPresenter(
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
@@ -172,7 +211,9 @@ class AppDeveloperSettingsPresenterTest {
|
|||||||
buildMeta = aBuildMeta(
|
buildMeta = aBuildMeta(
|
||||||
gitRevision = "1234567890",
|
gitRevision = "1234567890",
|
||||||
gitBranchName = "feature/awesome-feature"
|
gitBranchName = "feature/awesome-feature"
|
||||||
)
|
),
|
||||||
|
clipboardHelper = clipboardHelper,
|
||||||
|
toastHelper = toastHelper,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -13,6 +13,7 @@ sealed interface RoomDetailsEvent {
|
|||||||
data object MuteNotification : RoomDetailsEvent
|
data object MuteNotification : RoomDetailsEvent
|
||||||
data object UnmuteNotification : RoomDetailsEvent
|
data object UnmuteNotification : RoomDetailsEvent
|
||||||
data class CopyToClipboard(val text: String) : RoomDetailsEvent
|
data class CopyToClipboard(val text: String) : RoomDetailsEvent
|
||||||
|
data object CopyRoomId : RoomDetailsEvent
|
||||||
data class SetFavorite(val isFavorite: Boolean) : RoomDetailsEvent
|
data class SetFavorite(val isFavorite: Boolean) : RoomDetailsEvent
|
||||||
data object MarkAsRead : RoomDetailsEvent
|
data object MarkAsRead : RoomDetailsEvent
|
||||||
data object MarkAsUnread : 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.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.clipboard.ClipboardHelper
|
||||||
|
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
|
||||||
@@ -74,6 +75,7 @@ class RoomDetailsPresenter(
|
|||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val analyticsService: AnalyticsService,
|
private val analyticsService: AnalyticsService,
|
||||||
private val clipboardHelper: ClipboardHelper,
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val toastHelper: ToastHelper,
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
private val notificationCleaner: NotificationCleaner,
|
private val notificationCleaner: NotificationCleaner,
|
||||||
@@ -167,8 +169,14 @@ class RoomDetailsPresenter(
|
|||||||
}
|
}
|
||||||
is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite)
|
is RoomDetailsEvent.SetFavorite -> scope.setFavorite(event.isFavorite)
|
||||||
is RoomDetailsEvent.CopyToClipboard -> {
|
is RoomDetailsEvent.CopyToClipboard -> {
|
||||||
clipboardHelper.copyPlainText(event.text)
|
clipboardHelper.copyPlainText(event.text) {
|
||||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
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.MarkAsRead -> scope.markAsRead()
|
||||||
is RoomDetailsEvent.MarkAsUnread -> scope.markAsUnread()
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.api.UserProfileVerificationState
|
||||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
|
import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs
|
||||||
import io.element.android.features.userprofile.shared.blockuser.BlockUserSection
|
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.architecture.coverage.ExcludeFromCoverage
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.MatrixBadgeAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.MatrixBadgeRowMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.MatrixBadgeRowMolecule
|
||||||
@@ -339,6 +337,7 @@ fun RoomDetailsView(
|
|||||||
DebugInfoSection(
|
DebugInfoSection(
|
||||||
roomId = state.roomId,
|
roomId = state.roomId,
|
||||||
roomVersion = state.roomVersion,
|
roomVersion = state.roomVersion,
|
||||||
|
onCopyRoomIdClick = { state.eventSink(RoomDetailsEvent.CopyRoomId) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -865,10 +864,9 @@ private fun OtherActionsSection(
|
|||||||
private fun DebugInfoSection(
|
private fun DebugInfoSection(
|
||||||
roomId: RoomId,
|
roomId: RoomId,
|
||||||
roomVersion: String?,
|
roomVersion: String?,
|
||||||
|
onCopyRoomIdClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
PreferenceCategory {
|
PreferenceCategory {
|
||||||
val toastMessage = stringResource(CommonStrings.common_copied_to_clipboard)
|
|
||||||
ListItem(
|
ListItem(
|
||||||
content = {
|
content = {
|
||||||
Text("Internal room ID")
|
Text("Internal room ID")
|
||||||
@@ -882,12 +880,7 @@ private fun DebugInfoSection(
|
|||||||
},
|
},
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Code())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Code())),
|
||||||
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Copy())),
|
trailingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Copy())),
|
||||||
onClick = {
|
onClick = onCopyRoomIdClick,
|
||||||
context.copyToClipboard(
|
|
||||||
text = roomId.value,
|
|
||||||
toastMessage = toastMessage,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
ListItem(
|
ListItem(
|
||||||
content = {
|
content = {
|
||||||
|
|||||||
+3
-2
@@ -118,8 +118,9 @@ class RoomMemberDetailsPresenter(
|
|||||||
encryptionService.withdrawVerification(roomMemberId)
|
encryptionService.withdrawVerification(roomMemberId)
|
||||||
}
|
}
|
||||||
is UserProfileEvent.CopyToClipboard -> {
|
is UserProfileEvent.CopyToClipboard -> {
|
||||||
clipboardHelper.copyPlainText(event.text)
|
clipboardHelper.copyPlainText(event.text) {
|
||||||
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> userProfileState.eventSink(event)
|
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.features.userprofile.shared.aUserProfileState
|
||||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
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.preferences.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
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.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
@@ -85,6 +88,7 @@ class RoomDetailsPresenterTest {
|
|||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
encryptionService: FakeEncryptionService = FakeEncryptionService(),
|
||||||
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||||
|
toastHelper: ToastHelper = FakeToastHelper(),
|
||||||
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
|
||||||
navigator: RoomDetailsNavigator = FakeRoomDetailsNavigator(),
|
navigator: RoomDetailsNavigator = FakeRoomDetailsNavigator(),
|
||||||
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
|
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
|
||||||
@@ -115,12 +119,53 @@ class RoomDetailsPresenterTest {
|
|||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
clipboardHelper = clipboardHelper,
|
clipboardHelper = clipboardHelper,
|
||||||
|
toastHelper = toastHelper,
|
||||||
appPreferencesStore = appPreferencesStore,
|
appPreferencesStore = appPreferencesStore,
|
||||||
notificationCleaner = notificationCleaner,
|
notificationCleaner = notificationCleaner,
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
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
|
@Test
|
||||||
fun `present - initial state is created from initial room info`() = runTest {
|
fun `present - initial state is created from initial room info`() = runTest {
|
||||||
val room = aJoinedRoom(
|
val room = aJoinedRoom(
|
||||||
|
|||||||
+1
@@ -11,6 +11,7 @@ package io.element.android.features.securebackup.impl.setup
|
|||||||
sealed interface SecureBackupSetupEvent {
|
sealed interface SecureBackupSetupEvent {
|
||||||
data object CreateRecoveryKey : SecureBackupSetupEvent
|
data object CreateRecoveryKey : SecureBackupSetupEvent
|
||||||
data object RecoveryKeyHasBeenSaved : SecureBackupSetupEvent
|
data object RecoveryKeyHasBeenSaved : SecureBackupSetupEvent
|
||||||
|
data object CopyRecoveryKey : SecureBackupSetupEvent
|
||||||
data object Done : SecureBackupSetupEvent
|
data object Done : SecureBackupSetupEvent
|
||||||
data object DismissDialog : 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.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.AssistedInject
|
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.loggerTagSetup
|
||||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory
|
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory
|
||||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
|
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.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
@@ -38,6 +41,8 @@ class SecureBackupSetupPresenter(
|
|||||||
@Assisted private val isChangeRecoveryKeyUserStory: Boolean,
|
@Assisted private val isChangeRecoveryKeyUserStory: Boolean,
|
||||||
private val stateMachine: SecureBackupSetupStateMachine,
|
private val stateMachine: SecureBackupSetupStateMachine,
|
||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val toastHelper: ToastHelper,
|
||||||
) : Presenter<SecureBackupSetupState> {
|
) : Presenter<SecureBackupSetupState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
@@ -60,6 +65,13 @@ class SecureBackupSetupPresenter(
|
|||||||
}
|
}
|
||||||
SecureBackupSetupEvent.RecoveryKeyHasBeenSaved ->
|
SecureBackupSetupEvent.RecoveryKeyHasBeenSaved ->
|
||||||
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.UserSavedKey)
|
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 -> {
|
SecureBackupSetupEvent.DismissDialog -> {
|
||||||
showSaveConfirmationDialog = false
|
showSaveConfirmationDialog = false
|
||||||
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.ClearError)
|
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.ClearError)
|
||||||
|
|||||||
+1
-9
@@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.securebackup.impl.R
|
import io.element.android.features.securebackup.impl.R
|
||||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView
|
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.androidutils.system.startSharePlainTextIntent
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
@@ -114,17 +113,10 @@ private fun subtitle(state: SecureBackupSetupState): String {
|
|||||||
private fun Content(
|
private fun Content(
|
||||||
state: SecureBackupSetupState,
|
state: SecureBackupSetupState,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val formattedRecoveryKey = state.recoveryKeyViewState.formattedRecoveryKey
|
val formattedRecoveryKey = state.recoveryKeyViewState.formattedRecoveryKey
|
||||||
val toastMessage = stringResource(R.string.screen_recovery_key_copied_to_clipboard)
|
|
||||||
val clickLambda = if (formattedRecoveryKey != null) {
|
val clickLambda = if (formattedRecoveryKey != null) {
|
||||||
{
|
{
|
||||||
context.copyToClipboard(
|
state.eventSink.invoke(SecureBackupSetupEvent.CopyRecoveryKey)
|
||||||
text = formattedRecoveryKey,
|
|
||||||
toastMessage = toastMessage,
|
|
||||||
isSensitive = true,
|
|
||||||
)
|
|
||||||
state.eventSink.invoke(SecureBackupSetupEvent.RecoveryKeyHasBeenSaved)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!state.recoveryKeyViewState.inProgress) {
|
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.SecureBackupSetupNode
|
||||||
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupPresenter
|
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupPresenter
|
||||||
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupStateMachine
|
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.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.tests.testutils.node.TestParentNode
|
import io.element.android.tests.testutils.node.TestParentNode
|
||||||
@@ -40,6 +42,8 @@ class DefaultSecureBackupSetupEntryPointTest {
|
|||||||
isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory,
|
isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory,
|
||||||
stateMachine = SecureBackupSetupStateMachine(),
|
stateMachine = SecureBackupSetupStateMachine(),
|
||||||
encryptionService = FakeEncryptionService(),
|
encryptionService = FakeEncryptionService(),
|
||||||
|
clipboardHelper = FakeClipboardHelper(),
|
||||||
|
toastHelper = FakeToastHelper(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
|
|||||||
+59
@@ -12,8 +12,13 @@ import app.cash.molecule.RecompositionMode
|
|||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.RecoveryKeyUserStory
|
||||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
|
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.EnableRecoveryProgress
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.test.A_RECOVERY_KEY
|
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(
|
private fun createSecureBackupSetupPresenter(
|
||||||
isChangeRecoveryKeyUserStory: Boolean = false,
|
isChangeRecoveryKeyUserStory: Boolean = false,
|
||||||
encryptionService: EncryptionService = FakeEncryptionService(
|
encryptionService: EncryptionService = FakeEncryptionService(
|
||||||
enableRecoveryLambda = { _, _ -> Result.success("") },
|
enableRecoveryLambda = { _, _ -> Result.success("") },
|
||||||
),
|
),
|
||||||
|
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||||
|
toastHelper: ToastHelper = FakeToastHelper(),
|
||||||
): SecureBackupSetupPresenter {
|
): SecureBackupSetupPresenter {
|
||||||
return SecureBackupSetupPresenter(
|
return SecureBackupSetupPresenter(
|
||||||
isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory,
|
isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory,
|
||||||
stateMachine = SecureBackupSetupStateMachine(),
|
stateMachine = SecureBackupSetupStateMachine(),
|
||||||
encryptionService = encryptionService,
|
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.api.TextFileViewer
|
||||||
import io.element.android.features.viewfolder.impl.file.ColorationMode
|
import io.element.android.features.viewfolder.impl.file.ColorationMode
|
||||||
import io.element.android.features.viewfolder.impl.file.FileContent
|
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
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultTextFileViewer : TextFileViewer {
|
class DefaultTextFileViewer(
|
||||||
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val toastHelper: ToastHelper,
|
||||||
|
) : TextFileViewer {
|
||||||
@Composable
|
@Composable
|
||||||
override fun Render(
|
override fun Render(
|
||||||
lines: ImmutableList<String>,
|
lines: ImmutableList<String>,
|
||||||
@@ -27,6 +33,11 @@ class DefaultTextFileViewer : TextFileViewer {
|
|||||||
FileContent(
|
FileContent(
|
||||||
lines = lines,
|
lines = lines,
|
||||||
colorationMode = ColorationMode.None,
|
colorationMode = ColorationMode.None,
|
||||||
|
onLineClick = { line ->
|
||||||
|
clipboardHelper.copyPlainText(line) {
|
||||||
|
toastHelper.show(CommonStrings.common_line_copied_to_clipboard)
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-10
@@ -23,12 +23,10 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
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.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -37,6 +35,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||||||
internal fun FileContent(
|
internal fun FileContent(
|
||||||
lines: ImmutableList<String>,
|
lines: ImmutableList<String>,
|
||||||
colorationMode: ColorationMode,
|
colorationMode: ColorationMode,
|
||||||
|
onLineClick: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -60,6 +59,7 @@ internal fun FileContent(
|
|||||||
lineNumber = index + 1,
|
lineNumber = index + 1,
|
||||||
line = line,
|
line = line,
|
||||||
colorationMode = colorationMode,
|
colorationMode = colorationMode,
|
||||||
|
onClick = { onLineClick(line) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,18 +71,12 @@ private fun LineRow(
|
|||||||
lineNumber: Int,
|
lineNumber: Int,
|
||||||
line: String,
|
line: String,
|
||||||
colorationMode: ColorationMode,
|
colorationMode: ColorationMode,
|
||||||
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
val toastMessage = stringResource(CommonStrings.common_line_copied_to_clipboard)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = {
|
.clickable(onClick = onClick)
|
||||||
context.copyToClipboard(
|
|
||||||
text = line,
|
|
||||||
toastMessage = toastMessage,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
+1
@@ -11,4 +11,5 @@ package io.element.android.features.viewfolder.impl.file
|
|||||||
sealed interface ViewFileEvent {
|
sealed interface ViewFileEvent {
|
||||||
data object SaveOnDisk : ViewFileEvent
|
data object SaveOnDisk : ViewFileEvent
|
||||||
data object Share : 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.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.AssistedInject
|
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.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -30,6 +33,8 @@ class ViewFilePresenter(
|
|||||||
private val fileContentReader: FileContentReader,
|
private val fileContentReader: FileContentReader,
|
||||||
private val fileShare: FileShare,
|
private val fileShare: FileShare,
|
||||||
private val fileSave: FileSave,
|
private val fileSave: FileSave,
|
||||||
|
private val clipboardHelper: ClipboardHelper,
|
||||||
|
private val toastHelper: ToastHelper,
|
||||||
) : Presenter<ViewFileState> {
|
) : Presenter<ViewFileState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
@@ -48,6 +53,9 @@ class ViewFilePresenter(
|
|||||||
when (event) {
|
when (event) {
|
||||||
ViewFileEvent.Share -> coroutineScope.share(path)
|
ViewFileEvent.Share -> coroutineScope.share(path)
|
||||||
ViewFileEvent.SaveOnDisk -> coroutineScope.save(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),
|
modifier = Modifier.weight(1f),
|
||||||
lines = state.lines.data.toImmutableList(),
|
lines = state.lines.data.toImmutableList(),
|
||||||
colorationMode = state.colorationMode,
|
colorationMode = state.colorationMode,
|
||||||
|
onLineClick = { state.eventSink(ViewFileEvent.CopyToClipboard(it)) },
|
||||||
)
|
)
|
||||||
is AsyncData.Failure -> AsyncFailure(throwable = state.lines.error, onRetry = null)
|
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.FileShare
|
||||||
import io.element.android.features.viewfolder.impl.file.ViewFileEvent
|
import io.element.android.features.viewfolder.impl.file.ViewFileEvent
|
||||||
import io.element.android.features.viewfolder.impl.file.ViewFilePresenter
|
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.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
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 io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
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(
|
private fun createPresenter(
|
||||||
path: String = "aPath",
|
path: String = "aPath",
|
||||||
name: String = "aName",
|
name: String = "aName",
|
||||||
fileContentReader: FileContentReader = FakeFileContentReader(),
|
fileContentReader: FileContentReader = FakeFileContentReader(),
|
||||||
fileShare: FileShare = FakeFileShare(),
|
fileShare: FileShare = FakeFileShare(),
|
||||||
fileSave: FileSave = FakeFileSave(),
|
fileSave: FileSave = FakeFileSave(),
|
||||||
|
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||||
|
toastHelper: ToastHelper = FakeToastHelper(),
|
||||||
) = ViewFilePresenter(
|
) = ViewFilePresenter(
|
||||||
path = path,
|
path = path,
|
||||||
name = name,
|
name = name,
|
||||||
fileContentReader = fileContentReader,
|
fileContentReader = fileContentReader,
|
||||||
fileShare = fileShare,
|
fileShare = fileShare,
|
||||||
fileSave = fileSave,
|
fileSave = fileSave,
|
||||||
|
clipboardHelper = clipboardHelper,
|
||||||
|
toastHelper = toastHelper,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-2
@@ -9,8 +9,11 @@
|
|||||||
package io.element.android.libraries.androidutils.clipboard
|
package io.element.android.libraries.androidutils.clipboard
|
||||||
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
|
import android.content.ClipDescription
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.PersistableBundle
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
@@ -24,7 +27,30 @@ class AndroidClipboardHelper(
|
|||||||
) : ClipboardHelper {
|
) : ClipboardHelper {
|
||||||
private val clipboardManager = requireNotNull(context.getSystemService<ClipboardManager>())
|
private val clipboardManager = requireNotNull(context.getSystemService<ClipboardManager>())
|
||||||
|
|
||||||
override fun copyPlainText(text: String) {
|
override fun copyPlainText(
|
||||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("", text))
|
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
|
package io.element.android.libraries.androidutils.clipboard
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper class for handling clipboard operations so it can be used in JVM environments.
|
|
||||||
*/
|
|
||||||
interface ClipboardHelper {
|
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
|
package io.element.android.libraries.androidutils.clipboard
|
||||||
|
|
||||||
class FakeClipboardHelper : ClipboardHelper {
|
class FakeClipboardHelper(
|
||||||
|
private val isOldDevice: Boolean = true,
|
||||||
|
) : ClipboardHelper {
|
||||||
var clipboardContents: Any? = null
|
var clipboardContents: Any? = null
|
||||||
|
var isSensitive: Boolean? = null
|
||||||
|
|
||||||
override fun copyPlainText(text: String) {
|
override fun copyPlainText(
|
||||||
|
text: String,
|
||||||
|
isSensitive: Boolean,
|
||||||
|
onSuccessOnOldDevice: () -> Unit,
|
||||||
|
) {
|
||||||
clipboardContents = text
|
clipboardContents = text
|
||||||
|
this.isSensitive = isSensitive
|
||||||
|
if (isOldDevice) {
|
||||||
|
onSuccessOnOldDevice()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-43
@@ -1,43 +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.ClipDescription
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.PersistableBundle
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
|
|
||||||
class CopyToClipboardUseCase(
|
|
||||||
private val context: Context,
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* @param isSensitive if true, the clip is flagged so the system clipboard preview (Android 13+) and keyboards hide its content.
|
|
||||||
*/
|
|
||||||
fun execute(text: CharSequence, isSensitive: Boolean = false) {
|
|
||||||
val clipData = ClipData.newPlainText("", text)
|
|
||||||
if (isSensitive) {
|
|
||||||
clipData.description.extras = PersistableBundle().apply {
|
|
||||||
putBoolean(EXTRA_IS_SENSITIVE, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.getSystemService<ClipboardManager>()?.setPrimaryClip(clipData)
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-20
@@ -50,26 +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,
|
|
||||||
isSensitive: Boolean = false,
|
|
||||||
) {
|
|
||||||
CopyToClipboardUseCase(this).execute(text, isSensitive)
|
|
||||||
toastMessage?.let { toast(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows notification settings for the current app.
|
* 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
|
* 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