Edit / Add / Remove caption
This commit is contained in:
parent
f353ecdd45
commit
d3408c8f25
12 changed files with 246 additions and 69 deletions
|
|
@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.components.customreact
|
||||||
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState
|
||||||
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetState
|
||||||
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.TimelineItemEventContentWithAttachment
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||||
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.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
|
|
@ -273,6 +274,9 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
TimelineItemAction.CopyLink -> handleCopyLink(targetEvent)
|
||||||
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
TimelineItemAction.Redact -> handleActionRedact(targetEvent)
|
||||||
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState, enableTextFormatting)
|
||||||
|
TimelineItemAction.AddCaption -> handleActionAddCaption(targetEvent, composerState)
|
||||||
|
TimelineItemAction.EditCaption -> handleActionEditCaption(targetEvent, composerState)
|
||||||
|
TimelineItemAction.RemoveCaption -> handleRemoveCaption(targetEvent)
|
||||||
TimelineItemAction.Reply,
|
TimelineItemAction.Reply,
|
||||||
TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState, timelineProtectionState)
|
TimelineItemAction.ReplyInThread -> handleActionReply(targetEvent, composerState, timelineProtectionState)
|
||||||
TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent)
|
TimelineItemAction.ViewSource -> handleShowDebugInfoAction(targetEvent)
|
||||||
|
|
@ -285,6 +289,16 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun handleRemoveCaption(targetEvent: TimelineItem.Event) {
|
||||||
|
timelineController.invokeOnCurrentTimeline {
|
||||||
|
editCaption(
|
||||||
|
eventOrTransactionId = targetEvent.eventOrTransactionId,
|
||||||
|
caption = null,
|
||||||
|
formattedCaption = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun handlePinAction(targetEvent: TimelineItem.Event) {
|
private suspend fun handlePinAction(targetEvent: TimelineItem.Event) {
|
||||||
if (targetEvent.eventId == null) return
|
if (targetEvent.eventId == null) return
|
||||||
analyticsService.capture(
|
analyticsService.capture(
|
||||||
|
|
@ -387,6 +401,32 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleActionAddCaption(
|
||||||
|
targetEvent: TimelineItem.Event,
|
||||||
|
composerState: MessageComposerState,
|
||||||
|
) {
|
||||||
|
val composerMode = MessageComposerMode.EditCaption(
|
||||||
|
eventOrTransactionId = targetEvent.eventOrTransactionId,
|
||||||
|
content = "",
|
||||||
|
)
|
||||||
|
composerState.eventSink(
|
||||||
|
MessageComposerEvents.SetMode(composerMode)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleActionEditCaption(
|
||||||
|
targetEvent: TimelineItem.Event,
|
||||||
|
composerState: MessageComposerState,
|
||||||
|
) {
|
||||||
|
val composerMode = MessageComposerMode.EditCaption(
|
||||||
|
eventOrTransactionId = targetEvent.eventOrTransactionId,
|
||||||
|
content = (targetEvent.content as? TimelineItemEventContentWithAttachment)?.caption.orEmpty(),
|
||||||
|
)
|
||||||
|
composerState.eventSink(
|
||||||
|
MessageComposerEvents.SetMode(composerMode)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun handleActionReply(
|
private suspend fun handleActionReply(
|
||||||
targetEvent: TimelineItem.Event,
|
targetEvent: TimelineItem.Event,
|
||||||
composerState: MessageComposerState,
|
composerState: MessageComposerState,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser
|
||||||
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.TimelineItemCallNotifyContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentWithAttachment
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
|
||||||
|
|
@ -154,6 +155,16 @@ class DefaultActionListPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
if (timelineItem.isEditable) {
|
if (timelineItem.isEditable) {
|
||||||
add(TimelineItemAction.Edit)
|
add(TimelineItemAction.Edit)
|
||||||
|
} else {
|
||||||
|
// Caption
|
||||||
|
if (timelineItem.isMine && timelineItem.content is TimelineItemEventContentWithAttachment) {
|
||||||
|
if (timelineItem.content.caption == null) {
|
||||||
|
add(TimelineItemAction.AddCaption)
|
||||||
|
} else {
|
||||||
|
add(TimelineItemAction.EditCaption)
|
||||||
|
add(TimelineItemAction.RemoveCaption)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (canRedact && timelineItem.content is TimelineItemPollContent && !timelineItem.content.isEnded) {
|
if (canRedact && timelineItem.content is TimelineItemPollContent && !timelineItem.content.isEnded) {
|
||||||
add(TimelineItemAction.EndPoll)
|
add(TimelineItemAction.EndPoll)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ sealed class TimelineItemAction(
|
||||||
data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)
|
data object Reply : TimelineItemAction(CommonStrings.action_reply, CompoundDrawables.ic_compound_reply)
|
||||||
data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply)
|
data object ReplyInThread : TimelineItemAction(CommonStrings.action_reply_in_thread, CompoundDrawables.ic_compound_reply)
|
||||||
data object Edit : TimelineItemAction(CommonStrings.action_edit, CompoundDrawables.ic_compound_edit)
|
data object Edit : TimelineItemAction(CommonStrings.action_edit, CompoundDrawables.ic_compound_edit)
|
||||||
|
data object EditCaption : TimelineItemAction(CommonStrings.action_edit_caption, CompoundDrawables.ic_compound_edit)
|
||||||
|
data object AddCaption : TimelineItemAction(CommonStrings.action_add_caption, CompoundDrawables.ic_compound_edit)
|
||||||
|
data object RemoveCaption : TimelineItemAction(CommonStrings.action_remove_caption, CompoundDrawables.ic_compound_delete, destructive = true)
|
||||||
data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options)
|
data object ViewSource : TimelineItemAction(CommonStrings.action_view_source, CommonDrawables.ic_developer_options)
|
||||||
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true)
|
data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, CompoundDrawables.ic_compound_chat_problem, destructive = true)
|
||||||
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end)
|
data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end)
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
when (event) {
|
when (event) {
|
||||||
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
|
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
|
||||||
MessageComposerEvents.CloseSpecialMode -> {
|
MessageComposerEvents.CloseSpecialMode -> {
|
||||||
if (messageComposerContext.composerMode is MessageComposerMode.Edit) {
|
if (messageComposerContext.composerMode.isEditing) {
|
||||||
localCoroutineScope.launch {
|
localCoroutineScope.launch {
|
||||||
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = true)
|
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = true)
|
||||||
}
|
}
|
||||||
|
|
@ -431,7 +431,15 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is MessageComposerMode.EditCaption -> {
|
||||||
|
timelineController.invokeOnCurrentTimeline {
|
||||||
|
editCaption(
|
||||||
|
capturedMode.eventOrTransactionId,
|
||||||
|
caption = message.markdown,
|
||||||
|
formattedCaption = message.html
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
is MessageComposerMode.Reply -> {
|
is MessageComposerMode.Reply -> {
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
replyMessage(capturedMode.eventId, message.markdown, message.html, message.intentionalMentions)
|
replyMessage(capturedMode.eventId, message.markdown, message.html, message.intentionalMentions)
|
||||||
|
|
@ -570,6 +578,10 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
|
mode.eventOrTransactionId.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
|
||||||
}
|
}
|
||||||
is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId)
|
is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId)
|
||||||
|
is MessageComposerMode.EditCaption -> {
|
||||||
|
// TODO Need a new type to save caption in the SDK
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return if (draftType == null || message.markdown.isBlank()) {
|
return if (draftType == null || message.markdown.isBlank()) {
|
||||||
null
|
null
|
||||||
|
|
@ -644,7 +656,14 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
val currentComposerMode = messageComposerContext.composerMode
|
val currentComposerMode = messageComposerContext.composerMode
|
||||||
when (newComposerMode) {
|
when (newComposerMode) {
|
||||||
is MessageComposerMode.Edit -> {
|
is MessageComposerMode.Edit -> {
|
||||||
if (currentComposerMode !is MessageComposerMode.Edit) {
|
if (currentComposerMode.isEditing.not()) {
|
||||||
|
val draft = createDraftFromState(markdownTextEditorState, richTextEditorState)
|
||||||
|
updateDraft(draft, isVolatile = true).join()
|
||||||
|
}
|
||||||
|
setText(newComposerMode.content, markdownTextEditorState, richTextEditorState)
|
||||||
|
}
|
||||||
|
is MessageComposerMode.EditCaption -> {
|
||||||
|
if (currentComposerMode.isEditing.not()) {
|
||||||
val draft = createDraftFromState(markdownTextEditorState, richTextEditorState)
|
val draft = createDraftFromState(markdownTextEditorState, richTextEditorState)
|
||||||
updateDraft(draft, isVolatile = true).join()
|
updateDraft(draft, isVolatile = true).join()
|
||||||
}
|
}
|
||||||
|
|
@ -652,7 +671,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// When coming from edit, just clear the composer as it'd be weird to reset a volatile draft in this scenario.
|
// When coming from edit, just clear the composer as it'd be weird to reset a volatile draft in this scenario.
|
||||||
if (currentComposerMode is MessageComposerMode.Edit) {
|
if (currentComposerMode.isEditing) {
|
||||||
setText("", markdownTextEditorState, richTextEditorState)
|
setText("", markdownTextEditorState, richTextEditorState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,17 @@ interface Timeline : AutoCloseable {
|
||||||
|
|
||||||
suspend fun editMessage(
|
suspend fun editMessage(
|
||||||
eventOrTransactionId: EventOrTransactionId,
|
eventOrTransactionId: EventOrTransactionId,
|
||||||
body: String, htmlBody: String?,
|
body: String,
|
||||||
|
htmlBody: String?,
|
||||||
intentionalMentions: List<IntentionalMention>,
|
intentionalMentions: List<IntentionalMention>,
|
||||||
): Result<Unit>
|
): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun editCaption(
|
||||||
|
eventOrTransactionId: EventOrTransactionId,
|
||||||
|
caption: String?,
|
||||||
|
formattedCaption: String?,
|
||||||
|
): Result<Unit>
|
||||||
|
|
||||||
suspend fun replyMessage(
|
suspend fun replyMessage(
|
||||||
eventId: EventId,
|
eventId: EventId,
|
||||||
body: String,
|
body: String,
|
||||||
|
|
|
||||||
|
|
@ -295,22 +295,40 @@ class RustTimeline(
|
||||||
body: String,
|
body: String,
|
||||||
htmlBody: String?,
|
htmlBody: String?,
|
||||||
intentionalMentions: List<IntentionalMention>,
|
intentionalMentions: List<IntentionalMention>,
|
||||||
): Result<Unit> =
|
): Result<Unit> = withContext(dispatcher) {
|
||||||
withContext(dispatcher) {
|
runCatching<Unit> {
|
||||||
runCatching<Unit> {
|
val editedContent = EditedContent.RoomMessage(
|
||||||
val editedContent = EditedContent.RoomMessage(
|
content = MessageEventContent.from(
|
||||||
content = MessageEventContent.from(
|
body = body,
|
||||||
body = body,
|
htmlBody = htmlBody,
|
||||||
htmlBody = htmlBody,
|
intentionalMentions = intentionalMentions
|
||||||
intentionalMentions = intentionalMentions
|
),
|
||||||
),
|
)
|
||||||
)
|
inner.edit(
|
||||||
inner.edit(
|
newContent = editedContent,
|
||||||
newContent = editedContent,
|
eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(),
|
||||||
eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun editCaption(
|
||||||
|
eventOrTransactionId: EventOrTransactionId,
|
||||||
|
caption: String?,
|
||||||
|
formattedCaption: String?,
|
||||||
|
): Result<Unit> = withContext(dispatcher) {
|
||||||
|
runCatching<Unit> {
|
||||||
|
val editedContent = EditedContent.MediaCaption(
|
||||||
|
caption = caption,
|
||||||
|
formattedCaption = formattedCaption?.let {
|
||||||
|
FormattedBody(body = it, format = MessageFormat.Html)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
inner.edit(
|
||||||
|
newContent = editedContent,
|
||||||
|
eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun replyMessage(
|
override suspend fun replyMessage(
|
||||||
eventId: EventId,
|
eventId: EventId,
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,24 @@ class FakeTimeline(
|
||||||
intentionalMentions
|
intentionalMentions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var editCaptionLambda: (
|
||||||
|
eventOrTransactionId: EventOrTransactionId,
|
||||||
|
caption: String?,
|
||||||
|
formattedCaption: String?,
|
||||||
|
) -> Result<Unit> = { _, _, _ ->
|
||||||
|
lambdaError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun editCaption(
|
||||||
|
eventOrTransactionId: EventOrTransactionId,
|
||||||
|
caption: String?,
|
||||||
|
formattedCaption: String?,
|
||||||
|
): Result<Unit> = editCaptionLambda(
|
||||||
|
eventOrTransactionId,
|
||||||
|
caption,
|
||||||
|
formattedCaption,
|
||||||
|
)
|
||||||
|
|
||||||
var replyMessageLambda: (
|
var replyMessageLambda: (
|
||||||
eventId: EventId,
|
eventId: EventId,
|
||||||
body: String,
|
body: String,
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,16 @@ internal fun ComposerModeView(
|
||||||
when (composerMode) {
|
when (composerMode) {
|
||||||
is MessageComposerMode.Edit -> {
|
is MessageComposerMode.Edit -> {
|
||||||
EditingModeView(
|
EditingModeView(
|
||||||
|
text = stringResource(CommonStrings.common_editing),
|
||||||
|
modifier = modifier,
|
||||||
|
onResetComposerMode = onResetComposerMode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is MessageComposerMode.EditCaption -> {
|
||||||
|
EditingModeView(
|
||||||
|
text = stringResource(
|
||||||
|
if (composerMode.content.isEmpty()) CommonStrings.common_adding_caption else CommonStrings.common_editing_caption
|
||||||
|
),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onResetComposerMode = onResetComposerMode,
|
onResetComposerMode = onResetComposerMode,
|
||||||
)
|
)
|
||||||
|
|
@ -65,6 +75,7 @@ internal fun ComposerModeView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun EditingModeView(
|
private fun EditingModeView(
|
||||||
onResetComposerMode: () -> Unit,
|
onResetComposerMode: () -> Unit,
|
||||||
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -76,14 +87,14 @@ private fun EditingModeView(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Edit(),
|
imageVector = CompoundIcons.Edit(),
|
||||||
contentDescription = stringResource(CommonStrings.common_editing),
|
contentDescription = null,
|
||||||
tint = ElementTheme.materialColors.secondary,
|
tint = ElementTheme.materialColors.secondary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.size(16.dp),
|
.size(16.dp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
stringResource(CommonStrings.common_editing),
|
text = text,
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
color = ElementTheme.materialColors.secondary,
|
color = ElementTheme.materialColors.secondary,
|
||||||
|
|
|
||||||
|
|
@ -121,19 +121,25 @@ fun TextComposer(
|
||||||
}
|
}
|
||||||
|
|
||||||
val layoutModifier = modifier
|
val layoutModifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.height(IntrinsicSize.Min)
|
.height(IntrinsicSize.Min)
|
||||||
|
|
||||||
val composerOptionsButton: @Composable () -> Unit = remember {
|
val composerOptionsButton: @Composable () -> Unit = remember(composerMode) {
|
||||||
@Composable {
|
@Composable {
|
||||||
if (composerMode is MessageComposerMode.Attachment) {
|
when (composerMode) {
|
||||||
Spacer(modifier = Modifier.width(9.dp))
|
is MessageComposerMode.Attachment -> {
|
||||||
} else {
|
Spacer(modifier = Modifier.width(9.dp))
|
||||||
ComposerOptionsButton(
|
}
|
||||||
modifier = Modifier
|
is MessageComposerMode.EditCaption -> {
|
||||||
.size(48.dp),
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
onClick = onAddAttachment
|
}
|
||||||
)
|
else -> {
|
||||||
|
ComposerOptionsButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp),
|
||||||
|
onClick = onAddAttachment
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -331,8 +337,8 @@ private fun StandardLayout(
|
||||||
if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) {
|
if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp)
|
.padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp)
|
||||||
.size(48.dp),
|
.size(48.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
voiceDeleteButton()
|
voiceDeleteButton()
|
||||||
|
|
@ -342,8 +348,8 @@ private fun StandardLayout(
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(bottom = 8.dp, top = 8.dp)
|
.padding(bottom = 8.dp, top = 8.dp)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
voiceRecording()
|
voiceRecording()
|
||||||
}
|
}
|
||||||
|
|
@ -356,16 +362,16 @@ private fun StandardLayout(
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(bottom = 8.dp, top = 8.dp)
|
.padding(bottom = 8.dp, top = 8.dp)
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
textInput()
|
textInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
|
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
|
||||||
.size(48.dp),
|
.size(48.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
endButton()
|
endButton()
|
||||||
|
|
@ -387,8 +393,8 @@ private fun TextFormattingLayout(
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 12.dp)
|
.padding(horizontal = 12.dp)
|
||||||
) {
|
) {
|
||||||
textInput()
|
textInput()
|
||||||
}
|
}
|
||||||
|
|
@ -432,11 +438,11 @@ private fun TextInputBox(
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(roundedCorners)
|
.clip(roundedCorners)
|
||||||
.border(0.5.dp, borderColor, roundedCorners)
|
.border(0.5.dp, borderColor, roundedCorners)
|
||||||
.background(color = bgColor)
|
.background(color = bgColor)
|
||||||
.requiredHeightIn(min = 42.dp)
|
.requiredHeightIn(min = 42.dp)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
if (composerMode is MessageComposerMode.Special) {
|
if (composerMode is MessageComposerMode.Special) {
|
||||||
ComposerModeView(
|
ComposerModeView(
|
||||||
|
|
@ -447,9 +453,9 @@ private fun TextInputBox(
|
||||||
val defaultTypography = ElementTheme.typography.fontBodyLgRegular
|
val defaultTypography = ElementTheme.typography.fontBodyLgRegular
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp)
|
.padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp)
|
||||||
// Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail
|
// Apply test tag only once, otherwise 2 nodes will have it (both the normal and subcomposing one) and tests will fail
|
||||||
.then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier),
|
.then(if (!subcomposing) Modifier.testTag(TestTags.textEditor) else Modifier),
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
) {
|
) {
|
||||||
// Placeholder
|
// Placeholder
|
||||||
|
|
@ -495,8 +501,8 @@ private fun TextInput(
|
||||||
// This prevents it gaining focus and mutating the state.
|
// This prevents it gaining focus and mutating the state.
|
||||||
registerStateUpdates = !subcomposing,
|
registerStateUpdates = !subcomposing,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 6.dp, bottom = 6.dp)
|
.padding(top = 6.dp, bottom = 6.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus),
|
style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.hasFocus),
|
||||||
resolveMentionDisplay = resolveMentionDisplay,
|
resolveMentionDisplay = resolveMentionDisplay,
|
||||||
resolveRoomMentionDisplay = resolveRoomMentionDisplay,
|
resolveRoomMentionDisplay = resolveRoomMentionDisplay,
|
||||||
|
|
@ -573,6 +579,40 @@ internal fun TextComposerEditPreview() = ElementPreview {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TextComposerEditCaptionPreview() = ElementPreview {
|
||||||
|
PreviewColumn(
|
||||||
|
items = aTextEditorStateRichList()
|
||||||
|
) { _, textEditorState ->
|
||||||
|
ATextComposer(
|
||||||
|
state = textEditorState,
|
||||||
|
voiceMessageState = VoiceMessageState.Idle,
|
||||||
|
composerMode = aMessageComposerModeEditCaption(
|
||||||
|
content = "A caption",
|
||||||
|
),
|
||||||
|
enableVoiceMessages = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TextComposerAddCaptionPreview() = ElementPreview {
|
||||||
|
PreviewColumn(
|
||||||
|
items = aTextEditorStateRichList()
|
||||||
|
) { _, textEditorState ->
|
||||||
|
ATextComposer(
|
||||||
|
state = textEditorState,
|
||||||
|
voiceMessageState = VoiceMessageState.Idle,
|
||||||
|
composerMode = aMessageComposerModeEditCaption(
|
||||||
|
content = "",
|
||||||
|
),
|
||||||
|
enableVoiceMessages = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MarkdownTextComposerEditPreview() = ElementPreview {
|
internal fun MarkdownTextComposerEditPreview() = ElementPreview {
|
||||||
|
|
@ -717,6 +757,14 @@ fun aMessageComposerModeEdit(
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun aMessageComposerModeEditCaption(
|
||||||
|
eventOrTransactionId: EventOrTransactionId = EventId("$1234").toEventOrTransactionId(),
|
||||||
|
content: String = "Some caption",
|
||||||
|
) = MessageComposerMode.EditCaption(
|
||||||
|
eventOrTransactionId = eventOrTransactionId,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
|
||||||
fun aMessageComposerModeReply(
|
fun aMessageComposerModeReply(
|
||||||
replyToDetails: InReplyToDetails,
|
replyToDetails: InReplyToDetails,
|
||||||
hideImage: Boolean = false,
|
hideImage: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -53,16 +53,16 @@ internal fun SendButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
enabled = canSendMessage,
|
enabled = canSendMessage,
|
||||||
) {
|
) {
|
||||||
val iconVector = when (composerMode) {
|
val iconVector = when {
|
||||||
is MessageComposerMode.Edit -> CompoundIcons.Check()
|
composerMode.isEditing -> CompoundIcons.Check()
|
||||||
else -> CompoundIcons.SendSolid()
|
else -> CompoundIcons.SendSolid()
|
||||||
}
|
}
|
||||||
val iconStartPadding = when (composerMode) {
|
val iconStartPadding = when {
|
||||||
is MessageComposerMode.Edit -> 0.dp
|
composerMode.isEditing -> 0.dp
|
||||||
else -> 2.dp
|
else -> 2.dp
|
||||||
}
|
}
|
||||||
val contentDescription = when (composerMode) {
|
val contentDescription = when {
|
||||||
is MessageComposerMode.Edit -> stringResource(CommonStrings.action_edit)
|
composerMode.isEditing -> stringResource(CommonStrings.action_edit)
|
||||||
else -> stringResource(CommonStrings.action_send)
|
else -> stringResource(CommonStrings.action_send)
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,11 @@ sealed interface MessageComposerMode {
|
||||||
val content: String
|
val content: String
|
||||||
) : Special
|
) : Special
|
||||||
|
|
||||||
|
data class EditCaption(
|
||||||
|
val eventOrTransactionId: EventOrTransactionId,
|
||||||
|
val content: String
|
||||||
|
) : Special
|
||||||
|
|
||||||
data class Reply(
|
data class Reply(
|
||||||
val replyToDetails: InReplyToDetails,
|
val replyToDetails: InReplyToDetails,
|
||||||
val hideImage: Boolean,
|
val hideImage: Boolean,
|
||||||
|
|
@ -34,16 +39,8 @@ sealed interface MessageComposerMode {
|
||||||
val eventId: EventId = replyToDetails.eventId()
|
val eventId: EventId = replyToDetails.eventId()
|
||||||
}
|
}
|
||||||
|
|
||||||
val relatedEventId: EventId?
|
|
||||||
get() = when (this) {
|
|
||||||
is Normal,
|
|
||||||
is Attachment -> null
|
|
||||||
is Edit -> eventOrTransactionId.eventId
|
|
||||||
is Reply -> eventId
|
|
||||||
}
|
|
||||||
|
|
||||||
val isEditing: Boolean
|
val isEditing: Boolean
|
||||||
get() = this is Edit
|
get() = this is Edit || this is EditCaption
|
||||||
|
|
||||||
val isReply: Boolean
|
val isReply: Boolean
|
||||||
get() = this is Reply
|
get() = this is Reply
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
<string name="a11y_voice_message_record">"Record voice message."</string>
|
<string name="a11y_voice_message_record">"Record voice message."</string>
|
||||||
<string name="a11y_voice_message_stop_recording">"Stop recording"</string>
|
<string name="a11y_voice_message_stop_recording">"Stop recording"</string>
|
||||||
<string name="action_accept">"Accept"</string>
|
<string name="action_accept">"Accept"</string>
|
||||||
|
<string name="action_add_caption">"Add caption"</string>
|
||||||
<string name="action_add_to_timeline">"Add to timeline"</string>
|
<string name="action_add_to_timeline">"Add to timeline"</string>
|
||||||
<string name="action_back">"Back"</string>
|
<string name="action_back">"Back"</string>
|
||||||
<string name="action_call">"Call"</string>
|
<string name="action_call">"Call"</string>
|
||||||
|
|
@ -57,6 +58,7 @@
|
||||||
<string name="action_discard">"Discard"</string>
|
<string name="action_discard">"Discard"</string>
|
||||||
<string name="action_done">"Done"</string>
|
<string name="action_done">"Done"</string>
|
||||||
<string name="action_edit">"Edit"</string>
|
<string name="action_edit">"Edit"</string>
|
||||||
|
<string name="action_edit_caption">"Edit caption"</string>
|
||||||
<string name="action_edit_poll">"Edit poll"</string>
|
<string name="action_edit_poll">"Edit poll"</string>
|
||||||
<string name="action_enable">"Enable"</string>
|
<string name="action_enable">"Enable"</string>
|
||||||
<string name="action_end_poll">"End poll"</string>
|
<string name="action_end_poll">"End poll"</string>
|
||||||
|
|
@ -91,6 +93,7 @@
|
||||||
<string name="action_react">"React"</string>
|
<string name="action_react">"React"</string>
|
||||||
<string name="action_reject">"Reject"</string>
|
<string name="action_reject">"Reject"</string>
|
||||||
<string name="action_remove">"Remove"</string>
|
<string name="action_remove">"Remove"</string>
|
||||||
|
<string name="action_remove_caption">"Remove caption"</string>
|
||||||
<string name="action_reply">"Reply"</string>
|
<string name="action_reply">"Reply"</string>
|
||||||
<string name="action_reply_in_thread">"Reply in thread"</string>
|
<string name="action_reply_in_thread">"Reply in thread"</string>
|
||||||
<string name="action_report_bug">"Report bug"</string>
|
<string name="action_report_bug">"Report bug"</string>
|
||||||
|
|
@ -123,6 +126,7 @@
|
||||||
<string name="action_yes">"Yes"</string>
|
<string name="action_yes">"Yes"</string>
|
||||||
<string name="common_about">"About"</string>
|
<string name="common_about">"About"</string>
|
||||||
<string name="common_acceptable_use_policy">"Acceptable use policy"</string>
|
<string name="common_acceptable_use_policy">"Acceptable use policy"</string>
|
||||||
|
<string name="common_adding_caption">"Adding caption"</string>
|
||||||
<string name="common_advanced_settings">"Advanced settings"</string>
|
<string name="common_advanced_settings">"Advanced settings"</string>
|
||||||
<string name="common_analytics">"Analytics"</string>
|
<string name="common_analytics">"Analytics"</string>
|
||||||
<string name="common_appearance">"Appearance"</string>
|
<string name="common_appearance">"Appearance"</string>
|
||||||
|
|
@ -143,6 +147,7 @@
|
||||||
<string name="common_do_not_show_this_again">"Do not show this again"</string>
|
<string name="common_do_not_show_this_again">"Do not show this again"</string>
|
||||||
<string name="common_edited_suffix">"(edited)"</string>
|
<string name="common_edited_suffix">"(edited)"</string>
|
||||||
<string name="common_editing">"Editing"</string>
|
<string name="common_editing">"Editing"</string>
|
||||||
|
<string name="common_editing_caption">"Editing caption"</string>
|
||||||
<string name="common_emote">"* %1$s %2$s"</string>
|
<string name="common_emote">"* %1$s %2$s"</string>
|
||||||
<string name="common_encryption">"Encryption"</string>
|
<string name="common_encryption">"Encryption"</string>
|
||||||
<string name="common_encryption_enabled">"Encryption enabled"</string>
|
<string name="common_encryption_enabled">"Encryption enabled"</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue