From 41ea5e4cc7ecb1c6e7e90ba08106675cb3cddee9 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 26 Jun 2023 16:26:17 +0200 Subject: [PATCH] Moved files around, started bubbles --- .../ui/conversations/ConversationFragment.kt | 34 +++- .../ui/conversations/ConversationsFragment.kt | 2 + .../conversations/NewConversationFragment.kt | 1 + .../adapter/ChatEventLogsListAdapter.kt | 152 ++++++++++++++++++ .../{ => adapter}/ConversationsListAdapter.kt | 23 +-- .../ui/conversations/data/ChatMessageData.kt | 89 ++++++++++ .../conversations/{ => data}/ChatRoomData.kt | 2 +- .../ui/conversations/data/EventData.kt | 32 ++++ .../ui/conversations/data/EventLogData.kt | 50 ++++++ .../view/MultiLineWrapContentWidthTextView.kt | 77 +++++++++ .../{ => viewmodel}/ConversationViewModel.kt | 88 +++++++++- .../ConversationsListViewModel.kt | 3 +- .../NewConversationViewModel.kt | 2 +- .../org/linphone/utils/DataBindingUtils.kt | 12 +- ...ape_received_message_bubble_background.xml | 5 + .../shape_sent_message_bubble_background.xml | 5 + .../main/res/layout/chat_bubble_incoming.xml | 77 +++++++++ .../main/res/layout/chat_bubble_outgoing.xml | 77 +++++++++ app/src/main/res/layout/chat_event.xml | 53 ++++++ .../main/res/layout/chat_room_list_cell.xml | 2 +- .../main/res/layout/conversation_fragment.xml | 23 ++- .../res/layout/conversations_fragment.xml | 6 +- .../res/layout/new_conversation_fragment.xml | 2 +- app/src/main/res/values/colors.xml | 2 + 24 files changed, 782 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/conversations/adapter/ChatEventLogsListAdapter.kt rename app/src/main/java/org/linphone/ui/conversations/{ => adapter}/ConversationsListAdapter.kt (77%) create mode 100644 app/src/main/java/org/linphone/ui/conversations/data/ChatMessageData.kt rename app/src/main/java/org/linphone/ui/conversations/{ => data}/ChatRoomData.kt (99%) create mode 100644 app/src/main/java/org/linphone/ui/conversations/data/EventData.kt create mode 100644 app/src/main/java/org/linphone/ui/conversations/data/EventLogData.kt create mode 100644 app/src/main/java/org/linphone/ui/conversations/view/MultiLineWrapContentWidthTextView.kt rename app/src/main/java/org/linphone/ui/conversations/{ => viewmodel}/ConversationViewModel.kt (54%) rename app/src/main/java/org/linphone/ui/conversations/{ => viewmodel}/ConversationsListViewModel.kt (98%) rename app/src/main/java/org/linphone/ui/conversations/{ => viewmodel}/NewConversationViewModel.kt (98%) create mode 100644 app/src/main/res/drawable/shape_received_message_bubble_background.xml create mode 100644 app/src/main/res/drawable/shape_sent_message_bubble_background.xml create mode 100644 app/src/main/res/layout/chat_bubble_incoming.xml create mode 100644 app/src/main/res/layout/chat_bubble_outgoing.xml create mode 100644 app/src/main/res/layout/chat_event.xml diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationFragment.kt index bd2e41ff9..62d069231 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationFragment.kt @@ -23,16 +23,26 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.doOnPreDraw import androidx.fragment.app.Fragment import androidx.navigation.navGraphViewModels +import androidx.recyclerview.widget.LinearLayoutManager import org.linphone.R import org.linphone.databinding.ConversationFragmentBinding +import org.linphone.ui.conversations.adapter.ChatEventLogsListAdapter +import org.linphone.ui.conversations.viewmodel.ConversationViewModel class ConversationFragment : Fragment() { private lateinit var binding: ConversationFragmentBinding private val viewModel: ConversationViewModel by navGraphViewModels( R.id.conversationFragment ) + private lateinit var adapter: ChatEventLogsListAdapter + + override fun onDestroyView() { + binding.messagesList.adapter = null + super.onDestroyView() + } override fun onCreateView( inflater: LayoutInflater, @@ -57,11 +67,33 @@ class ConversationFragment : Fragment() { viewModel.loadChatRoom(localSipUri, remoteSipUri) } else { // Chat room not found, going back - // TODO FIXME : show error + (view.parent as? ViewGroup)?.doOnPreDraw { + requireActivity().onBackPressedDispatcher.onBackPressed() + } } arguments?.clear() + postponeEnterTransition() + + adapter = ChatEventLogsListAdapter(viewLifecycleOwner) + binding.messagesList.setHasFixedSize(false) + binding.messagesList.adapter = adapter + + val layoutManager = LinearLayoutManager(requireContext()) + binding.messagesList.layoutManager = layoutManager + + viewModel.events.observe( + viewLifecycleOwner + ) { + adapter.submitList(it) + + (view.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() + binding.messagesList.scrollToPosition(adapter.itemCount - 1) + } + } + binding.setBackClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt b/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt index e2275a408..380dd10e7 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt +++ b/app/src/main/java/org/linphone/ui/conversations/ConversationsFragment.kt @@ -35,6 +35,8 @@ import androidx.recyclerview.widget.RecyclerView import org.linphone.R import org.linphone.databinding.ConversationsFragmentBinding import org.linphone.ui.MainActivity +import org.linphone.ui.conversations.adapter.ConversationsListAdapter +import org.linphone.ui.conversations.viewmodel.ConversationsListViewModel class ConversationsFragment : Fragment() { private lateinit var binding: ConversationsFragmentBinding diff --git a/app/src/main/java/org/linphone/ui/conversations/NewConversationFragment.kt b/app/src/main/java/org/linphone/ui/conversations/NewConversationFragment.kt index a900e3cba..b7f0e5aaa 100644 --- a/app/src/main/java/org/linphone/ui/conversations/NewConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/conversations/NewConversationFragment.kt @@ -34,6 +34,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.contacts.ContactsSelectionAdapter import org.linphone.databinding.NewConversationFragmentBinding +import org.linphone.ui.conversations.viewmodel.NewConversationViewModel class NewConversationFragment : Fragment() { private lateinit var binding: NewConversationFragmentBinding diff --git a/app/src/main/java/org/linphone/ui/conversations/adapter/ChatEventLogsListAdapter.kt b/app/src/main/java/org/linphone/ui/conversations/adapter/ChatEventLogsListAdapter.kt new file mode 100644 index 000000000..7af5e2b0b --- /dev/null +++ b/app/src/main/java/org/linphone/ui/conversations/adapter/ChatEventLogsListAdapter.kt @@ -0,0 +1,152 @@ +package org.linphone.ui.conversations.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.linphone.R +import org.linphone.core.ChatMessage +import org.linphone.databinding.ChatBubbleIncomingBinding +import org.linphone.databinding.ChatBubbleOutgoingBinding +import org.linphone.databinding.ChatEventBinding +import org.linphone.ui.conversations.data.ChatMessageData +import org.linphone.ui.conversations.data.EventData +import org.linphone.ui.conversations.data.EventLogData + +class ChatEventLogsListAdapter( + private val viewLifecycleOwner: LifecycleOwner +) : ListAdapter(EventLogDiffCallback()) { + companion object { + const val INCOMING_CHAT_MESSAGE = 1 + const val OUTGOING_CHAT_MESSAGE = 2 + const val EVENT = 3 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + INCOMING_CHAT_MESSAGE -> createIncomingChatBubble(parent) + OUTGOING_CHAT_MESSAGE -> createOutgoingChatBubble(parent) + else -> createEvent(parent) + } + } + + override fun getItemViewType(position: Int): Int { + val data = getItem(position) + if (data.data is ChatMessageData) { + if (data.data.isOutgoing) { + return OUTGOING_CHAT_MESSAGE + } + return INCOMING_CHAT_MESSAGE + } + return EVENT + } + + private fun createIncomingChatBubble(parent: ViewGroup): IncomingBubbleViewHolder { + val binding: ChatBubbleIncomingBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.chat_bubble_incoming, + parent, + false + ) + return IncomingBubbleViewHolder(binding) + } + + private fun createOutgoingChatBubble(parent: ViewGroup): OutgoingBubbleViewHolder { + val binding: ChatBubbleOutgoingBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.chat_bubble_outgoing, + parent, + false + ) + return OutgoingBubbleViewHolder(binding) + } + + private fun createEvent(parent: ViewGroup): EventViewHolder { + val binding: ChatEventBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.chat_event, + parent, + false + ) + return EventViewHolder(binding) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val eventLog = getItem(position) + when (holder) { + is IncomingBubbleViewHolder -> holder.bind(eventLog.data as ChatMessageData) + is OutgoingBubbleViewHolder -> holder.bind(eventLog.data as ChatMessageData) + is EventViewHolder -> holder.bind(eventLog.data as EventData) + } + } + + inner class IncomingBubbleViewHolder( + val binding: ChatBubbleIncomingBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(chatMessageData: ChatMessageData) { + with(binding) { + data = chatMessageData + + lifecycleOwner = viewLifecycleOwner + executePendingBindings() + + // To ensure the measure is right since we do some computation for proper multi-line wrap_content + text.forceLayout() + } + } + } + + inner class OutgoingBubbleViewHolder( + val binding: ChatBubbleOutgoingBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(chatMessageData: ChatMessageData) { + with(binding) { + data = chatMessageData + + lifecycleOwner = viewLifecycleOwner + executePendingBindings() + + // To ensure the measure is right since we do some computation for proper multi-line wrap_content + text.forceLayout() + } + } + } + inner class EventViewHolder( + val binding: ChatEventBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(eventData: EventData) { + with(binding) { + data = eventData + + lifecycleOwner = viewLifecycleOwner + executePendingBindings() + } + } + } +} + +private class EventLogDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: EventLogData, newItem: EventLogData): Boolean { + return if (oldItem.isEvent && newItem.isEvent) { + oldItem.notifyId == newItem.notifyId + } else if (!oldItem.isEvent && !newItem.isEvent) { + val oldData = (oldItem.data as ChatMessageData) + val newData = (newItem.data as ChatMessageData) + oldData.id.isNotEmpty() && oldData.id == newData.id + } else { + false + } + } + + override fun areContentsTheSame(oldItem: EventLogData, newItem: EventLogData): Boolean { + return if (oldItem.isEvent && newItem.isEvent) { + true + } else { + val newData = (newItem.data as ChatMessageData) + newData.state.value == ChatMessage.State.Displayed + } + } +} diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt b/app/src/main/java/org/linphone/ui/conversations/adapter/ConversationsListAdapter.kt similarity index 77% rename from app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt rename to app/src/main/java/org/linphone/ui/conversations/adapter/ConversationsListAdapter.kt index 1c3dd5d9e..6e3e0441b 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/conversations/adapter/ConversationsListAdapter.kt @@ -1,23 +1,4 @@ -/* - * Copyright (c) 2010-2023 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.ui.conversations +package org.linphone.ui.conversations.adapter import android.view.LayoutInflater import android.view.ViewGroup @@ -29,6 +10,8 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.linphone.R import org.linphone.databinding.ChatRoomListCellBinding +import org.linphone.ui.conversations.data.ChatRoomData +import org.linphone.ui.conversations.data.ChatRoomDataListener import org.linphone.utils.Event class ConversationsListAdapter( diff --git a/app/src/main/java/org/linphone/ui/conversations/data/ChatMessageData.kt b/app/src/main/java/org/linphone/ui/conversations/data/ChatMessageData.kt new file mode 100644 index 000000000..37d046108 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/conversations/data/ChatMessageData.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.conversations.data + +import androidx.lifecycle.MutableLiveData +import org.linphone.R +import org.linphone.contacts.ContactData +import org.linphone.core.ChatMessage +import org.linphone.core.ChatMessageListenerStub +import org.linphone.utils.TimestampUtils + +class ChatMessageData(private val chatMessage: ChatMessage) { + val id = chatMessage.messageId + + val isOutgoing = chatMessage.isOutgoing + + val contactData = MutableLiveData() + + val state = MutableLiveData() + + val text = MutableLiveData() + + val time = MutableLiveData() + + val imdnIcon = MutableLiveData() + + private val chatMessageListener = object : ChatMessageListenerStub() { + override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) { + this@ChatMessageData.state.postValue(state) + computeImdnIcon() + } + } + + init { + state.postValue(chatMessage.state) + chatMessage.addListener(chatMessageListener) + + computeImdnIcon() + time.postValue(TimestampUtils.toString(chatMessage.time)) + for (content in chatMessage.contents) { + if (content.isText) { + text.postValue(content.utf8Text) + } + // TODO FIXME + } + contactLookup() + } + + fun destroy() { + chatMessage.removeListener(chatMessageListener) + } + + fun contactLookup() { + val remoteAddress = chatMessage.fromAddress + val friend = chatMessage.chatRoom.core.findFriend(remoteAddress) + if (friend != null) { + contactData.postValue(ContactData(friend)) + } + } + + private fun computeImdnIcon() { + imdnIcon.postValue( + when (chatMessage.state) { + ChatMessage.State.DeliveredToUser -> R.drawable.imdn_delivered + ChatMessage.State.Displayed -> R.drawable.imdn_read + ChatMessage.State.InProgress -> R.drawable.imdn_sent + // TODO FIXME + else -> R.drawable.imdn_sent + } + ) + } +} diff --git a/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt b/app/src/main/java/org/linphone/ui/conversations/data/ChatRoomData.kt similarity index 99% rename from app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt rename to app/src/main/java/org/linphone/ui/conversations/data/ChatRoomData.kt index 28306a868..0d7548206 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ChatRoomData.kt +++ b/app/src/main/java/org/linphone/ui/conversations/data/ChatRoomData.kt @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.ui.conversations +package org.linphone.ui.conversations.data import androidx.lifecycle.MutableLiveData import java.lang.StringBuilder diff --git a/app/src/main/java/org/linphone/ui/conversations/data/EventData.kt b/app/src/main/java/org/linphone/ui/conversations/data/EventData.kt new file mode 100644 index 000000000..e7a9ff641 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/conversations/data/EventData.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.conversations.data + +import org.linphone.core.EventLog + +class EventData(val eventLog: EventLog) { + fun destroy() { + // TODO + } + + fun contactLookup() { + // TODO + } +} diff --git a/app/src/main/java/org/linphone/ui/conversations/data/EventLogData.kt b/app/src/main/java/org/linphone/ui/conversations/data/EventLogData.kt new file mode 100644 index 000000000..736f9549c --- /dev/null +++ b/app/src/main/java/org/linphone/ui/conversations/data/EventLogData.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.conversations.data + +import org.linphone.core.EventLog + +class EventLogData(val eventLog: EventLog) { + val type: EventLog.Type = eventLog.type + + val isEvent = type != EventLog.Type.ConferenceChatMessage + + val data = if (isEvent) { + EventData(eventLog) + } else { + ChatMessageData(eventLog.chatMessage!!) + } + + val notifyId = eventLog.notifyId + + fun destroy() { + when (data) { + is EventData -> data.destroy() + is ChatMessageData -> data.destroy() + } + } + + fun contactLookup() { + when (data) { + is EventData -> data.contactLookup() + is ChatMessageData -> data.contactLookup() + } + } +} diff --git a/app/src/main/java/org/linphone/ui/conversations/view/MultiLineWrapContentWidthTextView.kt b/app/src/main/java/org/linphone/ui/conversations/view/MultiLineWrapContentWidthTextView.kt new file mode 100644 index 000000000..de5bcb632 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/conversations/view/MultiLineWrapContentWidthTextView.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.conversations.view + +import android.content.Context +import android.text.Layout +import android.text.method.LinkMovementMethod +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.round + +/** + * The purpose of this class is to have a TextView declared with wrap_content as width that won't + * fill it's parent if it is multi line. + */ +class MultiLineWrapContentWidthTextView : AppCompatTextView { + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int + ) : super(context, attrs, defStyleAttr) + + override fun setText(text: CharSequence?, type: BufferType?) { + super.setText(text, type) + // Required for PatternClickableSpan + movementMethod = LinkMovementMethod.getInstance() + } + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + super.onMeasure(widthSpec, heightSpec) + + if (layout != null && layout.lineCount >= 2) { + val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt() - paddingStart - paddingEnd + if (maxLineWidth < measuredWidth) { + super.onMeasure( + MeasureSpec.makeMeasureSpec( + maxLineWidth, + MeasureSpec.getMode(widthSpec) + ), + heightSpec + ) + } + } + } + + private fun getMaxLineWidth(layout: Layout): Float { + var maxWidth = 0.0f + val lines = layout.lineCount + for (i in 0 until lines) { + maxWidth = max(maxWidth, layout.getLineWidth(i)) + } + return round(maxWidth) + } +} diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/conversations/viewmodel/ConversationViewModel.kt similarity index 54% rename from app/src/main/java/org/linphone/ui/conversations/ConversationViewModel.kt rename to app/src/main/java/org/linphone/ui/conversations/viewmodel/ConversationViewModel.kt index cb56b502d..13a0b11e1 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/conversations/viewmodel/ConversationViewModel.kt @@ -17,32 +17,69 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.ui.conversations +package org.linphone.ui.conversations.viewmodel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.contacts.ContactData import org.linphone.contacts.ContactsListener +import org.linphone.core.Address import org.linphone.core.ChatRoom +import org.linphone.core.ChatRoomListenerStub +import org.linphone.core.EventLog import org.linphone.core.Factory import org.linphone.core.tools.Log +import org.linphone.ui.conversations.data.EventLogData import org.linphone.utils.LinphoneUtils class ConversationViewModel : ViewModel() { private lateinit var chatRoom: ChatRoom + val events = MutableLiveData>() + val contactName = MutableLiveData() val contactData = MutableLiveData() val subject = MutableLiveData() + val isComposing = MutableLiveData() + val isOneToOne = MutableLiveData() private val contactsListener = object : ContactsListener { override fun onContactsLoaded() { contactLookup() + events.value.orEmpty().forEach(EventLogData::contactLookup) + } + } + + private val chatRoomListener = object : ChatRoomListenerStub() { + override fun onIsComposingReceived( + chatRoom: ChatRoom, + remoteAddress: Address, + composing: Boolean + ) { + isComposing.postValue(composing) + } + + override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array) { + for (eventLog in eventLogs) { + addChatMessageEventLog(eventLog) + } + } + + override fun onChatMessageSending(chatRoom: ChatRoom, eventLog: EventLog) { + val position = events.value.orEmpty().size + + if (eventLog.type == EventLog.Type.ConferenceChatMessage) { + val chatMessage = eventLog.chatMessage + chatMessage ?: return + chatMessage.userData = position + } + + addChatMessageEventLog(eventLog) } } @@ -52,6 +89,12 @@ class ConversationViewModel : ViewModel() { override fun onCleared() { coreContext.contactsManager.removeListener(contactsListener) + coreContext.postOnCoreThread { + if (::chatRoom.isInitialized) { + chatRoom.removeListener(chatRoomListener) + } + events.value.orEmpty().forEach(EventLogData::destroy) + } } fun loadChatRoom(localSipUri: String, remoteSipUri: String) { @@ -69,10 +112,21 @@ class ConversationViewModel : ViewModel() { ) if (found != null) { chatRoom = found + chatRoom.addListener(chatRoomListener) isOneToOne.postValue(chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) subject.postValue(chatRoom.subject) + isComposing.postValue(chatRoom.isRemoteComposing) contactLookup() + + val list = arrayListOf() + list.addAll(events.value.orEmpty()) + + for (eventLog in chatRoom.getHistoryEvents(0)) { + list.add(EventLogData(eventLog)) + } + + events.postValue(list) } } } @@ -103,4 +157,36 @@ class ConversationViewModel : ViewModel() { } } } + + private fun addEvent(eventLog: EventLog) { + val list = arrayListOf() + list.addAll(events.value.orEmpty()) + + val found = list.find { data -> data.eventLog == eventLog } + if (found == null) { + list.add(EventLogData(eventLog)) + } + + events.postValue(list) + } + + private fun addChatMessageEventLog(eventLog: EventLog) { + if (eventLog.type == EventLog.Type.ConferenceChatMessage) { + val chatMessage = eventLog.chatMessage + chatMessage ?: return + chatMessage.userData = events.value.orEmpty().size + + val existingEvent = events.value.orEmpty().find { data -> + data.eventLog.type == EventLog.Type.ConferenceChatMessage && data.eventLog.chatMessage?.messageId == chatMessage.messageId + } + if (existingEvent != null) { + Log.w( + "[Chat Messages] Found already present chat message, don't add it it's probably the result of an auto download or an aggregated message received before but notified after the conversation was displayed" + ) + return + } + } + + addEvent(eventLog) + } } diff --git a/app/src/main/java/org/linphone/ui/conversations/ConversationsListViewModel.kt b/app/src/main/java/org/linphone/ui/conversations/viewmodel/ConversationsListViewModel.kt similarity index 98% rename from app/src/main/java/org/linphone/ui/conversations/ConversationsListViewModel.kt rename to app/src/main/java/org/linphone/ui/conversations/viewmodel/ConversationsListViewModel.kt index b87fbdfd1..ea77c8e6b 100644 --- a/app/src/main/java/org/linphone/ui/conversations/ConversationsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/conversations/viewmodel/ConversationsListViewModel.kt @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.ui.conversations +package org.linphone.ui.conversations.viewmodel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -29,6 +29,7 @@ import org.linphone.core.ChatRoom import org.linphone.core.Core import org.linphone.core.CoreListenerStub import org.linphone.core.tools.Log +import org.linphone.ui.conversations.data.ChatRoomData import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils diff --git a/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt b/app/src/main/java/org/linphone/ui/conversations/viewmodel/NewConversationViewModel.kt similarity index 98% rename from app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt rename to app/src/main/java/org/linphone/ui/conversations/viewmodel/NewConversationViewModel.kt index 969655c2d..ecc0cfde0 100644 --- a/app/src/main/java/org/linphone/ui/conversations/NewConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/conversations/viewmodel/NewConversationViewModel.kt @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.ui.conversations +package org.linphone.ui.conversations.viewmodel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 32aa00b79..831a3e698 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -43,10 +43,12 @@ fun TextView.setTypeface(typeface: Int) { @BindingAdapter("coilContact") fun loadContactPictureWithCoil(imageView: ImageView, contact: ContactData?) { - contact ?: return - - imageView.load(contact.avatar) { - transformations(CircleCropTransformation()) - error(R.drawable.contact_avatar) + if (contact == null) { + imageView.load(R.drawable.contact_avatar) + } else { + imageView.load(contact.avatar) { + transformations(CircleCropTransformation()) + error(R.drawable.contact_avatar) + } } } diff --git a/app/src/main/res/drawable/shape_received_message_bubble_background.xml b/app/src/main/res/drawable/shape_received_message_bubble_background.xml new file mode 100644 index 000000000..59dfa50a1 --- /dev/null +++ b/app/src/main/res/drawable/shape_received_message_bubble_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_sent_message_bubble_background.xml b/app/src/main/res/drawable/shape_sent_message_bubble_background.xml new file mode 100644 index 000000000..b371c9e06 --- /dev/null +++ b/app/src/main/res/drawable/shape_sent_message_bubble_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_incoming.xml b/app/src/main/res/layout/chat_bubble_incoming.xml new file mode 100644 index 000000000..fe1d39841 --- /dev/null +++ b/app/src/main/res/layout/chat_bubble_incoming.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_outgoing.xml b/app/src/main/res/layout/chat_bubble_outgoing.xml new file mode 100644 index 000000000..1ff9d1ff1 --- /dev/null +++ b/app/src/main/res/layout/chat_bubble_outgoing.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_event.xml b/app/src/main/res/layout/chat_event.xml new file mode 100644 index 000000000..2c4836bc8 --- /dev/null +++ b/app/src/main/res/layout/chat_event.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_room_list_cell.xml b/app/src/main/res/layout/chat_room_list_cell.xml index f281eb60e..334ee0ff7 100644 --- a/app/src/main/res/layout/chat_room_list_cell.xml +++ b/app/src/main/res/layout/chat_room_list_cell.xml @@ -8,7 +8,7 @@ + type="org.linphone.ui.conversations.data.ChatRoomData" /> + type="org.linphone.ui.conversations.viewmodel.ConversationViewModel" /> + + + + diff --git a/app/src/main/res/layout/conversations_fragment.xml b/app/src/main/res/layout/conversations_fragment.xml index 586a144b9..6521921e8 100644 --- a/app/src/main/res/layout/conversations_fragment.xml +++ b/app/src/main/res/layout/conversations_fragment.xml @@ -5,12 +5,12 @@ - + + type="org.linphone.ui.conversations.viewmodel.NewConversationViewModel" /> #DD5F5F #4FAE80 #09C5F4 + #DFECF2 + #F4F4F7 #6C7A87 #F9F9F9