Implement text formatting via markdown rendering for text messages

Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
This commit is contained in:
Andy Scherzinger 2023-07-06 19:04:25 +02:00
parent 09d53f6ed7
commit d124301809
No known key found for this signature in database
GPG Key ID: 6CADC7E3523C308B
5 changed files with 52 additions and 18 deletions

View File

@ -144,12 +144,13 @@ ext {
emojiVersion = "1.3.0" emojiVersion = "1.3.0"
lifecycleVersion = '2.6.1' lifecycleVersion = '2.6.1'
okhttpVersion = "4.11.0" okhttpVersion = "4.11.0"
markwonVersion = "4.6.2"
materialDialogsVersion = "3.3.0" materialDialogsVersion = "3.3.0"
parcelerVersion = "1.1.13" parcelerVersion = "1.1.13"
prismVersion = "2.0.0"
retrofit2Version = "2.9.0" retrofit2Version = "2.9.0"
roomVersion = "2.5.2" roomVersion = "2.5.2"
workVersion = "2.8.1" workVersion = "2.8.1"
markwonVersion = "4.6.2"
espressoVersion = "3.5.1" espressoVersion = "3.5.1"
} }
@ -157,6 +158,7 @@ configurations.all {
exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
exclude group: 'org.jetbrains', module: 'annotations-java5' // via prism4j, already using annotations explicitly
} }
dependencies { dependencies {
@ -262,12 +264,14 @@ dependencies {
implementation "com.afollestad.material-dialogs:lifecycle:${materialDialogsVersion}" implementation "com.afollestad.material-dialogs:lifecycle:${materialDialogsVersion}"
implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.google.android.exoplayer:exoplayer:2.19.0' implementation 'com.google.android.exoplayer:exoplayer:2.18.7'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.27' implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.27'
implementation "io.noties.markwon:core:$markwonVersion" implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:syntax-highlight:$markwonVersion"
implementation 'com.github.nextcloud-deps:ImagePicker:2.1.0.2' implementation 'com.github.nextcloud-deps:ImagePicker:2.1.0.2'
implementation 'com.elyeproj.libraries:loaderviewlibrary:2.0.0' implementation 'com.elyeproj.libraries:loaderviewlibrary:2.0.0'

View File

@ -29,8 +29,7 @@ package com.nextcloud.talk.adapters.messages
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.text.Spannable import android.text.Spanned
import android.text.SpannableString
import android.text.TextUtils import android.text.TextUtils
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
@ -84,13 +83,13 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
itemView.isSelected = false itemView.isSelected = false
var messageString: Spannable = SpannableString(message.message)
var textSize = context.resources!!.getDimension(R.dimen.chat_text_size) var textSize = context.resources!!.getDimension(R.dimen.chat_text_size)
var processedMessageText = DisplayUtils.getRenderedMarkdownText(context, message.message)
val messageParameters = message.messageParameters val messageParameters = message.messageParameters
if (messageParameters != null && messageParameters.size > 0) { if (messageParameters != null && messageParameters.size > 0) {
messageString = processMessageParameters(messageParameters, message, messageString) processedMessageText = processMessageParameters(messageParameters, message, processedMessageText)
} else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) { } else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) {
textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat() textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat()
itemView.isSelected = true itemView.isSelected = true
@ -98,7 +97,7 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
} }
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
binding.messageText.text = messageString binding.messageText.text = processedMessageText
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
@ -219,8 +218,8 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
private fun processMessageParameters( private fun processMessageParameters(
messageParameters: HashMap<String?, HashMap<String?, String?>>, messageParameters: HashMap<String?, HashMap<String?, String?>>,
message: ChatMessage, message: ChatMessage,
messageString: Spannable messageString: Spanned
): Spannable { ): Spanned {
var messageStringInternal = messageString var messageStringInternal = messageString
for (key in messageParameters.keys) { for (key in messageParameters.keys) {
val individualHashMap = message.messageParameters!![key] val individualHashMap = message.messageParameters!![key]
@ -253,6 +252,7 @@ class IncomingTextMessageViewHolder(itemView: View, payload: Any) :
} }
} }
} }
return messageStringInternal return messageStringInternal
} }

View File

@ -27,8 +27,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.net.Uri import android.net.Uri
import android.text.Spannable import android.text.Spanned
import android.text.SpannableString
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
@ -70,15 +69,17 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
super.onBind(message) super.onBind(message)
sharedApplication!!.componentApplication.inject(this) sharedApplication!!.componentApplication.inject(this)
val messageParameters: HashMap<String?, HashMap<String?, String?>>? = message.messageParameters val messageParameters: HashMap<String?, HashMap<String?, String?>>? = message.messageParameters
var messageString: Spannable = SpannableString(message.message)
realView.isSelected = false realView.isSelected = false
val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams
layoutParams.isWrapBefore = false layoutParams.isWrapBefore = false
var textSize = context!!.resources.getDimension(R.dimen.chat_text_size) var textSize = context!!.resources.getDimension(R.dimen.chat_text_size)
val textColor = viewThemeUtils.getScheme(binding.messageText.context).onSurfaceVariant val textColor = viewThemeUtils.getScheme(binding.messageText.context).onSurfaceVariant
var processedMessageText = DisplayUtils.getRenderedMarkdownText(context, message.message)
binding.messageTime.setTextColor(textColor) binding.messageTime.setTextColor(textColor)
if (messageParameters != null && messageParameters.size > 0) { if (messageParameters != null && messageParameters.size > 0) {
messageString = processMessageParameters(messageParameters, message, messageString) processedMessageText = processMessageParameters(messageParameters, message, processedMessageText)
} else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) { } else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) {
textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat() textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat()
layoutParams.isWrapBefore = true layoutParams.isWrapBefore = true
@ -90,7 +91,7 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
binding.messageTime.layoutParams = layoutParams binding.messageTime.layoutParams = layoutParams
binding.messageText.setTextColor(textColor) binding.messageText.setTextColor(textColor)
binding.messageText.text = messageString binding.messageText.text = processedMessageText
binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp) binding.messageTime.text = dateUtils.getLocalTimeStringFromTimestamp(message.timestamp)
@ -183,8 +184,8 @@ class OutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewH
private fun processMessageParameters( private fun processMessageParameters(
messageParameters: HashMap<String?, HashMap<String?, String?>>, messageParameters: HashMap<String?, HashMap<String?, String?>>,
message: ChatMessage, message: ChatMessage,
messageString: Spannable messageString: Spanned
): Spannable { ): Spanned {
var messageStringInternal = messageString var messageStringInternal = messageString
for (key in messageParameters.keys) { for (key in messageParameters.keys) {
val individualHashMap: HashMap<String?, String?>? = message.messageParameters!![key] val individualHashMap: HashMap<String?, String?>? = message.messageParameters!![key]

View File

@ -48,6 +48,7 @@ import android.text.style.AbsoluteSizeSpan;
import android.text.style.ClickableSpan; import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.View;
import android.view.Window; import android.view.Window;
@ -87,6 +88,11 @@ import coil.Coil;
import coil.request.ImageRequest; import coil.request.ImageRequest;
import coil.target.Target; import coil.target.Target;
import coil.transform.CircleCropTransformation; import coil.transform.CircleCropTransformation;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import third.parties.fresco.BetterImageSpan; import third.parties.fresco.BetterImageSpan;
import static com.nextcloud.talk.utils.FileSortOrder.sort_a_to_z_id; import static com.nextcloud.talk.utils.FileSortOrder.sort_a_to_z_id;
@ -97,6 +103,7 @@ import static com.nextcloud.talk.utils.FileSortOrder.sort_small_to_big_id;
import static com.nextcloud.talk.utils.FileSortOrder.sort_z_to_a_id; import static com.nextcloud.talk.utils.FileSortOrder.sort_z_to_a_id;
public class DisplayUtils { public class DisplayUtils {
private static final String TAG = DisplayUtils.class.getSimpleName();
private static final int INDEX_LUMINATION = 2; private static final int INDEX_LUMINATION = 2;
private static final double MAX_LIGHTNESS = 0.92; private static final double MAX_LIGHTNESS = 0.92;
@ -246,7 +253,28 @@ public class DisplayUtils {
return chip; return chip;
} }
public static Spannable searchAndReplaceWithMentionSpan(String key, Context context, Spannable text, public static Spanned getRenderedMarkdownText(Context context, String markdown) {
final Markwon markwon = Markwon.builder(context)
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
builder.headingBreakHeight(0);
}
@Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.linkResolver((view, link) -> {
Log.i(TAG, "Link action not implemented" );
});
}
})
.usePlugin(StrikethroughPlugin.create())
.build();
return markwon.toMarkdown(markdown);
}
public static Spannable searchAndReplaceWithMentionSpan(String key, Context context, Spanned text,
String id, String label, String type, String id, String label, String type,
User conversationUser, User conversationUser,
@XmlRes int chipXmlRes, @XmlRes int chipXmlRes,

View File

@ -47,6 +47,7 @@ buildscript {
} }
configurations.all { configurations.all {
exclude group: 'org.jetbrains', module: 'annotations-java5' // via prism4j, already using annotations explicitly
// check for updates every build // check for updates every build
resolutionStrategy.cacheChangingModulesFor 3600, 'seconds' resolutionStrategy.cacheChangingModulesFor 3600, 'seconds'
} }