[Rich text editor] Add formatting menu (#1261)

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
jonnyandrew 2023-09-08 17:23:15 +01:00 committed by GitHub
parent 5a0e0a89e5
commit 896c2325db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 706 additions and 210 deletions

1
changelog.d/1261.feature Normal file
View file

@ -0,0 +1 @@
[Rich text editor] Add formatting menu (accessible via the '+' button)

View file

@ -26,6 +26,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.BarChart
import androidx.compose.material.icons.filled.Collections
import androidx.compose.material.icons.filled.FormatColorText
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.PhotoCamera
import androidx.compose.material.icons.filled.Videocam
@ -145,6 +146,11 @@ internal fun AttachmentSourcePickerMenu(
text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) },
)
}
ListItem(
modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) },
icon = { Icon(Icons.Default.FormatColorText, null) },
text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) },
)
}
}

View file

@ -36,6 +36,7 @@ sealed interface MessageComposerEvents {
data object Location : PickAttachmentSource
data object Poll : PickAttachmentSource
}
data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents
data object CancelSendAttachment : MessageComposerEvents
data class Error(val error: Throwable) : MessageComposerEvents
}

View file

@ -110,6 +110,7 @@ class MessageComposerPresenter @Inject constructor(
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
var showTextFormatting: Boolean by remember { mutableStateOf(false) }
LaunchedEffect(messageComposerContext.composerMode) {
when (val modeValue = messageComposerContext.composerMode) {
@ -190,6 +191,10 @@ class MessageComposerPresenter @Inject constructor(
ongoingSendAttachmentJob.value == null
}
}
is MessageComposerEvents.ToggleTextFormatting -> {
showAttachmentSourcePicker = false
showTextFormatting = event.enabled
}
is MessageComposerEvents.Error -> {
analyticsService.trackError(event.error)
}
@ -201,6 +206,7 @@ class MessageComposerPresenter @Inject constructor(
isFullScreen = isFullScreen.value,
mode = messageComposerContext.composerMode,
showAttachmentSourcePicker = showAttachmentSourcePicker,
showTextFormatting = showTextFormatting,
canShareLocation = canShareLocation.value,
canCreatePoll = canCreatePoll.value,
attachmentsState = attachmentsState.value,

View file

@ -28,6 +28,7 @@ data class MessageComposerState(
val isFullScreen: Boolean,
val mode: MessageComposerMode,
val showAttachmentSourcePicker: Boolean,
val showTextFormatting: Boolean,
val canShareLocation: Boolean,
val canCreatePoll: Boolean,
val attachmentsState: AttachmentsState,

View file

@ -32,6 +32,7 @@ fun aMessageComposerState(
composerState: RichTextEditorState = RichTextEditorState("", fake = true),
isFullScreen: Boolean = false,
mode: MessageComposerMode = MessageComposerMode.Normal(content = ""),
showTextFormatting: Boolean = false,
showAttachmentSourcePicker: Boolean = false,
canShareLocation: Boolean = true,
canCreatePoll: Boolean = true,
@ -40,6 +41,7 @@ fun aMessageComposerState(
richTextEditorState = composerState.apply { if(requestFocus) requestFocus() },
isFullScreen = isFullScreen,
mode = mode,
showTextFormatting = showTextFormatting,
showAttachmentSourcePicker = showAttachmentSourcePicker,
canShareLocation = canShareLocation,
canCreatePoll = canCreatePoll,

View file

@ -49,6 +49,10 @@ fun MessageComposerView(
state.eventSink(MessageComposerEvents.CloseSpecialMode)
}
fun onDismissTextFormatting() {
state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = false))
}
fun onError(error: Throwable) {
state.eventSink(MessageComposerEvents.Error(error))
}
@ -66,8 +70,10 @@ fun MessageComposerView(
onRequestFocus = { state.richTextEditorState.requestFocus() },
onSendMessage = ::sendMessage,
composerMode = state.mode,
showTextFormatting = state.showTextFormatting,
onResetComposerMode = ::onCloseSpecialMode,
onAddAttachment = ::onAddAttachment,
onDismissTextFormatting = ::onDismissTextFormatting,
onError = ::onError,
)
}

View file

@ -539,6 +539,29 @@ class MessageComposerPresenterTest {
}
}
@Test
fun `present - ToggleTextFormatting toggles text formatting`() = runTest {
val presenter = createPresenter(this)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.showTextFormatting).isFalse()
initialState.eventSink(MessageComposerEvents.AddAttachment)
val composerOptions = awaitItem()
assertThat(composerOptions.showAttachmentSourcePicker).isTrue()
composerOptions.eventSink(MessageComposerEvents.ToggleTextFormatting(true))
awaitItem() // composer options closed
val showTextFormatting = awaitItem()
assertThat(showTextFormatting.showAttachmentSourcePicker).isFalse()
assertThat(showTextFormatting.showTextFormatting).isTrue()
showTextFormatting.eventSink(MessageComposerEvents.ToggleTextFormatting(false))
val finished = awaitItem()
assertThat(finished.showTextFormatting).isFalse()
}
}
private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0) {
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
skipItems(skipCount)

View file

@ -28,4 +28,16 @@ object VectorIcons {
val Groups = R.drawable.ic_groups
val Share = R.drawable.ic_share
val EndPoll = R.drawable.ic_poll_end
val Bold = R.drawable.ic_bold
val BulletList = R.drawable.ic_bullet_list
val CodeBlock = R.drawable.ic_code_block
val IndentIncrease = R.drawable.ic_indent_increase
val IndentDecrease = R.drawable.ic_indent_decrease
val InlineCode = R.drawable.ic_inline_code
val Italic = R.drawable.ic_italic
val Link = R.drawable.ic_link
val NumberedList = R.drawable.ic_numbered_list
val Quote = R.drawable.ic_quote
val Strikethrough = R.drawable.ic_strikethrough
val Underline = R.drawable.ic_underline
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M8.8,19C8.25,19 7.779,18.804 7.388,18.413C6.996,18.021 6.8,17.55 6.8,17V7C6.8,6.45 6.996,5.979 7.388,5.588C7.779,5.196 8.25,5 8.8,5H12.325C13.408,5 14.408,5.333 15.325,6C16.242,6.667 16.7,7.592 16.7,8.775C16.7,9.625 16.508,10.279 16.125,10.738C15.742,11.196 15.383,11.525 15.05,11.725C15.467,11.908 15.929,12.25 16.438,12.75C16.946,13.25 17.2,14 17.2,15C17.2,16.483 16.658,17.521 15.575,18.112C14.492,18.704 13.475,19 12.525,19H8.8ZM9.825,16.2H12.425C13.225,16.2 13.712,15.996 13.887,15.587C14.063,15.179 14.15,14.883 14.15,14.7C14.15,14.517 14.063,14.221 13.887,13.813C13.712,13.404 13.2,13.2 12.35,13.2H9.825V16.2ZM9.825,10.5H12.15C12.7,10.5 13.1,10.358 13.35,10.075C13.6,9.792 13.725,9.475 13.725,9.125C13.725,8.725 13.583,8.4 13.3,8.15C13.017,7.9 12.65,7.775 12.2,7.775H9.825V10.5Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10,19C9.717,19 9.479,18.904 9.288,18.712C9.096,18.521 9,18.283 9,18C9,17.717 9.096,17.479 9.288,17.288C9.479,17.096 9.717,17 10,17H20C20.283,17 20.521,17.096 20.712,17.288C20.904,17.479 21,17.717 21,18C21,18.283 20.904,18.521 20.712,18.712C20.521,18.904 20.283,19 20,19H10ZM10,13C9.717,13 9.479,12.904 9.288,12.712C9.096,12.521 9,12.283 9,12C9,11.717 9.096,11.479 9.288,11.288C9.479,11.096 9.717,11 10,11H20C20.283,11 20.521,11.096 20.712,11.288C20.904,11.479 21,11.717 21,12C21,12.283 20.904,12.521 20.712,12.712C20.521,12.904 20.283,13 20,13H10ZM10,7C9.717,7 9.479,6.904 9.288,6.713C9.096,6.521 9,6.283 9,6C9,5.717 9.096,5.479 9.288,5.287C9.479,5.096 9.717,5 10,5H20C20.283,5 20.521,5.096 20.712,5.287C20.904,5.479 21,5.717 21,6C21,6.283 20.904,6.521 20.712,6.713C20.521,6.904 20.283,7 20,7H10ZM5,20C4.45,20 3.979,19.804 3.588,19.413C3.196,19.021 3,18.55 3,18C3,17.45 3.196,16.979 3.588,16.587C3.979,16.196 4.45,16 5,16C5.55,16 6.021,16.196 6.412,16.587C6.804,16.979 7,17.45 7,18C7,18.55 6.804,19.021 6.412,19.413C6.021,19.804 5.55,20 5,20ZM5,14C4.45,14 3.979,13.804 3.588,13.413C3.196,13.021 3,12.55 3,12C3,11.45 3.196,10.979 3.588,10.587C3.979,10.196 4.45,10 5,10C5.55,10 6.021,10.196 6.412,10.587C6.804,10.979 7,11.45 7,12C7,12.55 6.804,13.021 6.412,13.413C6.021,13.804 5.55,14 5,14ZM5,8C4.45,8 3.979,7.804 3.588,7.412C3.196,7.021 3,6.55 3,6C3,5.45 3.196,4.979 3.588,4.588C3.979,4.196 4.45,4 5,4C5.55,4 6.021,4.196 6.412,4.588C6.804,4.979 7,5.45 7,6C7,6.55 6.804,7.021 6.412,7.412C6.021,7.804 5.55,8 5,8Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M8.825,12L10.3,10.525C10.5,10.325 10.6,10.092 10.6,9.825C10.6,9.558 10.5,9.325 10.3,9.125C10.1,8.925 9.863,8.825 9.587,8.825C9.313,8.825 9.075,8.925 8.875,9.125L6.7,11.3C6.6,11.4 6.529,11.508 6.488,11.625C6.446,11.742 6.425,11.867 6.425,12C6.425,12.133 6.446,12.258 6.488,12.375C6.529,12.492 6.6,12.6 6.7,12.7L8.875,14.875C9.075,15.075 9.313,15.175 9.587,15.175C9.863,15.175 10.1,15.075 10.3,14.875C10.5,14.675 10.6,14.442 10.6,14.175C10.6,13.908 10.5,13.675 10.3,13.475L8.825,12ZM15.175,12L13.7,13.475C13.5,13.675 13.4,13.908 13.4,14.175C13.4,14.442 13.5,14.675 13.7,14.875C13.9,15.075 14.137,15.175 14.413,15.175C14.688,15.175 14.925,15.075 15.125,14.875L17.3,12.7C17.4,12.6 17.471,12.492 17.513,12.375C17.554,12.258 17.575,12.133 17.575,12C17.575,11.867 17.554,11.742 17.513,11.625C17.471,11.508 17.4,11.4 17.3,11.3L15.125,9.125C15.025,9.025 14.913,8.95 14.788,8.9C14.663,8.85 14.538,8.825 14.413,8.825C14.288,8.825 14.163,8.85 14.038,8.9C13.913,8.95 13.8,9.025 13.7,9.125C13.5,9.325 13.4,9.558 13.4,9.825C13.4,10.092 13.5,10.325 13.7,10.525L15.175,12ZM5,21C4.45,21 3.979,20.804 3.588,20.413C3.196,20.021 3,19.55 3,19V5C3,4.45 3.196,3.979 3.588,3.588C3.979,3.196 4.45,3 5,3H19C19.55,3 20.021,3.196 20.413,3.588C20.804,3.979 21,4.45 21,5V19C21,19.55 20.804,20.021 20.413,20.413C20.021,20.804 19.55,21 19,21H5ZM5,19H19V5H5V19Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,21C3.717,21 3.479,20.904 3.287,20.712C3.096,20.521 3,20.283 3,20C3,19.717 3.096,19.479 3.287,19.288C3.479,19.096 3.717,19 4,19H20C20.283,19 20.521,19.096 20.712,19.288C20.904,19.479 21,19.717 21,20C21,20.283 20.904,20.521 20.712,20.712C20.521,20.904 20.283,21 20,21H4ZM12,17C11.717,17 11.479,16.904 11.288,16.712C11.096,16.521 11,16.283 11,16C11,15.717 11.096,15.479 11.288,15.288C11.479,15.096 11.717,15 12,15H20C20.283,15 20.521,15.096 20.712,15.288C20.904,15.479 21,15.717 21,16C21,16.283 20.904,16.521 20.712,16.712C20.521,16.904 20.283,17 20,17H12ZM12,13C11.717,13 11.479,12.904 11.288,12.712C11.096,12.521 11,12.283 11,12C11,11.717 11.096,11.479 11.288,11.288C11.479,11.096 11.717,11 12,11H20C20.283,11 20.521,11.096 20.712,11.288C20.904,11.479 21,11.717 21,12C21,12.283 20.904,12.521 20.712,12.712C20.521,12.904 20.283,13 20,13H12ZM12,9C11.717,9 11.479,8.904 11.288,8.712C11.096,8.521 11,8.283 11,8C11,7.717 11.096,7.479 11.288,7.287C11.479,7.096 11.717,7 12,7H20C20.283,7 20.521,7.096 20.712,7.287C20.904,7.479 21,7.717 21,8C21,8.283 20.904,8.521 20.712,8.712C20.521,8.904 20.283,9 20,9H12ZM4,5C3.717,5 3.479,4.904 3.287,4.713C3.096,4.521 3,4.283 3,4C3,3.717 3.096,3.479 3.287,3.287C3.479,3.096 3.717,3 4,3H20C20.283,3 20.521,3.096 20.712,3.287C20.904,3.479 21,3.717 21,4C21,4.283 20.904,4.521 20.712,4.713C20.521,4.904 20.283,5 20,5H4ZM6.15,15.15L3.35,12.35C3.25,12.25 3.2,12.133 3.2,12C3.2,11.867 3.25,11.75 3.35,11.65L6.15,8.85C6.317,8.683 6.5,8.642 6.7,8.725C6.9,8.808 7,8.967 7,9.2V14.8C7,15.033 6.9,15.192 6.7,15.275C6.5,15.358 6.317,15.317 6.15,15.15Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,21C3.717,21 3.479,20.904 3.287,20.712C3.096,20.521 3,20.283 3,20C3,19.717 3.096,19.479 3.287,19.288C3.479,19.096 3.717,19 4,19H20C20.283,19 20.521,19.096 20.712,19.288C20.904,19.479 21,19.717 21,20C21,20.283 20.904,20.521 20.712,20.712C20.521,20.904 20.283,21 20,21H4ZM12,17C11.717,17 11.479,16.904 11.288,16.712C11.096,16.521 11,16.283 11,16C11,15.717 11.096,15.479 11.288,15.288C11.479,15.096 11.717,15 12,15H20C20.283,15 20.521,15.096 20.712,15.288C20.904,15.479 21,15.717 21,16C21,16.283 20.904,16.521 20.712,16.712C20.521,16.904 20.283,17 20,17H12ZM12,13C11.717,13 11.479,12.904 11.288,12.712C11.096,12.521 11,12.283 11,12C11,11.717 11.096,11.479 11.288,11.288C11.479,11.096 11.717,11 12,11H20C20.283,11 20.521,11.096 20.712,11.288C20.904,11.479 21,11.717 21,12C21,12.283 20.904,12.521 20.712,12.712C20.521,12.904 20.283,13 20,13H12ZM12,9C11.717,9 11.479,8.904 11.288,8.712C11.096,8.521 11,8.283 11,8C11,7.717 11.096,7.479 11.288,7.287C11.479,7.096 11.717,7 12,7H20C20.283,7 20.521,7.096 20.712,7.287C20.904,7.479 21,7.717 21,8C21,8.283 20.904,8.521 20.712,8.712C20.521,8.904 20.283,9 20,9H12ZM4,5C3.717,5 3.479,4.904 3.287,4.713C3.096,4.521 3,4.283 3,4C3,3.717 3.096,3.479 3.287,3.287C3.479,3.096 3.717,3 4,3H20C20.283,3 20.521,3.096 20.712,3.287C20.904,3.479 21,3.717 21,4C21,4.283 20.904,4.521 20.712,4.713C20.521,4.904 20.283,5 20,5H4ZM3.85,15.15C3.683,15.317 3.5,15.358 3.3,15.275C3.1,15.192 3,15.033 3,14.8V9.2C3,8.967 3.1,8.808 3.3,8.725C3.5,8.642 3.683,8.683 3.85,8.85L6.65,11.65C6.75,11.75 6.8,11.867 6.8,12C6.8,12.133 6.75,12.25 6.65,12.35L3.85,15.15Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M14.958,5.621C15.116,5.092 14.816,4.534 14.287,4.375C13.758,4.217 13.201,4.517 13.042,5.046L9.042,18.379C8.883,18.908 9.184,19.466 9.713,19.625C10.242,19.783 10.799,19.483 10.958,18.954L14.958,5.621Z"
android:fillColor="#656D77"/>
<path
android:pathData="M5.974,7.232C5.549,6.878 4.919,6.936 4.565,7.36L1.232,11.36C0.923,11.731 0.923,12.269 1.232,12.64L4.565,16.64C4.919,17.065 5.549,17.122 5.974,16.768C6.398,16.415 6.455,15.784 6.102,15.36L3.302,12L6.102,8.64C6.455,8.216 6.398,7.585 5.974,7.232Z"
android:fillColor="#656D77"/>
<path
android:pathData="M18.027,7.232C18.451,6.878 19.081,6.936 19.435,7.36L22.768,11.36C23.077,11.731 23.077,12.269 22.768,12.64L19.435,16.64C19.081,17.065 18.451,17.122 18.027,16.768C17.602,16.415 17.545,15.784 17.898,15.36L20.698,12L17.898,8.64C17.545,8.216 17.602,7.585 18.027,7.232Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6.25,19C5.9,19 5.604,18.879 5.363,18.638C5.121,18.396 5,18.1 5,17.75C5,17.4 5.121,17.104 5.363,16.862C5.604,16.621 5.9,16.5 6.25,16.5H9L12,7.5H9.25C8.9,7.5 8.604,7.379 8.363,7.137C8.121,6.896 8,6.6 8,6.25C8,5.9 8.121,5.604 8.363,5.363C8.604,5.121 8.9,5 9.25,5H16.75C17.1,5 17.396,5.121 17.638,5.363C17.879,5.604 18,5.9 18,6.25C18,6.6 17.879,6.896 17.638,7.137C17.396,7.379 17.1,7.5 16.75,7.5H14.5L11.5,16.5H13.75C14.1,16.5 14.396,16.621 14.637,16.862C14.879,17.104 15,17.4 15,17.75C15,18.1 14.879,18.396 14.637,18.638C14.396,18.879 14.1,19 13.75,19H6.25Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,19.071C11.022,20.049 9.843,20.538 8.464,20.538C7.086,20.538 5.907,20.049 4.929,19.071C3.951,18.093 3.462,16.914 3.462,15.536C3.462,14.157 3.951,12.978 4.929,12L7.05,9.879C7.251,9.678 7.486,9.578 7.757,9.578C8.028,9.578 8.264,9.678 8.464,9.879C8.665,10.079 8.765,10.315 8.765,10.586C8.765,10.857 8.665,11.093 8.464,11.293L6.343,13.414C5.754,14.003 5.459,14.711 5.459,15.536C5.459,16.361 5.754,17.068 6.343,17.657C6.932,18.246 7.639,18.541 8.464,18.541C9.289,18.541 9.997,18.246 10.586,17.657L12.707,15.536C12.907,15.335 13.143,15.235 13.414,15.235C13.685,15.235 13.921,15.335 14.121,15.536C14.322,15.736 14.422,15.972 14.422,16.243C14.422,16.514 14.322,16.749 14.121,16.95L12,19.071ZM10.586,14.828C10.385,15.029 10.15,15.129 9.879,15.129C9.608,15.129 9.372,15.029 9.172,14.828C8.971,14.628 8.871,14.392 8.871,14.121C8.871,13.85 8.971,13.615 9.172,13.414L13.414,9.172C13.615,8.971 13.85,8.871 14.121,8.871C14.392,8.871 14.628,8.971 14.828,9.172C15.029,9.372 15.129,9.608 15.129,9.879C15.129,10.15 15.029,10.385 14.828,10.586L10.586,14.828ZM16.95,14.121C16.749,14.322 16.514,14.422 16.243,14.422C15.972,14.422 15.736,14.322 15.535,14.121C15.335,13.921 15.235,13.685 15.235,13.414C15.235,13.143 15.335,12.908 15.535,12.707L17.657,10.586C18.246,9.997 18.541,9.289 18.541,8.465C18.541,7.64 18.246,6.932 17.657,6.343C17.068,5.754 16.361,5.459 15.535,5.459C14.711,5.459 14.003,5.754 13.414,6.343L11.293,8.465C11.092,8.665 10.857,8.765 10.586,8.765C10.315,8.765 10.079,8.665 9.879,8.465C9.678,8.264 9.578,8.028 9.578,7.757C9.578,7.486 9.678,7.251 9.879,7.05L12,4.929C12.978,3.951 14.157,3.462 15.535,3.462C16.914,3.462 18.093,3.951 19.071,4.929C20.049,5.907 20.538,7.086 20.538,8.465C20.538,9.843 20.049,11.022 19.071,12L16.95,14.121Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3.75,22C3.533,22 3.354,21.929 3.213,21.788C3.071,21.646 3,21.467 3,21.25C3,21.033 3.071,20.854 3.213,20.712C3.354,20.571 3.533,20.5 3.75,20.5H5.5V19.75H4.75C4.533,19.75 4.354,19.679 4.213,19.538C4.071,19.396 4,19.217 4,19C4,18.783 4.071,18.604 4.213,18.462C4.354,18.321 4.533,18.25 4.75,18.25H5.5V17.5H3.75C3.533,17.5 3.354,17.429 3.213,17.288C3.071,17.146 3,16.967 3,16.75C3,16.533 3.071,16.354 3.213,16.212C3.354,16.071 3.533,16 3.75,16H6C6.283,16 6.521,16.096 6.713,16.288C6.904,16.479 7,16.717 7,17V18C7,18.283 6.904,18.521 6.713,18.712C6.521,18.904 6.283,19 6,19C6.283,19 6.521,19.096 6.713,19.288C6.904,19.479 7,19.717 7,20V21C7,21.283 6.904,21.521 6.713,21.712C6.521,21.904 6.283,22 6,22H3.75ZM3.75,15C3.533,15 3.354,14.929 3.213,14.788C3.071,14.646 3,14.467 3,14.25V12.25C3,11.967 3.096,11.729 3.287,11.538C3.479,11.346 3.717,11.25 4,11.25H5.5V10.5H3.75C3.533,10.5 3.354,10.429 3.213,10.288C3.071,10.146 3,9.967 3,9.75C3,9.533 3.071,9.354 3.213,9.212C3.354,9.071 3.533,9 3.75,9H6C6.283,9 6.521,9.096 6.713,9.288C6.904,9.479 7,9.717 7,10V11.75C7,12.033 6.904,12.271 6.713,12.462C6.521,12.654 6.283,12.75 6,12.75H4.5V13.5H6.25C6.467,13.5 6.646,13.571 6.787,13.712C6.929,13.854 7,14.033 7,14.25C7,14.467 6.929,14.646 6.787,14.788C6.646,14.929 6.467,15 6.25,15H3.75ZM5.25,8C5.033,8 4.854,7.929 4.713,7.787C4.571,7.646 4.5,7.467 4.5,7.25V3.5H3.75C3.533,3.5 3.354,3.429 3.213,3.287C3.071,3.146 3,2.967 3,2.75C3,2.533 3.071,2.354 3.213,2.213C3.354,2.071 3.533,2 3.75,2H5.25C5.467,2 5.646,2.071 5.787,2.213C5.929,2.354 6,2.533 6,2.75V7.25C6,7.467 5.929,7.646 5.787,7.787C5.646,7.929 5.467,8 5.25,8ZM10,19C9.717,19 9.479,18.904 9.288,18.712C9.096,18.521 9,18.283 9,18C9,17.717 9.096,17.479 9.288,17.288C9.479,17.096 9.717,17 10,17H20C20.283,17 20.521,17.096 20.712,17.288C20.904,17.479 21,17.717 21,18C21,18.283 20.904,18.521 20.712,18.712C20.521,18.904 20.283,19 20,19H10ZM10,13C9.717,13 9.479,12.904 9.288,12.712C9.096,12.521 9,12.283 9,12C9,11.717 9.096,11.479 9.288,11.288C9.479,11.096 9.717,11 10,11H20C20.283,11 20.521,11.096 20.712,11.288C20.904,11.479 21,11.717 21,12C21,12.283 20.904,12.521 20.712,12.712C20.521,12.904 20.283,13 20,13H10ZM10,7C9.717,7 9.479,6.904 9.288,6.713C9.096,6.521 9,6.283 9,6C9,5.717 9.096,5.479 9.288,5.287C9.479,5.096 9.717,5 10,5H20C20.283,5 20.521,5.096 20.712,5.287C20.904,5.479 21,5.717 21,6C21,6.283 20.904,6.521 20.712,6.713C20.521,6.904 20.283,7 20,7H10Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4.719,4.34C4.813,3.698 4.353,3.104 3.691,3.012C3.028,2.92 2.415,3.366 2.32,4.008L1.512,9.486C1.418,10.128 1.878,10.723 2.54,10.814C3.203,10.906 3.816,10.46 3.911,9.818L4.719,4.34Z"
android:fillColor="#656D77"/>
<path
android:pathData="M16.834,14.514C16.928,13.872 16.468,13.277 15.806,13.186C15.144,13.094 14.53,13.54 14.435,14.182L13.627,19.66C13.533,20.302 13.993,20.896 14.656,20.988C15.318,21.08 15.932,20.634 16.026,19.992L16.834,14.514Z"
android:fillColor="#656D77"/>
<path
android:pathData="M9.318,3.009C9.983,3.086 10.456,3.671 10.376,4.315L10.354,4.49C10.34,4.602 10.319,4.763 10.293,4.961C10.242,5.358 10.17,5.902 10.088,6.496C9.927,7.667 9.72,9.075 9.553,9.882C9.422,10.518 8.784,10.931 8.128,10.803C7.472,10.676 7.046,10.058 7.177,9.422C7.326,8.701 7.523,7.37 7.687,6.185C7.767,5.599 7.838,5.061 7.889,4.669C7.915,4.473 7.935,4.314 7.949,4.204L7.97,4.034C8.05,3.39 8.654,2.931 9.318,3.009Z"
android:fillColor="#656D77"/>
<path
android:pathData="M22.488,14.514C22.582,13.872 22.122,13.277 21.46,13.186C20.797,13.094 20.184,13.54 20.089,14.182L19.281,19.66C19.187,20.302 19.647,20.896 20.309,20.988C20.972,21.08 21.585,20.634 21.68,19.992L22.488,14.514Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.15,20C10.883,20 9.758,19.625 8.775,18.875C7.792,18.125 7.083,17.1 6.65,15.8L8.85,14.85C9.083,15.65 9.488,16.308 10.063,16.825C10.637,17.342 11.35,17.6 12.2,17.6C12.9,17.6 13.533,17.433 14.1,17.1C14.667,16.767 14.95,16.233 14.95,15.5C14.95,15.2 14.892,14.925 14.775,14.675C14.658,14.425 14.5,14.2 14.3,14H17.1C17.183,14.233 17.246,14.471 17.288,14.712C17.329,14.954 17.35,15.217 17.35,15.5C17.35,16.933 16.837,18.042 15.813,18.825C14.788,19.608 13.567,20 12.15,20ZM3,12C2.717,12 2.479,11.904 2.287,11.712C2.096,11.521 2,11.283 2,11C2,10.717 2.096,10.479 2.287,10.288C2.479,10.096 2.717,10 3,10H21C21.283,10 21.521,10.096 21.712,10.288C21.904,10.479 22,10.717 22,11C22,11.283 21.904,11.521 21.712,11.712C21.521,11.904 21.283,12 21,12H3ZM12.05,3.85C13.15,3.85 14.113,4.121 14.938,4.662C15.762,5.204 16.4,6.033 16.85,7.15L14.65,8.125C14.5,7.642 14.221,7.208 13.813,6.825C13.404,6.442 12.833,6.25 12.1,6.25C11.417,6.25 10.85,6.404 10.4,6.713C9.95,7.021 9.7,7.45 9.65,8H7.25C7.283,6.85 7.738,5.871 8.613,5.063C9.488,4.254 10.633,3.85 12.05,3.85Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,21C5.717,21 5.479,20.904 5.287,20.712C5.096,20.521 5,20.283 5,20C5,19.717 5.096,19.479 5.287,19.288C5.479,19.096 5.717,19 6,19H18C18.283,19 18.521,19.096 18.712,19.288C18.904,19.479 19,19.717 19,20C19,20.283 18.904,20.521 18.712,20.712C18.521,20.904 18.283,21 18,21H6ZM12,17C10.317,17 9.008,16.475 8.075,15.425C7.142,14.375 6.675,12.983 6.675,11.25V4.275C6.675,3.925 6.804,3.625 7.063,3.375C7.321,3.125 7.625,3 7.975,3C8.325,3 8.625,3.125 8.875,3.375C9.125,3.625 9.25,3.925 9.25,4.275V11.4C9.25,12.333 9.483,13.092 9.95,13.675C10.417,14.258 11.1,14.55 12,14.55C12.9,14.55 13.583,14.258 14.05,13.675C14.517,13.092 14.75,12.333 14.75,11.4V4.275C14.75,3.925 14.879,3.625 15.137,3.375C15.396,3.125 15.7,3 16.05,3C16.4,3 16.7,3.125 16.95,3.375C17.2,3.625 17.325,3.925 17.325,4.275V11.25C17.325,12.983 16.858,14.375 15.925,15.425C14.992,16.475 13.683,17 12,17Z"
android:fillColor="#656D77"/>
</vector>

View file

@ -31,6 +31,8 @@ dependencies {
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.constraintlayout.compose)
implementation(libs.matrix.richtexteditor)
api(libs.matrix.richtexteditor.compose)

View file

@ -18,29 +18,29 @@ package io.element.android.libraries.textcomposer
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -51,21 +51,22 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension.Companion.fillToConstraints
import androidx.constraintlayout.compose.Visibility
import io.element.android.libraries.designsystem.VectorIcons
import io.element.android.libraries.designsystem.modifiers.applyIf
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.text.applyScaleUp
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
@ -73,12 +74,17 @@ import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
import io.element.android.libraries.textcomposer.components.FormattingOption
import io.element.android.libraries.textcomposer.components.FormattingOptionState
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.wysiwyg.compose.RichTextEditor
import io.element.android.wysiwyg.compose.RichTextEditorDefaults
import io.element.android.wysiwyg.compose.RichTextEditorState
import io.element.android.wysiwyg.view.models.InlineFormat
import kotlinx.coroutines.android.awaitFrame
import uniffi.wysiwyg_composer.ActionState
import uniffi.wysiwyg_composer.ComposerAction
@Composable
fun TextComposer(
@ -86,112 +92,130 @@ fun TextComposer(
composerMode: MessageComposerMode,
canSendMessage: Boolean,
modifier: Modifier = Modifier,
showTextFormatting: Boolean = false,
onRequestFocus: () -> Unit = {},
onSendMessage: (Message) -> Unit = {},
onResetComposerMode: () -> Unit = {},
onAddAttachment: () -> Unit = {},
onDismissTextFormatting: () -> Unit = {},
onError: (Throwable) -> Unit = {},
) {
Row(
modifier.padding(
horizontal = 12.dp,
vertical = 8.dp
), verticalAlignment = Alignment.Bottom
) {
AttachmentButton(onClick = onAddAttachment, modifier = Modifier.padding(vertical = 6.dp))
Spacer(modifier = Modifier.width(12.dp))
val roundCornerSmall = 20.dp.applyScaleUp()
val roundCornerLarge = 28.dp.applyScaleUp()
val onSendClicked = {
onSendMessage(Message(html = state.messageHtml, markdown = state.messageMarkdown))
}
val roundedCornerSize = remember(state.lineCount, composerMode) {
if (state.lineCount > 1 || composerMode is MessageComposerMode.Special) {
roundCornerSmall
} else {
roundCornerLarge
}
}
val roundedCornerSizeState = animateDpAsState(
targetValue = roundedCornerSize,
animationSpec = tween(
durationMillis = 100,
Column(
modifier = modifier
.padding(
start = 3.dp,
end = 6.dp,
top = 8.dp,
bottom = 5.dp,
)
)
val roundedCorners = RoundedCornerShape(roundedCornerSizeState.value)
val minHeight = 42.dp.applyScaleUp()
val colors = ElementTheme.colors
val bgColor = colors.bgSubtleSecondary
val borderColor by remember(state.hasFocus, colors) {
derivedStateOf {
if (state.hasFocus) colors.borderDisabled else bgColor
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.clip(roundedCorners)
.background(color = bgColor)
.border(1.dp, borderColor, roundedCorners)
.fillMaxWidth(),
) {
ConstraintLayout(
modifier = Modifier.fillMaxWidth(),
) {
if (composerMode is MessageComposerMode.Special) {
ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode)
val (composeOptions, textInput, sendButton) = createRefs()
val showComposerOptionsButton by remember(showTextFormatting) {
derivedStateOf { !showTextFormatting }
}
val defaultTypography = ElementTheme.typography.fontBodyLgRegular
Box {
Box(
modifier = Modifier
.heightIn(min = minHeight)
.background(color = bgColor, shape = roundedCorners)
.padding(
PaddingValues(
top = 4.dp.applyScaleUp(),
bottom = 4.dp.applyScaleUp(),
start = 12.dp.applyScaleUp(),
end = 42.dp.applyScaleUp()
)
),
contentAlignment = Alignment.CenterStart,
) {
// Placeholder
if (state.messageHtml.isEmpty()) {
Text(
stringResource(CommonStrings.common_message),
style = defaultTypography.copy(
color = ElementTheme.colors.textDisabled,
),
)
}
RichTextEditor(
state = state,
modifier = Modifier
.fillMaxWidth(),
style = RichTextEditorDefaults.style(
text = RichTextEditorDefaults.textStyle(
color = if (state.hasFocus) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.secondary
}
),
cursor = RichTextEditorDefaults.cursorStyle(
color = ElementTheme.colors.iconAccentTertiary,
)
),
onError = onError
)
}
SendButton(
canSendMessage = canSendMessage,
onClick = { onSendMessage(Message(html = state.messageHtml, markdown = state.messageMarkdown)) },
composerMode = composerMode,
modifier = Modifier.padding(end = 6.dp.applyScaleUp(), bottom = 6.dp.applyScaleUp())
IconButton(
modifier = Modifier
.size(48.dp)
.constrainAs(composeOptions) {
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
visibility = if (showComposerOptionsButton) Visibility.Visible else Visibility.Gone
},
onClick = onAddAttachment
) {
Icon(
modifier = Modifier.size(30.dp.applyScaleUp()),
resourceId = R.drawable.ic_plus, // TODO Replace with design system icon when available
contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment),
tint = ElementTheme.colors.iconPrimary,
)
}
val roundCornerSmall = 20.dp.applyScaleUp()
val roundCornerLarge = 28.dp.applyScaleUp()
val roundedCornerSize = remember(state.lineCount, composerMode) {
if (state.lineCount > 1 || composerMode is MessageComposerMode.Special) {
roundCornerSmall
} else {
roundCornerLarge
}
}
val roundedCornerSizeState = animateDpAsState(
targetValue = roundedCornerSize,
animationSpec = tween(
durationMillis = 100,
)
)
val roundedCorners = RoundedCornerShape(roundedCornerSizeState.value)
val colors = ElementTheme.colors
val bgColor = colors.bgSubtleSecondary
val borderColor by remember(state.hasFocus, colors) {
derivedStateOf {
if (state.hasFocus) colors.borderDisabled else bgColor
}
}
Column(
modifier = Modifier
.constrainAs(textInput) {
start.linkTo(composeOptions.end, margin = 3.dp, goneMargin = 9.dp)
end.linkTo(sendButton.start, margin = 6.dp, goneMargin = 6.dp)
bottom.linkTo(parent.bottom)
width = fillToConstraints
}
.padding(vertical = 3.dp)
.fillMaxWidth()
.clip(roundedCorners)
.background(color = bgColor)
.border(1.dp, borderColor, roundedCorners)
) {
if (composerMode is MessageComposerMode.Special) {
ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode)
}
TextInput(
state = state,
roundedCorners = roundedCorners,
bgColor = bgColor,
onError = onError,
)
}
SendButton(
canSendMessage = canSendMessage,
onClick = onSendClicked,
composerMode = composerMode,
modifier = Modifier
.constrainAs(sendButton) {
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
visibility = if (!showTextFormatting) Visibility.Visible else Visibility.Gone
}
)
}
if (showTextFormatting) {
TextFormatting(
state = state,
onDismiss = onDismissTextFormatting,
sendButton = {
SendButton(
canSendMessage = canSendMessage,
onClick = onSendClicked,
composerMode = composerMode,
modifier = it
)
},
)
}
}
@ -208,6 +232,192 @@ fun TextComposer(
}
}
@Composable
private fun TextInput(
state: RichTextEditorState,
roundedCorners: RoundedCornerShape,
bgColor: Color,
modifier: Modifier = Modifier,
onError: (Throwable) -> Unit = {},
) {
val minHeight = 42.dp.applyScaleUp()
val defaultTypography = ElementTheme.typography.fontBodyLgRegular
Box(
modifier = modifier
.heightIn(min = minHeight)
.background(color = bgColor, shape = roundedCorners)
.padding(
PaddingValues(
top = 4.dp.applyScaleUp(),
bottom = 4.dp.applyScaleUp(),
start = 12.dp.applyScaleUp(),
end = 42.dp.applyScaleUp()
)
),
contentAlignment = Alignment.CenterStart,
) {
// Placeholder
if (state.messageHtml.isEmpty()) {
Text(
stringResource(CommonStrings.common_message),
style = defaultTypography.copy(
color = ElementTheme.colors.textDisabled,
),
)
}
RichTextEditor(
state = state,
modifier = Modifier
.fillMaxWidth(),
style = RichTextEditorDefaults.style(
text = RichTextEditorDefaults.textStyle(
color = if (state.hasFocus) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.secondary
}
),
cursor = RichTextEditorDefaults.cursorStyle(
color = ElementTheme.colors.iconAccentTertiary,
)
),
onError = onError
)
}
}
@Composable
private fun TextFormatting(
state: RichTextEditorState,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
sendButton: @Composable (modifier: Modifier) -> Unit,
) {
ConstraintLayout(
modifier = modifier
.fillMaxWidth()
) {
val (close, formatting, send) = createRefs()
IconButton(
modifier = Modifier
.size(48.dp)
.constrainAs(close) {
start.linkTo(parent.start)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
},
onClick = onDismiss
) {
Icon(
modifier = Modifier.size(30.dp.applyScaleUp()),
resourceId = R.drawable.ic_cancel, // TODO Replace with design system icon when available
contentDescription = stringResource(CommonStrings.action_close),
tint = ElementTheme.colors.iconPrimary,
)
}
val scrollState = rememberScrollState()
Row(
modifier = Modifier
.constrainAs(formatting) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(close.end, margin = 3.dp)
end.linkTo(send.start, margin = 20.dp)
width = fillToConstraints
}
.horizontalScroll(scrollState),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
FormattingOption(
state = state.actions[ComposerAction.BOLD].toButtonState(),
onClick = { state.toggleInlineFormat(InlineFormat.Bold) },
imageVector = ImageVector.vectorResource(VectorIcons.Bold),
contentDescription = stringResource(CommonStrings.rich_text_editor_format_bold)
)
FormattingOption(
state = state.actions[ComposerAction.ITALIC].toButtonState(),
onClick = { state.toggleInlineFormat(InlineFormat.Italic) },
imageVector = ImageVector.vectorResource(VectorIcons.Italic),
contentDescription = stringResource(CommonStrings.rich_text_editor_format_italic)
)
FormattingOption(
state = state.actions[ComposerAction.UNDERLINE].toButtonState(),
onClick = { state.toggleInlineFormat(InlineFormat.Underline) },
imageVector = ImageVector.vectorResource(VectorIcons.Underline),
contentDescription = stringResource(CommonStrings.rich_text_editor_format_underline)
)
FormattingOption(
state = state.actions[ComposerAction.STRIKE_THROUGH].toButtonState(),
onClick = { state.toggleInlineFormat(InlineFormat.StrikeThrough) },
imageVector = ImageVector.vectorResource(VectorIcons.Strikethrough),
contentDescription = stringResource(CommonStrings.rich_text_editor_format_strikethrough)
)
FormattingOption(
state = state.actions[ComposerAction.UNORDERED_LIST].toButtonState(),
onClick = { state.toggleList(ordered = false) },
imageVector = ImageVector.vectorResource(VectorIcons.BulletList),
contentDescription = stringResource(CommonStrings.rich_text_editor_bullet_list)
)
FormattingOption(
state = state.actions[ComposerAction.ORDERED_LIST].toButtonState(),
onClick = { state.toggleList(ordered = true) },
imageVector = ImageVector.vectorResource(VectorIcons.NumberedList),
contentDescription = stringResource(CommonStrings.rich_text_editor_numbered_list)
)
FormattingOption(
state = state.actions[ComposerAction.INDENT].toButtonState(),
onClick = { state.indent() },
imageVector = ImageVector.vectorResource(VectorIcons.IndentIncrease),
contentDescription = stringResource(CommonStrings.rich_text_editor_indent)
)
FormattingOption(
state = state.actions[ComposerAction.UNINDENT].toButtonState(),
onClick = { state.unindent() },
imageVector = ImageVector.vectorResource(VectorIcons.IndentDecrease),
contentDescription = stringResource(CommonStrings.rich_text_editor_unindent)
)
FormattingOption(
state = state.actions[ComposerAction.INLINE_CODE].toButtonState(),
onClick = { state.toggleInlineFormat(InlineFormat.InlineCode) },
imageVector = ImageVector.vectorResource(VectorIcons.InlineCode),
contentDescription = stringResource(CommonStrings.rich_text_editor_inline_code)
)
FormattingOption(
state = state.actions[ComposerAction.CODE_BLOCK].toButtonState(),
onClick = { state.toggleCodeBlock() },
imageVector = ImageVector.vectorResource(VectorIcons.CodeBlock),
contentDescription = stringResource(CommonStrings.rich_text_editor_code_block)
)
FormattingOption(
state = state.actions[ComposerAction.QUOTE].toButtonState(),
onClick = { state.toggleQuote() },
imageVector = ImageVector.vectorResource(VectorIcons.Quote),
contentDescription = stringResource(CommonStrings.rich_text_editor_quote)
)
}
sendButton(
Modifier.constrainAs(send) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
},
)
}
}
private fun ActionState?.toButtonState(): FormattingOptionState =
when (this) {
ActionState.ENABLED -> FormattingOptionState.Default
ActionState.REVERSED -> FormattingOptionState.Selected
ActionState.DISABLED, null -> FormattingOptionState.Disabled
}
@Composable
private fun ComposerModeView(
composerMode: MessageComposerMode,
@ -341,53 +551,17 @@ private fun ReplyToModeView(
}
@Composable
private fun AttachmentButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Surface(
modifier
.size(30.dp.applyScaleUp())
.clickable(onClick = onClick),
shape = CircleShape,
color = ElementTheme.colors.iconPrimary
) {
Image(
modifier = Modifier.size(12.5f.dp.applyScaleUp()),
painter = painterResource(R.drawable.ic_add_attachment),
contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment),
contentScale = ContentScale.Inside,
colorFilter = ColorFilter.tint(
LocalContentColor.current
)
)
}
}
@Composable
private fun BoxScope.SendButton(
private fun SendButton(
canSendMessage: Boolean,
onClick: () -> Unit,
composerMode: MessageComposerMode,
modifier: Modifier = Modifier,
) {
val interactionSource = remember { MutableInteractionSource() }
Box(
IconButton(
modifier = modifier
.clip(CircleShape)
.background(if (canSendMessage) ElementTheme.colors.iconAccentTertiary else Color.Transparent)
.size(30.dp.applyScaleUp())
.align(Alignment.BottomEnd)
.applyIf(composerMode !is MessageComposerMode.Edit, ifTrue = {
padding(start = 1.dp.applyScaleUp()) // Center the arrow in the circle
})
.clickable(
enabled = canSendMessage,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false),
onClick = onClick,
),
contentAlignment = Alignment.Center,
.size(48.dp.applyScaleUp()),
onClick = onClick,
enabled = canSendMessage,
) {
val iconId = when (composerMode) {
is MessageComposerMode.Edit -> R.drawable.ic_tick
@ -397,13 +571,22 @@ private fun BoxScope.SendButton(
is MessageComposerMode.Edit -> stringResource(CommonStrings.action_edit)
else -> stringResource(CommonStrings.action_send)
}
Icon(
modifier = Modifier.size(16.dp.applyScaleUp()),
resourceId = iconId,
contentDescription = contentDescription,
// Exception here, we use Color.White instead of ElementTheme.colors.iconOnSolidPrimary
tint = if (canSendMessage) Color.White else ElementTheme.colors.iconDisabled
)
Box(
modifier = Modifier
.clip(CircleShape)
.size(36.dp.applyScaleUp())
.background(if (canSendMessage) ElementTheme.colors.iconAccentTertiary else Color.Transparent)
) {
Icon(
modifier = Modifier
.height(18.dp.applyScaleUp())
.align(Alignment.Center),
resourceId = iconId,
contentDescription = contentDescription,
// Exception here, we use Color.White instead of ElementTheme.colors.iconOnSolidPrimary
tint = if (canSendMessage) Color.White else ElementTheme.colors.iconDisabled
)
}
}
}
@ -447,6 +630,31 @@ internal fun TextComposerSimplePreview() = ElementPreview {
}
}
@DayNightPreviews
@Composable
internal fun TextComposerFormattingPreview() = ElementPreview {
Column {
TextComposer(
RichTextEditorState("", fake = true),
canSendMessage = false,
showTextFormatting = true,
composerMode = MessageComposerMode.Normal(""),
)
TextComposer(
RichTextEditorState("A message", fake = true),
canSendMessage = true,
showTextFormatting = true,
composerMode = MessageComposerMode.Normal(""),
)
TextComposer(
RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", fake = true),
canSendMessage = true,
showTextFormatting = true,
composerMode = MessageComposerMode.Normal(""),
)
}
}
@DayNightPreviews
@Composable
internal fun TextComposerEditPreview() = ElementPreview {

View file

@ -0,0 +1,69 @@
/*
* 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.textcomposer.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.text.applyScaleUp
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.theme.compound.generated.SemanticColors
@Composable
internal fun FormattingOption(
state: FormattingOptionState,
onClick: () -> Unit,
imageVector: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
colors: SemanticColors = ElementTheme.colors,
) {
val backgroundColor = when (state) {
FormattingOptionState.Selected -> colors.bgActionPrimaryRest
FormattingOptionState.Default,
FormattingOptionState.Disabled -> Color.Transparent
}
val foregroundColor = when (state) {
FormattingOptionState.Selected -> colors.iconOnSolidPrimary
FormattingOptionState.Default -> colors.iconPrimary
FormattingOptionState.Disabled -> colors.iconDisabled
}
Box(
modifier = modifier
.clickable { onClick() }
.size(44.dp.applyScaleUp())
.background(backgroundColor, shape = RoundedCornerShape(4.dp.applyScaleUp()))
) {
Icon(
modifier = Modifier.align(Alignment.Center),
imageVector = imageVector,
contentDescription = contentDescription,
tint = foregroundColor,
)
}
}

View file

@ -0,0 +1,22 @@
/*
* 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.textcomposer.components
internal enum class FormattingOptionState {
Default, Selected, Disabled
}

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,773Q457.91,773 441.46,756.54Q425,740.09 425,718.31L425,535L241.69,535Q219.91,535 203.46,518.54Q187,502.09 187,480Q187,457.91 203.46,441.46Q219.91,425 241.69,425L425,425L425,241.69Q425,219.91 441.46,203.46Q457.91,187 480,187Q502.09,187 518.54,203.46Q535,219.91 535,241.69L535,425L718.31,425Q740.09,425 756.54,441.46Q773,457.91 773,480Q773,502.09 756.54,518.54Q740.09,535 718.31,535L535,535L535,718.31Q535,740.09 518.54,756.54Q502.09,773 480,773Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M16,18.121L19.182,21.303C19.483,21.604 19.836,21.754 20.243,21.754C20.649,21.754 21.003,21.604 21.303,21.303C21.604,21.003 21.754,20.649 21.754,20.243C21.754,19.836 21.604,19.483 21.303,19.182L18.121,16L21.303,12.818C21.604,12.517 21.754,12.164 21.754,11.757C21.754,11.351 21.604,10.997 21.303,10.697C21.003,10.396 20.649,10.246 20.243,10.246C19.836,10.246 19.483,10.396 19.182,10.697L16,13.879L12.818,10.697C12.517,10.396 12.164,10.246 11.757,10.246C11.351,10.246 10.997,10.396 10.697,10.697C10.396,10.997 10.246,11.351 10.246,11.757C10.246,12.164 10.396,12.517 10.697,12.818L13.879,16L10.697,19.182C10.396,19.483 10.246,19.836 10.246,20.243C10.246,20.649 10.396,21.003 10.697,21.303C10.997,21.604 11.351,21.754 11.757,21.754C12.164,21.754 12.517,21.604 12.818,21.303L16,18.121ZM26.607,26.607C25.139,28.074 23.482,29.174 21.635,29.908C19.787,30.642 17.909,31.008 16,31.008C14.091,31.008 12.213,30.642 10.365,29.908C8.518,29.174 6.861,28.074 5.393,26.607C3.926,25.139 2.826,23.482 2.092,21.635C1.358,19.787 0.992,17.909 0.992,16C0.992,14.091 1.358,12.213 2.092,10.365C2.826,8.518 3.926,6.861 5.393,5.393C6.861,3.926 8.518,2.826 10.365,2.092C12.213,1.358 14.091,0.992 16,0.992C17.909,0.992 19.787,1.358 21.635,2.092C23.482,2.826 25.139,3.926 26.607,5.393C28.074,6.861 29.174,8.518 29.908,10.365C30.642,12.213 31.008,14.091 31.008,16C31.008,17.909 30.642,19.787 29.908,21.635C29.174,23.482 28.074,25.139 26.607,26.607Z"
android:fillColor="#1B1D22"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="30"
android:viewportHeight="30">
<path
android:pathData="M13.5,16.5V21C13.5,21.425 13.644,21.781 13.931,22.069C14.219,22.356 14.575,22.5 15,22.5C15.425,22.5 15.781,22.356 16.069,22.069C16.356,21.781 16.5,21.425 16.5,21V16.5H21C21.425,16.5 21.781,16.356 22.069,16.069C22.356,15.781 22.5,15.425 22.5,15C22.5,14.575 22.356,14.219 22.069,13.931C21.781,13.644 21.425,13.5 21,13.5H16.5V9C16.5,8.575 16.356,8.219 16.069,7.931C15.781,7.644 15.425,7.5 15,7.5C14.575,7.5 14.219,7.644 13.931,7.931C13.644,8.219 13.5,8.575 13.5,9V13.5H9C8.575,13.5 8.219,13.644 7.931,13.931C7.644,14.219 7.5,14.575 7.5,15C7.5,15.425 7.644,15.781 7.931,16.069C8.219,16.356 8.575,16.5 9,16.5H13.5ZM15,30C12.925,30 10.975,29.606 9.15,28.819C7.325,28.031 5.738,26.962 4.387,25.612C3.037,24.263 1.969,22.675 1.181,20.85C0.394,19.025 0,17.075 0,15C0,12.925 0.394,10.975 1.181,9.15C1.969,7.325 3.037,5.738 4.387,4.387C5.738,3.037 7.325,1.969 9.15,1.181C10.975,0.394 12.925,0 15,0C17.075,0 19.025,0.394 20.85,1.181C22.675,1.969 24.263,3.037 25.612,4.387C26.962,5.738 28.031,7.325 28.819,9.15C29.606,10.975 30,12.925 30,15C30,17.075 29.606,19.025 28.819,20.85C28.031,22.675 26.962,24.263 25.612,25.612C24.263,26.962 22.675,28.031 20.85,28.819C19.025,29.606 17.075,30 15,30Z"
android:fillColor="#1B1D22"/>
</vector>

View file

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
android:width="21dp"
android:height="18dp"
android:viewportWidth="21"
android:viewportHeight="18">
<path
android:pathData="M15.404,8.965L1.563,15.882C0.631,16.348 -0.34,15.348 0.116,14.435C0.116,14.435 1.832,10.971 2.303,10.064C2.775,9.156 3.315,8.999 8.331,8.351C8.517,8.327 8.669,8.187 8.669,8C8.669,7.813 8.517,7.673 8.331,7.649C3.315,7.001 2.775,6.844 2.303,5.936C1.832,5.029 0.116,1.565 0.116,1.565C-0.34,0.653 0.631,-0.348 1.563,0.118L15.404,7.036C16.199,7.433 16.199,8.567 15.404,8.965Z"
android:fillColor="#A6ADB7"/>
android:pathData="M20.252,10.085 L4.681,17.867c-1.049,0.525 -2.141,-0.601 -1.628,-1.627 0,0 1.93,-3.897 2.461,-4.918 0.531,-1.021 1.138,-1.197 6.781,-1.927 0.209,-0.027 0.38,-0.185 0.38,-0.395 0,-0.21 -0.171,-0.368 -0.38,-0.395C6.652,7.876 6.045,7.699 5.514,6.678 4.983,5.658 3.053,1.76 3.053,1.76 2.54,0.734 3.632,-0.391 4.681,0.133L20.252,7.915c0.894,0.446 0.894,1.723 0,2.17z"
android:fillColor="@android:color/white"/>
</vector>

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79aeef6875265e119c3b4b97cea4d36ba3354ae52c4b94b69bbc09461b7bc319
size 22259
oid sha256:e67b171ca09fd2efb25338a20fe7af7464e0e1ecb1b02afd679dbfc7cd3cd7df
size 25646

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8dafa9a97ebc77f00fdb0432c7b94272f6ea1873c3475353be47ecde95e8b057
size 20670
oid sha256:e92e96cc117bf42ffb9fa282bcb9e40fd51c414e1d432862940c0f8fb6600fd0
size 23453

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4013454094701004f3c132df1ea1c3db2aa56a025b9edea80417fdd16fe55b19
size 10422
oid sha256:f3ea303577f655368800debe9c40e1292dc08a20da7f2ea6ddddb87f8407b112
size 10431

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:db74b1655e377e4fd8c35aa18c22ef5f5641586b64d5e582ab2079afeac4b8c3
size 10775
oid sha256:b646c06e55b50b64eb7b566fbc0bcafa7f7e348398f87152d008a47e7448f4e0
size 10736

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cfc6f54988e4fab08ff443f0c2ea285d47e3d030a8e6c92a1294ee0f85dc1269
size 51967
oid sha256:ba052f24e62aabc015da4b38bb893148c888a858a097669b2191e7880867e371
size 52114

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2ffd4fc93e7da38836fd860ce5e505c5f0c25fdd6558efef480aaeb05d00dd5
size 53327
oid sha256:4f8f7fd27e56aa2ca42053c249353836187467b99a9225c93c22f38365e63ccb
size 53285

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96864552336c65b464251c262f330856fc82c69a5f92f89b1357a185d654fccd
size 52252
oid sha256:fbfc5593de6c7dda915b3a8bc881f0485b4b8400f52412547aa914862d1d8840
size 52245

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2411212e87f95415e0b4ed6732387922faed891cde5e78fa9f0691a185873129
size 51026
oid sha256:d56d5666d1d7749484f19c983e3cb2125110039c80e5898684e20b7e24a3e56b
size 51312

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:255560f478c093f4ca7d021646f4cf9064c16b420d5eeec7cbce97d26c751d92
size 49540
oid sha256:db0107e648e2acfc253d83f5e3b2f3e41de1abc9bdc4922dfd923ad4e50b8f5f
size 49700

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a384aa84ae514aa4989d4b4a5ef8048cb8810f1f37056137c77fd0ee798a0bab
size 53941
oid sha256:398f4b1c7b21ab4c57802fb724d62b8c904f5a0c700eba507a811cb58881df49
size 53896

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a48badf15c59ab5c42fc7cc200971f00cefa7838461ad0cb59537c2b4065bfff
size 55409
oid sha256:2913fc7ee2c4b32e79e0d813ee8fa54e6d3f863466b798b8194462cbd9ef4fde
size 55137

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88a498b9070653513b3e27aa2b597367ee85c8526c6e4c3712a4cb0c06e5313b
size 54353
oid sha256:a20479945a552e2d3d3fb309c07ac7b9a66d4dfac0ad1ddf2c86c40fa0d34db9
size 54175

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf493a9cb7325b4f68924abd86864b37c72226afcf1c4179a06c17e923cfd0ae
size 55837
oid sha256:1e7697c3f1eb995fc0bbf084853fce1cc10e841d0275d6ff4f2d857afd81b31d
size 56103

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8ac3dbf36380fa273d82ebe8836c525bbeee004d6348822d965b216f9bcdb920
size 51335
oid sha256:7e162503c27609f01e05bb2c634c2d3123abf6e786ecfe763c902d400ead050f
size 51283

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce08e8c81d5494d2d803da43612a137df881e359646878b1d38cdfe77e0857ff
size 13829

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46683edccf9c7686a07d7098c4387f296556ad2678381aad35efe2787cf6ad0a
size 14173

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3c2ba0ac13c81c707f0d00e3ff69737163dfc450a3d21aa468dd0b34deeeb7e1
size 12916

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6485de8235af05a9815a558b1af03b51e8c4b30ad0372aea0aa9fb2303525c9b
size 13286

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d0e4c8da669ee5383a7d0f828a3300b923ec38ab5e04bc388da0e83f1cf1ccf3
size 38291

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f26aa7b13403cfaa864778a6ec5c26edde5932f3d887c7148d3a2b9f60f6e6a
size 36392

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f15bc37e2a3d052e8d79e663f2c03223c27ddff3c02cd69f95a140c33fc6952c
size 79320

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e65e15c721a939ed700719cacbdee57e1ccb89d984e88bdb5ed772c4b583472a
size 81494

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6aa9b8849a2bbd7b487de6fee7fd355300091bc14d647973c4beea02e6009ac4
size 76465

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:49f6d3b7f93008640abf30a5a65ddca6845e16343ef97c3450faf9dd6d9b9cec
size 78788

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:501734b297cec566a008d3492690cf75b46ab68e662d2835a0b195fd0740c13f
size 42912
oid sha256:2a998d3db9e4454fe74f31028b1dc61e4d6c07c824192f5e75563234dedf0b26
size 44108

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ff69ed7056380df3279a39b7544bc1887d2e455b43fb069b547e32cf5c92dc0f
size 40229
oid sha256:97ab0a0b64ea7704a1557fc34b24d7caea34f9951948f5f5637f0bda596288dd
size 41455