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..9dd30971e --- /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 a0b641d0d..7e4260e65 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 @@ -8,7 +8,7 @@ import com.habitrpg.android.habitica.R 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.ChatRecyclerMessageViewHolder import io.reactivex.BackpressureStrategy import io.reactivex.Flowable import io.reactivex.subjects.PublishSubject @@ -39,7 +39,7 @@ class ChatRecyclerViewAdapter(data: OrderedRealmCollection?, autoUp return if (viewType == 0) { SystemChatMessageViewHolder(parent.inflate(R.layout.system_chat_message)) } else { - ChatRecyclerViewHolder(parent.inflate(R.layout.chat_item), uuid, isTavern) + ChatRecyclerMessageViewHolder(parent.inflate(R.layout.chat_item), uuid, isTavern) } } @@ -48,7 +48,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 fe5f23111..efad2cf84 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 @@ -1,6 +1,8 @@ package com.habitrpg.android.habitica.ui.adapter.social import android.view.ViewGroup +import androidx.paging.DataSource +import androidx.paging.PagedList import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import com.habitrpg.android.habitica.R @@ -8,13 +10,18 @@ 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?, private var replyToUser : Member) : PagedListAdapter(DIFF_CALLBACK) { + private val FIRST_MESSAGE = 0 + private val NORMAL_MESSAGE = 1 -class InboxAdapter(private var user: User?) : PagedListAdapter(DIFF_CALLBACK) { private var expandedMessageId: String? = null - private val likeMessageEvents = PublishSubject.create() private val userLabelClickEvents = PublishSubject.create() private val deleteMessageEvents = PublishSubject.create() @@ -22,24 +29,49 @@ class InboxAdapter(private var user: User?) : PagedListAdapter() private val copyMessageEvents = PublishSubject.create() + private fun isPositionIntroMessage(position: Int) : Boolean { + return (position == 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 ChatRecyclerViewHolder(parent.inflate(R.layout.chat_item), user?.id ?: "", false) + return if (viewType == FIRST_MESSAGE) ChatRecyclerIntroViewHolder(parent.inflate(R.layout.tavern_chat_intro_item), replyToUser.id!!) + else ChatRecyclerMessageViewHolder(parent.inflate(R.layout.chat_item), user?.id ?: "", false) } override fun onBindViewHolder(holder: ChatRecyclerViewHolder, position: Int) { - val message = getItem(position) ?: return - - holder.bind(message, + val firstMessage : Boolean = getItemViewType(position) == FIRST_MESSAGE + 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 getUserLabelClickFlowable(): Flowable { @@ -59,6 +91,8 @@ class InboxAdapter(private var user: User?) : PagedListAdapter + chatAdapter = InboxAdapter(user, member) + viewModel?.messages?.observe(this.viewLifecycleOwner, Observer { chatAdapter?.submitList(it) }) - chatAdapter = InboxAdapter(user) - viewModel?.messages?.observe(this.viewLifecycleOwner, { chatAdapter?.submitList(it) }) - viewModel?.getMemberData()?.observe(this.viewLifecycleOwner, { - activity?.binding?.toolbarTitle?.text = it?.profile?.name - }) binding?.recyclerView?.adapter = chatAdapter binding?.recyclerView?.itemAnimator = SafeDefaultItemAnimator() - chatAdapter?.let { adapter -> - compositeSubscription.add(adapter.getUserLabelClickFlowable().subscribe({ - FullProfileActivity.open(it) - }, RxErrorHandler.handleEmptyError())) - compositeSubscription.add(adapter.getDeleteMessageFlowable().subscribe({ this.showDeleteConfirmationDialog(it) }, RxErrorHandler.handleEmptyError())) - compositeSubscription.add(adapter.getFlagMessageClickFlowable().subscribe({ this.showFlagConfirmationDialog(it) }, RxErrorHandler.handleEmptyError())) - compositeSubscription.add(adapter.getCopyMessageFlowable().subscribe({ this.copyMessageToClipboard(it) }, RxErrorHandler.handleEmptyError())) - } + 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())) + } + })) binding?.chatBarView?.sendAction = { sendMessage(it) } binding?.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 545c1cda0..d23ae1352 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 @@ -21,8 +21,31 @@ import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper 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 binding = TavernChatIntroItemBinding.bind(itemView) + + var onOpenProfile: ((String) -> Unit)? = null + + init { + binding.avatarView.setOnClickListener { onOpenProfile?.invoke(replyToUUID) } + binding.displayNameTextview.setOnClickListener { onOpenProfile?.invoke(replyToUUID) } + binding.sublineTextview.setOnClickListener { onOpenProfile?.invoke(replyToUUID) } + } + + fun bind(member: Member) { + binding.avatarView.setAvatar(member) + binding.displayNameTextview.username = member.displayName + binding.displayNameTextview.tier = member.contributor?.level ?: 0 + binding.sublineTextview.text = "@" + member.username + } +} + + +class ChatRecyclerMessageViewHolder(itemView: View, private var userId: String, private val isTavern: Boolean) : ChatRecyclerViewHolder(itemView) { val binding = ChatItemBinding.bind(itemView) val context: Context = itemView.context 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 c3c48c6ab..310537ceb 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 @@ -36,7 +36,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() @@ -85,7 +85,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) { @@ -97,9 +97,16 @@ private class MessagesDataSource(val socialRepository: SocialRepository, var rec if (recipientID?.isNotBlank() != true) { return@launch } val page = ceil(params.startPosition.toFloat() / params.loadSize.toFloat()).toInt() socialRepository.retrieveInboxMessages(recipientID ?: "", page) - .subscribe({ - if (it.size != 10) lastFetchWasEnd = true - callback.onResult(it) + .subscribe(Consumer { + if (it.size < 10) { + lastFetchWasEnd = true + if (footer != null) + callback.onResult(it.plusElement(footer!!)) + else + callback.onResult(it) + } + else + callback.onResult(it) }, RxErrorHandler.handleEmptyError()) } } @@ -115,23 +122,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({ - callback.onResult(it, 0) + .subscribe(Consumer { + 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 @@ -139,7 +151,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 }