Skip to content
This repository has been archived by the owner on Jun 7, 2020. It is now read-only.

[NEW] Add support for initial Rich Messaging #1557

Closed
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package chat.rocket.android.chatroom.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.chatroom.uimodel.ActionsAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
import kotlinx.android.synthetic.main.item_actions_attachment.view.*
import androidx.recyclerview.widget.LinearLayoutManager
import chat.rocket.android.R
import chat.rocket.android.util.TimberLogger
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.view.SimpleDraweeView
import com.google.android.material.button.MaterialButton

class ActionsAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
var actionAttachmentOnClickListener: ActionAttachmentOnClickListener
) : BaseViewHolder<ActionsAttachmentUiModel>(itemView, listener, reactionListener) {

init {
with(itemView) {
setupActionMenu(actions_attachment_container)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with is useless here since there is only one block being used. Could be itemView.setupActionMenu(actions_attachment_container) instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any issue using with even if it is just for one statement...

Copy link
Contributor

@philipbrito philipbrito Aug 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use it anywhere, anytime, but again: When there is only one block being used it is useless. What about spread with with only one block being used on our entire codebase? It will never been an issue but for sure it will be useless, helping only to increase the code lines.

Btw, do you see any advantage declaring:

  init {
        with(itemView) {
            setupActionMenu(actions_attachment_container)
        }
  }

instead of:

  init {
        itemView.setupActionMenu(actions_attachment_container)
  }

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@filipedelimabrito Unable to access setupActionMenu method without with(itemView)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

itemView.setupActionMenu(actions_attachment_container) works.

}
}

override fun bindViews(data: ActionsAttachmentUiModel) {
val actions = data.actions
TimberLogger.debug("no of actions : ${actions.size} : $actions")
with(itemView) {
title.text = data.title ?: ""
actions_list.layoutManager = LinearLayoutManager(itemView.context)
actions_list.adapter = ActionsListAdapter(actions, actionAttachmentOnClickListener)
}
}
}

interface ActionAttachmentOnClickListener {
fun onActionClicked(action: Action)
}

class ActionsListAdapter(actions: List<Action>, var actionAttachmentOnClickListener: ActionAttachmentOnClickListener) : RecyclerView.Adapter<ActionsListAdapter.ViewHolder>() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd prefer using a new package and multiple files instead of multiple classes and inner classes on the same file


var actions: List<Action> = actions

inner class ViewHolder(var layout: View) : RecyclerView.ViewHolder(layout) {
lateinit var action: ButtonAction

var button: MaterialButton = layout.findViewById(R.id.action_button)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for findView, can use synthetic accessors from kotlin extensions

val image: SimpleDraweeView = layout.findViewById(R.id.action_image_button)

private val onClickListener = View.OnClickListener {
actionAttachmentOnClickListener.onActionClicked(action)
}

init {
button.setOnClickListener(onClickListener)
image.setOnClickListener(onClickListener)
}

fun bindAction(action: Action) {
TimberLogger.debug("action : $action")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use 'TimberLogger' directly, use Timber.d() or other Timber variants.

this.action = action as ButtonAction

//TODO
if (action.imageUrl != null) {
button.visibility = View.GONE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with button.isVisible = false

image.visibility = View.VISIBLE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with image.isVisible = true


//Image button
val controller = Fresco.newDraweeControllerBuilder().apply {
setUri(action.imageUrl)
autoPlayAnimations = true
oldController = image.controller
}.build()
image.controller = controller

} else if (action.text != null) {
button.visibility = View.VISIBLE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with button.isVisible = true

image.visibility = View.GONE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with image.isVisible = false


this.button.setText(action.text)
}
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_action_button, parent, false)
return ViewHolder(view)
}

override fun getItemCount(): Int {
return actions.size
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val action = actions[position]
holder.bindAction(action)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,28 @@ package chat.rocket.android.chatroom.adapter

import android.app.AlertDialog
import android.content.Context
import android.net.Uri
import androidx.recyclerview.widget.RecyclerView
import android.view.MenuItem
import android.view.ViewGroup
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.res.ResourcesCompat
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.uimodel.*
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.util.extensions.openTabbedUrl
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
import chat.rocket.core.model.Message
import chat.rocket.core.model.isSystemMessage
import timber.log.Timber
import java.security.InvalidParameterException

class ChatRoomAdapter(
private val roomId: String? = null,
private val roomType: String? = null,
private val roomName: String? = null,
private val presenter: ChatRoomPresenter? = null,
Expand Down Expand Up @@ -73,6 +81,10 @@ class ChatRoomAdapter(
presenter?.openDirectMessage(roomName, permalink)
}
}
BaseUiModel.ViewType.ACTIONS_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_actions_attachment)
ActionsAttachmentViewHolder(view, actionsListener, reactionListener, actionAttachmentOnClickListener)
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
Expand Down Expand Up @@ -126,6 +138,8 @@ class ChatRoomAdapter(
holder.bind(dataSet[position] as GenericFileAttachmentUiModel)
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyUiModel)
is ActionsAttachmentViewHolder ->
holder.bind(dataSet[position] as ActionsAttachmentUiModel)
}
}

Expand Down Expand Up @@ -204,6 +218,42 @@ class ChatRoomAdapter(
}
}

private val actionAttachmentOnClickListener = object : ActionAttachmentOnClickListener {
override fun onActionClicked(action: Action) {
val temp = action as ButtonAction
if (temp.url != null && temp.isWebView != null) {
if (temp.isWebView!!) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you use temp.isWebView == true you don't need to override nullability (!!)

//Open in a configurable sizable webview
Toast.makeText(context, "Open in a configurable sizable webview", Toast.LENGTH_SHORT).show()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard coded string. Please, remove and use the string from the string.xml files.

} else {
//Open in chrome custom tab
with(this) {
val customTabsBuilder = CustomTabsIntent.Builder()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a View.openTabbedUrl() extension function on chat.rocket.android.util.extensions.View.kt

customTabsBuilder.setToolbarColor(ResourcesCompat.getColor(context!!.resources, R.color.colorPrimary, context.theme))
val customTabsIntent = customTabsBuilder.build()
try {
customTabsIntent.launchUrl(context, Uri.parse(temp.url!!))
} catch (ex: Exception) {
Timber.d(ex, "Unable to launch URL")
}
}
}
} else if (temp.message != null && temp.isMessageInChatWindow != null) {
if (temp.isMessageInChatWindow!!) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

//Send to chat window
temp.message.run {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use temp.message?.let { message -> that way you don't need to override nullability on temp.message, just use message

if (roomId != null) {
presenter?.sendMessage(roomId, temp.message!!, null)
}
}
} else {
//Send to bot but not in chat window
Toast.makeText(context, "Send to bot but not in chat window", Toast.LENGTH_SHORT).show()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard coded string. Please, remove and use the string from the string.xml files.

}
}
}
}

private val actionsListener = object : BaseViewHolder.ActionsListener {

override fun isActionsEnabled(): Boolean = enableActions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR

if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The adapter is already initialized on onCreate don't create it twice. Also, this is not compiling...

chatRoomId,
chatRoomType,
chatRoomName,
presenter,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package chat.rocket.android.chatroom.uimodel

import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ActionsAttachment

data class ActionsAttachmentUiModel(
override val attachmentUrl: String,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use 4 spaces indentation here, please.

val title: String?,
val actions: List<Action>,
override val message: Message,
override val rawData: ActionsAttachment,
override val messageId: String,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseAttachmentUiModel<ActionsAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.ACTIONS_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_actions_attachment
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ interface BaseUiModel<out T> {
AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8),
GENERIC_FILE_ATTACHMENT(9),
MESSAGE_REPLY(10)
MESSAGE_REPLY(10),
ACTIONS_ATTACHMENT(11)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.MessageAttachment
import chat.rocket.core.model.attachment.VideoAttachment
import chat.rocket.core.model.attachment.actions.ActionsAttachment
import chat.rocket.core.model.isSystemMessage
import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool
Expand Down Expand Up @@ -305,10 +306,26 @@ class UiModelMapper @Inject constructor(
is MessageAttachment -> mapMessageAttachment(message, attachment)
is AuthorAttachment -> mapAuthorAttachment(message, attachment)
is ColorAttachment -> mapColorAttachment(message, attachment)
is ActionsAttachment -> mapActionsAttachment(message, attachment)
else -> null
}
}

private fun mapActionsAttachment(message: Message, attachment: ActionsAttachment): BaseUiModel<*>? {
return with(attachment) {
val content = stripMessageQuotes(message)

val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context)

ActionsAttachmentUiModel(attachmentUrl = url, title = title,
actions = actions, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message), unread = message.unread,
showDayMarker = false, currentDayMarkerText = dayMarkerText)
}
}

private fun mapColorAttachment(message: Message, attachment: ColorAttachment): BaseUiModel<*>? {
return with(attachment) {
val content = stripMessageQuotes(message)
Expand Down
25 changes: 25 additions & 0 deletions app/src/main/res/layout/item_action_button.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">

<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button"
android:id="@+id/action_button"
android:textSize="12sp"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/action_image_button"
android:layout_width="match_parent"
android:layout_height="75dp"
android:layout_marginBottom="10dp"
android:visibility="gone"
fresco:actualImageScaleType="fitStart"
fresco:placeholderImage="@drawable/image_dummy" />

</RelativeLayout>
57 changes: 57 additions & 0 deletions app/src/main/res/layout/item_actions_attachment.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?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/actions_attachment_container"
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">

<TextView
android:id="@+id/title"
style="@style/Message.TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginStart="56dp"
android:layout_marginTop="2dp"
android:textDirection="locale"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
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!" />

<View
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:background="@drawable/quote_vertical_gray_bar"
app:layout_constraintBottom_toTopOf="@id/recycler_view_reactions"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title" />


<androidx.recyclerview.widget.RecyclerView
android:id="@+id/actions_list"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/colorAccent"
android:textDirection="locale"
app:layout_constraintEnd_toEndOf="@id/title"
app:layout_constraintStart_toEndOf="@id/quote_bar"
app:layout_constraintTop_toBottomOf="@id/title" />

<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/quote_bar"
app:layout_constraintTop_toBottomOf="@id/actions_list" />

</androidx.constraintlayout.widget.ConstraintLayout>