Skip to content

Commit f214493

Browse files
jonnyandrewElementBot
and
ElementBot
authored
[Rich text editor] Integrate rich text editor library (#1172)
* Integrate rich text editor * Also increase swapfile size in test CI Fixes issue where screenshot tests are terminated due to lack of CI resources. See https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749 --------- Co-authored-by: ElementBot <[email protected]>
1 parent f96ba8c commit f214493

File tree

62 files changed

+441
-289
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+441
-289
lines changed

.github/workflows/tests.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ jobs:
2222
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
2323
cancel-in-progress: true
2424
steps:
25+
# Increase swapfile size to prevent screenshot tests getting terminated
26+
# https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749
27+
- name: 💽 Increase swapfile size
28+
run: |
29+
sudo swapoff -a
30+
sudo fallocate -l 8G /mnt/swapfile
31+
sudo chmod 600 /mnt/swapfile
32+
sudo mkswap /mnt/swapfile
33+
sudo swapon /mnt/swapfile
34+
sudo swapon --show
2535
- name: ⏬ Checkout with LFS
2636
uses: nschloe/[email protected]
2737
with:

changelog.d/1172.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[Rich text editor] Integrate rich text editor library. Note that markdown is now not supported and further formatting support will be introduced through the rich text editor.
2+

features/messages/api/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ android {
2525
dependencies {
2626
implementation(projects.libraries.architecture)
2727
implementation(projects.libraries.matrix.api)
28-
api(projects.libraries.textcomposer)
28+
api(projects.libraries.textcomposer.impl)
2929
}

features/messages/impl/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ dependencies {
4141
implementation(projects.libraries.matrix.api)
4242
implementation(projects.libraries.matrixui)
4343
implementation(projects.libraries.designsystem)
44-
implementation(projects.libraries.textcomposer)
44+
implementation(projects.libraries.textcomposer.impl)
4545
implementation(projects.libraries.uiStrings)
4646
implementation(projects.libraries.dateformatter.api)
4747
implementation(projects.libraries.eventformatter.api)
@@ -76,6 +76,7 @@ dependencies {
7676
testImplementation(projects.libraries.featureflag.test)
7777
testImplementation(projects.libraries.mediaupload.test)
7878
testImplementation(projects.libraries.mediapickers.test)
79+
testImplementation(projects.libraries.textcomposer.test)
7980
testImplementation(libs.test.mockk)
8081

8182
ksp(libs.showkase.processor)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ class MessagesPresenter @AssistedInject constructor(
175175
snackbarMessage = snackbarMessage,
176176
showReinvitePrompt = showReinvitePrompt,
177177
inviteProgress = inviteProgress.value,
178-
eventSink = ::handleEvents
178+
eventSink = { handleEvents(it) }
179179
)
180180
}
181181

@@ -250,7 +250,9 @@ class MessagesPresenter @AssistedInject constructor(
250250
private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) {
251251
val composerMode = MessageComposerMode.Edit(
252252
targetEvent.eventId,
253-
(targetEvent.content as? TimelineItemTextBasedContent)?.body.orEmpty(),
253+
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
254+
it.htmlBody ?: it.body
255+
}.orEmpty(),
254256
targetEvent.transactionId,
255257
)
256258
composerState.eventSink(

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
3030
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
3131
import io.element.android.libraries.matrix.api.core.RoomId
3232
import io.element.android.libraries.textcomposer.MessageComposerMode
33+
import io.element.android.wysiwyg.compose.RichTextEditorState
3334
import kotlinx.collections.immutable.persistentSetOf
3435

3536
open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
@@ -54,7 +55,9 @@ fun aMessagesState() = MessagesState(
5455
userHasPermissionToSendMessage = true,
5556
userHasPermissionToRedact = false,
5657
composerState = aMessageComposerState().copy(
57-
text = "Hello",
58+
richTextEditorState = RichTextEditorState("Hello", fake = true).apply {
59+
requestFocus()
60+
},
5861
isFullScreen = false,
5962
mode = MessageComposerMode.Normal("Hello"),
6063
),

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class ActionListPresenter @Inject constructor(
7070
return ActionListState(
7171
target = target.value,
7272
displayEmojiReactions = displayEmojiReactions,
73-
eventSink = ::handleEvents
73+
eventSink = { handleEvents(it) }
7474
)
7575
}
7676

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,15 @@
1717
package io.element.android.features.messages.impl.messagecomposer
1818

1919
import androidx.compose.runtime.Immutable
20+
import io.element.android.libraries.textcomposer.Message
2021
import io.element.android.libraries.textcomposer.MessageComposerMode
2122

2223
@Immutable
2324
sealed interface MessageComposerEvents {
2425
data object ToggleFullScreenState : MessageComposerEvents
25-
data class FocusChanged(val hasFocus: Boolean) : MessageComposerEvents
26-
data class SendMessage(val message: String) : MessageComposerEvents
26+
data class SendMessage(val message: Message) : MessageComposerEvents
2727
data object CloseSpecialMode : MessageComposerEvents
2828
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents
29-
data class UpdateText(val text: String) : MessageComposerEvents
3029
data object AddAttachment : MessageComposerEvents
3130
data object DismissAttachmentMenu : MessageComposerEvents
3231
sealed interface PickAttachmentSource : MessageComposerEvents {
@@ -38,4 +37,5 @@ sealed interface MessageComposerEvents {
3837
data object Poll : PickAttachmentSource
3938
}
4039
data object CancelSendAttachment : MessageComposerEvents
40+
data class Error(val error: Throwable) : MessageComposerEvents
4141
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ import io.element.android.libraries.matrix.api.core.ProgressCallback
4444
import io.element.android.libraries.matrix.api.room.MatrixRoom
4545
import io.element.android.libraries.mediapickers.api.PickerProvider
4646
import io.element.android.libraries.mediaupload.api.MediaSender
47+
import io.element.android.libraries.textcomposer.Message
4748
import io.element.android.libraries.textcomposer.MessageComposerMode
4849
import io.element.android.services.analytics.api.AnalyticsService
50+
import io.element.android.wysiwyg.compose.RichTextEditorState
4951
import kotlinx.collections.immutable.persistentListOf
5052
import kotlinx.coroutines.CancellationException
5153
import kotlinx.coroutines.CoroutineScope
@@ -67,6 +69,7 @@ class MessageComposerPresenter @Inject constructor(
6769
private val snackbarDispatcher: SnackbarDispatcher,
6870
private val analyticsService: AnalyticsService,
6971
private val messageComposerContext: MessageComposerContextImpl,
72+
private val richTextEditorStateFactory: RichTextEditorStateFactory,
7073
) : Presenter<MessageComposerState> {
7174

7275
@SuppressLint("UnsafeOptInUsageError")
@@ -103,19 +106,15 @@ class MessageComposerPresenter @Inject constructor(
103106
val isFullScreen = rememberSaveable {
104107
mutableStateOf(false)
105108
}
106-
val hasFocus = remember {
107-
mutableStateOf(false)
108-
}
109-
val text: MutableState<String> = rememberSaveable {
110-
mutableStateOf("")
111-
}
109+
val richTextEditorState = richTextEditorStateFactory.create()
112110
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
113111

114112
var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) }
115113

116114
LaunchedEffect(messageComposerContext.composerMode) {
117115
when (val modeValue = messageComposerContext.composerMode) {
118-
is MessageComposerMode.Edit -> text.value = modeValue.defaultContent
116+
is MessageComposerMode.Edit ->
117+
richTextEditorState.setHtml(modeValue.defaultContent)
119118
else -> Unit
120119
}
121120
}
@@ -136,18 +135,15 @@ class MessageComposerPresenter @Inject constructor(
136135
when (event) {
137136
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
138137

139-
is MessageComposerEvents.FocusChanged -> hasFocus.value = event.hasFocus
140-
141-
is MessageComposerEvents.UpdateText -> text.value = event.text
142138
MessageComposerEvents.CloseSpecialMode -> {
143-
text.value = ""
139+
richTextEditorState.setHtml("")
144140
messageComposerContext.composerMode = MessageComposerMode.Normal("")
145141
}
146142

147143
is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(
148-
text = event.message,
144+
message = event.message,
149145
updateComposerMode = { messageComposerContext.composerMode = it },
150-
textState = text
146+
richTextEditorState = richTextEditorState,
151147
)
152148
is MessageComposerEvents.SetMode -> {
153149
messageComposerContext.composerMode = event.composerMode
@@ -194,43 +190,46 @@ class MessageComposerPresenter @Inject constructor(
194190
ongoingSendAttachmentJob.value == null
195191
}
196192
}
193+
is MessageComposerEvents.Error -> {
194+
analyticsService.trackError(event.error)
195+
}
197196
}
198197
}
199198

200199
return MessageComposerState(
201-
text = text.value,
200+
richTextEditorState = richTextEditorState,
202201
isFullScreen = isFullScreen.value,
203-
hasFocus = hasFocus.value,
204202
mode = messageComposerContext.composerMode,
205203
showAttachmentSourcePicker = showAttachmentSourcePicker,
206204
canShareLocation = canShareLocation.value,
207205
canCreatePoll = canCreatePoll.value,
208206
attachmentsState = attachmentsState.value,
209-
eventSink = ::handleEvents
207+
eventSink = { handleEvents(it) }
210208
)
211209
}
212210

213211
private fun CoroutineScope.sendMessage(
214-
text: String,
212+
message: Message,
215213
updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit,
216-
textState: MutableState<String>
214+
richTextEditorState: RichTextEditorState,
217215
) = launch {
218216
val capturedMode = messageComposerContext.composerMode
219217
// Reset composer right away
220-
textState.value = ""
218+
richTextEditorState.setHtml("")
221219
updateComposerMode(MessageComposerMode.Normal(""))
222220
when (capturedMode) {
223-
is MessageComposerMode.Normal -> room.sendMessage(text)
221+
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html)
224222
is MessageComposerMode.Edit -> {
225223
val eventId = capturedMode.eventId
226224
val transactionId = capturedMode.transactionId
227-
room.editMessage(eventId, transactionId, text)
225+
room.editMessage(eventId, transactionId, message.markdown, message.html)
228226
}
229227

230228
is MessageComposerMode.Quote -> TODO()
231229
is MessageComposerMode.Reply -> room.replyMessage(
232230
capturedMode.eventId,
233-
text
231+
message.markdown,
232+
message.html,
234233
)
235234
}
236235
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,22 @@ package io.element.android.features.messages.impl.messagecomposer
1919
import androidx.compose.runtime.Immutable
2020
import io.element.android.features.messages.impl.attachments.Attachment
2121
import io.element.android.libraries.textcomposer.MessageComposerMode
22+
import io.element.android.wysiwyg.compose.RichTextEditorState
2223
import kotlinx.collections.immutable.ImmutableList
2324

2425
@Immutable
2526
data class MessageComposerState(
26-
val text: String?,
27+
val richTextEditorState: RichTextEditorState,
2728
val isFullScreen: Boolean,
28-
val hasFocus: Boolean,
2929
val mode: MessageComposerMode,
3030
val showAttachmentSourcePicker: Boolean,
3131
val canShareLocation: Boolean,
3232
val canCreatePoll: Boolean,
3333
val attachmentsState: AttachmentsState,
34-
val eventSink: (MessageComposerEvents) -> Unit
34+
val eventSink: (MessageComposerEvents) -> Unit,
3535
) {
36-
val isSendButtonVisible: Boolean = text.isNullOrEmpty().not()
36+
val canSendMessage: Boolean = richTextEditorState.messageHtml.isNotEmpty()
37+
val hasFocus: Boolean = richTextEditorState.hasFocus
3738
}
3839

3940
@Immutable

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.messagecomposer
1818

1919
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
2020
import io.element.android.libraries.textcomposer.MessageComposerMode
21+
import io.element.android.wysiwyg.compose.RichTextEditorState
2122

2223
open class MessageComposerStateProvider : PreviewParameterProvider<MessageComposerState> {
2324
override val values: Sequence<MessageComposerState>
@@ -27,18 +28,17 @@ open class MessageComposerStateProvider : PreviewParameterProvider<MessageCompos
2728
}
2829

2930
fun aMessageComposerState(
30-
text: String = "",
31+
requestFocus: Boolean = true,
32+
composerState: RichTextEditorState = RichTextEditorState("", fake = true),
3133
isFullScreen: Boolean = false,
32-
hasFocus: Boolean = false,
3334
mode: MessageComposerMode = MessageComposerMode.Normal(content = ""),
3435
showAttachmentSourcePicker: Boolean = false,
3536
canShareLocation: Boolean = true,
3637
canCreatePoll: Boolean = true,
3738
attachmentsState: AttachmentsState = AttachmentsState.None,
3839
) = MessageComposerState(
39-
text = text,
40+
richTextEditorState = composerState.apply { if(requestFocus) requestFocus() },
4041
isFullScreen = isFullScreen,
41-
hasFocus = hasFocus,
4242
mode = mode,
4343
showAttachmentSourcePicker = showAttachmentSourcePicker,
4444
canShareLocation = canShareLocation,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview
2323
import androidx.compose.ui.tooling.preview.PreviewParameter
2424
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
2525
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
26+
import io.element.android.libraries.textcomposer.Message
2627
import io.element.android.libraries.textcomposer.TextComposer
2728

2829
@Composable
@@ -36,7 +37,7 @@ fun MessageComposerView(
3637
state.eventSink(MessageComposerEvents.ToggleFullScreenState)
3738
}
3839

39-
fun sendMessage(message: String) {
40+
fun sendMessage(message: Message) {
4041
state.eventSink(MessageComposerEvents.SendMessage(message))
4142
}
4243

@@ -48,12 +49,8 @@ fun MessageComposerView(
4849
state.eventSink(MessageComposerEvents.CloseSpecialMode)
4950
}
5051

51-
fun onComposerTextChange(text: String) {
52-
state.eventSink(MessageComposerEvents.UpdateText(text))
53-
}
54-
55-
fun onFocusChanged(hasFocus: Boolean) {
56-
state.eventSink(MessageComposerEvents.FocusChanged(hasFocus))
52+
fun onError(error: Throwable) {
53+
state.eventSink(MessageComposerEvents.Error(error))
5754
}
5855

5956
Box(modifier = modifier) {
@@ -64,14 +61,14 @@ fun MessageComposerView(
6461
)
6562

6663
TextComposer(
64+
state = state.richTextEditorState,
65+
canSendMessage = state.canSendMessage,
66+
onRequestFocus = { state.richTextEditorState.requestFocus() },
6767
onSendMessage = ::sendMessage,
6868
composerMode = state.mode,
6969
onResetComposerMode = ::onCloseSpecialMode,
70-
onComposerTextChange = ::onComposerTextChange,
7170
onAddAttachment = ::onAddAttachment,
72-
onFocusChanged = ::onFocusChanged,
73-
composerCanSendMessage = state.isSendButtonVisible,
74-
composerText = state.text
71+
onError = ::onError,
7572
)
7673
}
7774
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2023 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.features.messages.impl.messagecomposer
18+
19+
import androidx.compose.runtime.Composable
20+
import com.squareup.anvil.annotations.ContributesBinding
21+
import io.element.android.libraries.di.AppScope
22+
import io.element.android.wysiwyg.compose.RichTextEditorState
23+
import io.element.android.wysiwyg.compose.rememberRichTextEditorState
24+
import javax.inject.Inject
25+
26+
interface RichTextEditorStateFactory {
27+
@Composable
28+
fun create(): RichTextEditorState
29+
}
30+
31+
@ContributesBinding(AppScope::class)
32+
class DefaultRichTextEditorStateFactory @Inject constructor() : RichTextEditorStateFactory {
33+
@Composable
34+
override fun create(): RichTextEditorState {
35+
return rememberRichTextEditorState()
36+
}
37+
}
38+

0 commit comments

Comments
 (0)