Made chat message bottom sheet content dynamic

This commit is contained in:
Sylvain Berfini 2023-11-15 11:15:51 +01:00
parent 6fec1958b8
commit 6ae57158b7
5 changed files with 191 additions and 107 deletions

View file

@ -45,6 +45,7 @@ import androidx.navigation.NavDeepLinkBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
@ -158,16 +159,19 @@ class NotificationsManager @MainThread constructor(private val context: Context)
if (ShortcutUtils.isShortcutToChatRoomAlreadyCreated(context, chatRoom)) {
Log.i("$TAG Chat room shortcut already exists")
showChatRoomNotification(chatRoom, messages)
} else {
Log.i(
"$TAG Ensure chat room shortcut exists for bubble notification"
"$TAG Ensure chat room shortcut exists for 'conversation' notification"
)
scope.launch {
ShortcutUtils.createShortcutsToChatRooms(context)
val shortcuts = async {
ShortcutUtils.createShortcutsToChatRooms(context)
}
shortcuts.await()
showChatRoomNotification(chatRoom, messages)
}
}
showChatRoomNotification(chatRoom, messages)
}
@WorkerThread

View file

@ -46,14 +46,14 @@ class ChatMessageBottomSheetAdapter : ListAdapter<ChatMessageBottomSheetParticip
oldItem: ChatMessageBottomSheetParticipantModel,
newItem: ChatMessageBottomSheetParticipantModel
): Boolean {
return oldItem.sipUri == newItem.sipUri
return oldItem.sipUri == newItem.sipUri && oldItem.value == newItem.value
}
override fun areContentsTheSame(
oldItem: ChatMessageBottomSheetParticipantModel,
newItem: ChatMessageBottomSheetParticipantModel
): Boolean {
return oldItem.value == newItem.value
return true
}
}
}

View file

@ -41,7 +41,6 @@ import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.UiThread
import androidx.core.view.doOnPreDraw
import androidx.core.widget.addTextChangedListener
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -154,6 +153,10 @@ class ConversationFragment : GenericFragment() {
}
}
private var bottomSheetDeliveryModel: ChatMessageDeliveryModel? = null
private var bottomSheetReactionsModel: ChatMessageReactionsModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -454,6 +457,11 @@ class ConversationFragment : GenericFragment() {
}
override fun onPause() {
coreContext.postOnCoreThread {
bottomSheetReactionsModel?.destroy()
bottomSheetDeliveryModel?.destroy()
}
if (viewModel.isGroup.value == true) {
binding.sendArea.messageToSend.removeTextChangedListener(textObserver)
}
@ -547,11 +555,11 @@ class ConversationFragment : GenericFragment() {
showDelivery: Boolean = false,
showReactions: Boolean = false
) {
val deliveryBottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root)
deliveryBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
val bottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
binding.messageBottomSheet.setHandleClickedListener {
deliveryBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
if (binding.messageBottomSheet.bottomSheetList.adapter != bottomSheetAdapter) {
binding.messageBottomSheet.bottomSheetList.adapter = bottomSheetAdapter
@ -569,7 +577,7 @@ class ConversationFragment : GenericFragment() {
prepareBottomSheetForReactions(chatMessageModel)
}
deliveryBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
}
@ -578,107 +586,132 @@ class ConversationFragment : GenericFragment() {
@UiThread
private fun prepareBottomSheetForDeliveryStatus(chatMessageModel: ChatMessageModel) {
coreContext.postOnCoreThread {
val model = ChatMessageDeliveryModel(chatMessageModel.chatMessage)
bottomSheetDeliveryModel?.destroy()
coreContext.postOnMainThread {
val tabs = binding.messageBottomSheet.tabs
tabs.removeAllTabs()
tabs.addTab(
tabs.newTab().setText(model.readLabel.value).setId(
ChatMessage.State.Displayed.toInt()
)
)
tabs.addTab(
tabs.newTab().setText(
model.receivedLabel.value
).setId(
ChatMessage.State.DeliveredToUser.toInt()
)
)
tabs.addTab(
tabs.newTab().setText(model.sentLabel.value).setId(
ChatMessage.State.Delivered.toInt()
)
)
tabs.addTab(
tabs.newTab().setText(
model.errorLabel.value
).setId(
ChatMessage.State.NotDelivered.toInt()
)
)
tabs.setOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val state = tab?.id ?: ChatMessage.State.Displayed.toInt()
bottomSheetAdapter.submitList(
model.computeListForState(ChatMessage.State.fromInt(state))
)
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
val initialList = model.displayedModels
bottomSheetAdapter.submitList(initialList)
Log.i("$TAG Submitted [${initialList.size}] items for default delivery status list")
val model = ChatMessageDeliveryModel(chatMessageModel.chatMessage) { deliveryModel ->
coreContext.postOnMainThread {
displayDeliveryStatuses(deliveryModel)
}
}
bottomSheetDeliveryModel = model
}
}
@UiThread
private fun prepareBottomSheetForReactions(chatMessageModel: ChatMessageModel) {
coreContext.postOnCoreThread {
val model = ChatMessageReactionsModel(chatMessageModel.chatMessage)
val totalCount = model.allReactions.size
val label = getString(R.string.message_reactions_info_all_title, totalCount.toString())
bottomSheetReactionsModel?.destroy()
coreContext.postOnMainThread {
val tabs = binding.messageBottomSheet.tabs
tabs.removeAllTabs()
tabs.addTab(
tabs.newTab().setText(label).setId(0).setTag("")
)
var index = 1
for (reaction in model.differentReactions.value.orEmpty()) {
val count = model.reactionsMap[reaction]
val tabLabel = getString(
R.string.message_reactions_info_emoji_title,
reaction,
count.toString()
)
tabs.addTab(
tabs.newTab().setText(tabLabel).setId(index).setTag(reaction)
)
index += 1
val model = ChatMessageReactionsModel(chatMessageModel.chatMessage) { reactionsModel ->
coreContext.postOnMainThread {
if (reactionsModel.allReactions.isEmpty()) {
Log.i("$TAG No reaction to display, closing bottom sheet")
val bottomSheetBehavior = BottomSheetBehavior.from(
binding.messageBottomSheet.root
)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
} else {
displayReactions(reactionsModel)
}
}
tabs.setOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val filter = tab?.tag.toString()
if (filter.isEmpty()) {
bottomSheetAdapter.submitList(model.allReactions)
} else {
bottomSheetAdapter.submitList(model.filterReactions(filter))
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
val initialList = model.allReactions
bottomSheetAdapter.submitList(initialList)
Log.i("$TAG Submitted [${initialList.size}] items for default reactions list")
}
bottomSheetReactionsModel = model
}
}
@UiThread
private fun displayDeliveryStatuses(model: ChatMessageDeliveryModel) {
val tabs = binding.messageBottomSheet.tabs
tabs.removeAllTabs()
tabs.addTab(
tabs.newTab().setText(model.readLabel.value).setId(
ChatMessage.State.Displayed.toInt()
)
)
tabs.addTab(
tabs.newTab().setText(
model.receivedLabel.value
).setId(
ChatMessage.State.DeliveredToUser.toInt()
)
)
tabs.addTab(
tabs.newTab().setText(model.sentLabel.value).setId(
ChatMessage.State.Delivered.toInt()
)
)
tabs.addTab(
tabs.newTab().setText(
model.errorLabel.value
).setId(
ChatMessage.State.NotDelivered.toInt()
)
)
tabs.setOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val state = tab?.id ?: ChatMessage.State.Displayed.toInt()
bottomSheetAdapter.submitList(
model.computeListForState(ChatMessage.State.fromInt(state))
)
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
val initialList = model.displayedModels
bottomSheetAdapter.submitList(initialList)
Log.i("$TAG Submitted [${initialList.size}] items for default delivery status list")
}
@UiThread
private fun displayReactions(model: ChatMessageReactionsModel) {
val totalCount = model.allReactions.size
val label = getString(R.string.message_reactions_info_all_title, totalCount.toString())
val tabs = binding.messageBottomSheet.tabs
tabs.removeAllTabs()
tabs.addTab(
tabs.newTab().setText(label).setId(0).setTag("")
)
var index = 1
for (reaction in model.differentReactions.value.orEmpty()) {
val count = model.reactionsMap[reaction]
val tabLabel = getString(
R.string.message_reactions_info_emoji_title,
reaction,
count.toString()
)
tabs.addTab(
tabs.newTab().setText(tabLabel).setId(index).setTag(reaction)
)
index += 1
}
tabs.setOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val filter = tab?.tag.toString()
if (filter.isEmpty()) {
bottomSheetAdapter.submitList(model.allReactions)
} else {
bottomSheetAdapter.submitList(model.filterReactions(filter))
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
val initialList = model.allReactions
bottomSheetAdapter.submitList(initialList)
Log.i("$TAG Submitted [${initialList.size}] items for default reactions list")
}
}

View file

@ -6,12 +6,15 @@ import androidx.lifecycle.MutableLiveData
import org.linphone.R
import org.linphone.core.ChatMessage
import org.linphone.core.ChatMessage.State
import org.linphone.core.ChatMessageListenerStub
import org.linphone.core.ParticipantImdnState
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.TimestampUtils
class ChatMessageDeliveryModel @WorkerThread constructor(
private val chatMessage: ChatMessage
private val chatMessage: ChatMessage,
private val onDeliveryUpdated: ((model: ChatMessageDeliveryModel) -> Unit)? = null
) {
companion object {
private const val TAG = "[Chat Message Delivery Model]"
@ -33,10 +36,24 @@ class ChatMessageDeliveryModel @WorkerThread constructor(
private val errorModels = arrayListOf<ChatMessageBottomSheetParticipantModel>()
init {
computeDeliveryStatus()
private val chatMessageListener = object : ChatMessageListenerStub() {
@WorkerThread
override fun onParticipantImdnStateChanged(
message: ChatMessage,
state: ParticipantImdnState
) {
computeDeliveryStatus()
}
}
// TODO: add listener to update in real time the lists
init {
chatMessage.addListener(chatMessageListener)
computeDeliveryStatus()
}
@WorkerThread
fun destroy() {
chatMessage.removeListener(chatMessageListener)
}
@UiThread
@ -59,6 +76,11 @@ class ChatMessageDeliveryModel @WorkerThread constructor(
@WorkerThread
private fun computeDeliveryStatus() {
displayedModels.clear()
deliveredModels.clear()
sentModels.clear()
errorModels.clear()
for (participant in chatMessage.getParticipantsByImdnState(State.Displayed)) {
displayedModels.add(
ChatMessageBottomSheetParticipantModel(
@ -136,5 +158,6 @@ class ChatMessageDeliveryModel @WorkerThread constructor(
Log.i(
"$TAG There are [$readCount] that have read this message, [$receivedCount] that have received it, [$sentCount] that haven't received it yet and [$errorCount] that probably won't receive it due to an error"
)
onDeliveryUpdated?.invoke(this)
}
}

View file

@ -3,11 +3,15 @@ package org.linphone.ui.main.chat.model
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Address
import org.linphone.core.ChatMessage
import org.linphone.core.ChatMessageListenerStub
import org.linphone.core.ChatMessageReaction
import org.linphone.core.tools.Log
class ChatMessageReactionsModel @WorkerThread constructor(
private val chatMessage: ChatMessage
private val chatMessage: ChatMessage,
private val onReactionsUpdated: ((model: ChatMessageReactionsModel) -> Unit)? = null
) {
companion object {
private const val TAG = "[Chat Message Reactions Model]"
@ -19,9 +23,28 @@ class ChatMessageReactionsModel @WorkerThread constructor(
val reactionsMap = HashMap<String, Int>()
private val chatMessageListener = object : ChatMessageListenerStub() {
@WorkerThread
override fun onReactionRemoved(message: ChatMessage, address: Address) {
Log.i("$TAG Reaction has been removed, updating reactions list")
computeReactions()
}
@WorkerThread
override fun onNewMessageReaction(message: ChatMessage, reaction: ChatMessageReaction) {
Log.i("$TAG A new reaction has been received, updating reactions list")
computeReactions()
}
}
init {
chatMessage.addListener(chatMessageListener)
computeReactions()
// TODO: add listener to update in real time the lists
}
@WorkerThread
fun destroy() {
chatMessage.removeListener(chatMessageListener)
}
fun filterReactions(emoji: String): ArrayList<ChatMessageBottomSheetParticipantModel> {
@ -75,5 +98,6 @@ class ChatMessageReactionsModel @WorkerThread constructor(
"$TAG [${differentReactionsList.size}] reactions found on a total of [${allReactions.size}]"
)
differentReactions.postValue(differentReactionsList)
onReactionsUpdated?.invoke(this)
}
}