Unverified Commit 380d3688 authored by Rafael Kellermann Streit's avatar Rafael Kellermann Streit Committed by GitHub

Merge pull request #1204 from RocketChat/fix/reply-with-username

[FIX] Use username when building replies always
parents 6b2bdf3b 7eaa9971
......@@ -104,7 +104,6 @@ dependencies {
implementation libraries.frescoImageViewer
implementation libraries.markwon
implementation libraries.markwonImageLoader
implementation libraries.sheetMenu
......
......@@ -14,11 +14,11 @@ import timber.log.Timber
import java.security.InvalidParameterException
class ChatRoomAdapter(
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseViewModel<*>>()
......@@ -175,15 +175,15 @@ class ChatRoomAdapter(
}
}
val actionsListener = object : BaseViewHolder.ActionsListener {
private val actionsListener = object : BaseViewHolder.ActionsListener {
override fun isActionsEnabled(): Boolean = enableActions
override fun onActionSelected(item: MenuItem, message: Message) {
message.apply {
when (item.itemId) {
R.id.action_menu_msg_delete -> presenter?.deleteMessage(roomId, id)
R.id.action_menu_msg_quote -> presenter?.citeMessage(roomType, roomName, id, false)
R.id.action_menu_msg_reply -> presenter?.citeMessage(roomType, roomName, id, true)
R.id.action_menu_msg_quote -> presenter?.citeMessage(roomType, id, false)
R.id.action_menu_msg_reply -> presenter?.citeMessage(roomType, id, true)
R.id.action_menu_msg_copy -> presenter?.copyMessage(id)
R.id.action_menu_msg_edit -> presenter?.editMessage(roomId, id, message.message)
R.id.action_menu_msg_pin_unpin -> {
......
......@@ -31,7 +31,6 @@ import chat.rocket.core.internal.rest.*
import chat.rocket.core.model.Command
import chat.rocket.core.model.Message
import chat.rocket.core.model.Myself
import chat.rocket.core.model.Value
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
......@@ -59,10 +58,11 @@ class ChatRoomPresenter @Inject constructor(
private val mapper: ViewModelMapper,
private val jobSchedulerInteractor: JobSchedulerInteractor
) {
private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer)
private val client = manager.client
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
private val messagesChannel = Channel<Message>()
private var chatRoomId: String? = null
......@@ -194,7 +194,7 @@ class ChatRoomPresenter @Inject constructor(
}
} catch (ex: Exception) {
Timber.d(ex, "Error uploading file")
when(ex) {
when (ex) {
is RocketChatException -> view.showMessage(ex)
else -> view.showGenericErrorMessage()
}
......@@ -279,7 +279,6 @@ class ChatRoomPresenter @Inject constructor(
// TODO - we need to better treat connection problems here, but no let gaps
// on the messages list
Timber.d(ex, "Error fetching channel history")
ex.printStackTrace()
}
}
}
......@@ -326,43 +325,37 @@ class ChatRoomPresenter @Inject constructor(
* Quote or reply a message.
*
* @param roomType The current room type.
* @param roomName The name of the current room.
* @param messageId The id of the message to make citation for.
* @param mentionAuthor true means the citation is a reply otherwise it's a quote.
*/
fun citeMessage(roomType: String, roomName: String, messageId: String, mentionAuthor: Boolean) {
fun citeMessage(roomType: String, messageId: String, mentionAuthor: Boolean) {
launchUI(strategy) {
val message = messagesRepository.getById(messageId)
val me: Myself? = try {
retryIO("me()") { client.me() } //TODO: Cache this and use an interactor
} catch (ex: Exception) {
Timber.d(ex, "Error getting myself info.")
ex.printStackTrace()
Timber.e(ex)
null
}
message?.let { m ->
val id = m.id
val username = m.sender?.username
val user = "@" + if (settings.useRealName()) m.sender?.name
?: m.sender?.username else m.sender?.username
val mention = if (mentionAuthor && me?.username != username) user else ""
val type = roomTypeOf(roomType)
val room = when (type) {
is RoomType.Channel -> "channel"
is RoomType.DirectMessage -> "direct"
is RoomType.PrivateGroup -> "group"
is RoomType.Livechat -> "livechat"
is RoomType.Custom -> "custom" //TODO: put appropriate callback string here.
}
message?.let { msg ->
val id = msg.id
val username = msg.sender?.username ?: ""
val mention = if (mentionAuthor && me?.username != username) "@$username" else ""
val room = if (roomTypeOf(roomType) is RoomType.DirectMessage) username else roomType
view.showReplyingAction(
username = user,
replyMarkdown = "[ ]($currentServer/$room/$roomName?msg=$id) $mention ",
username = getDisplayName(msg.sender),
replyMarkdown = "[ ]($currentServer/$roomType/$room?msg=$id) $mention ",
quotedMessage = mapper.map(message).last().preview?.message ?: ""
)
}
}
}
private fun getDisplayName(user: SimpleUser?): String {
val username = user?.username ?: ""
return if (settings.useRealName()) user?.name ?: "@$username" else "@$username"
}
/**
* Copy message to clipboard.
*
......
......@@ -5,7 +5,6 @@ import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.support.v4.content.ContextCompat
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
......@@ -32,21 +31,21 @@ import okhttp3.HttpUrl
import java.security.InvalidParameterException
import javax.inject.Inject
class ViewModelMapper @Inject constructor(private val context: Context,
private val parser: MessageParser,
private val messagesRepository: MessagesRepository,
private val getAccountInteractor: GetAccountInteractor,
tokenRepository: TokenRepository,
serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor,
localRepository: LocalRepository) {
class ViewModelMapper @Inject constructor(
private val context: Context,
private val parser: MessageParser,
tokenRepository: TokenRepository,
serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor,
localRepository: LocalRepository
) {
private val currentServer = serverInteractor.get()!!
private val settings: Map<String, Value<Any>> = getSettingsInteractor.get(currentServer)
private val baseUrl = settings.baseUrl()
private val token = tokenRepository.get(currentServer)
private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
private val secundaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
private val secondaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
suspend fun map(message: Message): List<BaseViewModel<*>> {
return translate(message)
......@@ -99,10 +98,10 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val description = url.meta?.description
return UrlPreviewViewModel(message, url, message.id, title, hostname, description, thumb,
getReactions(message), preview = message.copy(message = url.url))
getReactions(message), preview = message.copy(message = url.url))
}
private suspend fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? {
private fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? {
return when (attachment) {
is FileAttachment -> mapFileAttachment(message, attachment)
is MessageAttachment -> mapMessageAttachment(message, attachment)
......@@ -112,19 +111,19 @@ class ViewModelMapper @Inject constructor(private val context: Context,
}
}
private suspend fun mapColorAttachment(message: Message, attachment: ColorAttachment): BaseViewModel<*>? {
private fun mapColorAttachment(message: Message, attachment: ColorAttachment): BaseViewModel<*>? {
return with(attachment) {
val content = stripMessageQuotes(message)
val id = attachmentId(message, attachment)
ColorAttachmentViewModel(attachmentUrl = url, id = id, color = color.color,
text = text, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message))
text = text, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message))
}
}
private suspend fun mapAuthorAttachment(message: Message, attachment: AuthorAttachment): AuthorAttachmentViewModel {
private fun mapAuthorAttachment(message: Message, attachment: AuthorAttachment): AuthorAttachmentViewModel {
return with(attachment) {
val content = stripMessageQuotes(message)
......@@ -146,13 +145,13 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val id = attachmentId(message, attachment)
AuthorAttachmentViewModel(attachmentUrl = url, id = id, name = authorName,
icon = authorIcon, fields = fieldsText, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message))
icon = authorIcon, fields = fieldsText, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message))
}
}
private suspend fun mapMessageAttachment(message: Message, attachment: MessageAttachment): MessageAttachmentViewModel {
private fun mapMessageAttachment(message: Message, attachment: MessageAttachment): MessageAttachmentViewModel {
val attachmentAuthor = attachment.author
val time = attachment.timestamp?.let { getTime(it) }
val attachmentText = when (attachment.attachments.orEmpty().firstOrNull()) {
......@@ -163,9 +162,9 @@ class ViewModelMapper @Inject constructor(private val context: Context,
}
val content = stripMessageQuotes(message)
return MessageAttachmentViewModel(message = content, rawData = message,
messageId = message.id, time = time, senderName = attachmentAuthor,
content = attachmentText, isPinned = message.pinned, reactions = getReactions(message),
preview = message.copy(message = content.message))
messageId = message.id, time = time, senderName = attachmentAuthor,
content = attachmentText, isPinned = message.pinned, reactions = getReactions(message),
preview = message.copy(message = content.message))
}
private fun mapFileAttachment(message: Message, attachment: FileAttachment): BaseViewModel<*>? {
......@@ -174,14 +173,14 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val id = attachmentId(message, attachment)
return when (attachment) {
is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_photo)))
attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_photo)))
is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_video)))
attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_video)))
is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_audio)))
attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_audio)))
else -> null
}
}
......@@ -230,12 +229,12 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val content = getContent(stripMessageQuotes(message))
MessageViewModel(message = stripMessageQuotes(message), rawData = message,
messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, reactions = getReactions(message),
isFirstUnread = false, preview = preview, isTemporary = isTemp)
messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, reactions = getReactions(message),
isFirstUnread = false, preview = preview, isTemporary = isTemp)
}
private suspend fun mapMessagePreview(message: Message): Message {
private fun mapMessagePreview(message: Message): Message {
return when (message.isSystemMessage()) {
false -> stripMessageQuotes(message)
true -> message.copy(message = getSystemMessage(message).toString())
......@@ -249,11 +248,11 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val usernames = it.getUsernames(shortname) ?: emptyList()
val count = usernames.size
list.add(
ReactionViewModel(messageId = message.id,
shortname = shortname,
unicode = EmojiParser.parse(shortname),
count = count,
usernames = usernames)
ReactionViewModel(messageId = message.id,
shortname = shortname,
unicode = EmojiParser.parse(shortname),
count = count,
usernames = usernames)
)
}
list
......@@ -261,10 +260,10 @@ class ViewModelMapper @Inject constructor(private val context: Context,
return reactions ?: emptyList()
}
private suspend fun stripMessageQuotes(message: Message): Message {
private fun stripMessageQuotes(message: Message): Message {
val baseUrl = settings.baseUrl()
return message.copy(
message = message.message.replace("\\[[^\\]]+\\]\\($baseUrl[^)]+\\)".toRegex(), "").trim()
message = message.message.replace("\\[[^\\]]+\\]\\($baseUrl[^)]+\\)".toRegex(), "").trim()
)
}
......@@ -276,7 +275,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
username?.let {
append(" ")
scale(0.8f) {
color(secundaryTextColor) {
color(secondaryTextColor) {
append("@$username")
}
}
......@@ -302,7 +301,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
private fun getTime(timestamp: Long) = DateTimeHelper.getTime(DateTimeHelper.getLocalDateTime(timestamp))
private suspend fun getContent(message: Message): CharSequence {
private fun getContent(message: Message): CharSequence {
return when (message.isSystemMessage()) {
true -> getSystemMessage(message)
false -> parser.renderMarkdown(message, currentUsername)
......@@ -325,9 +324,9 @@ class ViewModelMapper @Inject constructor(private val context: Context,
}
val spannableMsg = SpannableStringBuilder(content)
spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length,
0)
0)
spannableMsg.setSpan(ForegroundColorSpan(Color.GRAY), 0, spannableMsg.length,
0)
0)
return spannableMsg
}
......
......@@ -48,10 +48,8 @@ import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.il.AsyncDrawableLoader
import ru.noties.markwon.spans.SpannableTheme
import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
......@@ -264,11 +262,6 @@ class AppModule {
fun provideConfiguration(context: Application, client: OkHttpClient): SpannableConfiguration {
val res = context.resources
return SpannableConfiguration.builder(context)
.asyncDrawableLoader(AsyncDrawableLoader.builder()
.client(client)
.executorService(Executors.newCachedThreadPool())
.resources(res)
.build())
.theme(SpannableTheme.builder()
.linkColor(res.getColor(R.color.colorAccent))
.build())
......@@ -277,8 +270,9 @@ class AppModule {
@Provides
@Singleton
fun provideMessageParser(context: Application, configuration: SpannableConfiguration): MessageParser {
return MessageParser(context, configuration)
fun provideMessageParser(context: Application, configuration: SpannableConfiguration, serverInteractor: GetCurrentServerInteractor, settingsInteractor: GetSettingsInteractor): MessageParser {
val url = serverInteractor.get()!!
return MessageParser(context, configuration, settingsInteractor.get(url))
}
@Provides
......
package chat.rocket.android.helper
import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.net.Uri
import android.support.customtabs.CustomTabsIntent
import android.provider.Browser
import android.support.v4.content.res.ResourcesCompat
import android.text.Spanned
import android.text.style.ClickableSpan
......@@ -17,6 +14,8 @@ import android.text.style.ReplacementSpan
import android.util.Patterns
import android.view.View
import chat.rocket.android.R
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
......@@ -29,10 +28,13 @@ import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder
import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.renderer.SpannableMarkdownVisitor
import timber.log.Timber
import javax.inject.Inject
class MessageParser @Inject constructor(val context: Application, private val configuration: SpannableConfiguration) {
class MessageParser @Inject constructor(
private val context: Application,
private val configuration: SpannableConfiguration,
private val settings: PublicSettings
) {
private val parser = Markwon.createParser()
......@@ -53,7 +55,7 @@ class MessageParser @Inject constructor(val context: Application, private val co
parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(configuration, builder))
message.mentions?.let {
parentNode.accept(MentionVisitor(context, builder, it, selfUsername))
parentNode.accept(MentionVisitor(context, builder, it, selfUsername, settings))
}
return builder.text()
......@@ -62,23 +64,31 @@ class MessageParser @Inject constructor(val context: Application, private val co
// Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs.
private fun toLenientMarkdown(text: String): String {
return text.trim().replace("\\*(.+)\\*".toRegex()) { "**${it.groupValues[1].trim()}**" }
.replace("\\~(.+)\\~".toRegex()) { "~~${it.groupValues[1].trim()}~~" }
.replace("\\_(.+)\\_".toRegex()) { "_${it.groupValues[1].trim()}_" }
.replace("\\~(.+)\\~".toRegex()) { "~~${it.groupValues[1].trim()}~~" }
.replace("\\_(.+)\\_".toRegex()) { "_${it.groupValues[1].trim()}_" }
}
class MentionVisitor(context: Context,
private val builder: SpannableBuilder,
private val mentions: List<SimpleUser>,
private val currentUser: String?) : AbstractVisitor() {
class MentionVisitor(
context: Context,
private val builder: SpannableBuilder,
private val mentions: List<SimpleUser>,
private val currentUser: String?,
private val settings: PublicSettings
) : AbstractVisitor() {
private val othersTextColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme)
private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.white, context.theme)
private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val mentionPadding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
private val mentionRadius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
override fun visit(t: Text) {
val text = t.literal
val mentionsList = mentions.map { it.username }.toMutableList()
val mentionsList = mentions.map {
if (settings.useRealName()) it.name else it.username ?: ""
}.toMutableList()
mentionsList.add("all")
mentionsList.add("here")
......@@ -90,7 +100,7 @@ class MessageParser @Inject constructor(val context: Application, private val co
val textColor = if (mentionMe) myselfTextColor else othersTextColor
val backgroundColor = if (mentionMe) myselfBackgroundColor else othersBackgroundColor
val usernameSpan = MentionSpan(backgroundColor, textColor, mentionRadius, mentionPadding,
mentionMe)
mentionMe)
// Add 1 to end offset to include the @.
val end = offset + it.length + 1
builder.setSpan(usernameSpan, offset, end, 0)
......@@ -101,8 +111,11 @@ class MessageParser @Inject constructor(val context: Application, private val co
}
}
class EmojiVisitor(configuration: SpannableConfiguration, private val builder: SpannableBuilder)
: SpannableMarkdownVisitor(configuration, builder) {
class EmojiVisitor(
configuration: SpannableConfiguration,
private val builder: SpannableBuilder
) : SpannableMarkdownVisitor(configuration, builder) {
override fun visit(document: Document) {
val spannable = EmojiParser.parse(builder.text())
if (spannable is Spanned) {
......@@ -127,7 +140,7 @@ class MessageParser @Inject constructor(val context: Application, private val co
if (!link.startsWith("@") && link !in consumed) {
builder.setSpan(object : ClickableSpan() {
override fun onClick(view: View) {
with (view) {
with(view) {
val tabsbuilder = CustomTabsIntent.Builder()
tabsbuilder.setToolbarColor(ResourcesCompat.getColor(context.resources, R.color.colorPrimary, context.theme))
val customTabsIntent = tabsbuilder.build()
......@@ -150,11 +163,14 @@ class MessageParser @Inject constructor(val context: Application, private val co
}
}
class MentionSpan(private val backgroundColor: Int,
private val textColor: Int,
private val radius: Float,
padding: Float,
referSelf: Boolean) : ReplacementSpan() {
class MentionSpan(
private val backgroundColor: Int,
private val textColor: Int,
private val radius: Float,
padding: Float,
referSelf: Boolean
) : ReplacementSpan() {
private val padding: Float = if (referSelf) padding else 0F
override fun getSize(paint: Paint,
......
......@@ -91,7 +91,6 @@ ext {
frescoImageViewer : "com.github.luciofm:FrescoImageViewer:${versions.frescoImageViewer}",
markwon : "ru.noties:markwon:${versions.markwon}",
markwonImageLoader : "ru.noties:markwon-image-loader:${versions.markwon}",
sheetMenu : "com.github.whalemare:sheetmenu:${versions.sheetMenu}",
......
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