[Message Actions] Copy events to clipboard (#665)
* Add `Copy` action for text events * Remove 'Copy' action from the list for non-text events * Use `@ContributesBinding` to inject `AndroidClipboardHelper`.
This commit is contained in:
parent
1c1e65c70d
commit
95f65e2031
17 changed files with 193 additions and 22 deletions
|
|
@ -198,7 +198,6 @@ dependencies {
|
||||||
allLibrariesImpl()
|
allLibrariesImpl()
|
||||||
allServicesImpl()
|
allServicesImpl()
|
||||||
allFeaturesImpl(rootDir, logger)
|
allFeaturesImpl(rootDir, logger)
|
||||||
implementation(projects.libraries.deeplink)
|
|
||||||
implementation(projects.tests.uitests)
|
implementation(projects.tests.uitests)
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
implementation(projects.appnav)
|
implementation(projects.appnav)
|
||||||
|
|
|
||||||
1
changelog.d/663.feature
Normal file
1
changelog.d/663.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Add 'Copy' action to timeline item context menu, for text events
|
||||||
|
|
@ -38,4 +38,4 @@
|
||||||
<string name="screen_login_password_hint">"Password"</string>
|
<string name="screen_login_password_hint">"Password"</string>
|
||||||
<string name="screen_login_submit">"Continue"</string>
|
<string name="screen_login_submit">"Continue"</string>
|
||||||
<string name="screen_login_username_hint">"Username"</string>
|
<string name="screen_login_username_hint">"Username"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -50,11 +51,13 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
||||||
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
|
import io.element.android.features.messages.impl.utils.messagesummary.MessageSummaryFormatter
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
|
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||||
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.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||||
import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
|
import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
|
||||||
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.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
|
@ -66,7 +69,6 @@ import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class MessagesPresenter @AssistedInject constructor(
|
class MessagesPresenter @AssistedInject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
|
|
@ -79,6 +81,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
private val snackbarDispatcher: SnackbarDispatcher,
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val messageSummaryFormatter: MessageSummaryFormatter,
|
private val messageSummaryFormatter: MessageSummaryFormatter,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
private val clipboardHelper: ClipboardHelper,
|
||||||
@Assisted private val navigator: MessagesNavigator,
|
@Assisted private val navigator: MessagesNavigator,
|
||||||
) : Presenter<MessagesState> {
|
) : Presenter<MessagesState> {
|
||||||
|
|
||||||
|
|
@ -155,7 +158,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
composerState: MessageComposerState,
|
composerState: MessageComposerState,
|
||||||
) = launch {
|
) = launch {
|
||||||
when (action) {
|
when (action) {
|
||||||
TimelineItemAction.Copy -> notImplementedYet()
|
TimelineItemAction.Copy -> handleCopyContents(targetEvent)
|
||||||
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
||||||
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState)
|
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState)
|
||||||
TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState)
|
TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState)
|
||||||
|
|
@ -246,4 +249,18 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
if (event.eventId == null) return
|
if (event.eventId == null) return
|
||||||
navigator.onReportContentClicked(event.eventId, event.senderId)
|
navigator.onReportContentClicked(event.eventId, event.senderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun handleCopyContents(event: TimelineItem.Event) {
|
||||||
|
val content = when (event.content) {
|
||||||
|
is TimelineItemTextBasedContent -> event.content.body
|
||||||
|
is TimelineItemStateContent -> event.content.body
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboardHelper.copyPlainText(content)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
snackbarDispatcher.post(SnackbarMessage(R.string.screen_room_message_copied))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,6 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
|
@ -69,18 +67,15 @@ import io.element.android.libraries.androidutils.ui.hideKeyboard
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.designsystem.utils.LogCompositions
|
import io.element.android.libraries.designsystem.utils.LogCompositions
|
||||||
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -255,12 +250,7 @@ fun MessagesViewTopBar(
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBackPressed) {
|
BackButton(onClick = onBackPressed)
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.ArrowBack,
|
|
||||||
contentDescription = "Back"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
Row(
|
Row(
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.canBeCopied
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
@ -64,7 +65,9 @@ class ActionListPresenter @Inject constructor(
|
||||||
is TimelineItemRedactedContent,
|
is TimelineItemRedactedContent,
|
||||||
is TimelineItemStateContent -> {
|
is TimelineItemStateContent -> {
|
||||||
buildList {
|
buildList {
|
||||||
add(TimelineItemAction.Copy)
|
if (timelineItem.content.canBeCopied()) {
|
||||||
|
add(TimelineItemAction.Copy)
|
||||||
|
}
|
||||||
if (buildMeta.isDebuggable) {
|
if (buildMeta.isDebuggable) {
|
||||||
add(TimelineItemAction.Developer)
|
add(TimelineItemAction.Developer)
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +79,9 @@ class ActionListPresenter @Inject constructor(
|
||||||
if (timelineItem.isMine) {
|
if (timelineItem.isMine) {
|
||||||
add(TimelineItemAction.Edit)
|
add(TimelineItemAction.Edit)
|
||||||
}
|
}
|
||||||
add(TimelineItemAction.Copy)
|
if (timelineItem.content.canBeCopied()) {
|
||||||
|
add(TimelineItemAction.Copy)
|
||||||
|
}
|
||||||
if (buildMeta.isDebuggable) {
|
if (buildMeta.isDebuggable) {
|
||||||
add(TimelineItemAction.Developer)
|
add(TimelineItemAction.Developer)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,14 @@ import androidx.compose.runtime.Immutable
|
||||||
sealed interface TimelineItemEventContent {
|
sealed interface TimelineItemEventContent {
|
||||||
val type: String
|
val type: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only text based content and states can be copied.
|
||||||
|
*/
|
||||||
|
fun TimelineItemEventContent.canBeCopied(): Boolean =
|
||||||
|
when (this) {
|
||||||
|
is TimelineItemTextBasedContent,
|
||||||
|
is TimelineItemStateContent,
|
||||||
|
is TimelineItemRedactedContent -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
||||||
<string name="screen_room_invite_again_alert_message">"Would you like to invite them back?"</string>
|
<string name="screen_room_invite_again_alert_message">"Would you like to invite them back?"</string>
|
||||||
<string name="screen_room_invite_again_alert_title">"You are alone in this chat"</string>
|
<string name="screen_room_invite_again_alert_title">"You are alone in this chat"</string>
|
||||||
|
<string name="screen_room_message_copied">"Message copied"</string>
|
||||||
<string name="screen_room_no_permission_to_post">"You do not have permission to post to this room"</string>
|
<string name="screen_room_no_permission_to_post">"You do not have permission to post to this room"</string>
|
||||||
<string name="screen_room_retry_send_menu_send_again_action">"Send again"</string>
|
<string name="screen_room_retry_send_menu_send_again_action">"Send again"</string>
|
||||||
<string name="screen_room_retry_send_menu_title">"Your message failed to send"</string>
|
<string name="screen_room_retry_send_menu_title">"Your message failed to send"</string>
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,12 @@ import io.element.android.features.messages.impl.timeline.components.customreact
|
||||||
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
import io.element.android.features.messages.media.FakeLocalMediaFactory
|
||||||
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter
|
import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
|
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
|
|
@ -115,14 +117,17 @@ class MessagesPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - handle action copy`() = runTest {
|
fun `present - handle action copy`() = runTest {
|
||||||
val presenter = createMessagePresenter()
|
val clipboardHelper = FakeClipboardHelper()
|
||||||
|
val event = aMessageEvent()
|
||||||
|
val presenter = createMessagePresenter(clipboardHelper = clipboardHelper)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, aMessageEvent()))
|
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, event))
|
||||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||||
|
assertThat(clipboardHelper.clipboardContents).isEqualTo((event.content as TimelineItemTextContent).body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -355,6 +360,7 @@ class MessagesPresenterTest {
|
||||||
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
matrixRoom: MatrixRoom = FakeMatrixRoom(),
|
matrixRoom: MatrixRoom = FakeMatrixRoom(),
|
||||||
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
navigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||||
|
clipboardHelper: FakeClipboardHelper = FakeClipboardHelper(),
|
||||||
): MessagesPresenter {
|
): MessagesPresenter {
|
||||||
val messageComposerPresenter = MessageComposerPresenter(
|
val messageComposerPresenter = MessageComposerPresenter(
|
||||||
appCoroutineScope = this,
|
appCoroutineScope = this,
|
||||||
|
|
@ -396,6 +402,7 @@ class MessagesPresenterTest {
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
messageSummaryFormatter = FakeMessageSummaryFormatter(),
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
|
clipboardHelper = clipboardHelper,
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,10 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
|
|
@ -164,6 +166,38 @@ class ActionListPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - compute for a media item`() = runTest {
|
||||||
|
val presenter = anActionListPresenter(isBuildDebuggable = true)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val messageEvent = aMessageEvent(
|
||||||
|
isMine = true,
|
||||||
|
content = aTimelineItemImageContent(),
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent))
|
||||||
|
// val loadingState = awaitItem()
|
||||||
|
// assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent))
|
||||||
|
val successState = awaitItem()
|
||||||
|
assertThat(successState.target).isEqualTo(
|
||||||
|
ActionListState.Target.Success(
|
||||||
|
messageEvent,
|
||||||
|
persistentListOf(
|
||||||
|
TimelineItemAction.Reply,
|
||||||
|
TimelineItemAction.Forward,
|
||||||
|
TimelineItemAction.Edit,
|
||||||
|
TimelineItemAction.Developer,
|
||||||
|
TimelineItemAction.Redact,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
initialState.eventSink.invoke(ActionListEvents.Clear)
|
||||||
|
assertThat(awaitItem().target).isEqualTo(ActionListState.Target.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - compute message in non-debuggable build`() = runTest {
|
fun `present - compute message in non-debuggable build`() = runTest {
|
||||||
val presenter = anActionListPresenter(isBuildDebuggable = false)
|
val presenter = anActionListPresenter(isBuildDebuggable = false)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
<string name="screen_onboarding_sign_in_with_qr_code">"Sign in with QR code"</string>
|
<string name="screen_onboarding_sign_in_with_qr_code">"Sign in with QR code"</string>
|
||||||
<string name="screen_onboarding_sign_up">"Create account"</string>
|
<string name="screen_onboarding_sign_up">"Create account"</string>
|
||||||
<string name="screen_onboarding_subtitle">"Communicate and collaborate securely"</string>
|
<string name="screen_onboarding_subtitle">"Communicate and collaborate securely"</string>
|
||||||
|
<string name="screen_onboarding_welcome_message">"Welcome to the fastest Element ever. Supercharged for speed and simplicity."</string>
|
||||||
<string name="screen_onboarding_welcome_subtitle">"Welcome to %1$s. Supercharged, for speed and simplicity."</string>
|
<string name="screen_onboarding_welcome_subtitle">"Welcome to %1$s. Supercharged, for speed and simplicity."</string>
|
||||||
<string name="screen_onboarding_welcome_title">"Be in your Element"</string>
|
<string name="screen_onboarding_welcome_title">"Be in your element"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,28 @@
|
||||||
*/
|
*/
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-library")
|
id("io.element.android-library")
|
||||||
|
alias(libs.plugins.anvil)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.libraries.androidutils"
|
namespace = "io.element.android.libraries.androidutils"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
anvil {
|
||||||
|
generateDaggerFactories.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
anvil(projects.anvilcodegen)
|
||||||
|
implementation(projects.anvilannotations)
|
||||||
|
implementation(projects.libraries.di)
|
||||||
|
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(libs.dagger)
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(libs.androidx.activity.activity)
|
implementation(libs.androidx.activity.activity)
|
||||||
implementation(libs.androidx.exifinterface)
|
implementation(libs.androidx.exifinterface)
|
||||||
implementation(libs.androidx.security.crypto)
|
implementation(libs.androidx.security.crypto)
|
||||||
implementation(libs.androidx.browser)
|
implementation(libs.androidx.browser)
|
||||||
implementation(projects.libraries.core)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.androidutils.clipboard
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
|
class AndroidClipboardHelper @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) : ClipboardHelper {
|
||||||
|
|
||||||
|
private val clipboardManager = requireNotNull(context.getSystemService<ClipboardManager>())
|
||||||
|
|
||||||
|
override fun copyPlainText(text: String) {
|
||||||
|
clipboardManager.setPrimaryClip(ClipData.newPlainText("", text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.androidutils.clipboard
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper class for handling clipboard operations so it can be used in JVM environments.
|
||||||
|
*/
|
||||||
|
interface ClipboardHelper {
|
||||||
|
fun copyPlainText(text: String)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.androidutils.clipboard
|
||||||
|
|
||||||
|
class FakeClipboardHelper : ClipboardHelper {
|
||||||
|
|
||||||
|
var clipboardContents: Any? = null
|
||||||
|
|
||||||
|
override fun copyPlainText(text: String) {
|
||||||
|
clipboardContents = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
<string name="action_invite">"Invite"</string>
|
<string name="action_invite">"Invite"</string>
|
||||||
<string name="action_invite_friends">"Invite friends"</string>
|
<string name="action_invite_friends">"Invite friends"</string>
|
||||||
<string name="action_invite_friends_to_app">"Invite friends to %1$s"</string>
|
<string name="action_invite_friends_to_app">"Invite friends to %1$s"</string>
|
||||||
|
<string name="action_invite_people_to_app">"Invite people to %1$s"</string>
|
||||||
<string name="action_invites_list">"Invites"</string>
|
<string name="action_invites_list">"Invites"</string>
|
||||||
<string name="action_learn_more">"Learn more"</string>
|
<string name="action_learn_more">"Learn more"</string>
|
||||||
<string name="action_leave">"Leave"</string>
|
<string name="action_leave">"Leave"</string>
|
||||||
|
|
@ -108,6 +109,7 @@
|
||||||
<string name="common_server_not_supported">"Server not supported"</string>
|
<string name="common_server_not_supported">"Server not supported"</string>
|
||||||
<string name="common_server_url">"Server URL"</string>
|
<string name="common_server_url">"Server URL"</string>
|
||||||
<string name="common_settings">"Settings"</string>
|
<string name="common_settings">"Settings"</string>
|
||||||
|
<string name="common_shared_location">"Shared location"</string>
|
||||||
<string name="common_starting_chat">"Starting chat…"</string>
|
<string name="common_starting_chat">"Starting chat…"</string>
|
||||||
<string name="common_sticker">"Sticker"</string>
|
<string name="common_sticker">"Sticker"</string>
|
||||||
<string name="common_success">"Success"</string>
|
<string name="common_success">"Success"</string>
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ private fun DependencyHandlerScope.addImplementationProjects(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DependencyHandlerScope.allLibrariesImpl() {
|
fun DependencyHandlerScope.allLibrariesImpl() {
|
||||||
|
implementation(project(":libraries:androidutils"))
|
||||||
|
implementation(project(":libraries:deeplink"))
|
||||||
implementation(project(":libraries:designsystem"))
|
implementation(project(":libraries:designsystem"))
|
||||||
implementation(project(":libraries:matrix:impl"))
|
implementation(project(":libraries:matrix:impl"))
|
||||||
implementation(project(":libraries:matrixui"))
|
implementation(project(":libraries:matrixui"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue