diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt index fa2108aec..232ab1899 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/ChatMessage.kt @@ -1,6 +1,7 @@ package com.habitrpg.android.habitica.models.social import android.renderscript.BaseObj +import android.text.Spanned import com.habitrpg.android.habitica.models.BaseObject import com.habitrpg.android.habitica.models.user.Backer import com.habitrpg.android.habitica.models.user.ContributorInfo @@ -32,7 +33,7 @@ open class ChatMessage : RealmObject(), BaseObject { var text: String? = null @Ignore - var parsedText: CharSequence? = null + var parsedText: Spanned? = null var timestamp: Long? = null diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/MarkdownParser.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/MarkdownParser.kt index 157e668fd..b38d7ec60 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/MarkdownParser.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/MarkdownParser.kt @@ -1,80 +1,114 @@ -package com.habitrpg.android.habitica.ui.helpers - -import android.content.Context -import android.text.SpannableString -import android.text.Spanned -import android.text.method.LinkMovementMethod -import android.widget.TextView -import com.habitrpg.android.habitica.helpers.RxErrorHandler -import io.noties.markwon.Markwon -import io.noties.markwon.ext.strikethrough.StrikethroughPlugin -import io.noties.markwon.image.ImagesPlugin -import io.noties.markwon.image.file.FileSchemeHandler -import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler -import io.noties.markwon.movement.MovementMethodPlugin -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.functions.Consumer -import io.reactivex.rxjava3.schedulers.Schedulers - - -object MarkdownParser { - - internal var markwon: Markwon? = null - - fun setup(context: Context) { - markwon = Markwon.builder(context) - .usePlugin(StrikethroughPlugin.create()) - .usePlugin(ImagesPlugin.create { - it.addSchemeHandler(OkHttpNetworkSchemeHandler.create()) - .addSchemeHandler(FileSchemeHandler.createWithAssets(context.assets)) - }) - .usePlugin(MovementMethodPlugin.create(LinkMovementMethod.getInstance())) - .build() - } - - /** - * Parses formatted markdown and returns it as styled CharSequence - * - * @param input Markdown formatted String - * @return Stylized CharSequence - */ - fun parseMarkdown(input: String?): Spanned { - if (input == null) { - return SpannableString("") - } - val text = EmojiParser.parseEmojis(input) ?: input - return markwon?.toMarkdown(text) ?: SpannableString(text) - } - - fun parseMarkdownAsync(input: String?, onSuccess: Consumer) { - Single.just(input ?: "") - .map { this.parseMarkdown(it) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(onSuccess, RxErrorHandler.handleEmptyError()) - } - - /** - * Converts stylized CharSequence into markdown - * - * @param input Stylized CharSequence - * @return Markdown formatted String - */ - fun parseCompiled(input: CharSequence): String? { - return EmojiParser.convertToCheatCode(input.toString()) - } -} - - -fun TextView.setMarkdown(input: String?) { - MarkdownParser.markwon?.setParsedMarkdown(this, MarkdownParser.parseMarkdown(input)) -} - -fun TextView.setParsedMarkdown(input: Spanned?) { - if (input != null) { - MarkdownParser.markwon?.setParsedMarkdown(this, input) - } else { - text = null - } +package com.habitrpg.android.habitica.ui.helpers + +import android.content.Context +import android.graphics.Rect +import android.text.SpannableString +import android.text.Spanned +import android.text.method.LinkMovementMethod +import android.widget.TextView +import com.habitrpg.android.habitica.helpers.RxErrorHandler +import io.noties.markwon.AbstractMarkwonPlugin +import io.noties.markwon.Markwon +import io.noties.markwon.MarkwonConfiguration +import io.noties.markwon.MarkwonPlugin +import io.noties.markwon.ext.strikethrough.StrikethroughPlugin +import io.noties.markwon.image.ImageSize +import io.noties.markwon.image.ImageSizeResolverDef +import io.noties.markwon.image.ImagesPlugin +import io.noties.markwon.image.file.FileSchemeHandler +import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler +import io.noties.markwon.movement.MovementMethodPlugin +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.functions.Consumer +import io.reactivex.rxjava3.schedulers.Schedulers + + +object MarkdownParser { + + internal var markwon: Markwon? = null + + fun setup(context: Context) { + markwon = Markwon.builder(context) + .usePlugin(StrikethroughPlugin.create()) + .usePlugin(ImagesPlugin.create { + it.addSchemeHandler(OkHttpNetworkSchemeHandler.create()) + .addSchemeHandler(FileSchemeHandler.createWithAssets(context.assets)) + }) + .usePlugin(this.createImageSizeResolverScaleDpiPlugin(context)) + .usePlugin(MovementMethodPlugin.create(LinkMovementMethod.getInstance())) + .build() + } + + /** + * Custom markwon plugin to scale image size according to dpi + */ + private fun createImageSizeResolverScaleDpiPlugin(context: Context): MarkwonPlugin { + return object : AbstractMarkwonPlugin() { + override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { + builder.imageSizeResolver(object : ImageSizeResolverDef() { + override fun resolveImageSize(imageSize: ImageSize?, imageBounds: Rect, canvasWidth: Int, textSize: Float): Rect { + val dpi = context.resources.displayMetrics.density + var width = imageBounds.width() + if (dpi > 1) { + width = (dpi * width.toFloat()).toInt() + } + if (width > canvasWidth) { + width = canvasWidth + } + + val ratio = imageBounds.width().toFloat() / imageBounds.height() + val height = (width / ratio + .5f).toInt() + + return Rect(0, 0, width, height) + } + }) + } + } + } + + /** + * Parses formatted markdown and returns it as styled CharSequence + * + * @param input Markdown formatted String + * @return Stylized CharSequence + */ + fun parseMarkdown(input: String?): Spanned { + if (input == null) { + return SpannableString("") + } + val text = EmojiParser.parseEmojis(input) ?: input + return markwon?.toMarkdown(text) ?: SpannableString(text) + } + + fun parseMarkdownAsync(input: String?, onSuccess: Consumer) { + Single.just(input ?: "") + .map { this.parseMarkdown(it) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(onSuccess, RxErrorHandler.handleEmptyError()) + } + + /** + * Converts stylized CharSequence into markdown + * + * @param input Stylized CharSequence + * @return Markdown formatted String + */ + fun parseCompiled(input: CharSequence): String? { + return EmojiParser.convertToCheatCode(input.toString()) + } +} + + +fun TextView.setMarkdown(input: String?) { + MarkdownParser.markwon?.setParsedMarkdown(this, MarkdownParser.parseMarkdown(input)) +} + +fun TextView.setParsedMarkdown(input: Spanned?) { + if (input != null) { + MarkdownParser.markwon?.setParsedMarkdown(this, input) + } else { + text = null + } } \ No newline at end of file diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/ChatRecyclerViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/ChatRecyclerViewHolder.kt index 4ada82e34..2c9b79f42 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/ChatRecyclerViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/ChatRecyclerViewHolder.kt @@ -26,6 +26,7 @@ import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.schedulers.Schedulers import com.habitrpg.android.habitica.models.members.Member +import com.habitrpg.android.habitica.ui.helpers.setParsedMarkdown import com.habitrpg.android.habitica.ui.views.HabiticaSnackbar import com.habitrpg.android.habitica.ui.views.dialogs.HabiticaAlertDialog import org.greenrobot.eventbus.EventBus @@ -172,7 +173,7 @@ class ChatRecyclerMessageViewHolder(itemView: View, private var userId: String, itemView.setPadding(16.dpToPx(context), itemView.paddingTop, itemView.paddingRight, itemView.paddingBottom) } - binding.messageText.text = chatMessage?.parsedText + binding.messageText.setParsedMarkdown(chatMessage?.parsedText) if (msg.parsedText == null) { binding.messageText.text = chatMessage?.text Maybe.just(chatMessage?.text ?: "") @@ -181,7 +182,7 @@ class ChatRecyclerMessageViewHolder(itemView: View, private var userId: String, .observeOn(AndroidSchedulers.mainThread()) .subscribe({ parsedText -> chatMessage?.parsedText = parsedText - binding.messageText.text = chatMessage?.parsedText + binding.messageText.setParsedMarkdown(parsedText) }, { it.printStackTrace() }) }