Timeline: render inline and block code

This commit is contained in:
ganfra 2019-10-30 19:00:00 +01:00
parent 3517873156
commit b4ae331086
6 changed files with 97 additions and 95 deletions

View File

@ -16,17 +16,15 @@
package im.vector.riotx.core.platform
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.widget.ScrollView
import androidx.core.widget.NestedScrollView
import im.vector.riotx.R
private const val DEFAULT_MAX_HEIGHT = 200
class MaxHeightScrollView : ScrollView {
class MaxHeightScrollView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
: NestedScrollView(context, attrs, defStyle) {
var maxHeight: Int = 0
set(value) {
@ -34,28 +32,7 @@ class MaxHeightScrollView : ScrollView {
requestLayout()
}
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
if (!isInEditMode) {
init(context, attrs)
}
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
if (!isInEditMode) {
init(context, attrs)
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
if (!isInEditMode) {
init(context, attrs)
}
}
private fun init(context: Context, attrs: AttributeSet?) {
init {
if (attrs != null) {
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView)
maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)

View File

@ -480,7 +480,7 @@ class RoomDetailFragment :
jumpToReadMarkerView.render(show, readMarkerId)
}
}
recyclerView.setController(timelineEventController)
recyclerView.adapter = timelineEventController.adapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {

View File

@ -99,16 +99,8 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent()
// val ev = all.toModel<Event>()
return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData,
highlight,
callback,
attributes)
is MessageTextContent -> buildTextMessageItem(messageContent,
informationData,
highlight,
callback,
attributes)
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
@ -231,29 +223,43 @@ class MessageItemFactory @Inject constructor(
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
}
private fun buildTextMessageItem(messageContent: MessageTextContent,
private fun buildItemForTextContent(messageContent: MessageTextContent,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
return if (isFormatted) {
val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document
val codeVisitor = CodeVisitor()
codeVisitor.visit(localFormattedBody)
when (codeVisitor.codeKind) {
CodeVisitor.Kind.BLOCK -> {
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
buildCodeBlockItem(codeFormattedBlock, informationData, highlight, callback, attributes)
}
CodeVisitor.Kind.INLINE -> {
val codeFormatted = htmlRenderer.get().render(localFormattedBody)
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
}
CodeVisitor.Kind.NONE -> {
val formattedBody = htmlRenderer.get().render(messageContent.formattedBody!!)
buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
}
}
} else {
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
}
}
private fun buildMessageTextItem(body: CharSequence,
isFormatted: Boolean,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? {
val isFormatted = messageContent.formattedBody.isNullOrBlank().not()
val bodyToUse = if (isFormatted) {
val formattedBody = htmlRenderer.get().parse(messageContent.body) as Document
val codeVisitor = CodeVisitor()
codeVisitor.visit(formattedBody)
if (codeVisitor.codeKind == CodeVisitor.Kind.NONE) {
messageContent.formattedBody.let {
htmlRenderer.get().render(it!!.trim())
}
} else {
htmlRenderer.get().render(formattedBody)
}
} else {
messageContent.body
}
val linkifiedBody = linkifyBody(bodyToUse, callback)
val linkifiedBody = linkifyBody(body, callback)
return MessageTextItem_().apply {
if (informationData.hasBeenEdited) {
@ -269,7 +275,26 @@ class MessageItemFactory @Inject constructor(
.attributes(attributes)
.highlighted(highlight)
.urlClickCallback(callback)
// click on the text
}
private fun buildCodeBlockItem(formattedBody: CharSequence,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageBlockCodeItem? {
return MessageBlockCodeItem_()
.apply {
if (informationData.hasBeenEdited) {
val spannable = annotateWithEdited("", callback, informationData)
editedSpan(spannable)
}
}
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.message(formattedBody)
}
private fun annotateWithEdited(linkifiedBody: CharSequence,

View File

@ -16,26 +16,38 @@
package im.vector.riotx.features.home.room.detail.timeline.item
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.core.extensions.setTextOrHide
import me.saket.bettermovementmethod.BetterLinkMovementMethod
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageBlockCodeItem : AbsMessageItem<MessageBlockCodeItem.Holder>() {
@EpoxyAttribute
var message: CharSequence? = null
@EpoxyAttribute
var editedSpan: CharSequence? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.messageView.text = message
renderSendState(holder.messageView, holder.messageView)
holder.messageView.setOnClickListener(attributes.itemClickListener)
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
holder.editedView.movementMethod = BetterLinkMovementMethod.getInstance()
holder.editedView.setTextOrHide(editedSpan)
}
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val messageView by bind<TextView>(R.id.codeBlockTextView)
val editedView by bind<TextView>(R.id.codeBlockEditedView)
}
companion object {

View File

@ -86,7 +86,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomToolbar" />
<com.airbnb.epoxy.EpoxyRecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"

View File

@ -1,40 +1,28 @@
<?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/messagesAdapter_body_hsv"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="500dp">
android:orientation="vertical">
<androidx.core.widget.NestedScrollView
android:id="@+id/codeBlockScrollView"
android:layout_width="0dp"
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/codeBlockTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:fontFamily="monospace"
android:textSize="14sp" />
</HorizontalScrollView>
<TextView
android:id="@+id/codeBlockEditedView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fadeScrollbars="false"
android:scrollbars="vertical|horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_marginTop="4dp" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/codeBlockTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:autoLink="none"
android:fontFamily="monospace"
android:textSize="14sp"
tools:text="" />
</HorizontalScrollView>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>