Commit 102d920b authored by Leonardo Aramaki's avatar Leonardo Aramaki

Make quotes as different view type

parent e91b520d
...@@ -49,6 +49,10 @@ class ChatRoomAdapter( ...@@ -49,6 +49,10 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.message_url_preview) val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view, actionsListener, reactionListener) UrlPreviewViewHolder(view, actionsListener, reactionListener)
} }
BaseViewModel.ViewType.MESSAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_message_attachment)
MessageAttachmentViewHolder(view, actionsListener, reactionListener)
}
else -> { else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}") throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
} }
...@@ -87,6 +91,7 @@ class ChatRoomAdapter( ...@@ -87,6 +91,7 @@ class ChatRoomAdapter(
is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel) is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel)
is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel) is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel) is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel)
} }
} }
......
package chat.rocket.android.chatroom.adapter
import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageAttachmentViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message.view.*
class MessageAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
text_content.movementMethod = LinkMovementMethod()
setupActionMenu(text_content)
}
}
override fun bindViews(data: MessageAttachmentViewModel) {
with(itemView) {
text_message_time.text = data.time
text_sender.text = data.senderName
text_content.text = data.content
}
}
}
\ No newline at end of file
...@@ -43,14 +43,12 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> { ...@@ -43,14 +43,12 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
var text: String = "" var text: String = ""
set(value) { set(value) {
val spannable = SpannableStringBuilder.valueOf(value) val spannable = SpannableStringBuilder.valueOf(value)
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
messageTextView.content = spannable messageTextView.content = spannable
} }
var title: String = "" var title: String = ""
set(value) { set(value) {
val spannable = Markwon.markdown(this.context, value) as Spannable val spannable = Markwon.markdown(this.context, value) as Spannable
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
titleTextView.content = spannable titleTextView.content = spannable
} }
...@@ -69,17 +67,17 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> { ...@@ -69,17 +67,17 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
override fun animateContentOut(delay: Int, duration: Int) { override fun animateContentOut(delay: Int, duration: Int) {
ViewCompat.setScaleY(content, 1f) ViewCompat.setScaleY(content, 1f)
ViewCompat.animate(content) ViewCompat.animate(content)
.scaleY(0f) .scaleY(0f)
.setDuration(duration.toLong()) .setDuration(duration.toLong())
.startDelay = delay.toLong() .startDelay = delay.toLong()
} }
override fun animateContentIn(delay: Int, duration: Int) { override fun animateContentIn(delay: Int, duration: Int) {
ViewCompat.setScaleY(content, 0f) ViewCompat.setScaleY(content, 0f)
ViewCompat.animate(content) ViewCompat.animate(content)
.scaleY(1f) .scaleY(1f)
.setDuration(duration.toLong()) .setDuration(duration.toLong())
.startDelay = delay.toLong() .startDelay = delay.toLong()
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
data class MessageAttachmentViewModel(
override val message: Message,
override val rawData: Message,
override val messageId: String,
var senderName: String,
val time: CharSequence,
val content: CharSequence,
val isPinned: Boolean,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
var messageLink: String? = null
) : BaseViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_message_attachment
}
\ No newline at end of file
...@@ -4,9 +4,7 @@ import DateTimeHelper ...@@ -4,9 +4,7 @@ import DateTimeHelper
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan import android.text.style.StyleSpan
import chat.rocket.android.R import chat.rocket.android.R
...@@ -91,10 +89,21 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -91,10 +89,21 @@ class ViewModelMapper @Inject constructor(private val context: Context,
private fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? { private fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? {
return when (attachment) { return when (attachment) {
is FileAttachment -> mapFileAttachment(message, attachment) is FileAttachment -> mapFileAttachment(message, attachment)
is MessageAttachment -> mapMessageAttachment(message, attachment)
else -> null else -> null
} }
} }
private fun mapMessageAttachment(message: Message, attachment: MessageAttachment): MessageAttachmentViewModel {
val attachmentAuthor = attachment.author!!
val attachmentText = attachment.text ?: ""
val time = getTime(attachment.timestamp!!)
return MessageAttachmentViewModel(message = getMessageWithoutQuoteMarkdown(message), rawData = message,
messageId = message.id, time = time, senderName = attachmentAuthor,
content = attachmentText, isPinned = message.pinned, reactions = getReactions(message))
}
private fun mapFileAttachment(message: Message, attachment: FileAttachment): BaseViewModel<*>? { private fun mapFileAttachment(message: Message, attachment: FileAttachment): BaseViewModel<*>? {
val attachmentUrl = attachmentUrl(attachment) val attachmentUrl = attachmentUrl(attachment)
val attachmentTitle = attachmentTitle(attachment) val attachmentTitle = attachmentTitle(attachment)
...@@ -249,7 +258,6 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -249,7 +258,6 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val quoteMessage: Message = quote val quoteMessage: Message = quote
quoteViewModel = mapMessage(quoteMessage) quoteViewModel = mapMessage(quoteMessage)
} }
return parser.renderMarkdown(message, quoteViewModel, currentUsername) return parser.renderMarkdown(message, quoteViewModel, currentUsername)
} }
...@@ -262,68 +270,17 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -262,68 +270,17 @@ class ViewModelMapper @Inject constructor(private val context: Context,
is MessageType.UserAdded -> context.getString(R.string.message_user_added_by, message.message, message.sender?.username) is MessageType.UserAdded -> context.getString(R.string.message_user_added_by, message.message, message.sender?.username)
is MessageType.RoomNameChanged -> context.getString(R.string.message_room_name_changed, message.message, message.sender?.username) is MessageType.RoomNameChanged -> context.getString(R.string.message_room_name_changed, message.message, message.sender?.username)
is MessageType.UserRemoved -> context.getString(R.string.message_user_removed_by, message.message, message.sender?.username) is MessageType.UserRemoved -> context.getString(R.string.message_user_removed_by, message.message, message.sender?.username)
is MessageType.MessagePinned -> { is MessageType.MessagePinned -> context.getString(R.string.message_pinned)
val attachment = message.attachments?.get(0)
val pinnedSystemMessage = context.getString(R.string.message_pinned)
if (attachment != null && attachment is MessageAttachment) {
return SpannableStringBuilder(pinnedSystemMessage)
.apply {
setSpan(StyleSpan(Typeface.ITALIC), 0, length, 0)
setSpan(ForegroundColorSpan(Color.GRAY), 0, length, 0)
}
.append(quoteMessage(attachment.author!!, message, attachment.timestamp!!))
}
return pinnedSystemMessage
}
else -> { else -> {
throw InvalidParameterException("Invalid message type: ${message.type}") throw InvalidParameterException("Invalid message type: ${message.type}")
} }
} }
//isSystemMessage = true
val spannableMsg = SpannableStringBuilder(content) val spannableMsg = SpannableStringBuilder(content)
spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length, spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length,
0) 0)
spannableMsg.setSpan(ForegroundColorSpan(Color.GRAY), 0, spannableMsg.length, spannableMsg.setSpan(ForegroundColorSpan(Color.GRAY), 0, spannableMsg.length,
0) 0)
/*if (attachmentType == null) {
val username = message.sender?.username
val message = message.message
val usernameTextStartIndex = if (username != null) content.indexOf(username) else -1
val usernameTextEndIndex = if (username != null) usernameTextStartIndex + username.length else -1
val messageTextStartIndex = if (message.isNotEmpty()) content.indexOf(message) else -1
val messageTextEndIndex = messageTextStartIndex + message.length
if (usernameTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), usernameTextStartIndex, usernameTextEndIndex,
0)
}
if (messageTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), messageTextStartIndex, messageTextEndIndex,
0)
}
} else if (attachmentType == AttachmentType.Message) {
spannableMsg.append(quoteMessage(attachmentMessageAuthor!!, attachmentMessageText!!, attachmentTimestamp!!))
}*/
return spannableMsg return spannableMsg
} }
private fun quoteMessage(author: String, message: Message, timestamp: Long): CharSequence {
return SpannableStringBuilder().apply {
val header = "\n$author ${getTime(timestamp)}\n"
append(SpannableString(header).apply {
setSpan(StyleSpan(Typeface.BOLD), 1, author.length + 1, 0)
setSpan(MessageParser.QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), 1, length, 0)
setSpan(AbsoluteSizeSpan(context.resources.getDimensionPixelSize(R.dimen.message_time_text_size)),
author.length + 1, length, 0)
})
append(SpannableString(parser.renderMarkdown(message)).apply {
setSpan(MessageParser.QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), 0, length, 0)
})
}
}
} }
\ No newline at end of file
...@@ -4,15 +4,15 @@ import android.app.Application ...@@ -4,15 +4,15 @@ import android.app.Application
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.* import android.graphics.Canvas
import android.graphics.drawable.Drawable import android.graphics.Paint
import android.graphics.RectF
import android.net.Uri import android.net.Uri
import android.provider.Browser import android.provider.Browser
import android.support.v4.content.ContextCompat
import android.support.v4.content.res.ResourcesCompat import android.support.v4.content.res.ResourcesCompat
import android.text.Layout
import android.text.Spanned import android.text.Spanned
import android.text.style.* import android.text.style.ClickableSpan
import android.text.style.ReplacementSpan
import android.util.Patterns import android.util.Patterns
import android.view.View import android.view.View
import chat.rocket.android.R import chat.rocket.android.R
...@@ -23,7 +23,6 @@ import chat.rocket.android.widget.emoji.EmojiTypefaceSpan ...@@ -23,7 +23,6 @@ import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import chat.rocket.common.model.SimpleUser import chat.rocket.common.model.SimpleUser
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import org.commonmark.node.AbstractVisitor import org.commonmark.node.AbstractVisitor
import org.commonmark.node.BlockQuote
import org.commonmark.node.Document import org.commonmark.node.Document
import org.commonmark.node.Text import org.commonmark.node.Text
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
...@@ -51,20 +50,12 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -51,20 +50,12 @@ class MessageParser @Inject constructor(val context: Application, private val co
val builder = SpannableBuilder() val builder = SpannableBuilder()
val content = EmojiRepository.shortnameToUnicode(text, true) val content = EmojiRepository.shortnameToUnicode(text, true)
val parentNode = parser.parse(toLenientMarkdown(content)) val parentNode = parser.parse(toLenientMarkdown(content))
parentNode.accept(QuoteMessageBodyVisitor(context, configuration, builder)) parentNode.accept(SpannableMarkdownVisitor(configuration, builder))
quote?.apply {
var quoteNode = parser.parse("> $senderName $time")
parentNode.appendChild(quoteNode)
quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length))
quoteNode = parser.parse("> ${toLenientMarkdown(quote.rawData.message)}")
quoteNode.accept(EmojiVisitor(builder, configuration))
quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
}
parentNode.accept(LinkVisitor(builder)) parentNode.accept(LinkVisitor(builder))
if (message.mentions != null) {
parentNode.accept(MentionVisitor(context, builder, message.mentions!!, selfUsername))
}
parentNode.accept(EmojiVisitor(builder, configuration)) parentNode.accept(EmojiVisitor(builder, configuration))
message.mentions?.let {
parentNode.accept(MentionVisitor(context, builder, it, selfUsername))
}
return builder.text() return builder.text()
} }
...@@ -76,32 +67,6 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -76,32 +67,6 @@ class MessageParser @Inject constructor(val context: Application, private val co
.replace("\\_(.+)\\_".toRegex()) { "_${it.groupValues[1].trim()}_" } .replace("\\_(.+)\\_".toRegex()) { "_${it.groupValues[1].trim()}_" }
} }
class QuoteMessageSenderVisitor(private val context: Context,
configuration: SpannableConfiguration,
private val builder: SpannableBuilder,
private val senderNameLength: Int) : SpannableMarkdownVisitor(configuration, builder) {
override fun visit(blockQuote: BlockQuote) {
// mark current length
val length = builder.length()
// pass to super to apply markdown
super.visit(blockQuote)
val res = context.resources
val timeOffsetStart = length + senderNameLength + 1
builder.setSpan(QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), length, builder.length())
builder.setSpan(StyleSpan(Typeface.BOLD), length, length + senderNameLength)
builder.setSpan(ForegroundColorSpan(Color.BLACK), length, builder.length())
// set time spans
builder.setSpan(AbsoluteSizeSpan(res.getDimensionPixelSize(R.dimen.message_time_text_size)),
timeOffsetStart, builder.length())
builder.setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.darkGray)),
timeOffsetStart, builder.length())
}
}
class MentionVisitor(context: Context, class MentionVisitor(context: Context,
private val builder: SpannableBuilder, private val builder: SpannableBuilder,
private val mentions: List<SimpleUser>, private val mentions: List<SimpleUser>,
...@@ -191,58 +156,6 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -191,58 +156,6 @@ class MessageParser @Inject constructor(val context: Application, private val co
} }
} }
class QuoteMessageBodyVisitor(private val context: Context,
configuration: SpannableConfiguration,
private val builder: SpannableBuilder) : SpannableMarkdownVisitor(configuration, builder) {
override fun visit(blockQuote: BlockQuote) {
// mark current length
val length = builder.length()
// pass to super to apply markdown
super.visit(blockQuote)
val padding = context.resources.getDimensionPixelSize(R.dimen.padding_quote)
builder.setSpan(QuoteMarginSpan(context.getDrawable(R.drawable.quote), padding), length,
builder.length())
}
}
class QuoteMarginSpan(quoteDrawable: Drawable, private var pad: Int) : LeadingMarginSpan, LineHeightSpan {
private val drawable: Drawable = quoteDrawable
override fun getLeadingMargin(first: Boolean): Int {
return drawable.intrinsicWidth + pad
}
override fun drawLeadingMargin(c: Canvas, p: Paint, x: Int, dir: Int,
top: Int, baseline: Int, bottom: Int,
text: CharSequence, start: Int, end: Int,
first: Boolean, layout: Layout) {
val st = (text as Spanned).getSpanStart(this)
val itop = layout.getLineTop(layout.getLineForOffset(st))
val dw = drawable.intrinsicWidth
// XXX What to do about Paint?
drawable.setBounds(x, itop, x + dw, itop + layout.height)
drawable.draw(c)
}
override fun chooseHeight(text: CharSequence, start: Int, end: Int,
spanstartv: Int, v: Int,
fm: Paint.FontMetricsInt) {
if (end == (text as Spanned).getSpanEnd(this)) {
val ht = drawable.intrinsicHeight
var need = ht - (v + fm.descent - fm.ascent - spanstartv)
if (need > 0)
fm.descent += need
need = ht - (v + fm.bottom - fm.top - spanstartv)
if (need > 0)
fm.bottom += need
}
}
}
class MentionSpan(private val backgroundColor: Int, class MentionSpan(private val backgroundColor: Int,
private val textColor: Int, private val textColor: Int,
private val radius: Float, private val radius: Float,
......
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorPrimary" />
<size
android:width="4dp"
android:height="4dp" />
<corners android:radius="8dp" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding">
<View
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:layout_marginStart="56dp"
android:background="@drawable/quote_vertical_bar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/top_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="horizontal"
app:layout_constraintLeft_toRightOf="@+id/quote_bar"
app:layout_constraintTop_toBottomOf="@id/new_messages_notif">
<TextView
android:id="@+id/text_sender"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorPrimary"
tools:text="Ronald Perkins" />
<TextView
android:id="@+id/text_message_time"
style="@style/Timestamp.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
tools:text="11:45 PM" />
</LinearLayout>
<TextView
android:id="@+id/text_content"
style="@style/Message.Quote.TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/top_container"
app:layout_constraintTop_toBottomOf="@+id/top_container"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@+id/text_content"
app:layout_constraintTop_toBottomOf="@+id/text_content" />
<android.support.constraint.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="20dp" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -6,6 +6,17 @@ ...@@ -6,6 +6,17 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/colorPrimary"> android:background="@color/colorPrimary">
<View
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:background="@drawable/quote_vertical_bar"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_view_action_cancel_quote"
app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/text_view_action_text" android:id="@+id/text_view_action_text"
android:layout_width="0dp" android:layout_width="0dp"
......
...@@ -88,6 +88,10 @@ ...@@ -88,6 +88,10 @@
<item name="android:textColor">@color/colorPrimaryText</item> <item name="android:textColor">@color/colorPrimaryText</item>
</style> </style>
<style name="Message.Quote.TextView" parent="Message.TextView">
<item name="android:textColor">@color/colorPrimaryText</item>
</style>
<style name="Timestamp.TextView" parent="TextAppearance.AppCompat.Caption"> <style name="Timestamp.TextView" parent="TextAppearance.AppCompat.Caption">
<item name="android:textSize">10sp</item> <item name="android:textSize">10sp</item>
</style> </style>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment