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 package im.vector.riotx.core.platform
import android.annotation.TargetApi
import android.content.Context import android.content.Context
import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.ScrollView import androidx.core.widget.NestedScrollView
import im.vector.riotx.R import im.vector.riotx.R
private const val DEFAULT_MAX_HEIGHT = 200 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 var maxHeight: Int = 0
set(value) { set(value) {
@ -34,28 +32,7 @@ class MaxHeightScrollView : ScrollView {
requestLayout() requestLayout()
} }
constructor(context: Context) : super(context) {} init {
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?) {
if (attrs != null) { if (attrs != null) {
val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView) val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView)
maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT) maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.MaxHeightScrollView_maxHeight, DEFAULT_MAX_HEIGHT)

View File

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

View File

@ -99,16 +99,8 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent() // val all = event.root.toContent()
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()
return when (messageContent) { return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
informationData, is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
highlight,
callback,
attributes)
is MessageTextContent -> buildTextMessageItem(messageContent,
informationData,
highlight,
callback,
attributes)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(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) } .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, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageTextItem? { attributes: AbsMessageItem.Attributes): MessageTextItem? {
val linkifiedBody = linkifyBody(body, callback)
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)
return MessageTextItem_().apply { return MessageTextItem_().apply {
if (informationData.hasBeenEdited) { if (informationData.hasBeenEdited) {
@ -269,7 +275,26 @@ class MessageItemFactory @Inject constructor(
.attributes(attributes) .attributes(attributes)
.highlighted(highlight) .highlighted(highlight)
.urlClickCallback(callback) .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, private fun annotateWithEdited(linkifiedBody: CharSequence,

View File

@ -16,26 +16,38 @@
package im.vector.riotx.features.home.room.detail.timeline.item 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.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R 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) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageBlockCodeItem : AbsMessageItem<MessageBlockCodeItem.Holder>() { abstract class MessageBlockCodeItem : AbsMessageItem<MessageBlockCodeItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var message: CharSequence? = null var message: CharSequence? = null
@EpoxyAttribute
var editedSpan: CharSequence? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(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 override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(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 { companion object {

View File

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

View File

@ -1,40 +1,28 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout 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"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:maxHeight="500dp"> android:orientation="vertical">
<androidx.core.widget.NestedScrollView <HorizontalScrollView
android:id="@+id/codeBlockScrollView" android:layout_width="match_parent"
android:layout_width="0dp" 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:layout_height="wrap_content"
android:fadeScrollbars="false" android:layout_marginTop="4dp" />
android:scrollbars="vertical|horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<HorizontalScrollView </LinearLayout>
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>