Started displaying messages

This commit is contained in:
Sylvain Berfini 2023-10-06 17:31:13 +02:00
parent c12ddf43a2
commit 2fb97fbc51
16 changed files with 755 additions and 210 deletions

View file

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

View file

@ -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
}*/
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ChatMessage.State>()
val text = MutableLiveData<String>()
init {
state.postValue(chatMessage.state)
text.postValue(LinphoneUtils.getTextDescribingMessage(chatMessage))
}
}

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.chat.model
import androidx.annotation.WorkerThread
import org.linphone.core.EventLog
class EventModel @WorkerThread constructor(eventLog: EventLog)

View file

@ -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<ContactAvatarModel>()
val events = MutableLiveData<ArrayList<EventLogModel>>()
val chatRoomFoundEvent = MutableLiveData<Event<Boolean>>()
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<EventLogModel>()
val history = chatRoom.getHistoryEvents(0)
for (event in history) {
val model = EventLogModel(event, avatar)
eventsList.add(model)
}
events.postValue(eventsList)
}
}

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:bottomRightRadius="16dp" android:bottomLeftRadius="16dp" />
<solid android:color="@color/gray_100"/>
</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:radius="16dp" />
<solid android:color="@color/gray_100"/>
</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/gray_main2_100"/>
</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:radius="16dp" />
<solid android:color="@color/gray_main2_100"/>
</shape>

View file

@ -0,0 +1,70 @@
<?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" />
<import type="org.linphone.core.ConsolidatedPresence" />
<variable
name="onLongClickListener"
type="View.OnLongClickListener" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.ChatMessageModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onLongClick="@{onLongClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp">
<io.getstream.avatarview.AvatarView
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_bubble_size"
android:layout_height="@dimen/avatar_bubble_size"
android:adjustViewBounds="true"
android:background="@drawable/shape_circle_light_blue_background"
contactAvatar="@{model.avatarModel}"
app:avatarViewPlaceholder="@drawable/user_circle"
app:avatarViewInitialsBackgroundColor="@color/gray_main2_200"
app:avatarViewInitialsTextColor="@color/gray_main2_600"
app:avatarViewInitialsTextSize="16sp"
app:avatarViewInitialsTextStyle="bold"
app:avatarViewShape="circle"
app:avatarViewBorderWidth="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/presence_badge"
android:layout_width="@dimen/avatar_bubble_presence_badge_size"
android:layout_height="@dimen/avatar_bubble_presence_badge_size"
android:layout_marginEnd="@dimen/avatar_presence_badge_end_margin"
android:background="@drawable/led_background"
android:padding="@dimen/avatar_presence_badge_padding"
app:presenceIcon="@{model.avatarModel.presenceStatus}"
android:visibility="@{model.avatarModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="@{model.text, default=`Lorem ipsum dolor sit amet`}"
android:textSize="14sp"
android:textColor="@color/gray_main2_700"
android:gravity="center_vertical|start"
android:padding="16dp"
android:background="@drawable/shape_chat_bubble_incoming_first"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/avatar"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,39 @@
<?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="onLongClickListener"
type="View.OnLongClickListener" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.ChatMessageModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onLongClick="@{onLongClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp">
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{model.text, default=`Lorem ipsum dolor sit amet`}"
android:textSize="14sp"
android:textColor="@color/gray_main2_700"
android:gravity="center_vertical|end"
android:padding="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@drawable/shape_chat_bubble_outgoing_first"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -10,219 +10,152 @@
name="backClickListener"
type="View.OnClickListener" />
<variable
name="pickEmojiClickListener"
type="View.OnClickListener" />
<variable
name="attachFileClickListener"
type="View.OnClickListener" />
<variable
name="recordVoiceMessageClickListener"
name="openEmojiPickerClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.chat.viewmodel.ConversationViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
android:layout_height="match_parent">
<ImageView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:padding="15dp"
android:adjustViewBounds="true"
android:onClick="@{backClickListener}"
android:visibility="@{viewModel.showBackButton ? View.VISIBLE : View.GONE}"
android:src="@drawable/caret_left"
app:tint="@color/orange_main_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title"/>
<io.getstream.avatarview.AvatarView
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_list_cell_size"
android:layout_height="@dimen/avatar_list_cell_size"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:layout_marginStart="5dp"
android:adjustViewBounds="true"
android:background="@drawable/shape_circle_light_blue_background"
contactAvatar="@{viewModel.avatarModel}"
app:avatarViewPlaceholder="@drawable/user_circle"
app:avatarViewInitialsBackgroundColor="@color/gray_main2_200"
app:avatarViewInitialsTextColor="@color/gray_main2_600"
app:avatarViewInitialsTextSize="16sp"
app:avatarViewInitialsTextStyle="bold"
app:avatarViewShape="circle"
app:avatarViewBorderWidth="0dp"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="@id/back" />
<ImageView
android:id="@+id/trust_badge"
android:layout_width="@dimen/avatar_presence_badge_size"
android:layout_height="@dimen/avatar_presence_badge_size"
android:src="@{viewModel.avatarModel.trust == SecurityLevel.Encrypted ? @drawable/trusted : @drawable/not_trusted, default=@drawable/trusted}"
android:visibility="@{viewModel.avatarModel.trust == SecurityLevel.Encrypted || viewModel.avatarModel.trust == SecurityLevel.Unsafe ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:maxLines="1"
android:ellipsize="end"
android:text="@{viewModel.avatarModel.name, default=`John Doe`}"
android:textSize="16sp"
android:textColor="@color/gray_main2_600"
android:gravity="center_vertical"
app:layout_constraintEnd_toStartOf="@id/call"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/info"
android:layout_width="@dimen/icon_size"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:src="@drawable/info"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/video_call"
android:layout_width="@dimen/icon_size"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:src="@drawable/video_camera"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toStartOf="@id/info" />
<ImageView
android:id="@+id/call"
android:layout_width="@dimen/icon_size"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:src="@drawable/phone"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toStartOf="@id/video_call"/>
<androidx.recyclerview.widget.RecyclerView
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="16dp"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toTopOf="@id/composing" />
android:layout_height="match_parent"
android:layout_marginBottom="80dp"
android:background="@color/white">
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:id="@+id/composing"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginBottom="12dp"
android:textSize="12sp"
android:textColor="@color/gray_main2_400"
app:layout_constraintBottom_toTopOf="@id/send_area_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:padding="15dp"
android:adjustViewBounds="true"
android:onClick="@{backClickListener}"
android:visibility="@{viewModel.showBackButton ? View.VISIBLE : View.GONE}"
android:src="@drawable/caret_left"
app:tint="@color/orange_main_500"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title"/>
<View
android:id="@+id/send_area_background"
android:layout_width="0dp"
android:layout_height="80dp"
android:background="@color/gray_100"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/message_area_background" />
<io.getstream.avatarview.AvatarView
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_list_cell_size"
android:layout_height="@dimen/avatar_list_cell_size"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:layout_marginStart="5dp"
android:adjustViewBounds="true"
android:background="@drawable/shape_circle_light_blue_background"
contactAvatar="@{viewModel.avatarModel}"
app:avatarViewPlaceholder="@drawable/user_circle"
app:avatarViewInitialsBackgroundColor="@color/gray_main2_200"
app:avatarViewInitialsTextColor="@color/gray_main2_600"
app:avatarViewInitialsTextSize="16sp"
app:avatarViewInitialsTextStyle="bold"
app:avatarViewShape="circle"
app:avatarViewBorderWidth="0dp"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="@id/back" />
<ImageView
<ImageView
android:id="@+id/trust_badge"
android:layout_width="@dimen/avatar_presence_badge_size"
android:layout_height="@dimen/avatar_presence_badge_size"
android:src="@{viewModel.avatarModel.trust == SecurityLevel.Encrypted ? @drawable/trusted : @drawable/not_trusted, default=@drawable/trusted}"
android:visibility="@{viewModel.avatarModel.trust == SecurityLevel.Encrypted || viewModel.avatarModel.trust == SecurityLevel.Unsafe ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:maxLines="1"
android:ellipsize="end"
android:text="@{viewModel.avatarModel.name, default=`John Doe`}"
android:textSize="16sp"
android:textColor="@color/gray_main2_600"
android:gravity="center_vertical"
app:layout_constraintEnd_toStartOf="@id/call"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/info"
android:layout_width="@dimen/icon_size"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:src="@drawable/info"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/video_call"
android:layout_width="@dimen/icon_size"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:src="@drawable/video_camera"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toStartOf="@id/info" />
<ImageView
android:id="@+id/call"
android:layout_width="@dimen/icon_size"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:src="@drawable/phone"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toStartOf="@id/video_call"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/events_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="16dp"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toTopOf="@id/composing" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:id="@+id/composing"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginBottom="12dp"
android:textSize="12sp"
android:textColor="@color/gray_main2_400"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/events_list"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.emoji2.emojipicker.EmojiPickerView
android:id="@+id/emoji_picker"
android:onClick="@{pickEmojiClickListener}"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginStart="16dp"
android:src="@drawable/smiley"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="@+id/attach_file"
android:onClick="@{attachFileClickListener}"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:src="@drawable/paperclip"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintStart_toEndOf="@id/emoji_picker"
app:layout_constraintEnd_toStartOf="@id/message_area_background"/>
<ImageView
android:id="@+id/message_area_background"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:src="@drawable/edit_text_background"
app:layout_constraintStart_toEndOf="@id/attach_file"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/message_to_send"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="5dp"
android:background="@color/transparent_color"
android:textSize="14sp"
android:textColorHint="@color/gray_main2_400"
android:hint="@string/conversation_text_field_hint"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintStart_toStartOf="@id/message_area_background"
app:layout_constraintEnd_toStartOf="@id/voice_record" />
<ImageView
android:id="@+id/voice_record"
android:onClick="@{recordVoiceMessageClickListener}"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginEnd="12dp"
android:src="@drawable/microphone"
android:layout_marginBottom="80dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintEnd_toEndOf="@id/message_area_background" />
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"/>
<ImageView
android:id="@+id/send_message"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginEnd="12dp"
android:src="@drawable/paper_plane_tilt"
app:tint="@color/orange_main_500"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintEnd_toEndOf="@id/message_area_background" />
<include
android:id="@+id/send_area"
openEmojiPickerClickListener="@{openEmojiPickerClickListener}"
layout="@layout/chat_conversation_send_area"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,103 @@
<?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="openEmojiPickerClickListener"
type="View.OnClickListener" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:background="@color/gray_100"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<!-- Keep behavior to have it at the bottom -->
<ImageView
android:id="@+id/emoji_picker"
android:onClick="@{openEmojiPickerClickListener}"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginStart="16dp"
android:src="@drawable/smiley"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="@+id/attach_file"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:src="@drawable/paperclip"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintStart_toEndOf="@id/emoji_picker"
app:layout_constraintEnd_toStartOf="@id/message_area_background"/>
<ImageView
android:id="@+id/message_area_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:src="@drawable/edit_text_background"
app:layout_constraintStart_toEndOf="@id/attach_file"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/message_to_send"
app:layout_constraintBottom_toBottomOf="@id/message_to_send"/>
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/message_to_send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="16dp"
android:minHeight="48dp"
android:background="@color/transparent_color"
android:textSize="14sp"
android:textColorHint="@color/gray_main2_400"
android:maxLines="3"
android:hint="@string/conversation_text_field_hint"
app:layout_constraintStart_toStartOf="@id/message_area_background"
app:layout_constraintEnd_toStartOf="@id/send_barrier"
app:layout_constraintBottom_toBottomOf="parent"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/send_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="voice_record, send_message" />
<ImageView
android:id="@+id/voice_record"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginEnd="12dp"
android:src="@drawable/microphone"
android:visibility="gone"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintEnd_toEndOf="@id/message_area_background" />
<ImageView
android:id="@+id/send_message"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginEnd="12dp"
android:src="@drawable/paper_plane_tilt"
app:tint="@color/orange_main_500"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintEnd_toEndOf="@id/message_area_background" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,24 @@
<?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="model"
type="org.linphone.ui.main.chat.model.EventModel" />
</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">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -14,10 +14,12 @@
<dimen name="icon_size">24dp</dimen>
<dimen name="welcome_icon_size">100dp</dimen>
<dimen name="avatar_bubble_size">24dp</dimen>
<dimen name="avatar_list_cell_size">45dp</dimen>
<dimen name="avatar_favorite_list_cell_size">50dp</dimen>
<dimen name="avatar_big_size">100dp</dimen>
<dimen name="avatar_in_call_size">120dp</dimen>
<dimen name="avatar_bubble_presence_badge_size">5dp</dimen>
<dimen name="avatar_presence_badge_size">12dp</dimen>
<dimen name="avatar_presence_badge_in_call_size">26dp</dimen>
<dimen name="avatar_presence_badge_padding">2dp</dimen>