Skip to content

Integrate WYSIWYG editor #7288

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Oct 11, 2022
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ allprojects {
// To have XML report for Danger
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
}
filter {
exclude { element -> element.file.path.contains("$buildDir/generated/") }
}
disabledRules = [
// TODO Re-enable these 4 rules after reformatting project
"indent",
Expand Down
1 change: 1 addition & 0 deletions changelog.d/7288.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add WYSIWYG editor.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a 7288.sdk file to describe the changes in the SDK API? Thanks.

10 changes: 10 additions & 0 deletions changelog.d/7288.sdk
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Add `formattedText` or similar optional parameters in several methods:

* RelationService:
* editTextMessage
* editReply
* replyToMessage
* SendService:
* sendQuotedTextMessage

This allows us to send any HTML formatted text message without needing to rely on automatic Markdown > HTML translation. All these new parameters have a `null` value by default, so previous calls to these API methods remain compatible.
1 change: 1 addition & 0 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ ext.libs = [
],
element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0",
'wysiwyg' : "io.element.android:wysiwyg:0.1.0"
],
squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi",
Expand Down
1 change: 1 addition & 0 deletions dependencies_groups.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ ext.groups = [
'org.apache.httpcomponents',
'org.apache.sanselan',
'org.bouncycastle',
'org.ccil.cowan.tagsoup',
'org.checkerframework',
'org.codehaus',
'org.codehaus.groovy',
Expand Down
3 changes: 3 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@
<string name="labs_enable_deferred_dm_title">Enable deferred DMs</string>
<string name="labs_enable_deferred_dm_summary">Create DM only on first message</string>

<string name="labs_enable_rich_text_editor_title">Enable rich text editor</string>
<string name="labs_enable_rich_text_editor_summary">Use a rich text editor to send formatted messages</string>

<!-- Home fragment -->
<string name="invitations_header">Invites</string>
<string name="low_priority_header">Low priority</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,16 @@ interface RelationService {
* Edit a text message body. Limited to "m.text" contentType.
* @param targetEvent The event to edit
* @param msgType the message type
* @param newBodyText The edited body
* @param newBodyText The edited body in plain text
* @param newFormattedBodyText The edited body with format
* @param newBodyAutoMarkdown true to parse markdown on the new body
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
*/
fun editTextMessage(
targetEvent: TimelineEvent,
msgType: String,
newBodyText: CharSequence,
newFormattedBodyText: CharSequence? = null,
newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String = "* $newBodyText"
): Cancelable
Expand All @@ -108,13 +110,15 @@ interface RelationService {
* This method will take the new body (stripped from fallbacks) and re-add them before sending.
* @param replyToEdit The event to edit
* @param originalTimelineEvent the message that this reply (being edited) is relating to
* @param newBodyText The edited body (stripped from in reply to content)
* @param newBodyText The plain text edited body (stripped from in reply to content)
* @param newFormattedBodyText The formatted edited body (stripped from in reply to content)
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
*/
fun editReply(
replyToEdit: TimelineEvent,
originalTimelineEvent: TimelineEvent,
newBodyText: String,
newFormattedBodyText: String? = null,
compatibilityBodyText: String = "* $newBodyText"
): Cancelable

Expand All @@ -133,13 +137,15 @@ interface RelationService {
* by the sdk into pills.
* @param eventReplied the event referenced by the reply
* @param replyText the reply text
* @param replyFormattedText the reply text, formatted
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
* @param showInThread If true, relation will be added to the reply in order to be visible from within threads
* @param rootThreadEventId If show in thread is true then we need the rootThreadEventId to generate the relation
*/
fun replyToMessage(
eventReplied: TimelineEvent,
replyText: CharSequence,
replyFormattedText: CharSequence? = null,
autoMarkdown: Boolean = false,
showInThread: Boolean = false,
rootThreadEventId: String? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,19 @@ interface SendService {
/**
* Method to quote an events content.
* @param quotedEvent The event to which we will quote it's content.
* @param text the text message to send
* @param text the plain text message to send
* @param formattedText the formatted text message to send
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
* @param rootThreadEventId when this param is not null, the message will be sent in this specific thread
* @return a [Cancelable]
*/
fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String? = null): Cancelable
fun sendQuotedTextMessage(
quotedEvent: TimelineEvent,
text: String,
formattedText: String? = null,
autoMarkdown: Boolean,
rootThreadEventId: String? = null
): Cancelable

/**
* Method to send a media asynchronously.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
Expand Down Expand Up @@ -181,7 +182,8 @@ fun TimelineEvent.isRootThread(): Boolean {
* Get the latest message body, after a possible edition, stripping the reply prefix if necessary.
*/
fun TimelineEvent.getTextEditableContent(): String {
val lastContentBody = getLastMessageContent()?.body ?: return ""
val lastMessageContent = getLastMessageContent()
val lastContentBody = lastMessageContent.getFormattedBody() ?: return ""
return if (isReply()) {
extractUsefulTextFromReply(lastContentBody)
} else {
Expand All @@ -199,3 +201,11 @@ fun MessageContent.getTextDisplayableContent(): String {
?: (this as MessageTextContent?)?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) }
?: body
}

fun MessageContent?.getFormattedBody(): String? {
return if (this is MessageContentWithFormattedBody) {
formattedBody
} else {
this?.body
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,21 @@ internal class DefaultRelationService @AssistedInject constructor(
targetEvent: TimelineEvent,
msgType: String,
newBodyText: CharSequence,
newFormattedBodyText: CharSequence?,
newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String
): Cancelable {
return eventEditor.editTextMessage(targetEvent, msgType, newBodyText, newBodyAutoMarkdown, compatibilityBodyText)
return eventEditor.editTextMessage(targetEvent, msgType, newBodyText, newFormattedBodyText, newBodyAutoMarkdown, compatibilityBodyText)
}

override fun editReply(
replyToEdit: TimelineEvent,
originalTimelineEvent: TimelineEvent,
newBodyText: String,
newFormattedBodyText: String?,
compatibilityBodyText: String
): Cancelable {
return eventEditor.editReply(replyToEdit, originalTimelineEvent, newBodyText, compatibilityBodyText)
return eventEditor.editReply(replyToEdit, originalTimelineEvent, newBodyText, newFormattedBodyText, compatibilityBodyText)
}

override suspend fun fetchEditHistory(eventId: String): List<Event> {
Expand All @@ -127,6 +129,7 @@ internal class DefaultRelationService @AssistedInject constructor(
override fun replyToMessage(
eventReplied: TimelineEvent,
replyText: CharSequence,
replyFormattedText: CharSequence?,
autoMarkdown: Boolean,
showInThread: Boolean,
rootThreadEventId: String?
Expand All @@ -135,6 +138,7 @@ internal class DefaultRelationService @AssistedInject constructor(
roomId = roomId,
eventReplied = eventReplied,
replyText = replyText,
replyTextFormatted = replyFormattedText,
autoMarkdown = autoMarkdown,
rootThreadEventId = rootThreadEventId,
showInThread = showInThread
Expand Down Expand Up @@ -178,6 +182,7 @@ internal class DefaultRelationService @AssistedInject constructor(
roomId = roomId,
eventReplied = eventReplied,
replyText = replyInThreadText,
replyTextFormatted = formattedText,
autoMarkdown = autoMarkdown,
rootThreadEventId = rootThreadEventId,
showInThread = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.api.util.TextContent
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
Expand All @@ -42,19 +43,25 @@ internal class EventEditor @Inject constructor(
targetEvent: TimelineEvent,
msgType: String,
newBodyText: CharSequence,
newBodyFormattedText: CharSequence?,
newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String
): Cancelable {
val roomId = targetEvent.roomId
if (targetEvent.root.sendState.hasFailed()) {
// We create a new in memory event for the EventSenderProcessor but we keep the eventId of the failed event.
val editedEvent = eventFactory.createTextEvent(roomId, msgType, newBodyText, newBodyAutoMarkdown).copy(
val editedEvent = if (newBodyFormattedText != null) {
val content = TextContent(newBodyText.toString(), newBodyFormattedText.toString())
eventFactory.createFormattedTextEvent(roomId, content, msgType)
} else {
eventFactory.createTextEvent(roomId, msgType, newBodyText, newBodyAutoMarkdown)
}.copy(
eventId = targetEvent.eventId
)
return sendFailedEvent(targetEvent, editedEvent)
} else if (targetEvent.root.sendState.isSent()) {
val event = eventFactory
.createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
.createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyFormattedText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
return sendReplaceEvent(event)
} else {
// Should we throw?
Expand Down Expand Up @@ -100,6 +107,7 @@ internal class EventEditor @Inject constructor(
replyToEdit: TimelineEvent,
originalTimelineEvent: TimelineEvent,
newBodyText: String,
newBodyFormattedText: String?,
compatibilityBodyText: String
): Cancelable {
val roomId = replyToEdit.roomId
Expand All @@ -109,6 +117,7 @@ internal class EventEditor @Inject constructor(
roomId = roomId,
eventReplied = originalTimelineEvent,
replyText = newBodyText,
replyTextFormatted = newBodyFormattedText,
autoMarkdown = false,
showInThread = false
)?.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,18 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) }
}

override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String?): Cancelable {
override fun sendQuotedTextMessage(
quotedEvent: TimelineEvent,
text: String,
formattedText: String?,
autoMarkdown: Boolean,
rootThreadEventId: String?
): Cancelable {
return localEchoEventFactory.createQuotedTextEvent(
roomId = roomId,
quotedEvent = quotedEvent,
text = text,
formattedText = formattedText,
autoMarkdown = autoMarkdown,
rootThreadEventId = rootThreadEventId
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,23 @@ internal class LocalEchoEventFactory @Inject constructor(
roomId: String,
targetEventId: String,
newBodyText: CharSequence,
newBodyFormattedText: CharSequence?,
newBodyAutoMarkdown: Boolean,
msgType: String,
compatibilityText: String
): Event {
val content = if (newBodyFormattedText != null) {
TextContent(newBodyText.toString(), newBodyFormattedText.toString()).toMessageTextContent(msgType)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should consider changing TextContent to have 2 CharSequence members.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it probably makes sense to keep using String for these, as both text and formattedText are expected to be the plain text representation of the contents (either a plain text string or the HTML format of that same text). I'm no expert on this part of the code though, so if you think we should change it I can do it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you are right. Thanks

} else {
createTextContent(newBodyText, newBodyAutoMarkdown).toMessageTextContent(msgType)
}.toContent()
return createMessageEvent(
roomId,
MessageTextContent(
msgType = msgType,
body = compatibilityText,
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
newContent = createTextContent(newBodyText, newBodyAutoMarkdown)
.toMessageTextContent(msgType)
.toContent()
newContent = content,
)
)
}
Expand Down Expand Up @@ -581,6 +585,7 @@ internal class LocalEchoEventFactory @Inject constructor(
roomId: String,
eventReplied: TimelineEvent,
replyText: CharSequence,
replyTextFormatted: CharSequence?,
autoMarkdown: Boolean,
rootThreadEventId: String? = null,
showInThread: Boolean
Expand All @@ -594,15 +599,15 @@ internal class LocalEchoEventFactory @Inject constructor(
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply())

// As we always supply formatted body for replies we should force the MarkdownParser to produce html.
val replyTextFormatted = markdownParser.parse(replyText, force = true, advanced = autoMarkdown).takeFormatted()
val finalReplyTextFormatted = replyTextFormatted?.toString() ?: markdownParser.parse(replyText, force = true, advanced = autoMarkdown).takeFormatted()
// Body of the original message may not have formatted version, so may also have to convert to html.
val bodyFormatted = body.formattedText ?: markdownParser.parse(body.text, force = true, advanced = autoMarkdown).takeFormatted()
val replyFormatted = buildFormattedReply(
permalink,
userLink,
userId,
bodyFormatted,
replyTextFormatted
finalReplyTextFormatted
)
//
// > <@alice:example.org> This is the original body
Expand Down Expand Up @@ -765,18 +770,20 @@ internal class LocalEchoEventFactory @Inject constructor(
roomId: String,
quotedEvent: TimelineEvent,
text: String,
formattedText: String?,
autoMarkdown: Boolean,
rootThreadEventId: String?
): Event {
val messageContent = quotedEvent.getLastMessageContent()
val textMsg = messageContent?.body
val textMsg = if (messageContent is MessageContentWithFormattedBody) { messageContent.formattedBody } else { messageContent?.body }
val quoteText = legacyRiotQuoteText(textMsg, text)
val quoteFormattedText = "<blockquote>$textMsg</blockquote>$formattedText"

return if (rootThreadEventId != null) {
createMessageEvent(
roomId,
markdownParser
.parse(quoteText, force = true, advanced = autoMarkdown)
.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText)
.toThreadTextContent(
rootThreadEventId = rootThreadEventId,
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
Expand All @@ -786,7 +793,7 @@ internal class LocalEchoEventFactory @Inject constructor(
} else {
createFormattedTextEvent(
roomId,
markdownParser.parse(quoteText, force = true, advanced = autoMarkdown),
markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText),
MessageType.MSGTYPE_TEXT
)
}
Expand Down
2 changes: 2 additions & 0 deletions vector-config/src/main/res/values/config-settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
<bool name="settings_labs_new_app_layout_default">true</bool>
<bool name="settings_timeline_show_live_sender_info_visible">true</bool>
<bool name="settings_timeline_show_live_sender_info_default">false</bool>
<bool name="settings_labs_rich_text_editor_visible">true</bool>
<bool name="settings_labs_rich_text_editor_default">false</bool>
<!-- Level 1: Advanced settings -->

<!-- Level 1: Help and about -->
Expand Down
4 changes: 4 additions & 0 deletions vector/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ android {
}
}
dependencies {

implementation project(":vector-config")
api project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-flow")
Expand Down Expand Up @@ -143,6 +144,9 @@ dependencies {
// Opus Encoder
implementation libs.element.opusencoder

// WYSIWYG Editor
implementation libs.element.wysiwyg

// Log
api libs.jakewharton.timber

Expand Down
Loading