Moved files around, started bubbles

This commit is contained in:
Sylvain Berfini 2023-06-26 16:26:17 +02:00
parent 33a33867b5
commit 41ea5e4cc7
24 changed files with 782 additions and 37 deletions

View file

@ -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()
}

View file

@ -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

View file

@ -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

View file

@ -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<EventLogData, RecyclerView.ViewHolder>(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<EventLogData>() {
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
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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(

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ContactData>()
val state = MutableLiveData<ChatMessage.State>()
val text = MutableLiveData<String>()
val time = MutableLiveData<String>()
val imdnIcon = MutableLiveData<Int>()
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
}
)
}
}

View file

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.conversations
package org.linphone.ui.conversations.data
import androidx.lifecycle.MutableLiveData
import java.lang.StringBuilder

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.conversations.data
import org.linphone.core.EventLog
class EventData(val eventLog: EventLog) {
fun destroy() {
// TODO
}
fun contactLookup() {
// TODO
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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()
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -17,32 +17,69 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<ArrayList<EventLogData>>()
val contactName = MutableLiveData<String>()
val contactData = MutableLiveData<ContactData>()
val subject = MutableLiveData<String>()
val isComposing = MutableLiveData<Boolean>()
val isOneToOne = MutableLiveData<Boolean>()
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<out EventLog>) {
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<EventLogData>()
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<EventLogData>()
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)
}
}

View file

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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

View file

@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.conversations
package org.linphone.ui.conversations.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

View file

@ -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)
}
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:topRightRadius="16dp" android:bottomLeftRadius="16dp" android:bottomRightRadius="16dp" />
<solid android:color="@color/gray_incoming_message"/>
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:topLeftRadius="16dp" android:topRightRadius="16dp" android:bottomLeftRadius="16dp" />
<solid android:color="@color/blue_outgoing_message"/>
</shape>

View file

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="data"
type="org.linphone.ui.conversations.data.ChatMessageData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp">
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/shape_received_message_bubble_background"
app:layout_constraintStart_toStartOf="@id/text"
app:layout_constraintTop_toTopOf="@id/text"
app:layout_constraintEnd_toEndOf="@id/end_barrier"
app:layout_constraintBottom_toBottomOf="@id/timestamp"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/end_barrier"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:constraint_referenced_ids="text, timestamp"
app:barrierDirection="right" />
<ImageView
android:id="@+id/avatar"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:adjustViewBounds="true"
android:src="@drawable/contact_avatar"
coilContact="@{data.contactData}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<org.linphone.ui.conversations.view.MultiLineWrapContentWidthTextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{data.text, default=`Lorem Ipsum`}"
android:textSize="14sp"
android:textColor="#D9000000"
android:paddingTop="5dp"
android:paddingEnd="10dp"
android:paddingStart="10dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toTopOf="@id/timestamp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/avatar"/>
<TextView
android:id="@+id/timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.time, default=`15h42`}"
android:textSize="12sp"
android:textColor="#73000000"
android:layout_marginStart="18dp"
android:paddingBottom="5dp"
android:paddingEnd="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/avatar" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="data"
type="org.linphone.ui.conversations.data.ChatMessageData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp">
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/shape_sent_message_bubble_background"
app:layout_constraintStart_toStartOf="@id/start_barrier"
app:layout_constraintTop_toTopOf="@id/text"
app:layout_constraintEnd_toEndOf="@id/text"
app:layout_constraintBottom_toBottomOf="@id/timestamp"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/start_barrier"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:constraint_referenced_ids="text, imdn_status"
app:barrierDirection="left" />
<org.linphone.ui.conversations.view.MultiLineWrapContentWidthTextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.text, default=`Lorem Ipsum`}"
android:textSize="14sp"
android:textColor="#D9000000"
android:paddingTop="5dp"
android:paddingEnd="10dp"
android:paddingStart="10dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:gravity="end"
app:layout_constraintBottom_toTopOf="@id/timestamp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.time, default=`15h42`}"
android:textSize="12sp"
android:textColor="#73000000"
android:layout_marginEnd="18dp"
android:paddingBottom="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="@+id/imdn_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@{data.imdnIcon, default=@drawable/imdn_read}"
android:layout_marginEnd="8dp"
android:paddingStart="10dp"
app:layout_constraintEnd_toStartOf="@id/timestamp"
app:layout_constraintBottom_toBottomOf="@id/timestamp"
app:layout_constraintTop_toTopOf="@id/timestamp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="data"
type="org.linphone.ui.conversations.data.EventData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp">
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:background="#EEEEEE"
android:layout_marginEnd="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/event_text"
app:layout_constraintTop_toTopOf="@id/event_text"
app:layout_constraintBottom_toBottomOf="@id/event_text"/>
<TextView
android:id="@+id/event_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#73000000"
android:textSize="12sp"
android:text="John Doe has joined the chat"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:background="#EEEEEE"
android:layout_marginStart="20dp"
app:layout_constraintStart_toEndOf="@id/event_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/event_text"
app:layout_constraintBottom_toBottomOf="@id/event_text"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -8,7 +8,7 @@
<import type="android.graphics.Typeface" />
<variable
name="data"
type="org.linphone.ui.conversations.ChatRoomData" />
type="org.linphone.ui.conversations.data.ChatRoomData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout

View file

@ -10,7 +10,7 @@
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.conversations.ConversationViewModel" />
type="org.linphone.ui.conversations.viewmodel.ConversationViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
@ -102,15 +102,34 @@
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintEnd_toStartOf="@id/video_call" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messages_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
app:layout_constraintTop_toBottomOf="@id/back"
app:layout_constraintBottom_toTopOf="@id/messages_bottom_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/messages_bottom_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="composing_text, bottom_background"
app:barrierDirection="top" />
<TextView
android:id="@+id/composing_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="... est en train décrire..."
android:text="... est en train décrire"
android:textSize="12sp"
android:textColor="#73000000"
android:layout_marginStart="10dp"
android:layout_marginBottom="15dp"
android:visibility="@{viewModel.isComposing ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_background" />

View file

@ -5,12 +5,12 @@
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.conversations.ConversationsListViewModel" />
<variable
name="onNewConversationClicked"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.conversations.viewmodel.ConversationsListViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout

View file

@ -10,7 +10,7 @@
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.conversations.NewConversationViewModel" />
type="org.linphone.ui.conversations.viewmodel.NewConversationViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout

View file

@ -7,6 +7,8 @@
<color name="red_danger">#DD5F5F</color>
<color name="green_online">#4FAE80</color>
<color name="blue_filter">#09C5F4</color>
<color name="blue_outgoing_message">#DFECF2</color>
<color name="gray_incoming_message">#F4F4F7</color>
<color name="gray_1">#6C7A87</color>
<color name="gray_2">#F9F9F9</color>