diff --git a/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt
new file mode 100644
index 000000000..aae82319e
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.main.chat.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.main.chat.model.ChatMessageModel
+import org.linphone.ui.main.chat.model.EventLogModel
+import org.linphone.ui.main.chat.model.EventModel
+
+class ConversationEventAdapter(
+ private val viewLifecycleOwner: LifecycleOwner
+) : ListAdapter(EventLogDiffCallback()) {
+ companion object {
+ const val INCOMING_CHAT_MESSAGE = 1
+ const val OUTGOING_CHAT_MESSAGE = 2
+ const val EVENT = 3
+ }
+
+ var selectedAdapterPosition = -1
+
+ 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 ChatMessageModel) {
+ 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 ChatMessageModel)
+ is OutgoingBubbleViewHolder -> holder.bind(eventLog.data as ChatMessageModel)
+ is EventViewHolder -> holder.bind(eventLog.data as EventModel)
+ }
+ }
+
+ fun resetSelection() {
+ notifyItemChanged(selectedAdapterPosition)
+ selectedAdapterPosition = -1
+ }
+
+ inner class IncomingBubbleViewHolder(
+ val binding: ChatBubbleIncomingBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(chatMessageData: ChatMessageModel) {
+ with(binding) {
+ model = chatMessageData
+
+ binding.setOnLongClickListener {
+ selectedAdapterPosition = bindingAdapterPosition
+ binding.root.isSelected = true
+ true
+ }
+
+ lifecycleOwner = viewLifecycleOwner
+ executePendingBindings()
+ }
+ }
+ }
+
+ inner class OutgoingBubbleViewHolder(
+ val binding: ChatBubbleOutgoingBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(chatMessageData: ChatMessageModel) {
+ with(binding) {
+ model = chatMessageData
+
+ binding.setOnLongClickListener {
+ selectedAdapterPosition = bindingAdapterPosition
+ binding.root.isSelected = true
+ true
+ }
+
+ lifecycleOwner = viewLifecycleOwner
+ executePendingBindings()
+ }
+ }
+ }
+ inner class EventViewHolder(
+ val binding: ChatEventBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(eventData: EventModel) {
+ with(binding) {
+ model = eventData
+
+ lifecycleOwner = viewLifecycleOwner
+ executePendingBindings()
+ }
+ }
+ }
+
+ private class EventLogDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: EventLogModel, newItem: EventLogModel): Boolean {
+ return if (oldItem.isEvent && newItem.isEvent) {
+ oldItem.notifyId == newItem.notifyId
+ } else if (!oldItem.isEvent && !newItem.isEvent) {
+ val oldData = (oldItem.data as ChatMessageModel)
+ val newData = (newItem.data as ChatMessageModel)
+ oldData.id.isNotEmpty() && oldData.id == newData.id
+ } else {
+ false
+ }
+ }
+
+ override fun areContentsTheSame(oldItem: EventLogModel, newItem: EventLogModel): Boolean {
+ return if (oldItem.isEvent && newItem.isEvent) {
+ true
+ } else {
+ val newData = (newItem.data as ChatMessageModel)
+ newData.state.value == ChatMessage.State.Displayed
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt
index 8a5f6c9a8..2f48bfae0 100644
--- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt
@@ -23,11 +23,15 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.core.view.doOnPreDraw
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatConversationFragmentBinding
+import org.linphone.ui.main.chat.adapter.ConversationEventAdapter
import org.linphone.ui.main.chat.viewmodel.ConversationViewModel
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.Event
@@ -43,6 +47,8 @@ class ConversationFragment : GenericFragment() {
private val args: ConversationFragmentArgs by navArgs()
+ private lateinit var adapter: ConversationEventAdapter
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -67,6 +73,14 @@ class ConversationFragment : GenericFragment() {
viewModel = ViewModelProvider(this)[ConversationViewModel::class.java]
binding.viewModel = viewModel
+ binding.setBackClickListener {
+ goBack()
+ }
+
+ sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) { slideable ->
+ viewModel.showBackButton.value = slideable
+ }
+
val localSipUri = args.localSipUri
val remoteSipUri = args.remoteSipUri
Log.i(
@@ -74,27 +88,57 @@ class ConversationFragment : GenericFragment() {
)
viewModel.findChatRoom(localSipUri, remoteSipUri)
- binding.setBackClickListener {
- goBack()
- }
-
viewModel.chatRoomFoundEvent.observe(viewLifecycleOwner) {
it.consume { found ->
if (found) {
Log.i(
"$TAG Found matching chat room for local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
)
- startPostponedEnterTransition()
- sharedViewModel.openSlidingPaneEvent.value = Event(true)
} else {
- Log.e("$TAG Failed to find chat room, going back")
- goBack()
+ (view.parent as? ViewGroup)?.doOnPreDraw {
+ Log.e("$TAG Failed to find chat room, going back")
+ goBack()
+ }
}
}
}
- sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) { slideable ->
- viewModel.showBackButton.value = slideable
+ adapter = ConversationEventAdapter(viewLifecycleOwner)
+ binding.eventsList.setHasFixedSize(false)
+ binding.eventsList.adapter = adapter
+
+ val layoutManager = LinearLayoutManager(requireContext())
+ binding.eventsList.layoutManager = layoutManager
+
+ viewModel.events.observe(viewLifecycleOwner) {
+ val currentCount = adapter.itemCount
+ adapter.submitList(it)
+ Log.i("$TAG Events (messages) list updated with [${it.size}] items")
+
+ if (currentCount < it.size) {
+ binding.eventsList.scrollToPosition(it.size - 1)
+ }
+
+ (view.parent as? ViewGroup)?.doOnPreDraw {
+ startPostponedEnterTransition()
+ sharedViewModel.openSlidingPaneEvent.value = Event(true)
+ }
+ }
+
+ val emojisBottomSheetBehavior = BottomSheetBehavior.from(binding.emojiPicker)
+ emojisBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
+ emojisBottomSheetBehavior.isDraggable = false // To allow scrolling through the emojis
+
+ binding.setOpenEmojiPickerClickListener {
+ /*val state = emojisBottomSheetBehavior.state
+ if (state == BottomSheetBehavior.STATE_COLLAPSED) {
+ emojisBottomSheetBehavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
+ if (binding.emojiPicker.visibility == View.GONE) {
+ binding.emojiPicker.visibility = View.VISIBLE
+ }
+ } else {
+ emojisBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
+ }*/
}
}
}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt
new file mode 100644
index 000000000..2135c2fc9
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.main.chat.model
+
+import androidx.annotation.WorkerThread
+import androidx.lifecycle.MutableLiveData
+import org.linphone.core.ChatMessage
+import org.linphone.ui.main.contacts.model.ContactAvatarModel
+import org.linphone.utils.LinphoneUtils
+
+class ChatMessageModel @WorkerThread constructor(
+ chatMessage: ChatMessage,
+ val avatarModel: ContactAvatarModel
+) {
+ val id = chatMessage.messageId
+
+ val isOutgoing = chatMessage.isOutgoing
+
+ val state = MutableLiveData()
+
+ val text = MutableLiveData()
+
+ init {
+ state.postValue(chatMessage.state)
+ text.postValue(LinphoneUtils.getTextDescribingMessage(chatMessage))
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt
new file mode 100644
index 000000000..120058f05
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.main.chat.model
+
+import androidx.annotation.WorkerThread
+import org.linphone.core.EventLog
+import org.linphone.ui.main.contacts.model.ContactAvatarModel
+
+class EventLogModel @WorkerThread constructor(eventLog: EventLog, avatarModel: ContactAvatarModel) {
+ val type: EventLog.Type = eventLog.type
+
+ val isEvent = type != EventLog.Type.ConferenceChatMessage
+
+ val data = if (isEvent) {
+ EventModel(eventLog)
+ } else {
+ ChatMessageModel(eventLog.chatMessage!!, avatarModel)
+ }
+
+ val notifyId = eventLog.notifyId
+
+ fun destroy() {
+ /*when (data) {
+ is EventData -> data.destroy()
+ is ChatMessageModel -> data.destroy()
+ }*/
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/EventModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/EventModel.kt
new file mode 100644
index 000000000..d24131afd
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/model/EventModel.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.main.chat.model
+
+import androidx.annotation.WorkerThread
+import org.linphone.core.EventLog
+
+class EventModel @WorkerThread constructor(eventLog: EventLog)
diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt
index 953539711..2de78e2ae 100644
--- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt
@@ -27,6 +27,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.ChatRoom
import org.linphone.core.Factory
import org.linphone.core.tools.Log
+import org.linphone.ui.main.chat.model.EventLogModel
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.utils.Event
@@ -39,6 +40,8 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
val avatarModel = MutableLiveData()
+ val events = MutableLiveData>()
+
val chatRoomFoundEvent = MutableLiveData>()
private lateinit var chatRoom: ChatRoom
@@ -86,12 +89,21 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
}
val friend = coreContext.contactsManager.findContactByAddress(address)
- if (friend != null) {
- avatarModel.postValue(ContactAvatarModel(friend))
+ val avatar = if (friend != null) {
+ ContactAvatarModel(friend)
} else {
val fakeFriend = coreContext.core.createFriend()
fakeFriend.address = address
- avatarModel.postValue(ContactAvatarModel(fakeFriend))
+ ContactAvatarModel(fakeFriend)
}
+ avatarModel.postValue(avatar)
+
+ val eventsList = arrayListOf()
+ val history = chatRoom.getHistoryEvents(0)
+ for (event in history) {
+ val model = EventLogModel(event, avatar)
+ eventsList.add(model)
+ }
+ events.postValue(eventsList)
}
}
diff --git a/app/src/main/res/drawable/shape_chat_bubble_incoming_first.xml b/app/src/main/res/drawable/shape_chat_bubble_incoming_first.xml
new file mode 100644
index 000000000..e82f7df3c
--- /dev/null
+++ b/app/src/main/res/drawable/shape_chat_bubble_incoming_first.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shape_chat_bubble_incoming_full.xml b/app/src/main/res/drawable/shape_chat_bubble_incoming_full.xml
new file mode 100644
index 000000000..df3a98fea
--- /dev/null
+++ b/app/src/main/res/drawable/shape_chat_bubble_incoming_full.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shape_chat_bubble_outgoing_first.xml b/app/src/main/res/drawable/shape_chat_bubble_outgoing_first.xml
new file mode 100644
index 000000000..1d29b6a85
--- /dev/null
+++ b/app/src/main/res/drawable/shape_chat_bubble_outgoing_first.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shape_chat_bubble_outgoing_full.xml b/app/src/main/res/drawable/shape_chat_bubble_outgoing_full.xml
new file mode 100644
index 000000000..6ea2e234d
--- /dev/null
+++ b/app/src/main/res/drawable/shape_chat_bubble_outgoing_full.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..7d7bf2e03
--- /dev/null
+++ b/app/src/main/res/layout/chat_bubble_incoming.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..0144dfe78
--- /dev/null
+++ b/app/src/main/res/layout/chat_bubble_outgoing.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/chat_conversation_fragment.xml b/app/src/main/res/layout/chat_conversation_fragment.xml
index 711a4ae43..83b25ce01 100644
--- a/app/src/main/res/layout/chat_conversation_fragment.xml
+++ b/app/src/main/res/layout/chat_conversation_fragment.xml
@@ -10,219 +10,152 @@
name="backClickListener"
type="View.OnClickListener" />
-
-
-
+ android:layout_height="match_parent">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="match_parent"
+ android:layout_marginBottom="80dp"
+ android:background="@color/white">
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+ app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"/>
-
+
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/chat_conversation_send_area.xml b/app/src/main/res/layout/chat_conversation_send_area.xml
new file mode 100644
index 000000000..f94c6cd5d
--- /dev/null
+++ b/app/src/main/res/layout/chat_conversation_send_area.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..f190bcaa4
--- /dev/null
+++ b/app/src/main/res/layout/chat_event.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml
index f2251c70e..b2b796838 100644
--- a/app/src/main/res/values/dimen.xml
+++ b/app/src/main/res/values/dimen.xml
@@ -14,10 +14,12 @@
24dp
100dp
+ 24dp
45dp
50dp
100dp
120dp
+ 5dp
12dp
26dp
2dp