mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +00:00
Merge pull request #7679 from element-hq/feature/bma/messageDeletionConfirmation
Let the user give an optional reason when removing a message
This commit is contained in:
+2
@@ -18,6 +18,8 @@ sealed interface MessagesEvent {
|
||||
data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : MessagesEvent
|
||||
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvent
|
||||
data class OnUserClicked(val user: MatrixUser) : MessagesEvent
|
||||
data class ConfirmRedact(val reason: String?) : MessagesEvent
|
||||
data object CancelRedact : MessagesEvent
|
||||
data object StopLiveLocationShare : MessagesEvent
|
||||
data object ShowLiveLocationShare : MessagesEvent
|
||||
data object MarkAsFullyReadAndExit : MessagesEvent
|
||||
|
||||
+32
-3
@@ -58,6 +58,7 @@ import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
@@ -84,6 +85,7 @@ import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibilit
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||
import io.element.android.libraries.matrix.ui.messages.reply.map
|
||||
import io.element.android.libraries.matrix.ui.model.dmUserStatus
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
@@ -241,6 +243,8 @@ class MessagesPresenter(
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
val redactEventAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
|
||||
fun handleEvent(event: MessagesEvent) {
|
||||
when (event) {
|
||||
is MessagesEvent.HandleAction -> {
|
||||
@@ -251,8 +255,21 @@ class MessagesPresenter(
|
||||
enableTextFormatting = composerState.showTextFormatting,
|
||||
timelineState = timelineState,
|
||||
timelineProtectionState = timelineProtectionState,
|
||||
redactEventAction = redactEventAction,
|
||||
)
|
||||
}
|
||||
is MessagesEvent.ConfirmRedact -> {
|
||||
val confirming = redactEventAction.value as? MessagesState.ConfirmingRedaction
|
||||
redactEventAction.value = AsyncAction.Uninitialized
|
||||
if (confirming != null) {
|
||||
localCoroutineScope.launch {
|
||||
redact(confirming.eventId.toEventOrTransactionId(), event.reason?.takeIf { it.isNotBlank() })
|
||||
}
|
||||
}
|
||||
}
|
||||
MessagesEvent.CancelRedact -> {
|
||||
redactEventAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
is MessagesEvent.ToggleReaction -> {
|
||||
localCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId)
|
||||
}
|
||||
@@ -331,6 +348,7 @@ class MessagesPresenter(
|
||||
hasUnreadThreads = false,
|
||||
),
|
||||
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom && timelineState.timelineMode !is Timeline.Mode.Thread,
|
||||
redactEventAction = redactEventAction.value,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
@@ -369,12 +387,13 @@ class MessagesPresenter(
|
||||
timelineProtectionState: TimelineProtectionState,
|
||||
enableTextFormatting: Boolean,
|
||||
timelineState: TimelineState,
|
||||
redactEventAction: MutableState<AsyncAction<Unit>>,
|
||||
) = launch {
|
||||
when (action) {
|
||||
TimelineItemAction.CopyText -> handleCopyContents(targetEvent)
|
||||
TimelineItemAction.CopyCaption -> handleCopyCaption(targetEvent)
|
||||
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
||||
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
||||
TimelineItemAction.Redact -> handleActionRedact(targetEvent, redactEventAction)
|
||||
TimelineItemAction.Edit,
|
||||
TimelineItemAction.EditPoll -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
||||
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
|
||||
@@ -496,9 +515,19 @@ class MessagesPresenter(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun handleActionRedact(event: TimelineItem.Event) {
|
||||
private suspend fun handleActionRedact(event: TimelineItem.Event, redactEventAction: MutableState<AsyncAction<Unit>>) {
|
||||
val eventId = event.eventId
|
||||
if (eventId == null) {
|
||||
// The message was never sent, so there is nobody to give a reason to.
|
||||
redact(event.eventOrTransactionId, reason = null)
|
||||
} else {
|
||||
redactEventAction.value = MessagesState.ConfirmingRedaction(eventId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun redact(eventOrTransactionId: EventOrTransactionId, reason: String?) {
|
||||
timelineController.invokeOnCurrentTimeline {
|
||||
redactEvent(eventOrTransactionId = event.eventOrTransactionId, reason = null)
|
||||
redactEvent(eventOrTransactionId = eventOrTransactionId, reason = reason)
|
||||
.onFailure { Timber.e(it) }
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -21,9 +21,11 @@ import io.element.android.features.messages.impl.timeline.components.receipt.bot
|
||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
||||
import io.element.android.features.roomcall.api.RoomCallState
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
|
||||
@@ -61,6 +63,7 @@ data class MessagesState(
|
||||
val successorRoom: SuccessorRoom?,
|
||||
val threads: Threads,
|
||||
val showLiveLocationShareBanner: Boolean,
|
||||
val redactEventAction: AsyncAction<Unit>,
|
||||
val eventSink: (MessagesEvent) -> Unit
|
||||
) {
|
||||
val isTombstoned = successorRoom != null
|
||||
@@ -69,6 +72,11 @@ data class MessagesState(
|
||||
val hasThreads: Boolean,
|
||||
val hasUnreadThreads: Boolean,
|
||||
)
|
||||
|
||||
/** The user asked to remove [eventId] and has to confirm, optionally giving a reason. */
|
||||
data class ConfirmingRedaction(
|
||||
val eventId: EventId,
|
||||
) : AsyncAction.Confirming
|
||||
}
|
||||
|
||||
/** Type of "shared history" icon to show in the top bar. */
|
||||
|
||||
+5
@@ -41,10 +41,12 @@ import io.element.android.features.roomcall.api.aStandByCallState
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ROOM_NAME
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
@@ -92,6 +94,7 @@ open class MessagesStatePreviewParam : PreviewParameterProvider<MessagesState> {
|
||||
composerState = aMessageComposerState(textEditorState = aTextEditorStateMarkdown()),
|
||||
identityChangeState = anIdentityChangeState(listOf(aRoomMemberIdentityStateChange()))
|
||||
),
|
||||
aMessagesState(redactEventAction = MessagesState.ConfirmingRedaction(EventId("\$anEventId"))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,6 +134,7 @@ fun aMessagesState(
|
||||
isCurrentlySharingLiveLocationInRoom: Boolean = false,
|
||||
dmUserStatus: DisplayedStatus? = null,
|
||||
eventSink: (MessagesEvent) -> Unit = {},
|
||||
redactEventAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
) = MessagesState(
|
||||
roomId = RoomId("!id:domain"),
|
||||
roomName = roomName,
|
||||
@@ -161,6 +165,7 @@ fun aMessagesState(
|
||||
threads = threads,
|
||||
showLiveLocationShareBanner = isCurrentlySharingLiveLocationInRoom,
|
||||
dmUserStatus = dmUserStatus,
|
||||
redactEventAction = redactEventAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
|
||||
+15
@@ -104,6 +104,7 @@ import io.element.android.libraries.designsystem.atomic.molecules.ComposerAlertM
|
||||
import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayout
|
||||
import io.element.android.libraries.designsystem.components.ExpandableBottomSheetLayoutState
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.TextFieldDialog
|
||||
import io.element.android.libraries.designsystem.components.rememberExpandableBottomSheetLayoutState
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -455,6 +456,20 @@ fun MessagesView(
|
||||
state.timelineState.eventSink(TimelineEvent.HideSendFailureDialog)
|
||||
},
|
||||
)
|
||||
|
||||
if (state.redactEventAction is MessagesState.ConfirmingRedaction) {
|
||||
TextFieldDialog(
|
||||
title = stringResource(R.string.screen_room_confirm_removal_title),
|
||||
content = stringResource(R.string.screen_room_confirm_removal_message),
|
||||
placeholder = stringResource(R.string.screen_room_confirm_removal_reason_placeholder_android),
|
||||
supportingText = stringResource(R.string.screen_room_confirm_removal_reason_supporting_text),
|
||||
value = null,
|
||||
submitText = stringResource(CommonStrings.action_remove),
|
||||
destructiveSubmit = true,
|
||||
onSubmit = { reason -> state.eventSink(MessagesEvent.ConfirmRedact(reason)) },
|
||||
onDismissRequest = { state.eventSink(MessagesEvent.CancelRedact) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
<string name="screen_room_attachment_source_location">"Udostępnij lokalizację"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Ankieta"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"Formatowanie tekstu"</string>
|
||||
<string name="screen_room_confirm_removal_message">"Czy na pewno chcesz usunąć wiadomość?"</string>
|
||||
<string name="screen_room_confirm_removal_reason_label">"Powód (opcjonalne)"</string>
|
||||
<string name="screen_room_confirm_removal_reason_placeholder">"Powód…"</string>
|
||||
<string name="screen_room_confirm_removal_title">"Potwierdź usunięcie"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Historia wiadomości jest obecnie niedostępna."</string>
|
||||
<string name="screen_room_encrypted_history_banner_unverified">"Historia wiadomości jest niedostępna w tym pokoju. Zweryfikuj to urządzenie, aby zobaczyć historię wiadomości."</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Czy chcesz zaprosić ich z powrotem?"</string>
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
<string name="screen_room_attachment_source_location">"Поделиться местоположением"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Опрос"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"Форматирование текста"</string>
|
||||
<string name="screen_room_confirm_removal_message">"Вы уверены, что хотите удалить это сообщение?"</string>
|
||||
<string name="screen_room_confirm_removal_reason_label">"Причина (необязательно)"</string>
|
||||
<string name="screen_room_confirm_removal_reason_placeholder">"Введите…"</string>
|
||||
<string name="screen_room_confirm_removal_title">"Подтвердите удаление"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"В настоящее время история сообщений недоступна в этой комнате."</string>
|
||||
<string name="screen_room_encrypted_history_banner_unverified">"История сообщений в этой комнате недоступна. Проверьте это устройство, чтобы увидеть историю сообщений."</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Хотите пригласить их снова?"</string>
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
<string name="screen_room_attachment_source_location">"共享位置"</string>
|
||||
<string name="screen_room_attachment_source_poll">"投票"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"文本格式化"</string>
|
||||
<string name="screen_room_confirm_removal_message">"确定要移除此消息?"</string>
|
||||
<string name="screen_room_confirm_removal_reason_label">"理由(可选)"</string>
|
||||
<string name="screen_room_confirm_removal_reason_placeholder">"输入…"</string>
|
||||
<string name="screen_room_confirm_removal_title">"确认移除"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"消息历史记录当前不可用。"</string>
|
||||
<string name="screen_room_encrypted_history_banner_unverified">"消息历史在此房间不可用。请验证此设备以查看。"</string>
|
||||
<string name="screen_room_invite_again_alert_message">"你想邀请他们回来吗?"</string>
|
||||
|
||||
@@ -47,6 +47,12 @@
|
||||
<string name="screen_room_attachment_source_location">"Share location"</string>
|
||||
<string name="screen_room_attachment_source_poll">"Poll"</string>
|
||||
<string name="screen_room_attachment_text_formatting">"Text Formatting"</string>
|
||||
<string name="screen_room_confirm_removal_message">"Are you sure you wish to remove this message?"</string>
|
||||
<string name="screen_room_confirm_removal_reason_label">"Reason (optional)"</string>
|
||||
<string name="screen_room_confirm_removal_reason_placeholder">"Enter…"</string>
|
||||
<string name="screen_room_confirm_removal_reason_placeholder_android">"Reason…"</string>
|
||||
<string name="screen_room_confirm_removal_reason_supporting_text">"(Optional)"</string>
|
||||
<string name="screen_room_confirm_removal_title">"Confirm removal"</string>
|
||||
<string name="screen_room_encrypted_history_banner">"Message history is currently unavailable."</string>
|
||||
<string name="screen_room_encrypted_history_banner_unverified">"Message history is unavailable in this room. Verify this device to see your message history."</string>
|
||||
<string name="screen_room_invite_again_alert_message">"Would you like to invite them back?"</string>
|
||||
|
||||
+77
-1
@@ -45,6 +45,7 @@ import io.element.android.features.messages.test.timeline.voicemessages.composer
|
||||
import io.element.android.features.roomcall.api.aStandByCallState
|
||||
import io.element.android.features.roommembermoderation.api.RoomMemberModerationState
|
||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
@@ -78,10 +79,12 @@ import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.element.android.libraries.matrix.test.A_CAPTION
|
||||
import io.element.android.libraries.matrix.test.A_REASON
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_THREAD_ID
|
||||
import io.element.android.libraries.matrix.test.A_TRANSACTION_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.core.FakeSendHandle
|
||||
@@ -556,13 +559,86 @@ class MessagesPresenterTest {
|
||||
val initialState = awaitItem()
|
||||
val messageEvent = aMessageEvent()
|
||||
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, messageEvent))
|
||||
awaitItem()
|
||||
advanceUntilIdle()
|
||||
val confirmingState = expectMostRecentItem()
|
||||
assertThat(confirmingState.redactEventAction).isEqualTo(MessagesState.ConfirmingRedaction(messageEvent.eventId!!))
|
||||
assert(redactEventLambda).isNeverCalled()
|
||||
confirmingState.eventSink(MessagesEvent.ConfirmRedact(A_REASON))
|
||||
advanceUntilIdle()
|
||||
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assert(redactEventLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(messageEvent.eventOrTransactionId), value(A_REASON))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action redact - a blank reason is not sent`() = runTest {
|
||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
val liveTimeline = FakeTimeline()
|
||||
val redactEventLambda = lambdaRecorder { _: EventOrTransactionId, _: String? -> Result.success(Unit) }
|
||||
liveTimeline.redactEventLambda = redactEventLambda
|
||||
val presenter = createMessagesPresenter(
|
||||
timeline = liveTimeline,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
val messageEvent = aMessageEvent()
|
||||
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, messageEvent))
|
||||
advanceUntilIdle()
|
||||
expectMostRecentItem().eventSink(MessagesEvent.ConfirmRedact(" "))
|
||||
advanceUntilIdle()
|
||||
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assert(redactEventLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(messageEvent.eventOrTransactionId), value(null))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action redact - cancelling does not redact`() = runTest {
|
||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
val liveTimeline = FakeTimeline()
|
||||
val redactEventLambda = lambdaRecorder { _: EventOrTransactionId, _: String? -> Result.success(Unit) }
|
||||
liveTimeline.redactEventLambda = redactEventLambda
|
||||
val presenter = createMessagesPresenter(
|
||||
timeline = liveTimeline,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, aMessageEvent()))
|
||||
advanceUntilIdle()
|
||||
expectMostRecentItem().eventSink(MessagesEvent.CancelRedact)
|
||||
advanceUntilIdle()
|
||||
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assert(redactEventLambda).isNeverCalled()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action redact - a message which was never sent is redacted without asking`() = runTest {
|
||||
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
|
||||
val liveTimeline = FakeTimeline()
|
||||
val redactEventLambda = lambdaRecorder { _: EventOrTransactionId, _: String? -> Result.success(Unit) }
|
||||
liveTimeline.redactEventLambda = redactEventLambda
|
||||
val presenter = createMessagesPresenter(
|
||||
timeline = liveTimeline,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
presenter.testWithLifecycleOwner {
|
||||
val initialState = awaitItem()
|
||||
val localEcho = aMessageEvent(eventId = null, transactionId = A_TRANSACTION_ID)
|
||||
initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.Redact, localEcho))
|
||||
advanceUntilIdle()
|
||||
assertThat(expectMostRecentItem().redactEventAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assert(redactEventLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(localEcho.eventOrTransactionId), value(null))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - handle action report content`() = runTest {
|
||||
val onReportContentClickLambda = lambdaRecorder { _: EventId, _: UserId -> }
|
||||
|
||||
+2
@@ -47,6 +47,7 @@ fun TextFieldDialog(
|
||||
maxLines: Int = minLines,
|
||||
content: String? = null,
|
||||
label: String? = null,
|
||||
supportingText: String? = null,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
submitText: String = stringResource(CommonStrings.action_ok),
|
||||
destructiveSubmit: Boolean = false,
|
||||
@@ -84,6 +85,7 @@ fun TextFieldDialog(
|
||||
TextFieldListItem(
|
||||
placeholder = placeholder.orEmpty(),
|
||||
label = label,
|
||||
supportingText = supportingText,
|
||||
text = textFieldContents,
|
||||
onTextChange = {
|
||||
error = if (!validation(it.text)) onValidationErrorMessage else null
|
||||
|
||||
+2
-1
@@ -57,6 +57,7 @@ fun TextFieldListItem(
|
||||
minLines: Int = 1,
|
||||
maxLines: Int = minLines,
|
||||
label: String? = null,
|
||||
supportingText: String? = null,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
@@ -66,7 +67,7 @@ fun TextFieldListItem(
|
||||
placeholder = placeholder,
|
||||
label = label,
|
||||
validity = if (error != null) TextFieldValidity.Invalid else TextFieldValidity.None,
|
||||
supportingText = error,
|
||||
supportingText = error ?: supportingText,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
maxLines = maxLines,
|
||||
|
||||
@@ -564,10 +564,6 @@ Czy na pewno chcesz kontynuować?"</string>
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_subtitle">"Jedno lub więcej z Twoich urządzeń jest niezweryfikowanych. Wyślij wiadomość mimo to lub anuluj i spróbuj ponownie po zweryfikowaniu wszystkich swoich urządzeń."</string>
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_title">"Twoja wiadomość nie została wysłana, ponieważ nie zweryfikowałeś jednego lub więcej swoich urządzeń."</string>
|
||||
<string name="screen_room_change_role_administrators_or_owners_title">"Edytuj administratorów lub właścicieli"</string>
|
||||
<string name="screen_room_confirm_removal_message">"Czy na pewno chcesz usunąć wiadomość?"</string>
|
||||
<string name="screen_room_confirm_removal_reason_label">"Powód (opcjonalne)"</string>
|
||||
<string name="screen_room_confirm_removal_reason_placeholder">"Powód…"</string>
|
||||
<string name="screen_room_confirm_removal_title">"Potwierdź usunięcie"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Przetwarzanie multimediów do przesłania nie powiodło się, spróbuj ponownie."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Nie można pobrać danych użytkownika"</string>
|
||||
<string name="screen_room_event_pill">"Wiadomość w %1$s"</string>
|
||||
|
||||
@@ -564,10 +564,6 @@
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_subtitle">"Одно или несколько ваших устройств не подтверждены. Вы можете отправить сообщение в любом случае или отменить его пока и повторить попытку позже, подтвердив все свои устройства."</string>
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_title">"Ваше сообщение не было отправлено, поскольку вы не подтвердили одно или несколько своих устройств."</string>
|
||||
<string name="screen_room_change_role_administrators_or_owners_title">"Редактировать владельцев и администраторов"</string>
|
||||
<string name="screen_room_confirm_removal_message">"Вы уверены, что хотите удалить это сообщение?"</string>
|
||||
<string name="screen_room_confirm_removal_reason_label">"Причина (необязательно)"</string>
|
||||
<string name="screen_room_confirm_removal_reason_placeholder">"Введите…"</string>
|
||||
<string name="screen_room_confirm_removal_title">"Подтвердите удаление"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Не удалось обработать медиафайл для загрузки, попробуйте ещё раз."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Не удалось получить данные о пользователе"</string>
|
||||
<string name="screen_room_event_pill">"Сообщение в %1$s"</string>
|
||||
|
||||
@@ -536,10 +536,6 @@
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_subtitle">"你有至少 1 个未经验证的设备。你可以照常发送消息,也可以暂时取消,并在验证所有设备后重试。"</string>
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_title">"你的消息未能发送,因为你有尚未验证的设备。"</string>
|
||||
<string name="screen_room_change_role_administrators_or_owners_title">"编辑管理员或所有者"</string>
|
||||
<string name="screen_room_confirm_removal_message">"确定要移除此消息?"</string>
|
||||
<string name="screen_room_confirm_removal_reason_label">"理由(可选)"</string>
|
||||
<string name="screen_room_confirm_removal_reason_placeholder">"输入…"</string>
|
||||
<string name="screen_room_confirm_removal_title">"确认移除"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"处理要上传的媒体失败,请重试。"</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"无法获取用户信息"</string>
|
||||
<string name="screen_room_event_pill">"%1$s 中的消息"</string>
|
||||
|
||||
@@ -550,10 +550,6 @@ Are you sure you want to continue?"</string>
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_subtitle">"One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."</string>
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_title">"Your message was not sent because you have not verified one or more of your devices"</string>
|
||||
<string name="screen_room_change_role_administrators_or_owners_title">"Edit Admins or Owners"</string>
|
||||
<string name="screen_room_confirm_removal_message">"Are you sure you wish to remove this message?"</string>
|
||||
<string name="screen_room_confirm_removal_reason_label">"Reason (optional)"</string>
|
||||
<string name="screen_room_confirm_removal_reason_placeholder">"Enter…"</string>
|
||||
<string name="screen_room_confirm_removal_title">"Confirm removal"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
||||
<string name="screen_room_event_pill">"Message in %1$s"</string>
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -275,6 +275,7 @@
|
||||
"screen\\.room\\.timeline.*",
|
||||
"error\\.sending_failure\\..*",
|
||||
"screen\\.room_timeline.*",
|
||||
"screen\\.room\\.confirm_removal.*",
|
||||
"screen_room_typing.*",
|
||||
"screen\\.image_edition\\..*",
|
||||
"screen\\.media_upload.*"
|
||||
|
||||
Reference in New Issue
Block a user