From 75b61675656ec0114c821e2d097691d88975eca5 Mon Sep 17 00:00:00 2001 From: Eyal Date: Sat, 22 Aug 2020 01:43:52 +0300 Subject: [PATCH 1/2] added the intro message. there is still a bug that on entering the chat dialog, and on refreshing, the chat scroll moves to the bottom instead of to the top --- .../res/layout/tavern_chat_intro_item.xml | 80 +++++++++++++++++++ .../adapter/social/ChatRecyclerViewAdapter.kt | 6 +- .../ui/adapter/social/InboxAdapter.kt | 54 ++++++++++--- .../social/InboxMessageListFragment.kt | 34 ++++---- .../ui/viewHolders/ChatRecyclerViewHolder.kt | 30 ++++++- 5 files changed, 172 insertions(+), 32 deletions(-) create mode 100644 Habitica/res/layout/tavern_chat_intro_item.xml diff --git a/Habitica/res/layout/tavern_chat_intro_item.xml b/Habitica/res/layout/tavern_chat_intro_item.xml new file mode 100644 index 000000000..10fbf184c --- /dev/null +++ b/Habitica/res/layout/tavern_chat_intro_item.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/ChatRecyclerViewAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/ChatRecyclerViewAdapter.kt index afb4dad1e..00bbe1e3e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/ChatRecyclerViewAdapter.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/ChatRecyclerViewAdapter.kt @@ -9,7 +9,7 @@ import com.habitrpg.android.habitica.extensions.inflate import com.habitrpg.android.habitica.models.social.ChatMessage import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.ui.helpers.bindView -import com.habitrpg.android.habitica.ui.viewHolders.ChatRecyclerViewHolder +import com.habitrpg.android.habitica.ui.viewHolders.ChatRecyclerMessageViewHolder import io.reactivex.BackpressureStrategy import io.reactivex.Flowable import io.reactivex.subjects.PublishSubject @@ -40,7 +40,7 @@ class ChatRecyclerViewAdapter(data: OrderedRealmCollection?, autoUp return if (viewType == 0) { SystemChatMessageViewHolder(parent.inflate(R.layout.system_chat_message)) } else { - ChatRecyclerViewHolder(parent.inflate(R.layout.tavern_chat_item), uuid, isTavern) + ChatRecyclerMessageViewHolder(parent.inflate(R.layout.tavern_chat_item), uuid, isTavern) } } @@ -49,7 +49,7 @@ class ChatRecyclerViewAdapter(data: OrderedRealmCollection?, autoUp if (data[position].isSystemMessage) { (holder as? SystemChatMessageViewHolder)?.bind(data[position]) } else { - val chatHolder = holder as? ChatRecyclerViewHolder ?: return + val chatHolder = holder as? ChatRecyclerMessageViewHolder ?: return val message = data[position] chatHolder.bind(message, uuid, diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/InboxAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/InboxAdapter.kt index c14967dad..b14b872ee 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/InboxAdapter.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/social/InboxAdapter.kt @@ -8,11 +8,14 @@ import com.habitrpg.android.habitica.extensions.inflate import com.habitrpg.android.habitica.models.social.ChatMessage import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.ui.viewHolders.ChatRecyclerViewHolder +import com.habitrpg.android.habitica.ui.viewHolders.ChatRecyclerIntroViewHolder +import com.habitrpg.android.habitica.ui.viewHolders.ChatRecyclerMessageViewHolder import io.reactivex.BackpressureStrategy import io.reactivex.Flowable import io.reactivex.subjects.PublishSubject +import com.habitrpg.android.habitica.models.members.Member -class InboxAdapter(private var user: User?) : PagedListAdapter(DIFF_CALLBACK) { +class InboxAdapter(private var user: User?, private var replyToUser : Member) : PagedListAdapter(DIFF_CALLBACK) { private var expandedMessageId: String? = null private val likeMessageEvents = PublishSubject.create() @@ -22,24 +25,49 @@ class InboxAdapter(private var user: User?) : PagedListAdapter() private val copyMessageEvents = PublishSubject.create() - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatRecyclerViewHolder { - return ChatRecyclerViewHolder(parent.inflate(R.layout.tavern_chat_item), user?.id ?: "", false) + override fun getItemViewType(position: Int): Int { + return when (position == super.getItemCount()) { + true -> 1 + false -> 0 + } } - override fun onBindViewHolder(holder: ChatRecyclerViewHolder, position: Int) { - val message = getItem(position) ?: return + override fun getItemCount(): Int { + return super.getItemCount() + 1 + } - holder.bind(message, + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatRecyclerViewHolder { + return if (viewType == 1) ChatRecyclerIntroViewHolder(parent.inflate(R.layout.tavern_chat_intro_item), replyToUser.id!!) + else ChatRecyclerMessageViewHolder(parent.inflate(R.layout.tavern_chat_item), user?.id ?: "", false) + } + + fun getFirstMessage() : ChatMessage + { + var firstMessage = ChatMessage() + return firstMessage + } + override fun onBindViewHolder(holder: ChatRecyclerViewHolder, position: Int) { + val firstMessage : Boolean = getItemViewType(position) == 1 + if (firstMessage) { + val introHolder = holder as ChatRecyclerIntroViewHolder + introHolder.bind(replyToUser) + introHolder.onOpenProfile = { userLabelClickEvents.onNext(it) } + } + else { + val message : ChatMessage = getItem(position) ?: return + val messageHolder = holder as ChatRecyclerMessageViewHolder + messageHolder.bind(message, user?.id ?: "", user, expandedMessageId == message.id) - holder.onShouldExpand = { expandMessage(message.id, position) } - holder.onLikeMessage = { likeMessageEvents.onNext(it) } - holder.onOpenProfile = { userLabelClickEvents.onNext(it) } - holder.onReply = { replyMessageEvents.onNext(it) } - holder.onCopyMessage = { copyMessageEvents.onNext(it) } - holder.onFlagMessage = { flagMessageEvents.onNext(it) } - holder.onDeleteMessage = { deleteMessageEvents.onNext(it) } + messageHolder.onShouldExpand = { expandMessage(message.id, position) } + messageHolder.onLikeMessage = { likeMessageEvents.onNext(it) } + messageHolder.onOpenProfile = { userLabelClickEvents.onNext(it) } + messageHolder.onReply = { replyMessageEvents.onNext(it) } + messageHolder.onCopyMessage = { copyMessageEvents.onNext(it) } + messageHolder.onFlagMessage = { flagMessageEvents.onNext(it) } + messageHolder.onDeleteMessage = { deleteMessageEvents.onNext(it) } + } } fun getLikeMessageFlowable(): Flowable { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt index 165aef00b..d4701b0fd 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt @@ -68,21 +68,25 @@ class InboxMessageListFragment : BaseMainFragment(), androidx.swiperefreshlayout val layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this.getActivity()) recyclerView.layoutManager = layoutManager - chatAdapter = InboxAdapter(user) - viewModel?.messages?.observe(this.viewLifecycleOwner, Observer { chatAdapter?.submitList(it) }) - viewModel?.getMemberData()?.observe(this.viewLifecycleOwner, Observer { - activity?.binding?.toolbarTitle?.text = it?.profile?.name - }) - recyclerView.adapter = chatAdapter - recyclerView.itemAnimator = SafeDefaultItemAnimator() - chatAdapter?.let { adapter -> - compositeSubscription.add(adapter.getUserLabelClickFlowable().subscribe(Consumer { - FullProfileActivity.open(it) - }, RxErrorHandler.handleEmptyError())) - compositeSubscription.add(adapter.getDeleteMessageFlowable().subscribe(Consumer { this.showDeleteConfirmationDialog(it) }, RxErrorHandler.handleEmptyError())) - compositeSubscription.add(adapter.getFlagMessageClickFlowable().subscribe(Consumer { this.showFlagConfirmationDialog(it) }, RxErrorHandler.handleEmptyError())) - compositeSubscription.add(adapter.getCopyMessageFlowable().subscribe(Consumer { this.copyMessageToClipboard(it) }, RxErrorHandler.handleEmptyError())) - } + compositeSubscription.add(apiClient.getMember(replyToUserUUID!!).subscribe( Consumer + { member -> + chatAdapter = InboxAdapter(user, member) + viewModel?.messages?.observe(this.viewLifecycleOwner, Observer { chatAdapter?.submitList(it) }) + + viewModel?.getMemberData()?.observe(this.viewLifecycleOwner, Observer { + activity?.binding?.toolbarTitle?.text = it?.profile?.name + }) + recyclerView.adapter = chatAdapter + recyclerView.itemAnimator = SafeDefaultItemAnimator() + chatAdapter?.let { adapter -> + compositeSubscription.add(adapter.getUserLabelClickFlowable().subscribe(Consumer { + FullProfileActivity.open(it) + }, RxErrorHandler.handleEmptyError())) + compositeSubscription.add(adapter.getDeleteMessageFlowable().subscribe(Consumer { this.showDeleteConfirmationDialog(it) }, RxErrorHandler.handleEmptyError())) + compositeSubscription.add(adapter.getFlagMessageClickFlowable().subscribe(Consumer { this.showFlagConfirmationDialog(it) }, RxErrorHandler.handleEmptyError())) + compositeSubscription.add(adapter.getCopyMessageFlowable().subscribe(Consumer { this.copyMessageToClipboard(it) }, RxErrorHandler.handleEmptyError())) + } + })) chatBarView.sendAction = { sendMessage(it) } chatBarView.maxChatLength = configManager.maxChatLength() diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/ChatRecyclerViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/ChatRecyclerViewHolder.kt index 03e98f73f..7038a6dab 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/ChatRecyclerViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/ChatRecyclerViewHolder.kt @@ -28,8 +28,36 @@ import com.habitrpg.android.habitica.ui.views.social.UsernameLabel import io.reactivex.Maybe import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers +import com.habitrpg.android.habitica.models.members.Member -class ChatRecyclerViewHolder(itemView: View, private var userId: String, private val isTavern: Boolean) : RecyclerView.ViewHolder(itemView) { +open class ChatRecyclerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + +} + +class ChatRecyclerIntroViewHolder(itemView: View, replyToUUID: String) : ChatRecyclerViewHolder(itemView) { + private val avatarView: AvatarView by bindView(R.id.avatar_view) + private val displayNameView: UsernameLabel by bindView(R.id.display_name_textview) + private val sublineTextView: TextView by bindView(R.id.subline_textview) + private val messageText: HabiticaEmojiTextView by bindView(R.id.message_text) + + var onOpenProfile: ((String) -> Unit)? = null + + init { + avatarView.setOnClickListener { onOpenProfile?.invoke(replyToUUID) } + displayNameView.setOnClickListener { onOpenProfile?.invoke(replyToUUID) } + sublineTextView.setOnClickListener { onOpenProfile?.invoke(replyToUUID) } + } + + fun bind(member: Member) { + avatarView.setAvatar(member) + displayNameView.username = member.displayName + displayNameView.tier = member.contributor?.level ?: 0 + sublineTextView.text = "@" + member.username + } +} + + +class ChatRecyclerMessageViewHolder(itemView: View, private var userId: String, private val isTavern: Boolean) : ChatRecyclerViewHolder(itemView) { private val messageWrapper: ViewGroup by bindView(R.id.message_wrapper) private val avatarView: AvatarView by bindView(R.id.avatar_view) From 876e0da557d37a80a87ca073c8f12fe566c8787a Mon Sep 17 00:00:00 2001 From: Eyal Date: Wed, 26 Aug 2020 19:57:23 +0300 Subject: [PATCH 2/2] Fixed the final bug. Now there is a bug of opening a new chat with a new user. Not sure it is because of my changes. --- .../res/layout/tavern_chat_intro_item.xml | 6 ++-- .../ui/adapter/social/InboxAdapter.kt | 36 +++++++++++-------- .../social/InboxMessageListFragment.kt | 7 ++-- .../habitica/ui/viewmodels/InboxViewModel.kt | 30 +++++++++++----- 4 files changed, 48 insertions(+), 31 deletions(-) diff --git a/Habitica/res/layout/tavern_chat_intro_item.xml b/Habitica/res/layout/tavern_chat_intro_item.xml index 10fbf184c..9dd30971e 100644 --- a/Habitica/res/layout/tavern_chat_intro_item.xml +++ b/Habitica/res/layout/tavern_chat_intro_item.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="200dp" android:gravity="center_horizontal" android:orientation="vertical"> @@ -19,7 +19,7 @@ tools:visibility="visible" /> (DIFF_CALLBACK) { - private var expandedMessageId: String? = null + private val FIRST_MESSAGE = 0 + private val NORMAL_MESSAGE = 1 + private var expandedMessageId: String? = null private val likeMessageEvents = PublishSubject.create() private val userLabelClickEvents = PublishSubject.create() private val deleteMessageEvents = PublishSubject.create() @@ -25,29 +29,29 @@ class InboxAdapter(private var user: User?, private var replyToUser : Member) : private val replyMessageEvents = PublishSubject.create() private val copyMessageEvents = PublishSubject.create() - override fun getItemViewType(position: Int): Int { - return when (position == super.getItemCount()) { - true -> 1 - false -> 0 - } + private fun isPositionIntroMessage(position: Int) : Boolean { + return (position == super.getItemCount() - 1) } - override fun getItemCount(): Int { - return super.getItemCount() + 1 + override fun getItemViewType(position: Int): Int { + return if (isPositionIntroMessage(position)) FIRST_MESSAGE else NORMAL_MESSAGE + } + + override fun getItemId(position: Int): Long { + return if (isPositionIntroMessage(position)) -1 else super.getItemId(position) + } + + override fun getItem(position: Int) : ChatMessage? { + return if (isPositionIntroMessage(position)) ChatMessage() else super.getItem(position) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatRecyclerViewHolder { - return if (viewType == 1) ChatRecyclerIntroViewHolder(parent.inflate(R.layout.tavern_chat_intro_item), replyToUser.id!!) + return if (viewType == FIRST_MESSAGE) ChatRecyclerIntroViewHolder(parent.inflate(R.layout.tavern_chat_intro_item), replyToUser.id!!) else ChatRecyclerMessageViewHolder(parent.inflate(R.layout.tavern_chat_item), user?.id ?: "", false) } - fun getFirstMessage() : ChatMessage - { - var firstMessage = ChatMessage() - return firstMessage - } override fun onBindViewHolder(holder: ChatRecyclerViewHolder, position: Int) { - val firstMessage : Boolean = getItemViewType(position) == 1 + val firstMessage : Boolean = getItemViewType(position) == FIRST_MESSAGE if (firstMessage) { val introHolder = holder as ChatRecyclerIntroViewHolder introHolder.bind(replyToUser) @@ -95,6 +99,8 @@ class InboxAdapter(private var user: User?, private var replyToUser : Member) : } private fun expandMessage(id: String, position: Int) { + if (isPositionIntroMessage(position)) + return expandedMessageId = if (expandedMessageId == id) { null } else { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt index d4701b0fd..34b39eeb3 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/InboxMessageListFragment.kt @@ -8,6 +8,8 @@ import android.view.* import androidx.appcompat.app.AlertDialog import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders +import androidx.paging.DataSource +import androidx.paging.PagedList import com.habitrpg.android.habitica.MainNavDirections import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.components.UserComponent @@ -31,6 +33,7 @@ import io.reactivex.functions.Action import io.reactivex.functions.Consumer import kotlinx.android.synthetic.main.fragment_inbox_message_list.* import kotlinx.android.synthetic.main.tavern_chat_new_entry_item.* +import java.lang.Exception import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -67,15 +70,11 @@ class InboxMessageListFragment : BaseMainFragment(), androidx.swiperefreshlayout val layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this.getActivity()) recyclerView.layoutManager = layoutManager - compositeSubscription.add(apiClient.getMember(replyToUserUUID!!).subscribe( Consumer { member -> chatAdapter = InboxAdapter(user, member) viewModel?.messages?.observe(this.viewLifecycleOwner, Observer { chatAdapter?.submitList(it) }) - viewModel?.getMemberData()?.observe(this.viewLifecycleOwner, Observer { - activity?.binding?.toolbarTitle?.text = it?.profile?.name - }) recyclerView.adapter = chatAdapter recyclerView.itemAnimator = SafeDefaultItemAnimator() chatAdapter?.let { adapter -> diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/InboxViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/InboxViewModel.kt index bb1f81a9b..50aa1a3e3 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/InboxViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/InboxViewModel.kt @@ -37,7 +37,7 @@ class InboxViewModel(recipientID: String?, recipientUsername: String?) : BaseVie .setEnablePlaceholders(false) .build() - private val dataSourceFactory = MessagesDataSourceFactory(socialRepository, recipientID) + private val dataSourceFactory = MessagesDataSourceFactory(socialRepository, recipientID, ChatMessage()) val messages: LiveData> = dataSourceFactory.toLiveData(config) private val member: MutableLiveData by lazy { MutableLiveData() @@ -86,7 +86,7 @@ class InboxViewModel(recipientID: String?, recipientUsername: String?) : BaseVie } } -private class MessagesDataSource(val socialRepository: SocialRepository, var recipientID: String?): +private class MessagesDataSource(val socialRepository: SocialRepository, var recipientID: String?, var footer : ChatMessage?): PositionalDataSource() { private var lastFetchWasEnd = false override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { @@ -99,8 +99,15 @@ private class MessagesDataSource(val socialRepository: SocialRepository, var rec val page = ceil(params.startPosition.toFloat() / params.loadSize.toFloat()).toInt() socialRepository.retrieveInboxMessages(recipientID ?: "", page) .subscribe(Consumer { - if (it.size != 10) lastFetchWasEnd = true - callback.onResult(it) + if (it.size < 10) { + lastFetchWasEnd = true + if (footer != null) + callback.onResult(it.plusElement(footer!!)) + else + callback.onResult(it) + } + else + callback.onResult(it) }, RxErrorHandler.handleEmptyError()) } } @@ -116,23 +123,28 @@ private class MessagesDataSource(val socialRepository: SocialRepository, var rec if (recipientID?.isNotBlank() != true) { return@flatMapPublisher Flowable.just(it) } socialRepository.retrieveInboxMessages(recipientID ?: "", 0) .doOnNext { - messages -> if (messages.size != 10) lastFetchWasEnd = true + messages -> if (messages.size < 10) { + lastFetchWasEnd = true + } } } else { Flowable.just(it) } } .subscribe(Consumer { - callback.onResult(it, 0) + if (it.size < 10 && footer != null) + callback.onResult(it.plusElement(footer!!), 0) + else + callback.onResult(it, 0) }, RxErrorHandler.handleEmptyError()) } } } -private class MessagesDataSourceFactory(val socialRepository: SocialRepository, var recipientID: String?) : +private class MessagesDataSourceFactory(val socialRepository: SocialRepository, var recipientID: String?, val footer : ChatMessage?) : DataSource.Factory() { val sourceLiveData = MutableLiveData() - var latestSource: MessagesDataSource = MessagesDataSource(socialRepository, recipientID) + var latestSource: MessagesDataSource = MessagesDataSource(socialRepository, recipientID, footer) fun updateRecipientID(newID: String?) { recipientID = newID @@ -140,7 +152,7 @@ private class MessagesDataSourceFactory(val socialRepository: SocialRepository, } override fun create(): DataSource { - latestSource = MessagesDataSource(socialRepository, recipientID) + latestSource = MessagesDataSource(socialRepository, recipientID, footer) sourceLiveData.postValue(latestSource) return latestSource }