Merge pull request #1398 from vector-im/feature/date_time

Bugfixes
This commit is contained in:
Benoit Marty 2020-05-25 10:13:30 +02:00 committed by GitHub
commit ebbc432570
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 126 additions and 45 deletions

View file

@ -8,10 +8,13 @@ Features ✨:
Improvements 🙌: Improvements 🙌:
- Better connectivity lost indicator when airplane mode is on - Better connectivity lost indicator when airplane mode is on
- Add a setting to hide redacted events (#951) - Add a setting to hide redacted events (#951)
- Render formatted_body for m.notice and m.emote (#1196)
Bugfix 🐛: Bugfix 🐛:
- After jump to unread, newer messages are never loaded (#1008) - After jump to unread, newer messages are never loaded (#1008)
- Fix issues with FontScale switch (#69, #645) - Fix issues with FontScale switch (#69, #645)
- "Seen by" uses 12h time (#1378)
- Enable markdown (if active) when sending emote (#734)
Translations 🗣: Translations 🗣:
- -

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 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.matrix.android.api.session.room.model.message
interface MessageContentWithFormattedBody : MessageContent {
/**
* The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/
val format: String?
/**
* The formatted version of the body. This is required if format is specified.
*/
val formattedBody: String?
/**
* Get the formattedBody, only if not blank and if the format is equal to "org.matrix.custom.html"
*/
val matrixFormattedBody: String?
get() = formattedBody?.takeIf { it.isNotBlank() && format == MessageFormat.FORMAT_MATRIX_HTML }
}

View file

@ -34,15 +34,15 @@ data class MessageEmoteContent(
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,
/** /**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported. * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/ */
@Json(name = "format") val format: String? = null, @Json(name = "format") override val format: String? = null,
/** /**
* The formatted version of the body. This is required if format is specified. * The formatted version of the body. This is required if format is specified.
*/ */
@Json(name = "formatted_body") val formattedBody: String? = null, @Json(name = "formatted_body") override val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent ) : MessageContentWithFormattedBody

View file

@ -34,15 +34,15 @@ data class MessageNoticeContent(
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,
/** /**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported. * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/ */
@Json(name = "format") val format: String? = null, @Json(name = "format") override val format: String? = null,
/** /**
* The formatted version of the body. This is required if format is specified. * The formatted version of the body. This is required if format is specified.
*/ */
@Json(name = "formatted_body") val formattedBody: String? = null, @Json(name = "formatted_body") override val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent ) : MessageContentWithFormattedBody

View file

@ -34,15 +34,15 @@ data class MessageTextContent(
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,
/** /**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported. * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/ */
@Json(name = "format") val format: String? = null, @Json(name = "format") override val format: String? = null,
/** /**
* The formatted version of the body. This is required if format is specified. * The formatted version of the body. This is required if format is specified.
*/ */
@Json(name = "formatted_body") val formattedBody: String? = null, @Json(name = "formatted_body") override val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent ) : MessageContentWithFormattedBody

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 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.matrix.android.internal.extensions
/**
* Ex: "abcdef".subStringBetween("a", "f") -> "bcde"
* Ex: "abcdefff".subStringBetween("a", "f") -> "bcdeff"
* Ex: "aaabcdef".subStringBetween("a", "f") -> "aabcde"
*/
internal fun String.subStringBetween(prefix: String, suffix: String) = substringAfter(prefix).substringBeforeLast(suffix)

View file

@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.room.model.message.FileInfo
import im.vector.matrix.android.api.session.room.model.message.ImageInfo import im.vector.matrix.android.api.session.room.model.message.ImageInfo
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageContentWithFormattedBody
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageFormat import im.vector.matrix.android.api.session.room.model.message.MessageFormat
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
@ -56,6 +57,7 @@ import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.extensions.subStringBetween
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
@ -84,6 +86,7 @@ internal class LocalEchoEventFactory @Inject constructor(
) { ) {
// TODO Inject // TODO Inject
private val parser = Parser.builder().build() private val parser = Parser.builder().build()
// TODO Inject // TODO Inject
private val renderer = HtmlRenderer.builder().build() private val renderer = HtmlRenderer.builder().build()
@ -102,8 +105,15 @@ internal class LocalEchoEventFactory @Inject constructor(
val document = parser.parse(source) val document = parser.parse(source)
val htmlText = renderer.render(document) val htmlText = renderer.render(document)
if (isFormattedTextPertinent(source, htmlText)) { // Cleanup extra paragraph
return TextContent(text.toString(), htmlText) val cleanHtmlText = if (htmlText.startsWith("<p>") && htmlText.endsWith("</p>\n")) {
htmlText.subStringBetween("<p>", "</p>\n")
} else {
htmlText
}
if (isFormattedTextPertinent(source, cleanHtmlText)) {
return TextContent(text.toString(), cleanHtmlText)
} }
} else { } else {
// Try to detect pills // Try to detect pills
@ -433,10 +443,8 @@ internal class LocalEchoEventFactory @Inject constructor(
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE -> { MessageType.MSGTYPE_NOTICE -> {
var formattedText: String? = null var formattedText: String? = null
if (content is MessageTextContent) { if (content is MessageContentWithFormattedBody) {
if (content.format == MessageFormat.FORMAT_MATRIX_HTML) { formattedText = content.matrixFormattedBody
formattedText = content.formattedBody
}
} }
val isReply = content.isReply() || originalContent.isReply() val isReply = content.isReply() || originalContent.isReply()
return if (isReply) { return if (isReply) {

View file

@ -45,11 +45,12 @@ class VectorDateFormatter @Inject constructor(private val context: Context,
if (time == null) { if (time == null) {
return "" return ""
} }
return DateUtils.getRelativeDateTimeString(context, return DateUtils.getRelativeDateTimeString(
time, context,
DateUtils.DAY_IN_MILLIS, time,
2 * DateUtils.DAY_IN_MILLIS, DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_SHOW_WEEKDAY 2 * DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_TIME
).toString() ).toString()
} }
} }

View file

@ -117,8 +117,10 @@ class RoomDetailViewModel @AssistedInject constructor(
// Slot to keep a pending action during permission request // Slot to keep a pending action during permission request
var pendingAction: RoomDetailAction? = null var pendingAction: RoomDetailAction? = null
// Slot to keep a pending uri during permission request // Slot to keep a pending uri during permission request
var pendingUri: Uri? = null var pendingUri: Uri? = null
// Slot to store if we want to prevent preview of attachment // Slot to store if we want to prevent preview of attachment
var preventAttachmentPreview = false var preventAttachmentPreview = false
@ -390,7 +392,7 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented) _viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
} }
is ParsedCommand.SendEmote -> { is ParsedCommand.SendEmote -> {
room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE, autoMarkdown = action.autoMarkdown)
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft() popDraft()
} }

View file

@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.matrix.android.internal.session.room.send.TextContent
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.extensions.localDateTime
@ -90,17 +91,15 @@ class ViewEditHistoryEpoxyController(private val context: Context,
} }
lastDate = evDate lastDate = evDate
val cContent = getCorrectContent(timelineEvent, isOriginalReply) val cContent = getCorrectContent(timelineEvent, isOriginalReply)
val body = cContent.second?.let { eventHtmlRenderer.render(it) } val body = cContent.formattedText?.let { eventHtmlRenderer.render(it) } ?: cContent.text
?: cContent.first
val nextEvent = sourceEvents.getOrNull(index + 1) val nextEvent = sourceEvents.getOrNull(index + 1)
var spannedDiff: Spannable? = null var spannedDiff: Spannable? = null
if (nextEvent != null && cContent.second == null /*No diff for html*/) { if (nextEvent != null && cContent.formattedText == null /*No diff for html*/) {
// compares the body // compares the body
val nContent = getCorrectContent(nextEvent, isOriginalReply) val nContent = getCorrectContent(nextEvent, isOriginalReply)
val nextBody = nContent.second?.let { eventHtmlRenderer.render(it) } val nextBody = nContent.formattedText?.let { eventHtmlRenderer.render(it) } ?: nContent.text
?: nContent.first
val dmp = diff_match_patch() val dmp = diff_match_patch()
val diff = dmp.diff_main(nextBody.toString(), body.toString()) val diff = dmp.diff_main(nextBody.toString(), body.toString())
dmp.diff_cleanupSemantic(diff) dmp.diff_cleanupSemantic(diff)
@ -138,15 +137,14 @@ class ViewEditHistoryEpoxyController(private val context: Context,
} }
} }
private fun getCorrectContent(event: Event, isOriginalReply: Boolean): Pair<String, String?> { private fun getCorrectContent(event: Event, isOriginalReply: Boolean): TextContent {
val clearContent = event.getClearContent().toModel<MessageTextContent>() val clearContent = event.getClearContent().toModel<MessageTextContent>()
val newContent = clearContent val newContent = clearContent
?.newContent ?.newContent
?.toModel<MessageTextContent>() ?.toModel<MessageTextContent>()
if (isOriginalReply) { if (isOriginalReply) {
return extractUsefulTextFromReply(newContent?.body ?: clearContent?.body ?: "") to null return TextContent(extractUsefulTextFromReply(newContent?.body ?: clearContent?.body ?: ""))
} }
return (newContent?.body ?: clearContent?.body ?: "") to (newContent?.formattedBody return TextContent(newContent?.body ?: clearContent?.body ?: "", newContent?.formattedBody ?: clearContent?.formattedBody)
?: clearContent?.formattedBody)
} }
} }

View file

@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageContentWithFormattedBody
import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
@ -350,7 +351,7 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
val isFormatted = messageContent.formattedBody.isNullOrBlank().not() val isFormatted = messageContent.matrixFormattedBody.isNullOrBlank().not()
return if (isFormatted) { return if (isFormatted) {
// First detect if the message contains some code block(s) or inline code // First detect if the message contains some code block(s) or inline code
val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document
@ -462,14 +463,14 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? { attributes: AbsMessageItem.Attributes): MessageTextItem? {
val message = messageContent.body.let { val formattedBody = span {
val formattedBody = span { text = messageContent.getHtmlBody()
text = it textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) textStyle = "italic"
textStyle = "italic"
}
formattedBody.linkify(callback)
} }
val message = formattedBody.linkify(callback)
return MessageTextItem_() return MessageTextItem_()
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes) .attributes(attributes)
@ -483,10 +484,12 @@ class MessageItemFactory @Inject constructor(
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? { attributes: AbsMessageItem.Attributes): MessageTextItem? {
val message = messageContent.body.let { val formattedBody = SpannableStringBuilder()
val formattedBody = "* ${informationData.memberName} $it" formattedBody.append("* ${informationData.memberName} ")
formattedBody.linkify(callback) formattedBody.append(messageContent.getHtmlBody())
}
val message = formattedBody.linkify(callback)
return MessageTextItem_() return MessageTextItem_()
.apply { .apply {
if (informationData.hasBeenEdited) { if (informationData.hasBeenEdited) {
@ -502,6 +505,13 @@ class MessageItemFactory @Inject constructor(
.movementMethod(createLinkMovementMethod(callback)) .movementMethod(createLinkMovementMethod(callback))
} }
private fun MessageContentWithFormattedBody.getHtmlBody(): CharSequence {
return matrixFormattedBody
?.let { htmlCompressor.compress(it) }
?.let { htmlRenderer.get().render(it) }
?: body
}
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes, private fun buildRedactedItem(attributes: AbsMessageItem.Attributes,
highlight: Boolean): RedactedMessageItem? { highlight: Boolean): RedactedMessageItem? {
return RedactedMessageItem_() return RedactedMessageItem_()