Removed legacy chat views

This commit is contained in:
Sylvain Berfini 2023-09-11 16:51:25 +02:00
parent 548a597843
commit e3022f42ab
34 changed files with 9 additions and 2940 deletions

View file

@ -1,73 +0,0 @@
/*
* 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.contacts
import android.content.ContentUris
import android.net.Uri
import android.provider.ContactsContract
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.core.*
class ContactData(val friend: Friend) {
val presenceStatus = MutableLiveData<ConsolidatedPresence>()
val name = MutableLiveData<String>()
val avatar = getAvatarUri()
private val friendListener = object : FriendListenerStub() {
@WorkerThread
override fun onPresenceReceived(fr: Friend) {
presenceStatus.postValue(fr.consolidatedPresence)
}
}
init {
name.postValue(friend.name)
presenceStatus.postValue(friend.consolidatedPresence)
friend.addListener(friendListener)
presenceStatus.postValue(ConsolidatedPresence.Offline)
}
@WorkerThread
fun onDestroy() {
friend.removeListener(friendListener)
}
@WorkerThread
private fun getAvatarUri(): Uri? {
val refKey = friend.refKey
if (refKey != null) {
val lookupUri = ContentUris.withAppendedId(
ContactsContract.Contacts.CONTENT_URI,
refKey.toLong()
)
return Uri.withAppendedPath(
lookupUri,
ContactsContract.Contacts.Photo.CONTENT_DIRECTORY
)
}
return null
}
}

View file

@ -1,78 +0,0 @@
/*
* 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.contacts
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.databinding.ContactSelectionCellBinding
class ContactsSelectionAdapter(
private val viewLifecycleOwner: LifecycleOwner
) : ListAdapter<ContactData, RecyclerView.ViewHolder>(ContactDataDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding: ContactSelectionCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.contact_selection_cell,
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as ViewHolder).bind(getItem(position))
}
inner class ViewHolder(
private val binding: ContactSelectionCellBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(contactData: ContactData) {
with(binding) {
data = contactData
lifecycleOwner = viewLifecycleOwner
executePendingBindings()
}
}
}
}
private class ContactDataDiffCallback : DiffUtil.ItemCallback<ContactData>() {
override fun areItemsTheSame(
oldItem: ContactData,
newItem: ContactData
): Boolean {
return oldItem.friend.refKey == newItem.friend.refKey
}
override fun areContentsTheSame(
oldItem: ContactData,
newItem: ContactData
): Boolean {
return true
}
}

View file

@ -335,8 +335,8 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
val appName = context.getString(org.linphone.R.string.app_name)
val androidVersion = BuildConfig.VERSION_NAME
val userAgent = "$appName/$androidVersion ($deviceName) LinphoneSDK"
val sdkVersion = context.getString(org.linphone.core.R.string.linphone_sdk_version)
val sdkBranch = context.getString(org.linphone.core.R.string.linphone_sdk_branch)
val sdkVersion = context.getString(R.string.linphone_sdk_version)
val sdkBranch = context.getString(R.string.linphone_sdk_branch)
val sdkUserAgent = "$sdkVersion ($sdkBranch)"
core.setUserAgent(userAgent, sdkUserAgent)
}

View file

@ -124,19 +124,6 @@ class CallsFragment : GenericFragment() {
}
}
sharedViewModel.navigateToConversationsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.callsFragment) {
// To prevent any previously seen contact to show up when navigating back to here later
binding.callsNavContainer.findNavController().popBackStack()
val action =
CallsFragmentDirections.actionCallsFragmentToConversationsFragment()
findNavController().navigate(action)
}
}
}
sharedViewModel.navigateToContactsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.callsFragment) {

View file

@ -134,18 +134,6 @@ class ContactsFragment : GenericFragment() {
}
}
sharedViewModel.navigateToConversationsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.contactsFragment) {
// To prevent any previously seen contact to show up when navigating back to here later
binding.contactsNavContainer.findNavController().popBackStack()
val action = ContactsFragmentDirections.actionContactsFragmentToConversationsFragment()
findNavController().navigate(action)
}
}
}
sharedViewModel.navigateToCallsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.contactsFragment) {

View file

@ -134,11 +134,11 @@ class ContactViewModel @UiThread constructor() : ViewModel() {
val organization = friend.organization
if (!organization.isNullOrEmpty()) {
company.postValue(organization!!)
company.postValue(organization)
}
val jobTitle = friend.jobTitle
if (!jobTitle.isNullOrEmpty()) {
title.postValue(jobTitle!!)
title.postValue(jobTitle)
}
val addressesAndNumbers = friend.getListOfSipAddressesAndPhoneNumbers(listener)
@ -208,7 +208,7 @@ class ContactViewModel @UiThread constructor() : ViewModel() {
val vCard = friend.vcard?.asVcard4String()
if (!vCard.isNullOrEmpty()) {
Log.i("$TAG Friend has been successfully dumped as vCard string")
val fileName = friend.name.orEmpty().replace(" ", "_").toLowerCase(
val fileName = friend.name.orEmpty().replace(" ", "_").lowercase(
Locale.getDefault()
)
val file = FileUtils.getFileStorageCacheDir(

View file

@ -192,7 +192,7 @@ class ContactsListViewModel @UiThread constructor() : AbstractTopBarViewModel()
val vCard = friend.vcard?.asVcard4String()
if (!vCard.isNullOrEmpty()) {
Log.i("$TAG Friend has been successfully dumped as vCard string")
val fileName = friend.name.orEmpty().replace(" ", "_").toLowerCase(
val fileName = friend.name.orEmpty().replace(" ", "_").lowercase(
Locale.getDefault()
)
val file = FileUtils.getFileStorageCacheDir(

View file

@ -1,100 +0,0 @@
/*
* 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.conversations
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.main.conversations.adapter.ChatEventLogsListAdapter
import org.linphone.ui.main.conversations.viewmodel.ConversationViewModel
class ConversationFragment : Fragment() {
private lateinit var binding: ConversationFragmentBinding
private val viewModel: ConversationViewModel by navGraphViewModels(
R.id.main_nav_graph
)
private lateinit var adapter: ChatEventLogsListAdapter
override fun onDestroyView() {
binding.messagesList.adapter = null
super.onDestroyView()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ConversationFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
val localSipUri = arguments?.getString("localSipUri")
?: savedInstanceState?.getString("localSipUri")
val remoteSipUri = arguments?.getString("remoteSipUri")
?: savedInstanceState?.getString("remoteSipUri")
if (localSipUri != null && remoteSipUri != null) {
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()
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

@ -1,90 +0,0 @@
/*
* 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.conversations
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.ChatRoom
import org.linphone.databinding.ChatRoomMenuBinding
class ConversationMenuDialogFragment(
private val chatRoom: ChatRoom,
private val onDismiss: (() -> Unit)? = null
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "ConversationMenuDialogFragment"
}
override fun onCancel(dialog: DialogInterface) {
onDismiss?.invoke()
super.onCancel(dialog)
}
override fun onDismiss(dialog: DialogInterface) {
onDismiss?.invoke()
super.onDismiss(dialog)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = ChatRoomMenuBinding.inflate(layoutInflater)
view.isMuted = chatRoom.muted
view.isRead = chatRoom.unreadMessagesCount == 0
view.setMarkAsReadClickListener {
coreContext.postOnCoreThread {
chatRoom.markAsRead()
}
dismiss()
}
view.setMuteClickListener {
coreContext.postOnCoreThread {
chatRoom.muted = true
}
dismiss()
}
view.setUnMuteClickListener {
coreContext.postOnCoreThread {
chatRoom.muted = false
}
dismiss()
}
view.setDeleteClickListener {
coreContext.postOnCoreThread { core ->
core.deleteChatRoom(chatRoom)
}
dismiss()
}
return view.root
}
}

View file

@ -1,174 +0,0 @@
/*
* 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.conversations
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.core.os.bundleOf
import androidx.core.view.doOnPreDraw
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.AutoTransition
import org.linphone.R
import org.linphone.databinding.ConversationsFragmentBinding
import org.linphone.ui.main.conversations.adapter.ConversationsListAdapter
import org.linphone.ui.main.conversations.viewmodel.ConversationsListViewModel
class ConversationsFragment : Fragment() {
private lateinit var binding: ConversationsFragmentBinding
private val listViewModel: ConversationsListViewModel by navGraphViewModels(
R.id.main_nav_graph
)
private lateinit var adapter: ConversationsListAdapter
private val observer = object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
scrollToTop()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0 && itemCount == 1) {
scrollToTop()
}
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
scrollToTop()
}
}
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (findNavController().currentDestination?.id == R.id.newConversationFragment ||
findNavController().currentDestination?.id == R.id.conversationFragment
) {
// Holds fragment in place while (new) conversation fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
}
return super.onCreateAnimation(transit, enter, nextAnim)
}
override fun onDestroyView() {
binding.conversationsList.adapter = null
adapter.unregisterAdapterDataObserver(observer)
super.onDestroyView()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ConversationsFragmentBinding.inflate(layoutInflater)
sharedElementEnterTransition = AutoTransition()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = listViewModel
adapter = ConversationsListAdapter(viewLifecycleOwner)
adapter.registerAdapterDataObserver(observer)
binding.conversationsList.setHasFixedSize(true)
binding.conversationsList.adapter = adapter
adapter.chatRoomClickedEvent.observe(viewLifecycleOwner) {
it.consume { data ->
val bundle = bundleOf()
bundle.putString("localSipUri", data.localSipUri)
bundle.putString("remoteSipUri", data.remoteSipUri)
if (findNavController().currentDestination?.id == R.id.conversationsFragment) {
findNavController().navigate(
R.id.action_conversationsFragment_to_conversationFragment,
bundle
)
}
}
}
adapter.chatRoomLongClickedEvent.observe(viewLifecycleOwner) {
it.consume { data ->
val modalBottomSheet = ConversationMenuDialogFragment(data.chatRoom) {
adapter.resetSelection()
}
modalBottomSheet.show(parentFragmentManager, ConversationMenuDialogFragment.TAG)
}
}
val layoutManager = LinearLayoutManager(requireContext())
binding.conversationsList.layoutManager = layoutManager
listViewModel.chatRoomsList.observe(
viewLifecycleOwner
) {
adapter.submitList(it)
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
listViewModel.notifyItemChangedEvent.observe(viewLifecycleOwner) {
it.consume { index ->
adapter.notifyItemChanged(index)
}
}
/*listViewModel.focusSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { show ->
if (show) {
// To automatically open keyboard
binding.topBar.search.showKeyboard(requireActivity().window)
} else {
binding.topBar.search.hideKeyboard()
}
}
}*/
binding.setOnNewConversationClicked {
if (findNavController().currentDestination?.id == R.id.conversationsFragment) {
val action = ConversationsFragmentDirections.actionConversationsFragmentToNewConversationFragment()
findNavController().navigate(action)
}
}
binding.setOnContactsClicked {
if (findNavController().currentDestination?.id == R.id.conversationsFragment) {
val action = ConversationsFragmentDirections.actionConversationsFragmentToContactsFragment()
findNavController().navigate(action)
}
}
}
private fun scrollToTop() {
binding.conversationsList.scrollToPosition(0)
}
}

View file

@ -1,110 +0,0 @@
/*
* 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.conversations
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.core.view.doOnPreDraw
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.contacts.ContactsSelectionAdapter
import org.linphone.databinding.ConversationStartFragmentBinding
import org.linphone.ui.main.conversations.viewmodel.NewConversationViewModel
class NewConversationFragment : Fragment() {
private lateinit var binding: ConversationStartFragmentBinding
private lateinit var adapter: ContactsSelectionAdapter
private val viewModel: NewConversationViewModel by navGraphViewModels(
R.id.main_nav_graph
)
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
if (findNavController().currentDestination?.id == R.id.conversationFragment) {
// Holds fragment in place while created conversation fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
}
return super.onCreateAnimation(transit, enter, nextAnim)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ConversationStartFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
adapter = ContactsSelectionAdapter(viewLifecycleOwner)
binding.contactsList.adapter = adapter
binding.contactsList.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(requireContext())
binding.contactsList.layoutManager = layoutManager
viewModel.contactsList.observe(
viewLifecycleOwner
) {
adapter.submitList(it)
(view.parent as? ViewGroup)?.doOnPreDraw {
startPostponedEnterTransition()
}
}
viewModel.filter.observe(
viewLifecycleOwner
) {
val filter = it.orEmpty().trim()
coreContext.postOnCoreThread {
viewModel.applyFilter(filter)
}
}
binding.setBackClickListener {
requireActivity().onBackPressedDispatcher.onBackPressed()
}
viewModel.goToChatRoom.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.newConversationFragment) {
findNavController().navigate(
R.id.action_newConversationFragment_to_conversationFragment
)
}
}
}
}
}

View file

@ -1,152 +0,0 @@
package org.linphone.ui.main.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.main.conversations.data.ChatMessageData
import org.linphone.ui.main.conversations.data.EventData
import org.linphone.ui.main.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,86 +0,0 @@
package org.linphone.ui.main.conversations.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.linphone.R
import org.linphone.databinding.ChatRoomListCellBinding
import org.linphone.ui.main.conversations.data.ChatRoomData
import org.linphone.ui.main.conversations.data.ChatRoomDataListener
import org.linphone.utils.Event
class ConversationsListAdapter(
private val viewLifecycleOwner: LifecycleOwner
) : ListAdapter<ChatRoomData, RecyclerView.ViewHolder>(ConversationDiffCallback()) {
val chatRoomClickedEvent: MutableLiveData<Event<ChatRoomData>> by lazy {
MutableLiveData<Event<ChatRoomData>>()
}
val chatRoomLongClickedEvent: MutableLiveData<Event<ChatRoomData>> by lazy {
MutableLiveData<Event<ChatRoomData>>()
}
var selectedAdapterPosition = -1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding: ChatRoomListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.chat_room_list_cell,
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as ViewHolder).bind(getItem(position))
}
fun resetSelection() {
notifyItemChanged(selectedAdapterPosition)
selectedAdapterPosition = -1
}
inner class ViewHolder(
val binding: ChatRoomListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(chatRoomData: ChatRoomData) {
with(binding) {
data = chatRoomData
lifecycleOwner = viewLifecycleOwner
binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition
chatRoomData.chatRoomDataListener = object : ChatRoomDataListener() {
override fun onClicked() {
chatRoomClickedEvent.value = Event(chatRoomData)
}
override fun onLongClicked() {
selectedAdapterPosition = bindingAdapterPosition
binding.root.isSelected = true
chatRoomLongClickedEvent.value = Event(chatRoomData)
}
}
executePendingBindings()
}
}
}
}
private class ConversationDiffCallback : DiffUtil.ItemCallback<ChatRoomData>() {
override fun areItemsTheSame(oldItem: ChatRoomData, newItem: ChatRoomData): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ChatRoomData, newItem: ChatRoomData): Boolean {
return false
}
}

View file

@ -1,89 +0,0 @@
/*
* 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.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

@ -1,248 +0,0 @@
/*
* 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.conversations.data
import androidx.lifecycle.MutableLiveData
import java.lang.StringBuilder
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.contacts.ContactData
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.TimestampUtils
class ChatRoomData(val chatRoom: ChatRoom) {
val id = LinphoneUtils.getChatRoomId(chatRoom)
val localSipUri = chatRoom.localAddress.asString()
val remoteSipUri = chatRoom.peerAddress.asString()
val isOneToOne = chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())
val contactName = MutableLiveData<String>()
val subject = MutableLiveData<String>()
val lastMessage = MutableLiveData<String>()
val unreadChatCount = MutableLiveData<Int>()
val isComposing = MutableLiveData<Boolean>()
val isSecure = MutableLiveData<Boolean>()
val isSecureVerified = MutableLiveData<Boolean>()
val isEphemeral = MutableLiveData<Boolean>()
val isMuted = MutableLiveData<Boolean>()
val lastUpdate = MutableLiveData<String>()
val showLastMessageImdnIcon = MutableLiveData<Boolean>()
val lastMessageImdnIcon = MutableLiveData<Int>()
val contactData = MutableLiveData<ContactData>()
var chatRoomDataListener: ChatRoomDataListener? = null
private val coreListener = object : CoreListenerStub() {
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
if (chatRoom == this@ChatRoomData.chatRoom) {
unreadChatCount.postValue(chatRoom.unreadMessagesCount)
}
}
}
private val chatRoomListener = object : ChatRoomListenerStub() {
override fun onIsComposingReceived(
chatRoom: ChatRoom,
remoteAddress: Address,
composing: Boolean
) {
isComposing.postValue(composing)
}
override fun onMessagesReceived(chatRoom: ChatRoom, chatMessages: Array<out ChatMessage>) {
unreadChatCount.postValue(chatRoom.unreadMessagesCount)
computeLastMessage()
}
override fun onChatMessageSent(chatRoom: ChatRoom, eventLog: EventLog) {
computeLastMessage()
}
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
computeLastMessage()
}
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
subject.postValue(
chatRoom.subject ?: LinphoneUtils.getDisplayName(chatRoom.peerAddress)
)
}
}
init {
chatRoom.addListener(chatRoomListener)
coreContext.core.addListener(coreListener)
lastMessageImdnIcon.postValue(R.drawable.imdn_sent)
showLastMessageImdnIcon.postValue(false)
contactLookup()
subject.postValue(
chatRoom.subject ?: LinphoneUtils.getDisplayName(chatRoom.peerAddress)
)
computeLastMessage()
unreadChatCount.postValue(chatRoom.unreadMessagesCount)
isComposing.postValue(chatRoom.isRemoteComposing)
isSecure.postValue(chatRoom.securityLevel == ChatRoom.SecurityLevel.Encrypted)
isSecureVerified.postValue(chatRoom.securityLevel == ChatRoom.SecurityLevel.Safe)
isEphemeral.postValue(chatRoom.isEphemeralEnabled)
isMuted.postValue(chatRoom.muted)
}
fun onCleared() {
coreContext.postOnCoreThread { core ->
chatRoom.removeListener(chatRoomListener)
core.removeListener(coreListener)
}
}
fun onClicked() {
chatRoomDataListener?.onClicked()
}
fun onLongClicked(): Boolean {
chatRoomDataListener?.onLongClicked()
return true
}
fun contactLookup() {
if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) {
val remoteAddress = chatRoom.peerAddress
val friend = chatRoom.core.findFriend(remoteAddress)
if (friend != null) {
contactData.postValue(ContactData(friend))
}
contactName.postValue(friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress))
} else {
if (chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) {
val first = chatRoom.participants.firstOrNull()
if (first != null) {
val remoteAddress = first.address
val friend = chatRoom.core.findFriend(remoteAddress)
if (friend != null) {
contactData.postValue(ContactData(friend))
}
contactName.postValue(
friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress)
)
} else {
Log.e("[Chat Room Data] No participant in the chat room!")
}
}
}
computeLastMessage()
}
private fun computeLastMessageImdnIcon(message: ChatMessage) {
val state = message.state
showLastMessageImdnIcon.postValue(
if (message.isOutgoing) {
when (state) {
ChatMessage.State.DeliveredToUser, ChatMessage.State.Displayed,
ChatMessage.State.NotDelivered, ChatMessage.State.FileTransferError -> true
else -> false
}
} else {
false
}
)
lastMessageImdnIcon.postValue(
when (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
}
)
}
private fun computeLastMessage() {
val lastUpdateTime = chatRoom.lastUpdateTime
lastUpdate.postValue(TimestampUtils.toString(lastUpdateTime, true))
val builder = StringBuilder()
val message = chatRoom.lastMessageInHistory
if (message != null) {
val senderAddress = message.fromAddress.clone()
senderAddress.clean()
if (message.isOutgoing && message.state != ChatMessage.State.Displayed) {
message.addListener(object : ChatMessageListenerStub() {
override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
computeLastMessageImdnIcon(message)
if (state == ChatMessage.State.Displayed) {
message.removeListener(this)
}
}
})
}
computeLastMessageImdnIcon(message)
if (!isOneToOne) {
val sender = chatRoom.core.findFriend(senderAddress)
builder.append(sender?.name ?: LinphoneUtils.getDisplayName(senderAddress))
builder.append(": ")
}
for (content in message.contents) {
if (content.isFile || content.isFileTransfer) {
builder.append(content.name + " ")
} else if (content.isText) {
builder.append(content.utf8Text + " ")
}
}
builder.trim()
}
val text = builder.toString()
if (text.length > 128) { // This brings a huge performance improvement when scrolling
lastMessage.postValue(text.substring(0, 128))
} else {
lastMessage.postValue(text)
}
}
}
abstract class ChatRoomDataListener {
abstract fun onClicked()
abstract fun onLongClicked()
}

View file

@ -1,32 +0,0 @@
/*
* 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.conversations.data
import org.linphone.core.EventLog
class EventData(val eventLog: EventLog) {
fun destroy() {
// TODO
}
fun contactLookup() {
// TODO
}
}

View file

@ -1,50 +0,0 @@
/*
* 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.main.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

@ -1,77 +0,0 @@
/*
* 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.main.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

@ -1,194 +0,0 @@
/*
* 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.conversations.viewmodel
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.contacts.ContactData
import org.linphone.contacts.ContactsManager.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.main.conversations.data.EventLogData
import org.linphone.utils.LinphoneUtils
class ConversationViewModel @WorkerThread constructor() : 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 {
@WorkerThread
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)
}
}
init {
coreContext.contactsManager.addListener(contactsListener)
}
override fun onCleared() {
coreContext.postOnCoreThread {
coreContext.contactsManager.removeListener(contactsListener)
if (::chatRoom.isInitialized) {
chatRoom.removeListener(chatRoomListener)
}
events.value.orEmpty().forEach(EventLogData::destroy)
}
}
fun loadChatRoom(localSipUri: String, remoteSipUri: String) {
coreContext.postOnCoreThread { core ->
val localAddress = Factory.instance().createAddress(localSipUri)
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
val found = core.searchChatRoom(
null,
localAddress,
remoteSipAddress,
arrayOfNulls(
0
)
)
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)
}
}
}
private fun contactLookup() {
if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) {
val remoteAddress = chatRoom.peerAddress
val friend = chatRoom.core.findFriend(remoteAddress)
if (friend != null) {
contactData.postValue(ContactData(friend))
}
contactName.postValue(friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress))
} else {
if (chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) {
val first = chatRoom.participants.firstOrNull()
if (first != null) {
val remoteAddress = first.address
val friend = chatRoom.core.findFriend(remoteAddress)
if (friend != null) {
contactData.postValue(ContactData(friend))
}
contactName.postValue(
friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress)
)
} else {
Log.e("[Conversation View Model] No participant in the chat room!")
}
}
}
}
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

@ -1,191 +0,0 @@
/*
* 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.conversations.viewmodel
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.ArrayList
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.contacts.ContactsManager.ContactsListener
import org.linphone.core.ChatMessage
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.main.conversations.data.ChatRoomData
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
class ConversationsListViewModel : ViewModel() {
val chatRoomsList = MutableLiveData<ArrayList<ChatRoomData>>()
val notifyItemChangedEvent = MutableLiveData<Event<Int>>()
private val contactsListener = object : ContactsListener {
@WorkerThread
override fun onContactsLoaded() {
for (chatRoomData in chatRoomsList.value.orEmpty()) {
chatRoomData.contactLookup()
}
}
}
private val coreListener = object : CoreListenerStub() {
override fun onChatRoomStateChanged(
core: Core,
chatRoom: ChatRoom,
state: ChatRoom.State?
) {
Log.i(
"[Conversations List] Chat room [${LinphoneUtils.getChatRoomId(chatRoom)}] state changed [$state]"
)
when (state) {
ChatRoom.State.Created -> {
addChatRoomToList(chatRoom)
}
ChatRoom.State.Deleted -> {
removeChatRoomFromList(chatRoom)
}
else -> {}
}
}
override fun onMessageSent(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
onChatRoomMessageEvent(chatRoom)
}
override fun onMessagesReceived(
core: Core,
chatRoom: ChatRoom,
messages: Array<out ChatMessage>
) {
onChatRoomMessageEvent(chatRoom)
}
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
notifyChatRoomUpdate(chatRoom)
}
override fun onChatRoomEphemeralMessageDeleted(core: Core, chatRoom: ChatRoom) {
notifyChatRoomUpdate(chatRoom)
}
override fun onChatRoomSubjectChanged(core: Core, chatRoom: ChatRoom) {
notifyChatRoomUpdate(chatRoom)
}
}
init {
coreContext.postOnCoreThread { core ->
core.addListener(coreListener)
coreContext.contactsManager.addListener(contactsListener)
}
coreContext.postOnCoreThread { core ->
updateChatRoomsList()
}
}
override fun onCleared() {
coreContext.postOnCoreThread { core ->
coreContext.contactsManager.removeListener(contactsListener)
core.removeListener(coreListener)
}
super.onCleared()
}
private fun addChatRoomToList(chatRoom: ChatRoom) {
val index = findChatRoomIndex(chatRoom)
if (index != -1) {
Log.w("[Conversations List] Chat room already exists in list, do not add it again")
return
}
val list = arrayListOf<ChatRoomData>()
val data = ChatRoomData(chatRoom)
list.add(data)
list.addAll(chatRoomsList.value.orEmpty())
list.sortByDescending { data -> data.chatRoom.lastUpdateTime }
chatRoomsList.postValue(list)
}
private fun removeChatRoomFromList(chatRoom: ChatRoom) {
val list = arrayListOf<ChatRoomData>()
for (data in chatRoomsList.value.orEmpty()) {
if (LinphoneUtils.getChatRoomId(chatRoom) != LinphoneUtils.getChatRoomId(
data.chatRoom
)
) {
list.add(data)
}
}
chatRoomsList.postValue(list)
}
private fun findChatRoomIndex(chatRoom: ChatRoom): Int {
val id = LinphoneUtils.getChatRoomId(chatRoom)
for ((index, data) in chatRoomsList.value.orEmpty().withIndex()) {
if (id == data.id) {
return index
}
}
return -1
}
private fun notifyChatRoomUpdate(chatRoom: ChatRoom) {
when (val index = findChatRoomIndex(chatRoom)) {
-1 -> updateChatRoomsList()
else -> notifyItemChangedEvent.postValue(Event(index))
}
}
private fun onChatRoomMessageEvent(chatRoom: ChatRoom) {
when (findChatRoomIndex(chatRoom)) {
-1 -> updateChatRoomsList()
0 -> notifyItemChangedEvent.postValue(Event(0))
else -> reorderChatRoomsList()
}
}
private fun updateChatRoomsList() {
Log.i("[Conversations List] Updating chat rooms list")
chatRoomsList.value.orEmpty().forEach(ChatRoomData::onCleared)
val list = arrayListOf<ChatRoomData>()
val chatRooms = coreContext.core.chatRooms
for (chatRoom in chatRooms) {
list.add(ChatRoomData(chatRoom))
}
chatRoomsList.postValue(list)
}
private fun reorderChatRoomsList() {
Log.i("[Conversations List] Re-ordering chat rooms list")
val list = arrayListOf<ChatRoomData>()
list.addAll(chatRoomsList.value.orEmpty())
list.sortByDescending { data -> data.chatRoom.lastUpdateTime }
chatRoomsList.postValue(list)
}
}

View file

@ -1,124 +0,0 @@
/*
* 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.conversations.viewmodel
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.contacts.ContactData
import org.linphone.contacts.ContactsManager.ContactsListener
import org.linphone.core.MagicSearch
import org.linphone.core.MagicSearchListenerStub
import org.linphone.core.SearchResult
import org.linphone.core.tools.Log
import org.linphone.utils.Event
class NewConversationViewModel : ViewModel() {
val contactsList = MutableLiveData<ArrayList<ContactData>>()
val groupEnabled = MutableLiveData<Boolean>()
val goToChatRoom: MutableLiveData<Event<Pair<String, String>>> by lazy {
MutableLiveData<Event<Pair<String, String>>>()
}
val filter = MutableLiveData<String>()
private var previousFilter = "NotSet"
private val magicSearch: MagicSearch by lazy {
val magicSearch = coreContext.core.createMagicSearch()
magicSearch.limitedSearch = false
magicSearch
}
private val magicSearchListener = object : MagicSearchListenerStub() {
override fun onSearchResultsReceived(magicSearch: MagicSearch) {
processMagicSearchResults(magicSearch.lastSearch)
}
}
private val contactsListener = object : ContactsListener {
@WorkerThread
override fun onContactsLoaded() {
applyFilter(filter.value.orEmpty().trim())
}
}
init {
coreContext.postOnCoreThread {
magicSearch.addListener(magicSearchListener)
coreContext.contactsManager.addListener(contactsListener)
applyFilter("")
}
}
override fun onCleared() {
coreContext.postOnCoreThread {
coreContext.contactsManager.removeListener(contactsListener)
magicSearch.removeListener(magicSearchListener)
}
super.onCleared()
}
fun applyFilter(filterValue: String) {
Log.i("[New Conversation ViewModel] Filtering contacts using [$filterValue]")
if (previousFilter.isNotEmpty() && (
previousFilter.length > filterValue.length ||
(previousFilter.length == filterValue.length && previousFilter != filterValue)
)
) {
magicSearch.resetSearchCache()
}
previousFilter = filterValue
magicSearch.getContactsListAsync(
filterValue,
"",
MagicSearch.Source.Friends.toInt(),
MagicSearch.Aggregation.Friend
)
}
fun createGroup() {
goToChatRoom.value = Event(Pair("", ""))
}
fun enableGroupSelection() {
groupEnabled.value = true
}
@WorkerThread
private fun processMagicSearchResults(results: Array<SearchResult>) {
Log.i("[New Conversation ViewModel] [${results.size}] matching results")
contactsList.value.orEmpty().forEach(ContactData::onDestroy)
val list = arrayListOf<ContactData>()
for (searchResult in results) {
val friend = searchResult.friend
if (friend != null) {
val data = ContactData(friend)
list.add(data)
}
}
contactsList.postValue(list)
}
}

View file

@ -83,9 +83,7 @@ class BottomNavBarFragment : Fragment() {
}
binding.setOnConversationsClicked {
if (sharedViewModel.currentlyDisplayedFragment.value != R.id.conversationsFragment) {
sharedViewModel.navigateToConversationsEvent.value = Event(true)
}
// TODO
}
binding.setOnMeetingsClicked {
@ -95,7 +93,6 @@ class BottomNavBarFragment : Fragment() {
sharedViewModel.currentlyDisplayedFragment.observe(viewLifecycleOwner) {
viewModel.contactsSelected.value = it == R.id.contactsFragment
viewModel.callsSelected.value = it == R.id.callsFragment
viewModel.conversationsSelected.value = it == R.id.conversationsFragment
}
sharedViewModel.resetMissedCallsCountEvent.observe(viewLifecycleOwner) {

View file

@ -52,7 +52,6 @@ import io.getstream.avatarview.AvatarView
import io.getstream.avatarview.coil.loadImage
import org.linphone.BR
import org.linphone.R
import org.linphone.contacts.ContactData
import org.linphone.core.ConsolidatedPresence
import org.linphone.core.tools.Log
import org.linphone.ui.main.MainActivity
@ -182,20 +181,6 @@ fun loadPictureWithCoil(imageView: ImageView, file: String?) {
}
}
@UiThread
@BindingAdapter("coilContact")
fun loadContactPictureWithCoil2(imageView: ImageView, contact: ContactData?) {
Log.i("[Data Binding Utils] Loading contact picture [${contact?.avatar}] with coil")
if (contact == null) {
imageView.load(R.drawable.contact_avatar)
} else {
imageView.load(contact.avatar) {
transformations(CircleCropTransformation())
error(R.drawable.contact_avatar)
}
}
}
@UiThread
@BindingAdapter("presenceIcon")
fun ImageView.setPresenceIcon(presence: ConsolidatedPresence?) {

View file

@ -173,7 +173,8 @@ class TimestampUtils {
}
val millis = if (timestampInSecs) timestamp * 1000 else timestamp
return dateFormat.format(Date(millis)).capitalize(Locale.getDefault())
return dateFormat.format(Date(millis))
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
}
@AnyThread

View file

@ -1,77 +0,0 @@
<?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.main.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="@dimen/icon_size"
android:adjustViewBounds="true"
android:src="@drawable/contact_avatar"
coilContact="@{data.contactData}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<org.linphone.ui.main.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"/>
<androidx.appcompat.widget.AppCompatTextView
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

@ -1,77 +0,0 @@
<?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.main.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.main.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" />
<androidx.appcompat.widget.AppCompatTextView
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

@ -1,53 +0,0 @@
<?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.main.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"/>
<androidx.appcompat.widget.AppCompatTextView
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

@ -1,118 +0,0 @@
<?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="android.graphics.Typeface" />
<variable
name="data"
type="org.linphone.ui.main.conversations.data.ChatRoomData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> data.onClicked()}"
android:onLongClick="@{() -> data.onLongClicked()}"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="@drawable/cell_background">
<ImageView
android:id="@+id/avatar"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:src="@{data.isOneToOne ? @drawable/contact_avatar : @drawable/group_avatar, default=@drawable/contact_avatar}"
coilContact="@{data.contactData}"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:adjustViewBounds="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@{data.isOneToOne ? data.contactName : data.subject, default=`John Doe`}"
android:textColor="#000000"
android:textSize="14sp"
android:textStyle="@{data.unreadChatCount > 0 ? Typeface.BOLD : Typeface.NORMAL, default=normal}"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintBottom_toTopOf="@id/subtitle"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/date_time"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@{data.isComposing ? `... est en train d'écrire` : data.lastMessage, default=`Lorem Ipsum`}"
android:textColor="@{data.isComposing ? @color/primary_color : data.unreadChatCount > 0 ? @color/black : @color/gray_4, default=@color/gray_4}"
android:textSize="14sp"
android:textStyle="@{data.unreadChatCount > 0 ? Typeface.BOLD : Typeface.NORMAL, default=normal}"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/end_subtitle_barrier"
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/date_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.lastUpdate, default=`16:45`}"
android:textColor="#9AABB5"
android:textSize="12sp"
android:layout_marginEnd="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/end_subtitle_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="imdn, unread"
app:barrierDirection="left" />
<ImageView
android:id="@+id/imdn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@{data.lastMessageImdnIcon, default=@drawable/imdn_read}"
android:layout_marginEnd="10dp"
android:visibility="@{data.showLastMessageImdnIcon ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/subtitle"
app:layout_constraintBottom_toBottomOf="@id/subtitle"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/unread"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:background="@drawable/shape_conversation_cell_unread_count_background"
android:ellipsize="end"
android:gravity="center"
android:singleLine="true"
android:text="@{String.valueOf(data.unreadChatCount), default=`1`}"
android:textSize="10sp"
android:textColor="@color/white"
android:visibility="@{data.unreadChatCount == 0 ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/subtitle"
app:layout_constraintBottom_toBottomOf="@id/subtitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,135 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable
name="markAsReadClickListener"
type="View.OnClickListener" />
<variable
name="muteClickListener"
type="View.OnClickListener" />
<variable
name="unMuteClickListener"
type="View.OnClickListener" />
<variable
name="callClickListener"
type="View.OnClickListener" />
<variable
name="deleteClickListener"
type="View.OnClickListener" />
<variable
name="isMuted"
type="Boolean" />
<variable
name="isRead"
type="Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/separator">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/mark_as_read"
android:onClick="@{markAsReadClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Marquer comme lu"
android:padding="20dp"
android:visibility="@{isRead ? View.GONE : View.VISIBLE}"
android:textSize="17sp"
android:textColor="@color/gray_1"
android:gravity="center"
android:background="@color/gray_2"
android:layout_marginBottom="1dp"
app:layout_constraintBottom_toTopOf="@id/mute"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/mute"
android:onClick="@{muteClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Mettre en sourdine"
android:padding="20dp"
android:visibility="@{isMuted ? View.GONE : View.VISIBLE}"
android:textSize="17sp"
android:textColor="@color/gray_1"
android:gravity="center"
android:background="@color/gray_2"
android:layout_marginBottom="1dp"
app:layout_constraintBottom_toTopOf="@id/unmute"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/unmute"
android:onClick="@{unMuteClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Enlever la sourdine"
android:padding="20dp"
android:visibility="@{isMuted ? View.VISIBLE : View.GONE, default=gone}"
android:textSize="17sp"
android:textColor="@color/gray_1"
android:gravity="center"
android:background="@color/gray_2"
android:layout_marginBottom="1dp"
app:layout_constraintBottom_toTopOf="@id/call"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/call"
android:onClick="@{callClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Appeler"
android:padding="20dp"
android:textSize="17sp"
android:textColor="@color/gray_1"
android:gravity="center"
android:background="@color/gray_2"
android:layout_marginBottom="1dp"
app:layout_constraintBottom_toTopOf="@id/delete"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/delete"
android:onClick="@{deleteClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Supprimer la conversation"
android:padding="20dp"
android:textSize="17sp"
android:textColor="@color/red_danger"
android:gravity="center"
android:background="@color/gray_2"
android:layout_marginBottom="1dp"
app:layout_constraintBottom_toTopOf="@id/leave"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/leave"
android:onClick="@{deleteClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Quitter la conversation"
android:padding="20dp"
android:textSize="17sp"
android:textColor="@color/gray_1"
android:gravity="center"
android:background="@color/gray_2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,48 +0,0 @@
<?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="data"
type="org.linphone.contacts.ContactData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/avatar"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:src="@drawable/contact_avatar"
coilContact="@{data}"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:adjustViewBounds="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@{data.name, default=`John Doe`}"
android:textColor="@color/black"
android:textSize="14sp"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintBottom_toBottomOf="@id/avatar"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,202 +0,0 @@
<?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="backClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.conversations.viewmodel.ConversationViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<ImageView
android:id="@+id/back"
android:onClick="@{backClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/caret_left"
android:padding="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/avatar"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:adjustViewBounds="true"
android:src="@{viewModel.isOneToOne ? @drawable/contact_avatar : @drawable/group_avatar, default=@drawable/contact_avatar}"
coilContact="@{viewModel.contactData}"
app:layout_constraintStart_toEndOf="@id/back"
app:layout_constraintTop_toTopOf="@id/back"
app:layout_constraintBottom_toBottomOf="@id/back"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@{viewModel.isOneToOne ? viewModel.contactName : viewModel.subject, default=`John Doe`}"
android:textColor="@color/black"
android:textSize="14sp"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/phone_call"
app:layout_constraintTop_toTopOf="@id/avatar" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/presence"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="Online"
android:textColor="@color/green_online"
android:textSize="14sp"
android:textStyle=""
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/phone_call"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<ImageView
android:id="@+id/info"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:src="@drawable/info"
android:padding="10dp"
android:layout_marginEnd="5dp"
app:layout_constraintTop_toTopOf="@id/back"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="@+id/video_call"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:src="@drawable/video_camera"
android:padding="10dp"
app:layout_constraintTop_toTopOf="@id/back"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintEnd_toStartOf="@id/info" />
<ImageView
android:id="@+id/phone_call"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:src="@drawable/phone"
android:padding="10dp"
app:layout_constraintTop_toTopOf="@id/back"
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" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/composing_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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
android:id="@+id/bottom_background"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/gray_7"
android:layout_marginTop="-15dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/message"
app:layout_constraintBottom_toBottomOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/emoji_picker"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:text="🙂"
android:textSize="18sp"
android:padding="5dp"
android:gravity="center"
android:layout_marginStart="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/message"
app:layout_constraintBottom_toBottomOf="@id/message" />
<ImageView
android:id="@+id/attach_file"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:src="@drawable/plus_circle"
android:padding="5dp"
android:layout_marginStart="10dp"
app:layout_constraintStart_toEndOf="@id/emoji_picker"
app:layout_constraintTop_toTopOf="@id/message"
app:layout_constraintBottom_toBottomOf="@id/message" />
<EditText
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Dites quelque chose..."
android:layout_marginBottom="15dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:paddingStart="24dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:maxLines="5"
android:textSize="14sp"
android:background="@drawable/edit_text_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/attach_file"
app:layout_constraintEnd_toStartOf="@id/voice_message"/>
<ImageView
android:id="@+id/voice_message"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:src="@drawable/voice_message"
android:padding="5dp"
android:layout_marginEnd="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/message"
app:layout_constraintBottom_toBottomOf="@id/message" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,145 +0,0 @@
<?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="backClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.conversations.viewmodel.NewConversationViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent">
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:src="@drawable/shape_white_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/cancel"
android:onClick="@{backClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Annuler"
android:textSize="15sp"
android:padding="10dp"
android:textColor="@color/gray_1"
android:layout_marginStart="15dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/subtitle"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/create_group"
android:onClick="@{() -> viewModel.createGroup()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Créer"
android:textSize="15sp"
android:padding="10dp"
android:textColor="@color/primary_color"
android:layout_marginEnd="15dp"
android:visibility="@{viewModel.groupEnabled ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/subtitle"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nouvelle conversation"
android:textSize="18sp"
android:textColor="@color/black"
android:layout_marginTop="22dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Chiffrée de bout en bout"
android:textSize="14sp"
android:textColor="@color/gray_1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"/>
<EditText
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:padding="10dp"
android:drawableStart="@drawable/magnifying_glass"
android:drawablePadding="10dp"
android:background="@drawable/edit_text_background"
android:hint="Rechercher des contacts"
android:text="@={viewModel.filter}"
android:textSize="14sp"
android:inputType="textPersonName|textNoSuggestions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtitle" />
<ImageView
android:onClick="@{() -> viewModel.enableGroupSelection()}"
android:id="@+id/new_group_avatar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/users_three"
android:padding="10dp"
android:background="@drawable/shape_orange_round"
android:layout_marginTop="30dp"
android:layout_marginStart="20dp"
android:visibility="@{viewModel.filter.length() > 0 || viewModel.groupEnabled ? View.GONE : View.VISIBLE}"
app:layout_constraintTop_toBottomOf="@id/search_bar"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.enableGroupSelection()}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Créer un nouveau groupe"
android:textSize="16sp"
android:padding="5dp"
android:layout_marginStart="7dp"
android:layout_marginEnd="20dp"
android:drawableEnd="@drawable/caret_right"
android:drawablePadding="5dp"
android:visibility="@{viewModel.filter.length() > 0 || viewModel.groupEnabled ? View.GONE : View.VISIBLE}"
app:layout_constraintStart_toEndOf="@id/new_group_avatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/new_group_avatar"
app:layout_constraintBottom_toBottomOf="@id/new_group_avatar"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contacts_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/new_group_avatar"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -1,107 +0,0 @@
<?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"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="onNewConversationClicked"
type="View.OnClickListener" />
<variable
name="onContactsClicked"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.conversations.viewmodel.ConversationsListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/primary_color">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/top_bar"
android:name="org.linphone.ui.main.fragment.TopBarFragment"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
bind:layout="@layout/top_search_bar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="17dp"
android:src="@drawable/shape_white_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/sort_by_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:text="Trier par"
android:textSize="12sp"
android:textColor="@color/gray_5"
app:layout_constraintStart_toStartOf="@id/top_bar"
app:layout_constraintTop_toTopOf="@id/sort_by"
app:layout_constraintBottom_toBottomOf="@id/sort_by"/>
<TextView
android:id="@+id/sort_by"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:text="Récents"
android:textSize="14sp"
android:textColor="@color/blue_filter"
android:drawablePadding="5dp"
app:layout_constraintStart_toEndOf="@id/sort_by_label"
app:layout_constraintTop_toBottomOf="@id/top_bar"
app:drawableEndCompat="@drawable/caret_down" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/conversationsList"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="20dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/sort_by"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" />
<include
bind:onContactsClicked="@{onContactsClicked}"
android:id="@+id/bottom_nav_bar"
layout="@layout/bottom_nav_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/new_conversation"
android:onClick="@{onNewConversationClicked}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="16dp"
android:src="@drawable/plus_circle"
app:tint="@color/primary_color"
app:backgroundTint="@color/white"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -5,67 +5,11 @@
android:id="@+id/main_nav_graph"
app:startDestination="@id/callsFragment">
<fragment
android:id="@+id/conversationsFragment"
android:name="org.linphone.ui.main.conversations.ConversationsFragment"
android:label="ConversationsFragment"
tools:layout="@layout/conversations_fragment">
<action
android:id="@+id/action_conversationsFragment_to_newConversationFragment"
app:destination="@id/newConversationFragment"
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out"
app:launchSingleTop="true" />
<action
android:id="@+id/action_conversationsFragment_to_conversationFragment"
app:destination="@id/conversationFragment"
app:enterAnim="@anim/slide_in_right"
app:popExitAnim="@anim/slide_out_right" />
<action
android:id="@+id/action_conversationsFragment_to_contactsFragment"
app:destination="@id/contactsFragment"
app:popUpTo="@id/contactsFragment"
app:popUpToInclusive="true"
app:launchSingleTop="true" />
<action
android:id="@+id/action_conversationsFragment_to_callsFragment"
app:destination="@id/callsFragment" />
</fragment>
<fragment
android:id="@+id/newConversationFragment"
android:name="org.linphone.ui.main.conversations.NewConversationFragment"
android:label="NewConversationFragment"
tools:layout="@layout/conversation_start_fragment" >
<action
android:id="@+id/action_newConversationFragment_to_conversationFragment"
app:destination="@id/conversationFragment"
app:enterAnim="@anim/slide_in_right"
app:popExitAnim="@anim/slide_out_right"
app:popUpTo="@id/conversationsFragment" />
</fragment>
<fragment
android:id="@+id/conversationFragment"
android:name="org.linphone.ui.main.conversations.ConversationFragment"
android:label="ConversationFragment"
tools:layout="@layout/conversation_fragment" >
<argument
android:name="localSipUri"
app:argType="string" />
<argument
android:name="remoteSipUri"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/contactsFragment"
android:name="org.linphone.ui.main.contacts.fragment.ContactsFragment"
android:label="ContactsFragment"
tools:layout="@layout/contacts_fragment">
<action
android:id="@+id/action_contactsFragment_to_conversationsFragment"
app:destination="@id/conversationsFragment" />
<action
android:id="@+id/action_contactsFragment_to_callsFragment"
app:destination="@id/callsFragment"
@ -85,9 +29,6 @@
app:launchSingleTop="true"
app:popUpTo="@id/callsFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_callsFragment_to_conversationsFragment"
app:destination="@id/conversationsFragment" />
</fragment>
<fragment