Skip to content

Commit

Permalink
Support rendering a subset of CommonMark* in the chat
Browse files Browse the repository at this point in the history
This is a very simple (and non-compliant!) implementation and will be
replaced by something more proper later. Overlapping styles aren't
handled at all right now.
  • Loading branch information
robinlinden committed Jun 29, 2021
1 parent 71cbc6e commit e711a82
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 0 deletions.
32 changes: 32 additions & 0 deletions atox/src/main/kotlin/ui/chat/ChatAdapter.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package ltd.evilcorp.atox.ui.chat

import android.content.res.Resources
import android.graphics.Typeface
import android.text.Spannable
import android.text.format.Formatter
import android.text.style.RelativeSizeSpan
import android.text.style.StyleSpan
import android.text.style.TypefaceSpan
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
Expand All @@ -15,6 +20,7 @@ import android.widget.ListView
import android.widget.ProgressBar
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.text.toSpannable
import com.squareup.picasso.Picasso
import java.net.URLConnection
import java.text.DateFormat
Expand All @@ -31,6 +37,27 @@ import ltd.evilcorp.core.vo.isStarted

private const val TAG = "ChatAdapter"

private fun applyStyle(
view: TextView,
regex: Regex,
typefaceStyle: Int,
fontFamily: String = "",
size: Float = 1f,
) {
val spannable = view.text.toSpannable()
for (match in regex.findAll(spannable)) {
val start = match.range.first
val end = match.range.last + 1
spannable.setSpan(RelativeSizeSpan(size), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(StyleSpan(typefaceStyle), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
if (fontFamily.isNotEmpty()) {
spannable.setSpan(TypefaceSpan(fontFamily), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}

view.text = spannable
}

private fun FileTransfer.isImage() = try {
URLConnection.guessContentTypeFromName(fileName).startsWith("image/")
} catch (e: Exception) {
Expand Down Expand Up @@ -151,6 +178,11 @@ class ChatAdapter(
}
}

applyStyle(vh.message, Regex("(?<!`)`[^`\n]+?`(?!`)"), Typeface.BOLD, "monospace", 0.8f)
applyStyle(vh.message, Regex("(?<!\\*)\\*\\*\\*[^*\n]+?\\*\\*\\*(?!\\*)"), Typeface.BOLD_ITALIC)
applyStyle(vh.message, Regex("(?<!\\*)\\*\\*[^*\n]+?\\*\\*(?!\\*)"), Typeface.BOLD)
applyStyle(vh.message, Regex("(?<!\\*)\\*[^*\n]+?\\*(?!\\*)"), Typeface.ITALIC)

view
}
ChatItemType.ReceivedFileTransfer, ChatItemType.SentFileTransfer -> {
Expand Down
46 changes: 46 additions & 0 deletions atox/src/main/kotlin/ui/chat/ChatFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,27 @@ import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
import android.text.Spannable
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.StyleSpan
import android.text.style.TypefaceSpan
import android.view.ContextMenu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.widget.AdapterView
import android.widget.EditText
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf
import androidx.core.text.getSpans
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
Expand Down Expand Up @@ -50,6 +58,39 @@ import ltd.evilcorp.domain.tox.PublicKey
const val CONTACT_PUBLIC_KEY = "publicKey"
private const val MAX_CONFIRM_DELETE_STRING_LENGTH = 20

private inline fun <reified T : Any> clearStyle(e: Spannable) {
for (span in e.getSpans<T>()) {
e.removeSpan(span)
}
}

private fun clearStyles(view: EditText) {
val spannable = view.text
clearStyle<ForegroundColorSpan>(spannable)
clearStyle<RelativeSizeSpan>(spannable)
clearStyle<StyleSpan>(spannable)
clearStyle<TypefaceSpan>(spannable)
}

private fun applyStyle(
view: EditText,
regex: Regex,
typefaceStyle: Int,
fontFamily: String = "",
size: Float = 1f,
) {
val spannable = view.text
for (match in regex.findAll(spannable)) {
val start = match.range.first
val end = match.range.last + 1
spannable.setSpan(RelativeSizeSpan(size), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(StyleSpan(typefaceStyle), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
if (fontFamily.isNotEmpty()) {
spannable.setSpan(TypefaceSpan(fontFamily), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}

class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::inflate) {
private val viewModel: ChatViewModel by viewModels { vmFactory }

Expand Down Expand Up @@ -265,6 +306,11 @@ class ChatFragment : BaseFragment<FragmentChatBinding>(FragmentChatBinding::infl
outgoingMessage.doAfterTextChanged {
viewModel.setTyping(outgoingMessage.text.isNotEmpty())
updateActions()
clearStyles(outgoingMessage)
applyStyle(outgoingMessage, Regex("(?<!`)`[^`\n]+?`(?!`)"), Typeface.BOLD, "monospace", 0.8f)
applyStyle(outgoingMessage, Regex("(?<!\\*)\\*\\*\\*[^*\n]+?\\*\\*\\*(?!\\*)"), Typeface.BOLD_ITALIC)
applyStyle(outgoingMessage, Regex("(?<!\\*)\\*\\*[^*\n]+?\\*\\*(?!\\*)"), Typeface.BOLD)
applyStyle(outgoingMessage, Regex("(?<!\\*)\\*[^*\n]+?\\*(?!\\*)"), Typeface.ITALIC)
}

updateActions()
Expand Down

0 comments on commit e711a82

Please sign in to comment.