Unverified Commit df93748c authored by Filipe de Lima Brito's avatar Filipe de Lima Brito Committed by GitHub

Merge pull request #776 from filipedelimabrito/fix/message-composer-becomes-empty-with-new-messages

[NEW][FIX] Prevents losing all entered text when a new message arrives on the chat
parents 518c28a9 64388f43
...@@ -37,8 +37,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -37,8 +37,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val mapper: MessageViewModelMapper) { private val mapper: MessageViewModelMapper) {
private val client = factory.create(serverInteractor.get()!!) private val client = factory.create(serverInteractor.get()!!)
private var subId: String? = null private var subId: String? = null
private val settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private val stateChannel = Channel<State>() private val stateChannel = Channel<State>()
fun loadMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) { fun loadMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
...@@ -71,23 +70,23 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -71,23 +70,23 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
fun sendMessage(chatRoomId: String, text: String, messageId: String?) { fun sendMessage(chatRoomId: String, text: String, messageId: String?) {
launchUI(strategy) { launchUI(strategy) {
view.disableMessageInput() view.disableSendMessageButton()
try { try {
// ignore message for now, will receive it on the stream
val message = if (messageId == null) { val message = if (messageId == null) {
client.sendMessage(chatRoomId, text) client.sendMessage(chatRoomId, text)
} else { } else {
client.updateMessage(chatRoomId, messageId, text) client.updateMessage(chatRoomId, messageId, text)
} }
// ignore message for now, will receive it on the stream view.clearMessageComposition()
view.enableMessageInput(clear = true)
} catch (ex: Exception) { } catch (ex: Exception) {
ex.printStackTrace()
ex.message?.let { ex.message?.let {
view.showMessage(it) view.showMessage(it)
}.ifNull { }.ifNull {
view.showGenericErrorMessage() view.showGenericErrorMessage()
} }
view.enableMessageInput() } finally {
view.enableSendMessageButton()
} }
} }
} }
......
...@@ -80,9 +80,21 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -80,9 +80,21 @@ interface ChatRoomView : LoadingView, MessageView {
*/ */
fun showEditingAction(roomId: String, messageId: String, text: String) fun showEditingAction(roomId: String, messageId: String, text: String)
fun disableMessageInput() /**
* Disabling the send message button avoids the user tap this button multiple
* times to send a same message.
*/
fun disableSendMessageButton()
fun enableMessageInput(clear: Boolean = false) /**
* Enables the send message button.
*/
fun enableSendMessageButton()
/**
* Clears the message composition.
*/
fun clearMessageComposition()
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int) fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
} }
\ No newline at end of file
...@@ -21,9 +21,12 @@ import chat.rocket.android.helper.EndlessRecyclerViewScrollListener ...@@ -21,9 +21,12 @@ import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.fragment_chat_room.* import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.* import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.message_composer.* import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.*
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Fragment { fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Fragment {
...@@ -57,12 +60,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -57,12 +60,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
private var citation: String? = null private var citation: String? = null
private var editingMessageId: String? = null private var editingMessageId: String? = null
private val compositeDisposable = CompositeDisposable()
private var playComposeMessageButtonsAnimation = true
// For reveal and unreveal anim. // For reveal and unreveal anim.
private val hypotenuse by lazy { Math.hypot(root_layout.width.toDouble(), root_layout.height.toDouble()).toFloat() } private val hypotenuse by lazy { Math.hypot(root_layout.width.toDouble(), root_layout.height.toDouble()).toFloat() }
private val max by lazy { Math.max(layout_message_attachment_options.width.toDouble(), layout_message_attachment_options.height.toDouble()).toFloat() } private val max by lazy { Math.max(layout_message_attachment_options.width.toDouble(), layout_message_attachment_options.height.toDouble()).toFloat() }
private val centerX by lazy { recycler_view.right } private val centerX by lazy { recycler_view.right }
private val centerY by lazy { recycler_view.bottom } private val centerY by lazy { recycler_view.bottom }
val handler = Handler() private val handler = Handler()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -85,13 +91,17 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -85,13 +91,17 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
presenter.loadMessages(chatRoomId, chatRoomType) presenter.loadMessages(chatRoomId, chatRoomType)
setupComposer()
setupRecyclerView()
setupFab()
setupMessageComposer()
setupActionSnackbar() setupActionSnackbar()
} }
override fun onDestroyView() { override fun onDestroyView() {
presenter.unsubscribeMessages() presenter.unsubscribeMessages()
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
unsubscribeTextMessage()
super.onDestroyView() super.onDestroyView()
} }
...@@ -156,20 +166,23 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -156,20 +166,23 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun showInvalidFileMessage() = showMessage(getString(R.string.msg_invalid_file)) override fun showInvalidFileMessage() = showMessage(getString(R.string.msg_invalid_file))
override fun showNewMessage(message: MessageViewModel) { override fun showNewMessage(message: MessageViewModel) {
text_message.textContent = ""
adapter.addItem(message) adapter.addItem(message)
recycler_view.smoothScrollToPosition(0) recycler_view.scrollToPosition(0)
} }
override fun disableMessageInput() { override fun disableSendMessageButton() {
button_send.isEnabled = false button_send.isEnabled = false
text_message.isEnabled = false
} }
override fun enableMessageInput(clear: Boolean) { override fun enableSendMessageButton() {
button_send.isEnabled = true button_send.isEnabled = true
text_message.isEnabled = true }
if (clear) text_message.textContent = ""
override fun clearMessageComposition() {
citation = null
editingMessageId = null
text_message.textContent = ""
actionSnackbar.dismiss()
} }
override fun dispatchUpdateMessage(index: Int, message: MessageViewModel) { override fun dispatchUpdateMessage(index: Int, message: MessageViewModel) {
...@@ -227,35 +240,43 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -227,35 +240,43 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
showMessage(getString(R.string.max_file_size_exceeded, fileSize, maxFileSize)) showMessage(getString(R.string.max_file_size_exceeded, fileSize, maxFileSize))
} }
private fun setupComposer() { private fun setupRecyclerView() {
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
Timber.i("Scrolling vertically: $dy")
if (!recyclerView.canScrollVertically(1)) {
button_fab.hide()
} else {
if (dy > 0 && !button_fab.isVisible()) {
button_fab.show()
} else if (dy < 0 && button_fab.isVisible()) {
button_fab.hide()
}
}
}
})
}
private fun setupFab() {
button_fab.setOnClickListener {
recycler_view.scrollToPosition(0)
button_fab.hide()
}
}
private fun setupMessageComposer() {
if (isChatRoomReadOnly) { if (isChatRoomReadOnly) {
text_room_is_read_only.setVisible(true) text_room_is_read_only.setVisible(true)
input_container.setVisible(false) input_container.setVisible(false)
} else { } else {
var playAnimation = true subscribeTextMessage()
text_message.asObservable(0)
.subscribe({ t ->
if (t.isNotEmpty() && playAnimation) {
button_show_attachment_options.fadeInOrOut(1F, 0F, 120)
button_send.fadeInOrOut(0F, 1F, 120)
playAnimation = false
}
if (t.isEmpty()) {
button_send.fadeInOrOut(1F, 0F, 120)
button_show_attachment_options.fadeInOrOut(0F, 1F, 120)
playAnimation = true
}
})
button_send.setOnClickListener { button_send.setOnClickListener {
var textMessage = citation ?: "" var message = citation ?: ""
textMessage += text_message.textContent message += text_message.textContent
sendMessage(textMessage) sendMessage(message)
clearActionMessage()
} }
button_show_attachment_options.setOnClickListener { button_show_attachment_options.setOnClickListener {
if (layout_message_attachment_options.isShown) { if (layout_message_attachment_options.isShown) {
hideAttachmentOptions() hideAttachmentOptions()
...@@ -264,7 +285,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -264,7 +285,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
} }
} }
view_dim.setOnClickListener { hideAttachmentOptions() } view_dim.setOnClickListener {
hideAttachmentOptions()
}
button_files.setOnClickListener { button_files.setOnClickListener {
handler.postDelayed({ handler.postDelayed({
...@@ -281,15 +304,35 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -281,15 +304,35 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
private fun setupActionSnackbar() { private fun setupActionSnackbar() {
actionSnackbar = ActionSnackbar.make(message_list_container, parser = parser) actionSnackbar = ActionSnackbar.make(message_list_container, parser = parser)
actionSnackbar.cancelView.setOnClickListener({ actionSnackbar.cancelView.setOnClickListener({
clearActionMessage() clearMessageComposition()
}) })
} }
private fun clearActionMessage() { private fun subscribeTextMessage() {
citation = null val disposable = text_message.asObservable(0)
editingMessageId = null .subscribe({ t -> setupComposeMessageButtons(t) })
text_message.text.clear()
actionSnackbar.dismiss() compositeDisposable.add(disposable)
}
private fun unsubscribeTextMessage() {
if (!compositeDisposable.isDisposed) {
compositeDisposable.dispose()
}
}
private fun setupComposeMessageButtons(charSequence: CharSequence) {
if (charSequence.isNotEmpty() && playComposeMessageButtonsAnimation) {
button_show_attachment_options.fadeOut(1F, 0F, 120)
button_send.fadeIn(0F, 1F, 120)
playComposeMessageButtonsAnimation = false
}
if (charSequence.isEmpty()) {
button_send.fadeOut(1F, 0F, 120)
button_show_attachment_options.fadeIn(0F, 1F, 120)
playComposeMessageButtonsAnimation = true
}
} }
private fun showAttachmentOptions() { private fun showAttachmentOptions() {
......
package chat.rocket.android.chatrooms.ui package chat.rocket.android.chatrooms.ui
import DateTimeHelper import DateTimeHelper
import DrawableHelper
import android.content.Context import android.content.Context
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import com.facebook.drawee.view.SimpleDraweeView import com.facebook.drawee.view.SimpleDraweeView
import kotlinx.android.synthetic.main.avatar.view.* import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_chat.view.* import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class ChatRoomsAdapter(private val context: Context, class ChatRoomsAdapter(private val context: Context,
private val listener: (ChatRoom) -> Unit) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() { private val listener: (ChatRoom) -> Unit) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() {
......
...@@ -12,7 +12,7 @@ fun View.rotateBy(value: Float, duration: Long = 200) { ...@@ -12,7 +12,7 @@ fun View.rotateBy(value: Float, duration: Long = 200) {
.start() .start()
} }
fun View.fadeInOrOut(startValue: Float, finishValue: Float, duration: Long = 200) { fun View.fadeIn(startValue: Float, finishValue: Float, duration: Long = 200) {
animate() animate()
.alpha(startValue) .alpha(startValue)
.setDuration(duration) .setDuration(duration)
...@@ -24,11 +24,22 @@ fun View.fadeInOrOut(startValue: Float, finishValue: Float, duration: Long = 200 ...@@ -24,11 +24,22 @@ fun View.fadeInOrOut(startValue: Float, finishValue: Float, duration: Long = 200
.setInterpolator(AccelerateInterpolator()).start() .setInterpolator(AccelerateInterpolator()).start()
}).start() }).start()
if (startValue > finishValue) { setVisible(true)
setVisible(false) }
} else {
setVisible(true) fun View.fadeOut(startValue: Float, finishValue: Float, duration: Long = 200) {
} animate()
.alpha(startValue)
.setDuration(duration)
.setInterpolator(DecelerateInterpolator())
.withEndAction({
animate()
.alpha(finishValue)
.setDuration(duration)
.setInterpolator(AccelerateInterpolator()).start()
}).start()
setVisible(false)
} }
fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float, endRadius: Float, duration: Long = 600) { fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float, endRadius: Float, duration: Long = 600) {
......
...@@ -21,6 +21,10 @@ fun View.setVisible(visible: Boolean) { ...@@ -21,6 +21,10 @@ fun View.setVisible(visible: Boolean) {
} }
} }
fun View.isVisible(): Boolean {
return visibility == View.VISIBLE
}
fun ViewGroup.inflate(@LayoutRes resource: Int): View = LayoutInflater.from(context).inflate(resource, this, false) fun ViewGroup.inflate(@LayoutRes resource: Int): View = LayoutInflater.from(context).inflate(resource, this, false)
fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) { fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) {
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF010101"
android:pathData="M20,12l-1.41,-1.41L13,16.17V4h-2v12.17l-5.58,-5.59L4,12l8,8 8,-8z" />
</vector>
\ No newline at end of file
...@@ -4,8 +4,7 @@ ...@@ -4,8 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout" android:id="@+id/root_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:orientation="vertical">
<com.wang.avi.AVLoadingIndicatorView <com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading" android:id="@+id/view_loading"
...@@ -20,16 +19,23 @@ ...@@ -20,16 +19,23 @@
<FrameLayout <FrameLayout
android:id="@+id/message_list_container" android:id="@+id/message_list_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_above="@+id/layout_message_composer"> android:layout_above="@+id/layout_message_composer">
<android.support.v7.widget.RecyclerView <include
android:id="@+id/recycler_view" android:id="@+id/layout_message_list"
layout="@layout/message_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent" />
android:scrollbars="vertical" />
</FrameLayout> </FrameLayout>
<include
android:id="@+id/layout_message_composer"
layout="@layout/message_composer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />
<View <View
android:id="@+id/view_dim" android:id="@+id/view_dim"
android:layout_width="match_parent" android:layout_width="match_parent"
...@@ -45,14 +51,6 @@ ...@@ -45,14 +51,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_above="@+id/layout_message_composer" android:layout_above="@+id/layout_message_composer"
android:layout_margin="5dp" android:layout_margin="5dp"
android:visibility="gone" android:visibility="gone" />
tools:visibility="visible" />
<include
android:id="@+id/layout_message_composer"
layout="@layout/message_composer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />
</RelativeLayout> </RelativeLayout>
...@@ -60,23 +60,16 @@ ...@@ -60,23 +60,16 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/guideline_one" app:layout_constraintBottom_toTopOf="@+id/guideline_one"
app:layout_constraintRight_toRightOf="@+id/text_total_unread_messages" app:layout_constraintRight_toRightOf="@+id/layout_unread_messages_badge"
tools:text="11:45" /> tools:text="11:45" />
<TextView <include
android:id="@+id/text_total_unread_messages" android:id="@+id/layout_unread_messages_badge"
style="@style/TextAppearance.AppCompat.Caption" layout="@layout/unread_messages_badge"
android:layout_width="20dp" android:layout_width="20dp"
android:layout_height="20dp" android:layout_height="20dp"
android:background="@drawable/style_total_unread_messages"
android:gravity="center"
android:textColor="@color/white"
android:textSize="10sp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/guideline_two" app:layout_constraintBottom_toTopOf="@+id/guideline_two"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent" />
tools:text="99+"
tools:visibility="visible" />
<android.support.constraint.Guideline <android.support.constraint.Guideline
android:id="@+id/guideline_one" android:id="@+id/guideline_one"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/button_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_arrow_downward_24dp"
android:theme="@style/Theme.AppCompat"
android:tint="@color/gray_material"
android:visibility="invisible"
app:backgroundTint="@color/white"
app:fabSize="mini"
app:layout_anchor="@id/recycler_view"
app:layout_anchorGravity="bottom|end" />
</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_total_unread_messages"
style="@style/TextAppearance.AppCompat.Caption"
android:layout_width="20dp"
android:layout_height="20dp"
android:background="@drawable/style_total_unread_messages"
android:gravity="center"
android:textColor="@color/white"
android:textSize="10sp"
android:visibility="gone"
tools:text="99+"
tools:visibility="visible" />
</LinearLayout>
\ No newline at end of file
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