Timeline: render inline and block code
This commit is contained in:
parent
3517873156
commit
b4ae331086
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
|
Loading…
x
Reference in New Issue
Block a user