mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-29 15:40:55 +08:00
Integrate WYSIWYG editor (#7288)
* Add WYSIWYG lib dependency * Replace EditText with RichTextEditor * Add bold button, fix sending formatting messages issues * Add missing inline formatting buttons, make scrollview horizontal * Disable autocomplete for rich text editor * Add formatted text to messages sent, replies, quotes and edited messages. * Several fixes * Add changelog * Try to fix lint issues * Address review comments. * Exclude Epoxy KSP generated files from ktlint checks
This commit is contained in:
parent
2fe636e93b
commit
def67b2e7d
@ -148,6 +148,9 @@ allprojects {
|
|||||||
// To have XML report for Danger
|
// To have XML report for Danger
|
||||||
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
|
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
|
||||||
}
|
}
|
||||||
|
filter {
|
||||||
|
exclude { element -> element.file.path.contains("$buildDir/generated/") }
|
||||||
|
}
|
||||||
disabledRules = [
|
disabledRules = [
|
||||||
// TODO Re-enable these 4 rules after reformatting project
|
// TODO Re-enable these 4 rules after reformatting project
|
||||||
"indent",
|
"indent",
|
||||||
|
1
changelog.d/7288.feature
Normal file
1
changelog.d/7288.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add WYSIWYG editor.
|
10
changelog.d/7288.sdk
Normal file
10
changelog.d/7288.sdk
Normal file
@ -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.
|
@ -102,6 +102,7 @@ ext.libs = [
|
|||||||
],
|
],
|
||||||
element : [
|
element : [
|
||||||
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
'opusencoder' : "io.element.android:opusencoder:1.1.0",
|
||||||
|
'wysiwyg' : "io.element.android:wysiwyg:0.1.0"
|
||||||
],
|
],
|
||||||
squareup : [
|
squareup : [
|
||||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||||
|
@ -178,6 +178,7 @@ ext.groups = [
|
|||||||
'org.apache.httpcomponents',
|
'org.apache.httpcomponents',
|
||||||
'org.apache.sanselan',
|
'org.apache.sanselan',
|
||||||
'org.bouncycastle',
|
'org.bouncycastle',
|
||||||
|
'org.ccil.cowan.tagsoup',
|
||||||
'org.checkerframework',
|
'org.checkerframework',
|
||||||
'org.codehaus',
|
'org.codehaus',
|
||||||
'org.codehaus.groovy',
|
'org.codehaus.groovy',
|
||||||
|
@ -446,6 +446,9 @@
|
|||||||
<string name="labs_enable_deferred_dm_title">Enable deferred DMs</string>
|
<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_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 -->
|
<!-- Home fragment -->
|
||||||
<string name="invitations_header">Invites</string>
|
<string name="invitations_header">Invites</string>
|
||||||
<string name="low_priority_header">Low priority</string>
|
<string name="low_priority_header">Low priority</string>
|
||||||
|
@ -91,7 +91,8 @@ interface RelationService {
|
|||||||
* Edit a text message body. Limited to "m.text" contentType.
|
* Edit a text message body. Limited to "m.text" contentType.
|
||||||
* @param targetEvent The event to edit
|
* @param targetEvent The event to edit
|
||||||
* @param msgType the message type
|
* @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 newBodyAutoMarkdown true to parse markdown on the new body
|
||||||
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
|
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
|
||||||
*/
|
*/
|
||||||
@ -99,6 +100,7 @@ interface RelationService {
|
|||||||
targetEvent: TimelineEvent,
|
targetEvent: TimelineEvent,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
newBodyText: CharSequence,
|
newBodyText: CharSequence,
|
||||||
|
newFormattedBodyText: CharSequence? = null,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
compatibilityBodyText: String = "* $newBodyText"
|
compatibilityBodyText: String = "* $newBodyText"
|
||||||
): Cancelable
|
): Cancelable
|
||||||
@ -108,13 +110,15 @@ interface RelationService {
|
|||||||
* This method will take the new body (stripped from fallbacks) and re-add them before sending.
|
* This method will take the new body (stripped from fallbacks) and re-add them before sending.
|
||||||
* @param replyToEdit The event to edit
|
* @param replyToEdit The event to edit
|
||||||
* @param originalTimelineEvent the message that this reply (being edited) is relating to
|
* @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
|
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
|
||||||
*/
|
*/
|
||||||
fun editReply(
|
fun editReply(
|
||||||
replyToEdit: TimelineEvent,
|
replyToEdit: TimelineEvent,
|
||||||
originalTimelineEvent: TimelineEvent,
|
originalTimelineEvent: TimelineEvent,
|
||||||
newBodyText: String,
|
newBodyText: String,
|
||||||
|
newFormattedBodyText: String? = null,
|
||||||
compatibilityBodyText: String = "* $newBodyText"
|
compatibilityBodyText: String = "* $newBodyText"
|
||||||
): Cancelable
|
): Cancelable
|
||||||
|
|
||||||
@ -133,6 +137,7 @@ interface RelationService {
|
|||||||
* by the sdk into pills.
|
* by the sdk into pills.
|
||||||
* @param eventReplied the event referenced by the reply
|
* @param eventReplied the event referenced by the reply
|
||||||
* @param replyText the reply text
|
* @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 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 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
|
* @param rootThreadEventId If show in thread is true then we need the rootThreadEventId to generate the relation
|
||||||
@ -140,6 +145,7 @@ interface RelationService {
|
|||||||
fun replyToMessage(
|
fun replyToMessage(
|
||||||
eventReplied: TimelineEvent,
|
eventReplied: TimelineEvent,
|
||||||
replyText: CharSequence,
|
replyText: CharSequence,
|
||||||
|
replyFormattedText: CharSequence? = null,
|
||||||
autoMarkdown: Boolean = false,
|
autoMarkdown: Boolean = false,
|
||||||
showInThread: Boolean = false,
|
showInThread: Boolean = false,
|
||||||
rootThreadEventId: String? = null
|
rootThreadEventId: String? = null
|
||||||
|
@ -60,12 +60,19 @@ interface SendService {
|
|||||||
/**
|
/**
|
||||||
* Method to quote an events content.
|
* Method to quote an events content.
|
||||||
* @param quotedEvent The event to which we will quote it's 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 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
|
* @param rootThreadEventId when this param is not null, the message will be sent in this specific thread
|
||||||
* @return a [Cancelable]
|
* @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.
|
* Method to send a media asynchronously.
|
||||||
|
@ -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.MessageBeaconInfoContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
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.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.MessagePollContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
||||||
@ -181,7 +182,8 @@ fun TimelineEvent.isRootThread(): Boolean {
|
|||||||
* Get the latest message body, after a possible edition, stripping the reply prefix if necessary.
|
* Get the latest message body, after a possible edition, stripping the reply prefix if necessary.
|
||||||
*/
|
*/
|
||||||
fun TimelineEvent.getTextEditableContent(): String {
|
fun TimelineEvent.getTextEditableContent(): String {
|
||||||
val lastContentBody = getLastMessageContent()?.body ?: return ""
|
val lastMessageContent = getLastMessageContent()
|
||||||
|
val lastContentBody = lastMessageContent.getFormattedBody() ?: return ""
|
||||||
return if (isReply()) {
|
return if (isReply()) {
|
||||||
extractUsefulTextFromReply(lastContentBody)
|
extractUsefulTextFromReply(lastContentBody)
|
||||||
} else {
|
} else {
|
||||||
@ -199,3 +201,11 @@ fun MessageContent.getTextDisplayableContent(): String {
|
|||||||
?: (this as MessageTextContent?)?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) }
|
?: (this as MessageTextContent?)?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) }
|
||||||
?: body
|
?: body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MessageContent?.getFormattedBody(): String? {
|
||||||
|
return if (this is MessageContentWithFormattedBody) {
|
||||||
|
formattedBody
|
||||||
|
} else {
|
||||||
|
this?.body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -105,19 +105,21 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
targetEvent: TimelineEvent,
|
targetEvent: TimelineEvent,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
newBodyText: CharSequence,
|
newBodyText: CharSequence,
|
||||||
|
newFormattedBodyText: CharSequence?,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
compatibilityBodyText: String
|
compatibilityBodyText: String
|
||||||
): Cancelable {
|
): Cancelable {
|
||||||
return eventEditor.editTextMessage(targetEvent, msgType, newBodyText, newBodyAutoMarkdown, compatibilityBodyText)
|
return eventEditor.editTextMessage(targetEvent, msgType, newBodyText, newFormattedBodyText, newBodyAutoMarkdown, compatibilityBodyText)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun editReply(
|
override fun editReply(
|
||||||
replyToEdit: TimelineEvent,
|
replyToEdit: TimelineEvent,
|
||||||
originalTimelineEvent: TimelineEvent,
|
originalTimelineEvent: TimelineEvent,
|
||||||
newBodyText: String,
|
newBodyText: String,
|
||||||
|
newFormattedBodyText: String?,
|
||||||
compatibilityBodyText: String
|
compatibilityBodyText: String
|
||||||
): Cancelable {
|
): Cancelable {
|
||||||
return eventEditor.editReply(replyToEdit, originalTimelineEvent, newBodyText, compatibilityBodyText)
|
return eventEditor.editReply(replyToEdit, originalTimelineEvent, newBodyText, newFormattedBodyText, compatibilityBodyText)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchEditHistory(eventId: String): List<Event> {
|
override suspend fun fetchEditHistory(eventId: String): List<Event> {
|
||||||
@ -127,6 +129,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
override fun replyToMessage(
|
override fun replyToMessage(
|
||||||
eventReplied: TimelineEvent,
|
eventReplied: TimelineEvent,
|
||||||
replyText: CharSequence,
|
replyText: CharSequence,
|
||||||
|
replyFormattedText: CharSequence?,
|
||||||
autoMarkdown: Boolean,
|
autoMarkdown: Boolean,
|
||||||
showInThread: Boolean,
|
showInThread: Boolean,
|
||||||
rootThreadEventId: String?
|
rootThreadEventId: String?
|
||||||
@ -135,6 +138,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
eventReplied = eventReplied,
|
eventReplied = eventReplied,
|
||||||
replyText = replyText,
|
replyText = replyText,
|
||||||
|
replyTextFormatted = replyFormattedText,
|
||||||
autoMarkdown = autoMarkdown,
|
autoMarkdown = autoMarkdown,
|
||||||
rootThreadEventId = rootThreadEventId,
|
rootThreadEventId = rootThreadEventId,
|
||||||
showInThread = showInThread
|
showInThread = showInThread
|
||||||
@ -178,6 +182,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
eventReplied = eventReplied,
|
eventReplied = eventReplied,
|
||||||
replyText = replyInThreadText,
|
replyText = replyInThreadText,
|
||||||
|
replyTextFormatted = formattedText,
|
||||||
autoMarkdown = autoMarkdown,
|
autoMarkdown = autoMarkdown,
|
||||||
rootThreadEventId = rootThreadEventId,
|
rootThreadEventId = rootThreadEventId,
|
||||||
showInThread = false
|
showInThread = false
|
||||||
|
@ -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.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
import org.matrix.android.sdk.api.util.NoOpCancellable
|
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.database.mapper.toEntity
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||||
@ -42,19 +43,25 @@ internal class EventEditor @Inject constructor(
|
|||||||
targetEvent: TimelineEvent,
|
targetEvent: TimelineEvent,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
newBodyText: CharSequence,
|
newBodyText: CharSequence,
|
||||||
|
newBodyFormattedText: CharSequence?,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
compatibilityBodyText: String
|
compatibilityBodyText: String
|
||||||
): Cancelable {
|
): Cancelable {
|
||||||
val roomId = targetEvent.roomId
|
val roomId = targetEvent.roomId
|
||||||
if (targetEvent.root.sendState.hasFailed()) {
|
if (targetEvent.root.sendState.hasFailed()) {
|
||||||
// We create a new in memory event for the EventSenderProcessor but we keep the eventId of the failed event.
|
// 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
|
eventId = targetEvent.eventId
|
||||||
)
|
)
|
||||||
return sendFailedEvent(targetEvent, editedEvent)
|
return sendFailedEvent(targetEvent, editedEvent)
|
||||||
} else if (targetEvent.root.sendState.isSent()) {
|
} else if (targetEvent.root.sendState.isSent()) {
|
||||||
val event = eventFactory
|
val event = eventFactory
|
||||||
.createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
|
.createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyFormattedText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
|
||||||
return sendReplaceEvent(event)
|
return sendReplaceEvent(event)
|
||||||
} else {
|
} else {
|
||||||
// Should we throw?
|
// Should we throw?
|
||||||
@ -100,6 +107,7 @@ internal class EventEditor @Inject constructor(
|
|||||||
replyToEdit: TimelineEvent,
|
replyToEdit: TimelineEvent,
|
||||||
originalTimelineEvent: TimelineEvent,
|
originalTimelineEvent: TimelineEvent,
|
||||||
newBodyText: String,
|
newBodyText: String,
|
||||||
|
newBodyFormattedText: String?,
|
||||||
compatibilityBodyText: String
|
compatibilityBodyText: String
|
||||||
): Cancelable {
|
): Cancelable {
|
||||||
val roomId = replyToEdit.roomId
|
val roomId = replyToEdit.roomId
|
||||||
@ -109,6 +117,7 @@ internal class EventEditor @Inject constructor(
|
|||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
eventReplied = originalTimelineEvent,
|
eventReplied = originalTimelineEvent,
|
||||||
replyText = newBodyText,
|
replyText = newBodyText,
|
||||||
|
replyTextFormatted = newBodyFormattedText,
|
||||||
autoMarkdown = false,
|
autoMarkdown = false,
|
||||||
showInThread = false
|
showInThread = false
|
||||||
)?.copy(
|
)?.copy(
|
||||||
|
@ -99,11 +99,18 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
.let { sendEvent(it) }
|
.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(
|
return localEchoEventFactory.createQuotedTextEvent(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
quotedEvent = quotedEvent,
|
quotedEvent = quotedEvent,
|
||||||
text = text,
|
text = text,
|
||||||
|
formattedText = formattedText,
|
||||||
autoMarkdown = autoMarkdown,
|
autoMarkdown = autoMarkdown,
|
||||||
rootThreadEventId = rootThreadEventId
|
rootThreadEventId = rootThreadEventId
|
||||||
)
|
)
|
||||||
|
@ -124,19 +124,23 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
roomId: String,
|
roomId: String,
|
||||||
targetEventId: String,
|
targetEventId: String,
|
||||||
newBodyText: CharSequence,
|
newBodyText: CharSequence,
|
||||||
|
newBodyFormattedText: CharSequence?,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
compatibilityText: String
|
compatibilityText: String
|
||||||
): Event {
|
): Event {
|
||||||
|
val content = if (newBodyFormattedText != null) {
|
||||||
|
TextContent(newBodyText.toString(), newBodyFormattedText.toString()).toMessageTextContent(msgType)
|
||||||
|
} else {
|
||||||
|
createTextContent(newBodyText, newBodyAutoMarkdown).toMessageTextContent(msgType)
|
||||||
|
}.toContent()
|
||||||
return createMessageEvent(
|
return createMessageEvent(
|
||||||
roomId,
|
roomId,
|
||||||
MessageTextContent(
|
MessageTextContent(
|
||||||
msgType = msgType,
|
msgType = msgType,
|
||||||
body = compatibilityText,
|
body = compatibilityText,
|
||||||
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
|
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
|
||||||
newContent = createTextContent(newBodyText, newBodyAutoMarkdown)
|
newContent = content,
|
||||||
.toMessageTextContent(msgType)
|
|
||||||
.toContent()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -581,6 +585,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
roomId: String,
|
roomId: String,
|
||||||
eventReplied: TimelineEvent,
|
eventReplied: TimelineEvent,
|
||||||
replyText: CharSequence,
|
replyText: CharSequence,
|
||||||
|
replyTextFormatted: CharSequence?,
|
||||||
autoMarkdown: Boolean,
|
autoMarkdown: Boolean,
|
||||||
rootThreadEventId: String? = null,
|
rootThreadEventId: String? = null,
|
||||||
showInThread: Boolean
|
showInThread: Boolean
|
||||||
@ -594,7 +599,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply())
|
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply())
|
||||||
|
|
||||||
// As we always supply formatted body for replies we should force the MarkdownParser to produce html.
|
// 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.
|
// 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 bodyFormatted = body.formattedText ?: markdownParser.parse(body.text, force = true, advanced = autoMarkdown).takeFormatted()
|
||||||
val replyFormatted = buildFormattedReply(
|
val replyFormatted = buildFormattedReply(
|
||||||
@ -602,7 +607,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
userLink,
|
userLink,
|
||||||
userId,
|
userId,
|
||||||
bodyFormatted,
|
bodyFormatted,
|
||||||
replyTextFormatted
|
finalReplyTextFormatted
|
||||||
)
|
)
|
||||||
//
|
//
|
||||||
// > <@alice:example.org> This is the original body
|
// > <@alice:example.org> This is the original body
|
||||||
@ -765,18 +770,20 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
roomId: String,
|
roomId: String,
|
||||||
quotedEvent: TimelineEvent,
|
quotedEvent: TimelineEvent,
|
||||||
text: String,
|
text: String,
|
||||||
|
formattedText: String?,
|
||||||
autoMarkdown: Boolean,
|
autoMarkdown: Boolean,
|
||||||
rootThreadEventId: String?
|
rootThreadEventId: String?
|
||||||
): Event {
|
): Event {
|
||||||
val messageContent = quotedEvent.getLastMessageContent()
|
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 quoteText = legacyRiotQuoteText(textMsg, text)
|
||||||
|
val quoteFormattedText = "<blockquote>$textMsg</blockquote>$formattedText"
|
||||||
|
|
||||||
return if (rootThreadEventId != null) {
|
return if (rootThreadEventId != null) {
|
||||||
createMessageEvent(
|
createMessageEvent(
|
||||||
roomId,
|
roomId,
|
||||||
markdownParser
|
markdownParser
|
||||||
.parse(quoteText, force = true, advanced = autoMarkdown)
|
.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText)
|
||||||
.toThreadTextContent(
|
.toThreadTextContent(
|
||||||
rootThreadEventId = rootThreadEventId,
|
rootThreadEventId = rootThreadEventId,
|
||||||
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
|
latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
|
||||||
@ -786,7 +793,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
createFormattedTextEvent(
|
createFormattedTextEvent(
|
||||||
roomId,
|
roomId,
|
||||||
markdownParser.parse(quoteText, force = true, advanced = autoMarkdown),
|
markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText),
|
||||||
MessageType.MSGTYPE_TEXT
|
MessageType.MSGTYPE_TEXT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,8 @@
|
|||||||
<bool name="settings_labs_new_app_layout_default">true</bool>
|
<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_visible">true</bool>
|
||||||
<bool name="settings_timeline_show_live_sender_info_default">false</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: Advanced settings -->
|
||||||
|
|
||||||
<!-- Level 1: Help and about -->
|
<!-- Level 1: Help and about -->
|
||||||
|
@ -104,6 +104,7 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation project(":vector-config")
|
implementation project(":vector-config")
|
||||||
api project(":matrix-sdk-android")
|
api project(":matrix-sdk-android")
|
||||||
implementation project(":matrix-sdk-android-flow")
|
implementation project(":matrix-sdk-android-flow")
|
||||||
@ -143,6 +144,9 @@ dependencies {
|
|||||||
// Opus Encoder
|
// Opus Encoder
|
||||||
implementation libs.element.opusencoder
|
implementation libs.element.opusencoder
|
||||||
|
|
||||||
|
// WYSIWYG Editor
|
||||||
|
implementation libs.element.wysiwyg
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
api libs.jakewharton.timber
|
api libs.jakewharton.timber
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.command
|
|||||||
|
|
||||||
import im.vector.app.core.extensions.isEmail
|
import im.vector.app.core.extensions.isEmail
|
||||||
import im.vector.app.core.extensions.isMsisdn
|
import im.vector.app.core.extensions.isMsisdn
|
||||||
|
import im.vector.app.core.extensions.orEmpty
|
||||||
import im.vector.app.features.home.room.detail.ChatEffect
|
import im.vector.app.features.home.room.detail.ChatEffect
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns
|
import org.matrix.android.sdk.api.MatrixPatterns
|
||||||
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
||||||
@ -30,39 +31,30 @@ class CommandParser @Inject constructor() {
|
|||||||
/**
|
/**
|
||||||
* Convert the text message into a Slash command.
|
* Convert the text message into a Slash command.
|
||||||
*
|
*
|
||||||
* @param textMessage the text message
|
* @param textMessage the text message in plain text
|
||||||
|
* @param formattedMessage the text messaged in HTML format
|
||||||
* @param isInThreadTimeline true if the user is currently typing in a thread
|
* @param isInThreadTimeline true if the user is currently typing in a thread
|
||||||
* @return a parsed slash command (ok or error)
|
* @return a parsed slash command (ok or error)
|
||||||
*/
|
*/
|
||||||
fun parseSlashCommand(textMessage: CharSequence, isInThreadTimeline: Boolean): ParsedCommand {
|
@Suppress("NAME_SHADOWING")
|
||||||
|
fun parseSlashCommand(textMessage: CharSequence, formattedMessage: String?, isInThreadTimeline: Boolean): ParsedCommand {
|
||||||
// check if it has the Slash marker
|
// check if it has the Slash marker
|
||||||
return if (!textMessage.startsWith("/")) {
|
val message = formattedMessage ?: textMessage
|
||||||
|
return if (!message.startsWith("/")) {
|
||||||
ParsedCommand.ErrorNotACommand
|
ParsedCommand.ErrorNotACommand
|
||||||
} else {
|
} else {
|
||||||
// "/" only
|
// "/" only
|
||||||
if (textMessage.length == 1) {
|
if (message.length == 1) {
|
||||||
return ParsedCommand.ErrorEmptySlashCommand
|
return ParsedCommand.ErrorEmptySlashCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclude "//"
|
// Exclude "//"
|
||||||
if ("/" == textMessage.substring(1, 2)) {
|
if ("/" == message.substring(1, 2)) {
|
||||||
return ParsedCommand.ErrorNotACommand
|
return ParsedCommand.ErrorNotACommand
|
||||||
}
|
}
|
||||||
|
|
||||||
val messageParts = try {
|
val (messageParts, message) = extractMessage(message.toString()) ?: return ParsedCommand.ErrorEmptySlashCommand
|
||||||
textMessage.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "## parseSlashCommand() : split failed")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
// test if the string cut fails
|
|
||||||
if (messageParts.isNullOrEmpty()) {
|
|
||||||
return ParsedCommand.ErrorEmptySlashCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
val slashCommand = messageParts.first()
|
val slashCommand = messageParts.first()
|
||||||
val message = textMessage.substring(slashCommand.length).trim()
|
|
||||||
|
|
||||||
getNotSupportedByThreads(isInThreadTimeline, slashCommand)?.let {
|
getNotSupportedByThreads(isInThreadTimeline, slashCommand)?.let {
|
||||||
return ParsedCommand.ErrorCommandNotSupportedInThreads(it)
|
return ParsedCommand.ErrorCommandNotSupportedInThreads(it)
|
||||||
@ -71,7 +63,12 @@ class CommandParser @Inject constructor() {
|
|||||||
when {
|
when {
|
||||||
Command.PLAIN.matches(slashCommand) -> {
|
Command.PLAIN.matches(slashCommand) -> {
|
||||||
if (message.isNotEmpty()) {
|
if (message.isNotEmpty()) {
|
||||||
ParsedCommand.SendPlainText(message = message)
|
if (formattedMessage != null) {
|
||||||
|
val trimmedPlainTextMessage = extractMessage(textMessage.toString())?.second.orEmpty()
|
||||||
|
ParsedCommand.SendFormattedText(message = trimmedPlainTextMessage, formattedMessage = message)
|
||||||
|
} else {
|
||||||
|
ParsedCommand.SendPlainText(message = message)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ParsedCommand.ErrorSyntax(Command.PLAIN)
|
ParsedCommand.ErrorSyntax(Command.PLAIN)
|
||||||
}
|
}
|
||||||
@ -415,6 +412,25 @@ class CommandParser @Inject constructor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun extractMessage(message: String): Pair<List<String>, String>? {
|
||||||
|
val messageParts = try {
|
||||||
|
message.split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "## parseSlashCommand() : split failed")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// test if the string cut fails
|
||||||
|
if (messageParts.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val slashCommand = messageParts.first()
|
||||||
|
val trimmedMessage = message.substring(slashCommand.length).trim()
|
||||||
|
|
||||||
|
return messageParts to trimmedMessage
|
||||||
|
}
|
||||||
|
|
||||||
private val notSupportedThreadsCommands: List<Command> by lazy {
|
private val notSupportedThreadsCommands: List<Command> by lazy {
|
||||||
Command.values().filter {
|
Command.values().filter {
|
||||||
!it.isThreadCommand
|
!it.isThreadCommand
|
||||||
|
@ -39,6 +39,7 @@ sealed interface ParsedCommand {
|
|||||||
// Valid commands:
|
// Valid commands:
|
||||||
|
|
||||||
data class SendPlainText(val message: CharSequence) : ParsedCommand
|
data class SendPlainText(val message: CharSequence) : ParsedCommand
|
||||||
|
data class SendFormattedText(val message: CharSequence, val formattedMessage: String) : ParsedCommand
|
||||||
data class SendEmote(val message: CharSequence) : ParsedCommand
|
data class SendEmote(val message: CharSequence) : ParsedCommand
|
||||||
data class SendRainbow(val message: CharSequence) : ParsedCommand
|
data class SendRainbow(val message: CharSequence) : ParsedCommand
|
||||||
data class SendRainbowEmote(val message: CharSequence) : ParsedCommand
|
data class SendRainbowEmote(val message: CharSequence) : ParsedCommand
|
||||||
|
@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
|||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||||
|
|
||||||
sealed class MessageComposerAction : VectorViewModelAction {
|
sealed class MessageComposerAction : VectorViewModelAction {
|
||||||
data class SendMessage(val text: CharSequence, val autoMarkdown: Boolean) : MessageComposerAction()
|
data class SendMessage(val text: CharSequence, val formattedText: String?, val autoMarkdown: Boolean) : MessageComposerAction()
|
||||||
data class EnterEditMode(val eventId: String) : MessageComposerAction()
|
data class EnterEditMode(val eventId: String) : MessageComposerAction()
|
||||||
data class EnterQuoteMode(val eventId: String) : MessageComposerAction()
|
data class EnterQuoteMode(val eventId: String) : MessageComposerAction()
|
||||||
data class EnterReplyMode(val eventId: String) : MessageComposerAction()
|
data class EnterReplyMode(val eventId: String) : MessageComposerAction()
|
||||||
|
@ -37,6 +37,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.text.buildSpannedString
|
import androidx.core.text.buildSpannedString
|
||||||
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
@ -161,6 +162,14 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel()
|
private val messageComposerViewModel: MessageComposerViewModel by parentFragmentViewModel()
|
||||||
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
||||||
|
|
||||||
|
private val composer: MessageComposerView get() {
|
||||||
|
return if (vectorPreferences.isRichTextEditorEnabled()) {
|
||||||
|
views.richTextComposerLayout
|
||||||
|
} else {
|
||||||
|
views.composerLayout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentComposerBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentComposerBinding {
|
||||||
return FragmentComposerBinding.inflate(inflater, container, false)
|
return FragmentComposerBinding.inflate(inflater, container, false)
|
||||||
}
|
}
|
||||||
@ -175,6 +184,9 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
setupComposer()
|
setupComposer()
|
||||||
setupEmojiButton()
|
setupEmojiButton()
|
||||||
|
|
||||||
|
views.composerLayout.isGone = vectorPreferences.isRichTextEditorEnabled()
|
||||||
|
views.richTextComposerLayout.isVisible = vectorPreferences.isRichTextEditorEnabled()
|
||||||
|
|
||||||
messageComposerViewModel.observeViewEvents {
|
messageComposerViewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is MessageComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
|
is MessageComposerViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
|
||||||
@ -218,29 +230,33 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
if (withState(messageComposerViewModel) { it.isVoiceRecording } && requireActivity().isChangingConfigurations) {
|
if (withState(messageComposerViewModel) { it.isVoiceRecording } && requireActivity().isChangingConfigurations) {
|
||||||
// we're rotating, maintain any active recordings
|
// we're rotating, maintain any active recordings
|
||||||
} else {
|
} else {
|
||||||
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(views.composerLayout.text.toString()))
|
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
|
||||||
autoCompleter.clear()
|
if (!vectorPreferences.isRichTextEditorEnabled()) {
|
||||||
|
autoCompleter.clear()
|
||||||
|
}
|
||||||
messageComposerViewModel.endAllVoiceActions()
|
messageComposerViewModel.endAllVoiceActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState ->
|
override fun invalidate() = withState(timelineViewModel, messageComposerViewModel) { mainState, messageComposerState ->
|
||||||
if (mainState.tombstoneEvent != null) return@withState
|
if (mainState.tombstoneEvent != null) return@withState
|
||||||
|
|
||||||
views.root.isInvisible = !messageComposerState.isComposerVisible
|
composer.setInvisible(!messageComposerState.isComposerVisible)
|
||||||
views.composerLayout.views.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
|
composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupComposer() {
|
private fun setupComposer() {
|
||||||
val composerEditText = views.composerLayout.views.composerEditText
|
val composerEditText = composer.editText
|
||||||
composerEditText.setHint(R.string.room_message_placeholder)
|
composerEditText.setHint(R.string.room_message_placeholder)
|
||||||
|
|
||||||
autoCompleter.setup(composerEditText)
|
if (!vectorPreferences.isRichTextEditorEnabled()) {
|
||||||
|
autoCompleter.setup(composerEditText)
|
||||||
|
}
|
||||||
|
|
||||||
observerUserTyping()
|
observerUserTyping()
|
||||||
|
|
||||||
@ -257,20 +273,22 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
!keyEvent.isShiftPressed &&
|
!keyEvent.isShiftPressed &&
|
||||||
keyEvent.keyCode == KeyEvent.KEYCODE_ENTER &&
|
keyEvent.keyCode == KeyEvent.KEYCODE_ENTER &&
|
||||||
resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS
|
resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS
|
||||||
if (isSendAction || externalKeyboardPressedEnter) {
|
val result = if (isSendAction || externalKeyboardPressedEnter) {
|
||||||
sendTextMessage(v.text)
|
sendTextMessage(v.text)
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
|
composer.setTextIfDifferent(null)
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
views.composerLayout.views.composerEmojiButton.isVisible = vectorPreferences.showEmojiKeyboard()
|
composer.emojiButton?.isVisible = vectorPreferences.showEmojiKeyboard()
|
||||||
|
|
||||||
val showKeyboard = withState(timelineViewModel) { it.showKeyboardWhenPresented }
|
val showKeyboard = withState(timelineViewModel) { it.showKeyboardWhenPresented }
|
||||||
if (isThreadTimeLine() && showKeyboard) {
|
if (isThreadTimeLine() && showKeyboard) {
|
||||||
// Show keyboard when the user started a thread
|
// Show keyboard when the user started a thread
|
||||||
views.composerLayout.views.composerEditText.showKeyboard(andRequestFocus = true)
|
composerEditText.showKeyboard(andRequestFocus = true)
|
||||||
}
|
}
|
||||||
views.composerLayout.callback = object : MessageComposerView.Callback {
|
composer.callback = object : PlainTextComposerLayout.Callback {
|
||||||
override fun onAddAttachment() {
|
override fun onAddAttachment() {
|
||||||
if (!::attachmentTypeSelector.isInitialized) {
|
if (!::attachmentTypeSelector.isInitialized) {
|
||||||
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment)
|
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this@MessageComposerFragment)
|
||||||
@ -286,15 +304,15 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
vectorFeatures.isVoiceBroadcastEnabled(), // TODO check user permission
|
vectorFeatures.isVoiceBroadcastEnabled(), // TODO check user permission
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
attachmentTypeSelector.show(views.composerLayout.views.attachmentButton)
|
attachmentTypeSelector.show(composer.attachmentButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExpandOrCompactChange() {
|
override fun onExpandOrCompactChange() {
|
||||||
views.composerLayout.views.composerEmojiButton.isVisible = isEmojiKeyboardVisible
|
composer.emojiButton?.isVisible = isEmojiKeyboardVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSendMessage(text: CharSequence) {
|
override fun onSendMessage(text: CharSequence) {
|
||||||
sendTextMessage(text)
|
sendTextMessage(text, composer.formattedText)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCloseRelatedMessage() {
|
override fun onCloseRelatedMessage() {
|
||||||
@ -311,16 +329,20 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendTextMessage(text: CharSequence) {
|
private fun sendTextMessage(text: CharSequence, formattedText: String? = null) {
|
||||||
if (lockSendButton) {
|
if (lockSendButton) {
|
||||||
Timber.w("Send button is locked")
|
Timber.w("Send button is locked")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (text.isNotBlank()) {
|
if (text.isNotBlank()) {
|
||||||
// We collapse ASAP, if not there will be a slight annoying delay
|
// We collapse ASAP, if not there will be a slight annoying delay
|
||||||
views.composerLayout.collapse(true)
|
composer.collapse(true)
|
||||||
lockSendButton = true
|
lockSendButton = true
|
||||||
messageComposerViewModel.handle(MessageComposerAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
|
if (formattedText != null) {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.SendMessage(text, formattedText, false))
|
||||||
|
} else {
|
||||||
|
messageComposerViewModel.handle(MessageComposerAction.SendMessage(text, null, vectorPreferences.isMarkdownEnabled()))
|
||||||
|
}
|
||||||
emojiPopup.dismiss()
|
emojiPopup.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -336,22 +358,22 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
return isHandled
|
return isHandled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderRegularMode(content: String) {
|
private fun renderRegularMode(content: CharSequence) {
|
||||||
autoCompleter.exitSpecialMode()
|
autoCompleter.exitSpecialMode()
|
||||||
views.composerLayout.collapse()
|
composer.collapse()
|
||||||
views.composerLayout.setTextIfDifferent(content)
|
composer.setTextIfDifferent(content)
|
||||||
views.composerLayout.views.sendButton.contentDescription = getString(R.string.action_send)
|
composer.sendButton.contentDescription = getString(R.string.action_send)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderSpecialMode(
|
private fun renderSpecialMode(
|
||||||
event: TimelineEvent,
|
event: TimelineEvent,
|
||||||
@DrawableRes iconRes: Int,
|
@DrawableRes iconRes: Int,
|
||||||
@StringRes descriptionRes: Int,
|
@StringRes descriptionRes: Int,
|
||||||
defaultContent: String
|
defaultContent: CharSequence,
|
||||||
) {
|
) {
|
||||||
autoCompleter.enterSpecialMode()
|
autoCompleter.enterSpecialMode()
|
||||||
// switch to expanded bar
|
// switch to expanded bar
|
||||||
views.composerLayout.views.composerRelatedMessageTitle.apply {
|
composer.composerRelatedMessageTitle.apply {
|
||||||
text = event.senderInfo.disambiguatedDisplayName
|
text = event.senderInfo.disambiguatedDisplayName
|
||||||
setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(event.root.senderId ?: "@")))
|
setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(event.root.senderId ?: "@")))
|
||||||
}
|
}
|
||||||
@ -369,32 +391,32 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
|
val document = parser.parse(messageContent.formattedBody ?: messageContent.body)
|
||||||
formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor)
|
formattedBody = eventHtmlRenderer.render(document, pillsPostProcessor)
|
||||||
}
|
}
|
||||||
views.composerLayout.views.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody)
|
composer.composerRelatedMessageContent.text = (formattedBody ?: nonFormattedBody)
|
||||||
|
|
||||||
// Image Event
|
// Image Event
|
||||||
val data = event.buildImageContentRendererData(dimensionConverter.dpToPx(66))
|
val data = event.buildImageContentRendererData(dimensionConverter.dpToPx(66))
|
||||||
val isImageVisible = if (data != null) {
|
val isImageVisible = if (data != null) {
|
||||||
imageContentRenderer.render(data, ImageContentRenderer.Mode.THUMBNAIL, views.composerLayout.views.composerRelatedMessageImage)
|
imageContentRenderer.render(data, ImageContentRenderer.Mode.THUMBNAIL, composer.composerRelatedMessageImage)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
imageContentRenderer.clear(views.composerLayout.views.composerRelatedMessageImage)
|
imageContentRenderer.clear(composer.composerRelatedMessageImage)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
views.composerLayout.views.composerRelatedMessageImage.isVisible = isImageVisible
|
composer.composerRelatedMessageImage.isVisible = isImageVisible
|
||||||
|
|
||||||
views.composerLayout.setTextIfDifferent(defaultContent)
|
composer.replaceFormattedContent(defaultContent)
|
||||||
|
|
||||||
views.composerLayout.views.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
composer.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
||||||
views.composerLayout.views.sendButton.contentDescription = getString(descriptionRes)
|
composer.sendButton.contentDescription = getString(descriptionRes)
|
||||||
|
|
||||||
avatarRenderer.render(event.senderInfo.toMatrixItem(), views.composerLayout.views.composerRelatedMessageAvatar)
|
avatarRenderer.render(event.senderInfo.toMatrixItem(), composer.composerRelatedMessageAvatar)
|
||||||
|
|
||||||
views.composerLayout.expand {
|
composer.expand {
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
// need to do it here also when not using quick reply
|
// need to do it here also when not using quick reply
|
||||||
focusComposerAndShowKeyboard()
|
focusComposerAndShowKeyboard()
|
||||||
views.composerLayout.views.composerRelatedMessageImage.isVisible = isImageVisible
|
composer.composerRelatedMessageImage.isVisible = isImageVisible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
focusComposerAndShowKeyboard()
|
focusComposerAndShowKeyboard()
|
||||||
@ -402,7 +424,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
|
|
||||||
private fun observerUserTyping() {
|
private fun observerUserTyping() {
|
||||||
if (isThreadTimeLine()) return
|
if (isThreadTimeLine()) return
|
||||||
views.composerLayout.views.composerEditText.textChanges()
|
composer.editText.textChanges()
|
||||||
.skipInitialValue()
|
.skipInitialValue()
|
||||||
.debounce(300)
|
.debounce(300)
|
||||||
.map { it.isNotEmpty() }
|
.map { it.isNotEmpty() }
|
||||||
@ -412,7 +434,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
}
|
}
|
||||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
|
|
||||||
views.composerLayout.views.composerEditText.focusChanges()
|
composer.editText.focusChanges()
|
||||||
.onEach {
|
.onEach {
|
||||||
timelineViewModel.handle(RoomDetailAction.ComposerFocusChange(it))
|
timelineViewModel.handle(RoomDetailAction.ComposerFocusChange(it))
|
||||||
}
|
}
|
||||||
@ -420,18 +442,18 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun focusComposerAndShowKeyboard() {
|
private fun focusComposerAndShowKeyboard() {
|
||||||
if (views.composerLayout.isVisible) {
|
if (composer.isVisible) {
|
||||||
views.composerLayout.views.composerEditText.showKeyboard(andRequestFocus = true)
|
composer.editText.showKeyboard(andRequestFocus = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSendButtonVisibilityChanged(event: MessageComposerViewEvents.AnimateSendButtonVisibility) {
|
private fun handleSendButtonVisibilityChanged(event: MessageComposerViewEvents.AnimateSendButtonVisibility) {
|
||||||
if (event.isVisible) {
|
if (event.isVisible) {
|
||||||
views.root.views.sendButton.alpha = 0f
|
composer.sendButton.alpha = 0f
|
||||||
views.root.views.sendButton.isVisible = true
|
composer.sendButton.isVisible = true
|
||||||
views.root.views.sendButton.animate().alpha(1f).setDuration(150).start()
|
composer.sendButton.animate().alpha(1f).setDuration(150).start()
|
||||||
} else {
|
} else {
|
||||||
views.root.views.sendButton.isInvisible = true
|
composer.sendButton.isInvisible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,18 +477,18 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
rootView = views.root,
|
rootView = views.root,
|
||||||
keyboardAnimationStyle = R.style.emoji_fade_animation_style,
|
keyboardAnimationStyle = R.style.emoji_fade_animation_style,
|
||||||
onEmojiPopupShownListener = {
|
onEmojiPopupShownListener = {
|
||||||
views.composerLayout.views.composerEmojiButton.apply {
|
composer.emojiButton?.apply {
|
||||||
contentDescription = getString(R.string.a11y_close_emoji_picker)
|
contentDescription = getString(R.string.a11y_close_emoji_picker)
|
||||||
setImageResource(R.drawable.ic_keyboard)
|
setImageResource(R.drawable.ic_keyboard)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEmojiPopupDismissListener = lifecycleAwareDismissAction {
|
onEmojiPopupDismissListener = lifecycleAwareDismissAction {
|
||||||
views.composerLayout.views.composerEmojiButton.apply {
|
composer.emojiButton?.apply {
|
||||||
contentDescription = getString(R.string.a11y_open_emoji_picker)
|
contentDescription = getString(R.string.a11y_open_emoji_picker)
|
||||||
setImageResource(R.drawable.ic_insert_emoji)
|
setImageResource(R.drawable.ic_insert_emoji)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editText = views.composerLayout.views.composerEditText
|
editText = composer.editText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,7 +505,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupEmojiButton() {
|
private fun setupEmojiButton() {
|
||||||
views.composerLayout.views.composerEmojiButton.debouncedClicks {
|
composer.emojiButton?.debouncedClicks {
|
||||||
emojiPopup.toggle()
|
emojiPopup.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -494,7 +516,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleJoinedToAnotherRoom(action: MessageComposerViewEvents.JoinRoomCommandSuccess) {
|
private fun handleJoinedToAnotherRoom(action: MessageComposerViewEvents.JoinRoomCommandSuccess) {
|
||||||
views.composerLayout.setTextIfDifferent("")
|
composer.setTextIfDifferent("")
|
||||||
lockSendButton = false
|
lockSendButton = false
|
||||||
navigator.openRoom(vectorBaseActivity, action.roomId)
|
navigator.openRoom(vectorBaseActivity, action.roomId)
|
||||||
}
|
}
|
||||||
@ -549,7 +571,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
|
|
||||||
private fun handleSlashCommandResultOk(parsedCommand: ParsedCommand) {
|
private fun handleSlashCommandResultOk(parsedCommand: ParsedCommand) {
|
||||||
dismissLoadingDialog()
|
dismissLoadingDialog()
|
||||||
views.composerLayout.setTextIfDifferent("")
|
composer.setTextIfDifferent("")
|
||||||
when (parsedCommand) {
|
when (parsedCommand) {
|
||||||
is ParsedCommand.DevTools -> {
|
is ParsedCommand.DevTools -> {
|
||||||
navigator.openDevTools(requireContext(), roomId)
|
navigator.openDevTools(requireContext(), roomId)
|
||||||
@ -608,7 +630,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
|
|
||||||
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
||||||
val formattedContact = contactAttachment.toHumanReadable()
|
val formattedContact = contactAttachment.toHumanReadable()
|
||||||
messageComposerViewModel.handle(MessageComposerAction.SendMessage(formattedContact, false))
|
messageComposerViewModel.handle(MessageComposerAction.SendMessage(formattedContact, null, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachmentError(throwable: Throwable) {
|
override fun onAttachmentError(throwable: Throwable) {
|
||||||
@ -718,13 +740,13 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
private fun insertUserDisplayNameInTextEditor(userId: String) {
|
private fun insertUserDisplayNameInTextEditor(userId: String) {
|
||||||
val startToCompose = views.composerLayout.text.isNullOrBlank()
|
val startToCompose = composer.text.isNullOrBlank()
|
||||||
|
|
||||||
if (startToCompose &&
|
if (startToCompose &&
|
||||||
userId == session.myUserId) {
|
userId == session.myUserId) {
|
||||||
// Empty composer, current user: start an emote
|
// Empty composer, current user: start an emote
|
||||||
views.composerLayout.views.composerEditText.setText("${Command.EMOTE.command} ")
|
composer.editText.setText("${Command.EMOTE.command} ")
|
||||||
views.composerLayout.views.composerEditText.setSelection(Command.EMOTE.command.length + 1)
|
composer.editText.setSelection(Command.EMOTE.command.length + 1)
|
||||||
} else {
|
} else {
|
||||||
val roomMember = timelineViewModel.getMember(userId)
|
val roomMember = timelineViewModel.getMember(userId)
|
||||||
val displayName = sanitizeDisplayName(roomMember?.displayName ?: userId)
|
val displayName = sanitizeDisplayName(roomMember?.displayName ?: userId)
|
||||||
@ -737,7 +759,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
requireContext(),
|
requireContext(),
|
||||||
MatrixItem.UserItem(userId, displayName, roomMember?.avatarUrl)
|
MatrixItem.UserItem(userId, displayName, roomMember?.avatarUrl)
|
||||||
)
|
)
|
||||||
.also { it.bind(views.composerLayout.views.composerEditText) },
|
.also { it.bind(composer.editText) },
|
||||||
0,
|
0,
|
||||||
displayName.length,
|
displayName.length,
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
@ -747,11 +769,11 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
|
|||||||
if (startToCompose) {
|
if (startToCompose) {
|
||||||
if (displayName.startsWith("/")) {
|
if (displayName.startsWith("/")) {
|
||||||
// Ensure displayName will not be interpreted as a Slash command
|
// Ensure displayName will not be interpreted as a Slash command
|
||||||
views.composerLayout.views.composerEditText.append("\\")
|
composer.editText.append("\\")
|
||||||
}
|
}
|
||||||
views.composerLayout.views.composerEditText.append(pill)
|
composer.editText.append(pill)
|
||||||
} else {
|
} else {
|
||||||
views.composerLayout.views.composerEditText.text?.insert(views.composerLayout.views.composerEditText.selectionStart, pill)
|
composer.editText.text?.insert(composer.editText.selectionStart, pill)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
focusComposerAndShowKeyboard()
|
focusComposerAndShowKeyboard()
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 New Vector Ltd
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -16,137 +16,34 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.composer
|
package im.vector.app.features.home.room.detail.composer
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.util.AttributeSet
|
import android.widget.EditText
|
||||||
import android.view.ViewGroup
|
import android.widget.ImageButton
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import android.widget.ImageView
|
||||||
import androidx.constraintlayout.widget.ConstraintSet
|
import android.widget.TextView
|
||||||
import androidx.core.text.toSpannable
|
|
||||||
import androidx.transition.ChangeBounds
|
|
||||||
import androidx.transition.Fade
|
|
||||||
import androidx.transition.Transition
|
|
||||||
import androidx.transition.TransitionManager
|
|
||||||
import androidx.transition.TransitionSet
|
|
||||||
import im.vector.app.R
|
|
||||||
import im.vector.app.core.extensions.setTextIfDifferent
|
|
||||||
import im.vector.app.databinding.ComposerLayoutBinding
|
|
||||||
|
|
||||||
/**
|
interface MessageComposerView {
|
||||||
* Encapsulate the timeline composer UX.
|
|
||||||
*/
|
|
||||||
class MessageComposerView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
interface Callback : ComposerEditText.Callback {
|
|
||||||
fun onCloseRelatedMessage()
|
|
||||||
fun onSendMessage(text: CharSequence)
|
|
||||||
fun onAddAttachment()
|
|
||||||
fun onExpandOrCompactChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
val views: ComposerLayoutBinding
|
|
||||||
|
|
||||||
var callback: Callback? = null
|
|
||||||
|
|
||||||
private var currentConstraintSetId: Int = -1
|
|
||||||
|
|
||||||
private val animationDuration = 100L
|
|
||||||
|
|
||||||
val text: Editable?
|
val text: Editable?
|
||||||
get() = views.composerEditText.text
|
val formattedText: String?
|
||||||
|
val editText: EditText
|
||||||
|
val emojiButton: ImageButton?
|
||||||
|
val sendButton: ImageButton
|
||||||
|
val attachmentButton: ImageButton
|
||||||
|
val composerRelatedMessageTitle: TextView
|
||||||
|
val composerRelatedMessageContent: TextView
|
||||||
|
val composerRelatedMessageImage: ImageView
|
||||||
|
val composerRelatedMessageActionIcon: ImageView
|
||||||
|
val composerRelatedMessageAvatar: ImageView
|
||||||
|
|
||||||
init {
|
var callback: PlainTextComposerLayout.Callback?
|
||||||
inflate(context, R.layout.composer_layout, this)
|
|
||||||
views = ComposerLayoutBinding.bind(this)
|
|
||||||
|
|
||||||
collapse(false)
|
var isVisible: Boolean
|
||||||
|
|
||||||
views.composerEditText.callback = object : ComposerEditText.Callback {
|
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null)
|
||||||
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null)
|
||||||
return callback?.onRichContentSelected(contentUri) ?: false
|
fun setTextIfDifferent(text: CharSequence?): Boolean
|
||||||
}
|
fun replaceFormattedContent(text: CharSequence)
|
||||||
|
|
||||||
override fun onTextChanged(text: CharSequence) {
|
fun setInvisible(isInvisible: Boolean)
|
||||||
callback?.onTextChanged(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
views.composerRelatedMessageCloseButton.setOnClickListener {
|
|
||||||
collapse()
|
|
||||||
callback?.onCloseRelatedMessage()
|
|
||||||
}
|
|
||||||
|
|
||||||
views.sendButton.setOnClickListener {
|
|
||||||
val textMessage = text?.toSpannable() ?: ""
|
|
||||||
callback?.onSendMessage(textMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
views.attachmentButton.setOnClickListener {
|
|
||||||
callback?.onAddAttachment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
|
||||||
if (currentConstraintSetId == R.layout.composer_layout_constraint_set_compact) {
|
|
||||||
// ignore we good
|
|
||||||
return
|
|
||||||
}
|
|
||||||
currentConstraintSetId = R.layout.composer_layout_constraint_set_compact
|
|
||||||
applyNewConstraintSet(animate, transitionComplete)
|
|
||||||
callback?.onExpandOrCompactChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
|
||||||
if (currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded) {
|
|
||||||
// ignore we good
|
|
||||||
return
|
|
||||||
}
|
|
||||||
currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded
|
|
||||||
applyNewConstraintSet(animate, transitionComplete)
|
|
||||||
callback?.onExpandOrCompactChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTextIfDifferent(text: CharSequence?): Boolean {
|
|
||||||
return views.composerEditText.setTextIfDifferent(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
|
||||||
// val wasSendButtonInvisible = views.sendButton.isInvisible
|
|
||||||
if (animate) {
|
|
||||||
configureAndBeginTransition(transitionComplete)
|
|
||||||
}
|
|
||||||
ConstraintSet().also {
|
|
||||||
it.clone(context, currentConstraintSetId)
|
|
||||||
it.applyTo(this)
|
|
||||||
}
|
|
||||||
// Might be updated by view state just after, but avoid blinks
|
|
||||||
// views.sendButton.isInvisible = wasSendButtonInvisible
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun configureAndBeginTransition(transitionComplete: (() -> Unit)? = null) {
|
|
||||||
val transition = TransitionSet().apply {
|
|
||||||
ordering = TransitionSet.ORDERING_SEQUENTIAL
|
|
||||||
addTransition(ChangeBounds())
|
|
||||||
addTransition(Fade(Fade.IN))
|
|
||||||
duration = animationDuration
|
|
||||||
addListener(object : Transition.TransitionListener {
|
|
||||||
override fun onTransitionEnd(transition: Transition) {
|
|
||||||
transitionComplete?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTransitionResume(transition: Transition) {}
|
|
||||||
|
|
||||||
override fun onTransitionPause(transition: Transition) {}
|
|
||||||
|
|
||||||
override fun onTransitionCancel(transition: Transition) {}
|
|
||||||
|
|
||||||
override fun onTransitionStart(transition: Transition) {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
|
|||||||
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
|
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm
|
import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread
|
import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread
|
||||||
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
||||||
@ -201,6 +202,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
is SendMode.Regular -> {
|
is SendMode.Regular -> {
|
||||||
when (val parsedCommand = commandParser.parseSlashCommand(
|
when (val parsedCommand = commandParser.parseSlashCommand(
|
||||||
textMessage = action.text,
|
textMessage = action.text,
|
||||||
|
formattedMessage = action.formattedText,
|
||||||
isInThreadTimeline = state.isInThreadTimeline()
|
isInThreadTimeline = state.isInThreadTimeline()
|
||||||
)) {
|
)) {
|
||||||
is ParsedCommand.ErrorNotACommand -> {
|
is ParsedCommand.ErrorNotACommand -> {
|
||||||
@ -209,10 +211,15 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
room.relationService().replyInThread(
|
room.relationService().replyInThread(
|
||||||
rootThreadEventId = state.rootThreadEventId,
|
rootThreadEventId = state.rootThreadEventId,
|
||||||
replyInThreadText = action.text,
|
replyInThreadText = action.text,
|
||||||
|
formattedText = action.formattedText,
|
||||||
autoMarkdown = action.autoMarkdown
|
autoMarkdown = action.autoMarkdown
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
room.sendService().sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
if (action.formattedText != null) {
|
||||||
|
room.sendService().sendFormattedTextMessage(action.text.toString(), action.formattedText)
|
||||||
|
} else {
|
||||||
|
room.sendService().sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_viewEvents.post(MessageComposerViewEvents.MessageSent)
|
_viewEvents.post(MessageComposerViewEvents.MessageSent)
|
||||||
@ -244,6 +251,24 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
_viewEvents.post(MessageComposerViewEvents.MessageSent)
|
_viewEvents.post(MessageComposerViewEvents.MessageSent)
|
||||||
popDraft()
|
popDraft()
|
||||||
}
|
}
|
||||||
|
is ParsedCommand.SendFormattedText -> {
|
||||||
|
// Send the text message to the room, without markdown
|
||||||
|
if (state.rootThreadEventId != null) {
|
||||||
|
room.relationService().replyInThread(
|
||||||
|
rootThreadEventId = state.rootThreadEventId,
|
||||||
|
replyInThreadText = parsedCommand.message,
|
||||||
|
formattedText = parsedCommand.formattedMessage,
|
||||||
|
autoMarkdown = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
room.sendService().sendFormattedTextMessage(
|
||||||
|
text = parsedCommand.message.toString(),
|
||||||
|
formattedText = parsedCommand.formattedMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_viewEvents.post(MessageComposerViewEvents.MessageSent)
|
||||||
|
popDraft()
|
||||||
|
}
|
||||||
is ParsedCommand.ChangeRoomName -> {
|
is ParsedCommand.ChangeRoomName -> {
|
||||||
handleChangeRoomNameSlashCommand(parsedCommand)
|
handleChangeRoomNameSlashCommand(parsedCommand)
|
||||||
}
|
}
|
||||||
@ -510,16 +535,24 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
if (inReplyTo != null) {
|
if (inReplyTo != null) {
|
||||||
// TODO check if same content?
|
// TODO check if same content?
|
||||||
room.getTimelineEvent(inReplyTo)?.let {
|
room.getTimelineEvent(inReplyTo)?.let {
|
||||||
room.relationService().editReply(state.sendMode.timelineEvent, it, action.text.toString())
|
room.relationService().editReply(state.sendMode.timelineEvent, it, action.text.toString(), action.formattedText)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val messageContent = state.sendMode.timelineEvent.getVectorLastMessageContent()
|
val messageContent = state.sendMode.timelineEvent.getVectorLastMessageContent()
|
||||||
val existingBody = messageContent?.body ?: ""
|
val existingBody: String
|
||||||
if (existingBody != action.text) {
|
val needsEdit = if (messageContent is MessageContentWithFormattedBody) {
|
||||||
|
existingBody = messageContent.formattedBody ?: ""
|
||||||
|
existingBody != action.formattedText
|
||||||
|
} else {
|
||||||
|
existingBody = messageContent?.body ?: ""
|
||||||
|
existingBody != action.text
|
||||||
|
}
|
||||||
|
if (needsEdit) {
|
||||||
room.relationService().editTextMessage(
|
room.relationService().editTextMessage(
|
||||||
state.sendMode.timelineEvent,
|
state.sendMode.timelineEvent,
|
||||||
messageContent?.msgType ?: MessageType.MSGTYPE_TEXT,
|
messageContent?.msgType ?: MessageType.MSGTYPE_TEXT,
|
||||||
action.text,
|
action.text,
|
||||||
|
(messageContent as? MessageContentWithFormattedBody)?.formattedBody,
|
||||||
action.autoMarkdown
|
action.autoMarkdown
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -533,6 +566,7 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
room.sendService().sendQuotedTextMessage(
|
room.sendService().sendQuotedTextMessage(
|
||||||
quotedEvent = state.sendMode.timelineEvent,
|
quotedEvent = state.sendMode.timelineEvent,
|
||||||
text = action.text.toString(),
|
text = action.text.toString(),
|
||||||
|
formattedText = action.formattedText,
|
||||||
autoMarkdown = action.autoMarkdown,
|
autoMarkdown = action.autoMarkdown,
|
||||||
rootThreadEventId = state.rootThreadEventId
|
rootThreadEventId = state.rootThreadEventId
|
||||||
)
|
)
|
||||||
@ -549,11 +583,13 @@ class MessageComposerViewModel @AssistedInject constructor(
|
|||||||
rootThreadEventId = it,
|
rootThreadEventId = it,
|
||||||
replyInThreadText = action.text.toString(),
|
replyInThreadText = action.text.toString(),
|
||||||
autoMarkdown = action.autoMarkdown,
|
autoMarkdown = action.autoMarkdown,
|
||||||
|
formattedText = action.formattedText,
|
||||||
eventReplied = timelineEvent
|
eventReplied = timelineEvent
|
||||||
)
|
)
|
||||||
} ?: room.relationService().replyToMessage(
|
} ?: room.relationService().replyToMessage(
|
||||||
eventReplied = timelineEvent,
|
eventReplied = timelineEvent,
|
||||||
replyText = action.text.toString(),
|
replyText = action.text.toString(),
|
||||||
|
replyFormattedText = action.formattedText,
|
||||||
autoMarkdown = action.autoMarkdown,
|
autoMarkdown = action.autoMarkdown,
|
||||||
showInThread = showInThread,
|
showInThread = showInThread,
|
||||||
rootThreadEventId = rootThreadEventId
|
rootThreadEventId = rootThreadEventId
|
||||||
|
@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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 im.vector.app.features.home.room.detail.composer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.Editable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.transition.ChangeBounds
|
||||||
|
import androidx.transition.Fade
|
||||||
|
import androidx.transition.Transition
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
|
import androidx.transition.TransitionSet
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.animations.SimpleTransitionListener
|
||||||
|
import im.vector.app.core.extensions.setTextIfDifferent
|
||||||
|
import im.vector.app.databinding.ComposerLayoutBinding
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulate the timeline composer UX.
|
||||||
|
*/
|
||||||
|
class PlainTextComposerLayout @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView {
|
||||||
|
|
||||||
|
interface Callback : ComposerEditText.Callback {
|
||||||
|
fun onCloseRelatedMessage()
|
||||||
|
fun onSendMessage(text: CharSequence)
|
||||||
|
fun onAddAttachment()
|
||||||
|
fun onExpandOrCompactChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val views: ComposerLayoutBinding
|
||||||
|
|
||||||
|
override var callback: Callback? = null
|
||||||
|
|
||||||
|
private var currentConstraintSetId: Int = -1
|
||||||
|
|
||||||
|
private val animationDuration = 100L
|
||||||
|
|
||||||
|
override val text: Editable?
|
||||||
|
get() = views.composerEditText.text
|
||||||
|
|
||||||
|
override val formattedText: String? = null
|
||||||
|
|
||||||
|
override val editText: EditText
|
||||||
|
get() = views.composerEditText
|
||||||
|
|
||||||
|
override val emojiButton: ImageButton?
|
||||||
|
get() = views.composerEmojiButton
|
||||||
|
|
||||||
|
override val sendButton: ImageButton
|
||||||
|
get() = views.sendButton
|
||||||
|
|
||||||
|
override fun setInvisible(isInvisible: Boolean) {
|
||||||
|
this.isInvisible = isInvisible
|
||||||
|
}
|
||||||
|
override val attachmentButton: ImageButton
|
||||||
|
get() = views.attachmentButton
|
||||||
|
override val composerRelatedMessageActionIcon: ImageView
|
||||||
|
get() = views.composerRelatedMessageActionIcon
|
||||||
|
override val composerRelatedMessageAvatar: ImageView
|
||||||
|
get() = views.composerRelatedMessageAvatar
|
||||||
|
override val composerRelatedMessageContent: TextView
|
||||||
|
get() = views.composerRelatedMessageContent
|
||||||
|
override val composerRelatedMessageImage: ImageView
|
||||||
|
get() = views.composerRelatedMessageImage
|
||||||
|
override val composerRelatedMessageTitle: TextView
|
||||||
|
get() = views.composerRelatedMessageTitle
|
||||||
|
override var isVisible: Boolean
|
||||||
|
get() = views.root.isVisible
|
||||||
|
set(value) { views.root.isVisible = value }
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(context, R.layout.composer_layout, this)
|
||||||
|
views = ComposerLayoutBinding.bind(this)
|
||||||
|
|
||||||
|
collapse(false)
|
||||||
|
|
||||||
|
views.composerEditText.callback = object : ComposerEditText.Callback {
|
||||||
|
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
||||||
|
return callback?.onRichContentSelected(contentUri) ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(text: CharSequence) {
|
||||||
|
callback?.onTextChanged(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
views.composerRelatedMessageCloseButton.setOnClickListener {
|
||||||
|
collapse()
|
||||||
|
callback?.onCloseRelatedMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
views.sendButton.setOnClickListener {
|
||||||
|
val textMessage = text?.toSpannable() ?: ""
|
||||||
|
callback?.onSendMessage(textMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
views.attachmentButton.setOnClickListener {
|
||||||
|
callback?.onAddAttachment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun replaceFormattedContent(text: CharSequence) {
|
||||||
|
setTextIfDifferent(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||||
|
if (currentConstraintSetId == R.layout.composer_layout_constraint_set_compact) {
|
||||||
|
// ignore we good
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentConstraintSetId = R.layout.composer_layout_constraint_set_compact
|
||||||
|
applyNewConstraintSet(animate, transitionComplete)
|
||||||
|
callback?.onExpandOrCompactChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||||
|
if (currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded) {
|
||||||
|
// ignore we good
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded
|
||||||
|
applyNewConstraintSet(animate, transitionComplete)
|
||||||
|
callback?.onExpandOrCompactChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTextIfDifferent(text: CharSequence?): Boolean {
|
||||||
|
return views.composerEditText.setTextIfDifferent(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||||
|
// val wasSendButtonInvisible = views.sendButton.isInvisible
|
||||||
|
if (animate) {
|
||||||
|
configureAndBeginTransition(transitionComplete)
|
||||||
|
}
|
||||||
|
ConstraintSet().also {
|
||||||
|
it.clone(context, currentConstraintSetId)
|
||||||
|
it.applyTo(this)
|
||||||
|
}
|
||||||
|
// Might be updated by view state just after, but avoid blinks
|
||||||
|
// views.sendButton.isInvisible = wasSendButtonInvisible
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureAndBeginTransition(transitionComplete: (() -> Unit)? = null) {
|
||||||
|
val transition = TransitionSet().apply {
|
||||||
|
ordering = TransitionSet.ORDERING_SEQUENTIAL
|
||||||
|
addTransition(ChangeBounds())
|
||||||
|
addTransition(Fade(Fade.IN))
|
||||||
|
duration = animationDuration
|
||||||
|
addListener(object : SimpleTransitionListener() {
|
||||||
|
override fun onTransitionEnd(transition: Transition) {
|
||||||
|
transitionComplete?.invoke()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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 im.vector.app.features.home.room.detail.composer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.constraintlayout.widget.ConstraintSet
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.transition.ChangeBounds
|
||||||
|
import androidx.transition.Fade
|
||||||
|
import androidx.transition.Transition
|
||||||
|
import androidx.transition.TransitionManager
|
||||||
|
import androidx.transition.TransitionSet
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.animations.SimpleTransitionListener
|
||||||
|
import im.vector.app.core.extensions.setTextIfDifferent
|
||||||
|
import im.vector.app.databinding.ComposerRichTextLayoutBinding
|
||||||
|
import im.vector.app.databinding.ViewRichTextMenuButtonBinding
|
||||||
|
import io.element.android.wysiwyg.InlineFormat
|
||||||
|
|
||||||
|
class RichTextComposerLayout @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : ConstraintLayout(context, attrs, defStyleAttr), MessageComposerView {
|
||||||
|
|
||||||
|
private val views: ComposerRichTextLayoutBinding
|
||||||
|
|
||||||
|
override var callback: PlainTextComposerLayout.Callback? = null
|
||||||
|
|
||||||
|
private var currentConstraintSetId: Int = -1
|
||||||
|
|
||||||
|
private val animationDuration = 100L
|
||||||
|
|
||||||
|
override val text: Editable?
|
||||||
|
get() = views.composerEditText.text
|
||||||
|
override val formattedText: String?
|
||||||
|
get() = views.composerEditText.getHtmlOutput()
|
||||||
|
override val editText: EditText
|
||||||
|
get() = views.composerEditText
|
||||||
|
override val emojiButton: ImageButton?
|
||||||
|
get() = null
|
||||||
|
override val sendButton: ImageButton
|
||||||
|
get() = views.sendButton
|
||||||
|
override val attachmentButton: ImageButton
|
||||||
|
get() = views.attachmentButton
|
||||||
|
override val composerRelatedMessageActionIcon: ImageView
|
||||||
|
get() = views.composerRelatedMessageActionIcon
|
||||||
|
override val composerRelatedMessageAvatar: ImageView
|
||||||
|
get() = views.composerRelatedMessageAvatar
|
||||||
|
override val composerRelatedMessageContent: TextView
|
||||||
|
get() = views.composerRelatedMessageContent
|
||||||
|
override val composerRelatedMessageImage: ImageView
|
||||||
|
get() = views.composerRelatedMessageImage
|
||||||
|
override val composerRelatedMessageTitle: TextView
|
||||||
|
get() = views.composerRelatedMessageTitle
|
||||||
|
override var isVisible: Boolean
|
||||||
|
get() = views.root.isVisible
|
||||||
|
set(value) { views.root.isVisible = value }
|
||||||
|
|
||||||
|
init {
|
||||||
|
inflate(context, R.layout.composer_rich_text_layout, this)
|
||||||
|
views = ComposerRichTextLayoutBinding.bind(this)
|
||||||
|
|
||||||
|
collapse(false)
|
||||||
|
|
||||||
|
views.composerEditText.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
override fun afterTextChanged(s: Editable) {
|
||||||
|
callback?.onTextChanged(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
views.composerRelatedMessageCloseButton.setOnClickListener {
|
||||||
|
collapse()
|
||||||
|
callback?.onCloseRelatedMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
views.sendButton.setOnClickListener {
|
||||||
|
val textMessage = text?.toSpannable() ?: ""
|
||||||
|
callback?.onSendMessage(textMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
views.attachmentButton.setOnClickListener {
|
||||||
|
callback?.onAddAttachment()
|
||||||
|
}
|
||||||
|
|
||||||
|
setupRichTextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRichTextMenu() {
|
||||||
|
addRichTextMenuItem(R.drawable.ic_composer_bold, "Bold") {
|
||||||
|
views.composerEditText.toggleInlineFormat(InlineFormat.Bold)
|
||||||
|
}
|
||||||
|
addRichTextMenuItem(R.drawable.ic_composer_italic, "Italic") {
|
||||||
|
views.composerEditText.toggleInlineFormat(InlineFormat.Italic)
|
||||||
|
}
|
||||||
|
addRichTextMenuItem(R.drawable.ic_composer_underlined, "Underline") {
|
||||||
|
views.composerEditText.toggleInlineFormat(InlineFormat.Underline)
|
||||||
|
}
|
||||||
|
addRichTextMenuItem(R.drawable.ic_composer_strikethrough, "Strikethrough") {
|
||||||
|
views.composerEditText.toggleInlineFormat(InlineFormat.StrikeThrough)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addRichTextMenuItem(@DrawableRes iconId: Int, description: String, action: () -> Unit) {
|
||||||
|
val inflater = LayoutInflater.from(context)
|
||||||
|
val button = ViewRichTextMenuButtonBinding.inflate(inflater, views.richTextMenu, true)
|
||||||
|
with(button.root) {
|
||||||
|
contentDescription = description
|
||||||
|
setImageResource(iconId)
|
||||||
|
setOnClickListener {
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun replaceFormattedContent(text: CharSequence) {
|
||||||
|
views.composerEditText.setHtml(text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun collapse(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||||
|
if (currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_compact) {
|
||||||
|
// ignore we good
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_compact
|
||||||
|
applyNewConstraintSet(animate, transitionComplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun expand(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||||
|
if (currentConstraintSetId == R.layout.composer_rich_text_layout_constraint_set_expanded) {
|
||||||
|
// ignore we good
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentConstraintSetId = R.layout.composer_rich_text_layout_constraint_set_expanded
|
||||||
|
applyNewConstraintSet(animate, transitionComplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTextIfDifferent(text: CharSequence?): Boolean {
|
||||||
|
return views.composerEditText.setTextIfDifferent(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) {
|
||||||
|
// val wasSendButtonInvisible = views.sendButton.isInvisible
|
||||||
|
if (animate) {
|
||||||
|
configureAndBeginTransition(transitionComplete)
|
||||||
|
}
|
||||||
|
ConstraintSet().also {
|
||||||
|
it.clone(context, currentConstraintSetId)
|
||||||
|
it.applyTo(this)
|
||||||
|
}
|
||||||
|
// Might be updated by view state just after, but avoid blinks
|
||||||
|
// views.sendButton.isInvisible = wasSendButtonInvisible
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureAndBeginTransition(transitionComplete: (() -> Unit)? = null) {
|
||||||
|
val transition = TransitionSet().apply {
|
||||||
|
ordering = TransitionSet.ORDERING_SEQUENTIAL
|
||||||
|
addTransition(ChangeBounds())
|
||||||
|
addTransition(Fade(Fade.IN))
|
||||||
|
duration = animationDuration
|
||||||
|
addListener(object : SimpleTransitionListener() {
|
||||||
|
override fun onTransitionEnd(transition: Transition) {
|
||||||
|
transitionComplete?.invoke()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setInvisible(isInvisible: Boolean) {
|
||||||
|
this.isInvisible = isInvisible
|
||||||
|
}
|
||||||
|
}
|
@ -37,6 +37,9 @@ import im.vector.app.features.home.room.detail.TimelineViewModel
|
|||||||
import im.vector.app.features.home.room.detail.composer.MessageComposerAction
|
import im.vector.app.features.home.room.detail.composer.MessageComposerAction
|
||||||
import im.vector.app.features.home.room.detail.composer.MessageComposerViewEvents
|
import im.vector.app.features.home.room.detail.composer.MessageComposerViewEvents
|
||||||
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
|
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
|
||||||
|
import im.vector.app.features.home.room.detail.composer.MessageComposerViewState
|
||||||
|
import im.vector.app.features.home.room.detail.composer.SendMode
|
||||||
|
import im.vector.app.features.home.room.detail.composer.boolean
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -70,6 +73,15 @@ class VoiceRecorderFragment : VectorBaseFragment<FragmentVoiceRecorderBinding>()
|
|||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend ->
|
||||||
|
if (!canSend.boolean()) {
|
||||||
|
return@onEach
|
||||||
|
}
|
||||||
|
if (mode is SendMode.Voice) {
|
||||||
|
views.voiceMessageRecorderView.isVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -71,6 +71,7 @@ class VectorPreferences @Inject constructor(
|
|||||||
const val SETTINGS_LABS_PREFERENCE_KEY = "SETTINGS_LABS_PREFERENCE_KEY"
|
const val SETTINGS_LABS_PREFERENCE_KEY = "SETTINGS_LABS_PREFERENCE_KEY"
|
||||||
const val SETTINGS_LABS_NEW_APP_LAYOUT_KEY = "SETTINGS_LABS_NEW_APP_LAYOUT_KEY"
|
const val SETTINGS_LABS_NEW_APP_LAYOUT_KEY = "SETTINGS_LABS_NEW_APP_LAYOUT_KEY"
|
||||||
const val SETTINGS_LABS_DEFERRED_DM_KEY = "SETTINGS_LABS_DEFERRED_DM_KEY"
|
const val SETTINGS_LABS_DEFERRED_DM_KEY = "SETTINGS_LABS_DEFERRED_DM_KEY"
|
||||||
|
const val SETTINGS_LABS_RICH_TEXT_EDITOR_KEY = "SETTINGS_LABS_RICH_TEXT_EDITOR_KEY"
|
||||||
const val SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY"
|
const val SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY"
|
||||||
const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY"
|
const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY"
|
||||||
const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
|
const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
|
||||||
@ -1182,4 +1183,8 @@ class VectorPreferences @Inject constructor(
|
|||||||
fun showLiveSenderInfo(): Boolean {
|
fun showLiveSenderInfo(): Boolean {
|
||||||
return defaultPrefs.getBoolean(SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO, getDefault(R.bool.settings_timeline_show_live_sender_info_default))
|
return defaultPrefs.getBoolean(SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO, getDefault(R.bool.settings_timeline_show_live_sender_info_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isRichTextEditorEnabled(): Boolean {
|
||||||
|
return defaultPrefs.getBoolean(SETTINGS_LABS_RICH_TEXT_EDITOR_KEY, getDefault(R.bool.settings_labs_rich_text_editor_default))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
10
vector/src/main/res/drawable/ic_composer_bold.xml
Normal file
10
vector/src/main/res/drawable/ic_composer_bold.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="44dp"
|
||||||
|
android:height="44dp"
|
||||||
|
android:viewportWidth="44"
|
||||||
|
android:viewportHeight="44">
|
||||||
|
<path
|
||||||
|
android:pathData="M16,14.5C16,13.672 16.672,13 17.5,13H22.288C25.139,13 27.25,15.466 27.25,18.25C27.25,19.38 26.902,20.458 26.298,21.34C27.765,22.268 28.75,23.882 28.75,25.75C28.75,28.689 26.311,31 23.393,31H17.5C16.672,31 16,30.328 16,29.5V14.5ZM19,16V20.5H22.288C23.261,20.5 24.25,19.608 24.25,18.25C24.25,16.892 23.261,16 22.288,16H19ZM19,23.5V28H23.393C24.735,28 25.75,26.953 25.75,25.75C25.75,24.547 24.735,23.5 23.393,23.5H19Z"
|
||||||
|
android:fillColor="#8D97A5"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
10
vector/src/main/res/drawable/ic_composer_italic.xml
Normal file
10
vector/src/main/res/drawable/ic_composer_italic.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="44dp"
|
||||||
|
android:height="44dp"
|
||||||
|
android:viewportWidth="44"
|
||||||
|
android:viewportHeight="44">
|
||||||
|
<path
|
||||||
|
android:pathData="M22.619,14.999L19.747,29.005H17.2C16.758,29.005 16.4,29.363 16.4,29.805C16.4,30.247 16.758,30.605 17.2,30.605H20.389C20.397,30.605 20.405,30.605 20.412,30.605H23.6C24.042,30.605 24.4,30.247 24.4,29.805C24.4,29.363 24.042,29.005 23.6,29.005H21.381L24.253,14.999H26.8C27.242,14.999 27.6,14.64 27.6,14.199C27.6,13.757 27.242,13.399 26.8,13.399H23.615C23.604,13.398 23.594,13.398 23.583,13.399H20.4C19.958,13.399 19.6,13.757 19.6,14.199C19.6,14.64 19.958,14.999 20.4,14.999H22.619Z"
|
||||||
|
android:fillColor="#8D97A5"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
12
vector/src/main/res/drawable/ic_composer_strikethrough.xml
Normal file
12
vector/src/main/res/drawable/ic_composer_strikethrough.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="44dp"
|
||||||
|
android:height="44dp"
|
||||||
|
android:viewportWidth="44"
|
||||||
|
android:viewportHeight="44">
|
||||||
|
<path
|
||||||
|
android:pathData="M24.897,17.154C24.235,15.821 22.876,15.21 21.374,15.372C19.05,15.622 18.44,17.423 18.722,18.592C19.032,19.872 20.046,20.37 21.839,20.826H29.92C30.517,20.826 31,21.351 31,22C31,22.648 30.517,23.174 29.92,23.174H14.08C13.483,23.174 13,22.648 13,22C13,21.351 13.483,20.826 14.08,20.826H17.355C17.041,20.377 16.791,19.839 16.633,19.189C16.003,16.581 17.554,13.424 21.16,13.036C23.285,12.807 25.615,13.661 26.798,16.038C27.081,16.608 26.886,17.32 26.361,17.629C25.836,17.937 25.181,17.725 24.897,17.154Z"
|
||||||
|
android:fillColor="#8D97A5"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M25.427,25.13H27.67C27.888,26.306 27.721,27.56 27.05,28.632C26.114,30.125 24.37,31 21.985,31C18.076,31 16.279,28.584 15.912,26.986C15.768,26.357 16.12,25.72 16.698,25.563C17.277,25.406 17.863,25.788 18.008,26.417C18.119,26.902 19.002,28.652 21.985,28.652C23.907,28.652 24.854,27.965 25.264,27.31C25.642,26.707 25.708,25.909 25.427,25.13Z"
|
||||||
|
android:fillColor="#8D97A5"/>
|
||||||
|
</vector>
|
13
vector/src/main/res/drawable/ic_composer_underlined.xml
Normal file
13
vector/src/main/res/drawable/ic_composer_underlined.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="44dp"
|
||||||
|
android:height="44dp"
|
||||||
|
android:viewportWidth="44"
|
||||||
|
android:viewportHeight="44">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M10,10h24v24h-24z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M22.79,26.95C25.82,26.56 28,23.84 28,20.79V14.25C28,13.56 27.44,13 26.75,13C26.06,13 25.5,13.56 25.5,14.25V20.9C25.5,22.57 24.37,24.09 22.73,24.42C20.48,24.89 18.5,23.17 18.5,21V14.25C18.5,13.56 17.94,13 17.25,13C16.56,13 16,13.56 16,14.25V21C16,24.57 19.13,27.42 22.79,26.95ZM15,30C15,30.55 15.45,31 16,31H28C28.55,31 29,30.55 29,30C29,29.45 28.55,29 28,29H16C15.45,29 15,29.45 15,30Z"
|
||||||
|
android:fillColor="#8D97A5"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
156
vector/src/main/res/layout/composer_rich_text_layout.xml
Normal file
156
vector/src/main/res/layout/composer_rich_text_layout.xml
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:constraintSet="@layout/composer_rich_text_layout_constraint_set_compact"
|
||||||
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||||
|
|
||||||
|
<!-- ========================
|
||||||
|
/!\ Constraints for this layout are defined in external layout files that are used as constraint set for animation.
|
||||||
|
/!\ These 3 files must be modified to stay coherent!
|
||||||
|
======================== -->
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?colorSurface"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background_top_separator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?vctr_list_separator"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageAvatar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composerRelatedMessageTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:text="@tools:sample/first_names"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composerRelatedMessageContent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="3"
|
||||||
|
android:textColor="?vctr_message_text_color"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:text="@tools:sample/lorem"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageActionIcon"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:tint="?vctr_content_primary"
|
||||||
|
tools:ignore="MissingConstraints,MissingPrefix" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageImage"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composerRelatedMessageCloseButton"
|
||||||
|
android:layout_width="22dp"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/action_cancel"
|
||||||
|
android:src="@drawable/ic_close_round"
|
||||||
|
app:tint="?colorError"
|
||||||
|
tools:ignore="MissingConstraints,MissingPrefix" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/composer_preview_barrier"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:barrierMargin="8dp"
|
||||||
|
app:constraint_referenced_ids="composerRelatedMessageContent,composerRelatedMessageActionIcon"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/option_send_files"
|
||||||
|
android:src="@drawable/ic_attachment"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/composerEditTextOuterBorder"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="@drawable/bg_composer_edit_text" />
|
||||||
|
|
||||||
|
<io.element.android.wysiwyg.EditorEditText
|
||||||
|
android:id="@+id/composerEditText"
|
||||||
|
style="@style/Widget.Vector.EditText.Composer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:nextFocusLeft="@id/composerEditText"
|
||||||
|
android:nextFocusUp="@id/composerEditText"
|
||||||
|
tools:hint="@string/room_message_placeholder"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/sendButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="@drawable/bg_send"
|
||||||
|
android:contentDescription="@string/action_send"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<HorizontalScrollView android:id="@+id/richTextMenuScrollView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:scrollbars="none"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/sendButton"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/attachmentButton"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/sendButton"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/richTextMenu"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/voiceMessageMicButton"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/a11y_start_voice_message"
|
||||||
|
android:src="@drawable/ic_voice_mic" />
|
||||||
|
-->
|
||||||
|
|
||||||
|
</merge>
|
@ -0,0 +1,200 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/composerLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?colorSurface"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:layout_height="40dp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background_top_separator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?vctr_list_separator"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageAvatar"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composerRelatedMessageTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/composerRelatedMessageContent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="@tools:sample/first_names" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composerRelatedMessageContent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageActionIcon"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="38dp"
|
||||||
|
android:alpha="0"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintEnd_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="parent"
|
||||||
|
app:tint="?vctr_content_primary"
|
||||||
|
tools:ignore="MissingConstraints,MissingPrefix"
|
||||||
|
tools:src="@drawable/ic_edit" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageImage"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="parent"
|
||||||
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composerRelatedMessageCloseButton"
|
||||||
|
android:layout_width="22dp"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/action_cancel"
|
||||||
|
android:src="@drawable/ic_close_round"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="parent"
|
||||||
|
app:tint="?colorError"
|
||||||
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/composer_preview_barrier"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:barrierMargin="8dp"
|
||||||
|
app:constraint_referenced_ids="composerRelatedMessageContent,composerRelatedMessageActionIcon"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentButton"
|
||||||
|
android:layout_width="@dimen/composer_attachment_size"
|
||||||
|
android:layout_height="@dimen/composer_attachment_size"
|
||||||
|
android:layout_margin="@dimen/composer_attachment_margin"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/option_send_files"
|
||||||
|
android:src="@drawable/ic_attachment"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/sendButton"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/sendButton"
|
||||||
|
app:layout_goneMarginBottom="57dp"
|
||||||
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/composerEditTextOuterBorder"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composerEditText"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/composerEditText"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/composerEditText"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composerEditText"
|
||||||
|
app:layout_goneMarginEnd="12dp" />
|
||||||
|
|
||||||
|
<io.element.android.wysiwyg.EditorEditText
|
||||||
|
android:id="@+id/composerEditText"
|
||||||
|
style="@style/Widget.Vector.EditText.Composer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/room_message_placeholder"
|
||||||
|
android:nextFocusLeft="@id/composerEditText"
|
||||||
|
android:nextFocusUp="@id/composerEditText"
|
||||||
|
android:layout_marginHorizontal="10dp"
|
||||||
|
app:layout_constraintVertical_bias="0"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/sendButton"
|
||||||
|
android:layout_width="56dp"
|
||||||
|
android:layout_height="@dimen/composer_min_height"
|
||||||
|
android:layout_marginEnd="2dp"
|
||||||
|
android:background="@drawable/bg_send"
|
||||||
|
android:contentDescription="@string/action_send"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/composerEditText"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<HorizontalScrollView android:id="@+id/richTextMenuScrollView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/sendButton"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/attachmentButton"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/sendButton"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/richTextMenu"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/voiceMessageMicButton"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/a11y_start_voice_message"
|
||||||
|
android:src="@drawable/ic_voice_mic"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
-->
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,198 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/composerLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="?colorSurface"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composer_preview_barrier"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/related_message_background_top_separator"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?vctr_list_separator"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/related_message_background"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/related_message_background"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/related_message_background" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageAvatar"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/composerRelatedMessageActionIcon"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/composerRelatedMessageTitle"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/composerRelatedMessageTitle"
|
||||||
|
tools:src="@sample/user_round_avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composerRelatedMessageTitle"
|
||||||
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/composerRelatedMessageCloseButton"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/composerRelatedMessageAvatar"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@tools:sample/first_names" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageImage"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="66dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/composerRelatedMessageTitle"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/composerRelatedMessageTitle"
|
||||||
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/composerRelatedMessageContent"
|
||||||
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="?vctr_message_text_color"
|
||||||
|
app:layout_constrainedHeight="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/composerRelatedMessageTitle"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/composerRelatedMessageTitle"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/composerRelatedMessageImage"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/composerRelatedMessageActionIcon"
|
||||||
|
android:layout_width="10dp"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginBottom="38dp"
|
||||||
|
android:alpha="1"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/composerRelatedMessageAvatar"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/composerRelatedMessageAvatar"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/composerRelatedMessageAvatar"
|
||||||
|
app:tint="?vctr_content_primary"
|
||||||
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:src="@drawable/ic_edit" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/composerRelatedMessageCloseButton"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/action_cancel"
|
||||||
|
android:src="@drawable/ic_close_round"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/composer_preview_barrier"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="?colorError"
|
||||||
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
android:id="@+id/composer_preview_barrier"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:barrierDirection="bottom"
|
||||||
|
app:barrierMargin="8dp"
|
||||||
|
app:constraint_referenced_ids="composerRelatedMessageContent,composerRelatedMessageActionIcon"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/attachmentButton"
|
||||||
|
android:layout_width="@dimen/composer_attachment_size"
|
||||||
|
android:layout_height="@dimen/composer_attachment_size"
|
||||||
|
android:layout_margin="@dimen/composer_attachment_margin"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:contentDescription="@string/option_send_files"
|
||||||
|
android:src="@drawable/ic_attachment"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/sendButton"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/sendButton"
|
||||||
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/composerEditTextOuterBorder"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/sendButton"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
|
||||||
|
app:layout_goneMarginEnd="12dp" />
|
||||||
|
|
||||||
|
<io.element.android.wysiwyg.EditorEditText
|
||||||
|
android:id="@+id/composerEditText"
|
||||||
|
style="@style/Widget.Vector.EditText.Composer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:nextFocusLeft="@id/composerEditText"
|
||||||
|
android:nextFocusUp="@id/composerEditText"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/sendButton"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
|
||||||
|
tools:text="@tools:sample/lorem/random" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/sendButton"
|
||||||
|
android:layout_width="56dp"
|
||||||
|
android:layout_height="@dimen/composer_min_height"
|
||||||
|
android:layout_marginEnd="2dp"
|
||||||
|
android:background="@drawable/bg_send"
|
||||||
|
android:contentDescription="@string/action_send"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/composerEditText"
|
||||||
|
app:layout_constraintVertical_bias="1"
|
||||||
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<HorizontalScrollView android:id="@+id/richTextMenuScrollView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/sendButton"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/attachmentButton"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/sendButton"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/richTextMenu"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,13 +1,29 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<im.vector.app.features.home.room.detail.composer.MessageComposerView
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/composerLayout"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:background="?android:colorBackground"
|
|
||||||
android:minHeight="56dp"
|
<im.vector.app.features.home.room.detail.composer.PlainTextComposerLayout
|
||||||
android:transitionName="composer"
|
android:id="@+id/composerLayout"
|
||||||
android:visibility="gone"
|
android:layout_width="match_parent"
|
||||||
tools:visibility="visible" />
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
android:minHeight="56dp"
|
||||||
|
android:transitionName="composer"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<im.vector.app.features.home.room.detail.composer.RichTextComposerLayout
|
||||||
|
android:id="@+id/richTextComposerLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
android:minHeight="56dp"
|
||||||
|
android:transitionName="composer"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
10
vector/src/main/res/layout/view_rich_text_menu_button.xml
Normal file
10
vector/src/main/res/layout/view_rich_text_menu_button.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginHorizontal="2dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/app_name">
|
||||||
|
<!-- The contentDescription attr is populated programmatically. This is just to fix lint issues. -->
|
||||||
|
|
||||||
|
</ImageButton>
|
@ -96,4 +96,11 @@
|
|||||||
android:title="@string/labs_enable_deferred_dm_title"
|
android:title="@string/labs_enable_deferred_dm_title"
|
||||||
app:isPreferenceVisible="@bool/settings_labs_deferred_dm_visible" />
|
app:isPreferenceVisible="@bool/settings_labs_deferred_dm_visible" />
|
||||||
|
|
||||||
|
<im.vector.app.core.preference.VectorSwitchPreference
|
||||||
|
android:defaultValue="@bool/settings_labs_rich_text_editor_default"
|
||||||
|
android:key="SETTINGS_LABS_RICH_TEXT_EDITOR_KEY"
|
||||||
|
android:summary="@string/labs_enable_rich_text_editor_summary"
|
||||||
|
android:title="@string/labs_enable_rich_text_editor_title"
|
||||||
|
app:isPreferenceVisible="@bool/settings_labs_rich_text_editor_visible" />
|
||||||
|
|
||||||
</androidx.preference.PreferenceScreen>
|
</androidx.preference.PreferenceScreen>
|
||||||
|
@ -71,7 +71,7 @@ class CommandParserTest {
|
|||||||
|
|
||||||
private fun test(message: String, expectedResult: ParsedCommand) {
|
private fun test(message: String, expectedResult: ParsedCommand) {
|
||||||
val commandParser = CommandParser()
|
val commandParser = CommandParser()
|
||||||
val result = commandParser.parseSlashCommand(message, false)
|
val result = commandParser.parseSlashCommand(message, null, false)
|
||||||
result shouldBeEqualTo expectedResult
|
result shouldBeEqualTo expectedResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user