Started to display conversations list with long press menu

This commit is contained in:
Sylvain Berfini 2023-10-05 12:32:08 +02:00
parent 3f24e73978
commit 0d880dda50
33 changed files with 1611 additions and 32 deletions

View file

@ -488,7 +488,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
coreContext.contactsManager.findContactByAddress(address)
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(address)
val originalMessage = getTextDescribingMessage(message)
val originalMessage = LinphoneUtils.getTextDescribingMessage(message)
val text = AppUtils.getString(R.string.notification_chat_message_reaction_received).format(
displayName,
reaction,
@ -583,7 +583,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
coreContext.contactsManager.findContactByAddress(message.fromAddress)
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(message.fromAddress)
val text = getTextDescribingMessage(message)
val text = LinphoneUtils.getTextDescribingMessage(message)
val notifiableMessage = NotifiableMessage(
text,
contact,
@ -1003,30 +1003,6 @@ class NotificationsManager @MainThread constructor(private val context: Context)
notificationManager.createNotificationChannel(channel)
}
@WorkerThread
private fun getTextDescribingMessage(message: ChatMessage): String {
// If message contains text, then use that
var text = message.contents.find { content -> content.isText }?.utf8Text ?: ""
if (text.isEmpty()) {
val firstContent = message.contents.firstOrNull()
if (firstContent?.isIcalendar == true) {
text = "meeting invite" // TODO: use translated string
} else if (firstContent?.isVoiceRecording == true) {
text = "voice message" // TODO: use translated string
} else {
for (content in message.contents) {
if (text.isNotEmpty()) {
text += ", "
}
text += content.name
}
}
}
return text
}
class Notifiable(val notificationId: Int) {
var myself: String? = null
var callId: String? = null

View file

@ -0,0 +1,86 @@
package org.linphone.ui.main.chat.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.UiThread
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.ChatListCellBinding
import org.linphone.ui.main.chat.model.ConversationModel
import org.linphone.utils.Event
class ConversationsListAdapter(
private val viewLifecycleOwner: LifecycleOwner
) : ListAdapter<ConversationModel, RecyclerView.ViewHolder>(ChatRoomDiffCallback()) {
var selectedAdapterPosition = -1
val conversationClickedEvent: MutableLiveData<Event<ConversationModel>> by lazy {
MutableLiveData<Event<ConversationModel>>()
}
val conversationLongClickedEvent: MutableLiveData<Event<ConversationModel>> by lazy {
MutableLiveData<Event<ConversationModel>>()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding: ChatListCellBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.chat_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: ChatListCellBinding
) : RecyclerView.ViewHolder(binding.root) {
@UiThread
fun bind(conversationModel: ConversationModel) {
with(binding) {
model = conversationModel
lifecycleOwner = viewLifecycleOwner
binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition
binding.setOnClickListener {
conversationClickedEvent.value = Event(conversationModel)
}
binding.setOnLongClickListener {
selectedAdapterPosition = bindingAdapterPosition
binding.root.isSelected = true
conversationLongClickedEvent.value = Event(conversationModel)
true
}
executePendingBindings()
}
}
}
private class ChatRoomDiffCallback : DiffUtil.ItemCallback<ConversationModel>() {
override fun areItemsTheSame(oldItem: ConversationModel, newItem: ConversationModel): Boolean {
return oldItem.id == newItem.id && oldItem.lastUpdateTime == newItem.lastUpdateTime
}
override fun areContentsTheSame(oldItem: ConversationModel, newItem: ConversationModel): Boolean {
return oldItem.avatarModel.id == newItem.avatarModel.id
}
}
}

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.chat.fragment
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.linphone.databinding.ChatLongPressMenuBinding
@UiThread
class ConversationDialogFragment(
private val isMuted: Boolean,
private val isGroup: Boolean,
private val onDismiss: (() -> Unit)? = null,
private val onMarkConversationAsRead: (() -> Unit)? = null,
private val onToggleMute: (() -> Unit)? = null,
private val onCall: (() -> Unit)? = null,
private val onDeleteConversation: (() -> Unit)? = null,
private val onLeaveGroup: (() -> Unit)? = null
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "ConversationDialogFragment"
}
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 = ChatLongPressMenuBinding.inflate(layoutInflater)
view.isMuted = isMuted
view.isGroup = isGroup
view.setMarkAsReadClickListener {
onMarkConversationAsRead?.invoke()
dismiss()
}
view.setToggleMuteClickListener {
onToggleMute?.invoke()
dismiss()
}
view.setCallClickListener {
onCall?.invoke()
dismiss()
}
view.setDeleteClickListener {
onDeleteConversation?.invoke()
dismiss()
}
view.setLeaveClickListener {
onLeaveGroup?.invoke()
dismiss()
}
return view.root
}
}

View file

@ -0,0 +1,125 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.chat.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import androidx.core.view.doOnPreDraw
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.slidingpanelayout.widget.SlidingPaneLayout
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatFragmentBinding
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.SlidingPaneBackPressedCallback
class ConversationsFragment : GenericFragment() {
companion object {
private const val TAG = "[Conversations Fragment]"
}
private lateinit var binding: ChatFragmentBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ChatFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
/*if (findNavController().currentDestination?.id == R.id.newConversationFragment) {
// Holds fragment in place while new contact fragment slides over it
return AnimationUtils.loadAnimation(activity, R.anim.hold)
}*/
return super.onCreateAnimation(transit, enter, nextAnim)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.root.doOnPreDraw {
val slidingPane = binding.slidingPaneLayout
slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
sharedViewModel.isSlidingPaneSlideable.value = slidingPane.isSlideable
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
SlidingPaneBackPressedCallback(slidingPane)
)
}
sharedViewModel.closeSlidingPaneEvent.observe(
viewLifecycleOwner
) {
it.consume {
Log.i("$TAG Closing sliding pane")
binding.slidingPaneLayout.closePane()
}
}
sharedViewModel.openSlidingPaneEvent.observe(
viewLifecycleOwner
) {
it.consume {
Log.i("$TAG Opening sliding pane")
binding.slidingPaneLayout.openPane()
}
}
sharedViewModel.navigateToContactsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.conversationsFragment) {
// To prevent any previously seen conversation to show up when navigating back to here later
binding.chatNavContainer.findNavController().popBackStack()
val action = ConversationsFragmentDirections.actionConversationsFragmentToContactsFragment()
findNavController().navigate(action)
}
}
}
sharedViewModel.navigateToCallsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.conversationsFragment) {
// To prevent any previously seen conversation to show up when navigating back to here later
binding.chatNavContainer.findNavController().popBackStack()
val action = ConversationsFragmentDirections.actionConversationsFragmentToHistoryFragment()
findNavController().navigate(action)
}
}
}
}
override fun onResume() {
super.onResume()
sharedViewModel.currentlyDisplayedFragment.value = R.id.conversationsFragment
}
}

View file

@ -0,0 +1,158 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.chat.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatListFragmentBinding
import org.linphone.ui.main.chat.adapter.ConversationsListAdapter
import org.linphone.ui.main.chat.viewmodel.ConversationsListViewModel
import org.linphone.ui.main.fragment.AbstractTopBarFragment
import org.linphone.ui.main.history.fragment.HistoryMenuDialogFragment
import org.linphone.utils.hideKeyboard
import org.linphone.utils.showKeyboard
class ConversationsListFragment : AbstractTopBarFragment() {
companion object {
private const val TAG = "[Conversations List Fragment]"
}
private lateinit var binding: ChatListFragmentBinding
private lateinit var listViewModel: ConversationsListViewModel
private lateinit var adapter: ConversationsListAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = ChatListFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listViewModel = requireActivity().run {
ViewModelProvider(this)[ConversationsListViewModel::class.java]
}
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = listViewModel
binding.setOnNewConversationClicked {
// TODO: open start conversation fragment
}
adapter = ConversationsListAdapter(viewLifecycleOwner)
binding.conversationsList.setHasFixedSize(true)
binding.conversationsList.adapter = adapter
val layoutManager = LinearLayoutManager(requireContext())
binding.conversationsList.layoutManager = layoutManager
adapter.conversationLongClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
val modalBottomSheet = ConversationDialogFragment(
model.isMuted.value == true,
model.isGroup,
{ // onDismiss
adapter.resetSelection()
},
{ // onMarkConversationAsRead
Log.i("$TAG Marking conversation [${model.id}] as read")
model.markAsRead()
},
{ // onToggleMute
Log.i("$TAG Changing mute status of conversation [${model.id}]")
model.toggleMute()
},
{ // onCall
Log.i("$TAG Calling conversation [${model.id}]")
model.call()
},
{ // onDeleteConversation
Log.i("$TAG Deleting conversation [${model.id}]")
model.delete()
listViewModel.applyFilter()
},
{ // onLeaveGroup
Log.i("$TAG Leaving group conversation [${model.id}]")
model.leaveGroup()
}
)
modalBottomSheet.show(parentFragmentManager, HistoryMenuDialogFragment.TAG)
}
}
adapter.conversationClickedEvent.observe(viewLifecycleOwner) {
it.consume { model ->
Log.i("$TAG Show conversation with ID [${model.id}]")
}
}
listViewModel.conversations.observe(viewLifecycleOwner) {
val currentCount = adapter.itemCount
adapter.submitList(it)
Log.i("$TAG Conversations list ready with [${it.size}] items")
if (currentCount < it.size) {
binding.conversationsList.scrollToPosition(0)
}
}
sharedViewModel.defaultAccountChangedEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i(
"$TAG Default account changed, updating avatar in top bar & re-computing conversations"
)
}
}
// TopBarFragment related
setViewModelAndTitle(
listViewModel,
getString(R.string.bottom_navigation_conversations_label)
)
listViewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
listViewModel.applyFilter(filter.trim())
}
listViewModel.focusSearchBarEvent.observe(viewLifecycleOwner) {
it.consume { show ->
if (show) {
// To automatically open keyboard
binding.topBar.search.showKeyboard()
} else {
binding.topBar.search.hideKeyboard()
}
}
}
}
}

View file

@ -0,0 +1,206 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.chat.model
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.ChatMessage
import org.linphone.core.ChatRoom
import org.linphone.core.ChatRoom.Capabilities
import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.utils.AppUtils
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.TimestampUtils
class ConversationModel @WorkerThread constructor(private val chatRoom: ChatRoom) {
companion object {
private const val TAG = "[Conversation Model]"
}
val id = LinphoneUtils.getChatRoomId(chatRoom)
val isGroup = !chatRoom.hasCapability(Capabilities.OneToOne.toInt())
val lastUpdateTime = MutableLiveData<Long>()
val isComposing = MutableLiveData<Boolean>()
val isMuted = MutableLiveData<Boolean>()
val isEphemeral = MutableLiveData<Boolean>()
val composingLabel = MutableLiveData<Boolean>()
val lastMessage = MutableLiveData<String>()
val lastMessageIcon = MutableLiveData<Int>()
val isLastMessageOutgoing = MutableLiveData<Boolean>()
val dateTime = MutableLiveData<String>()
val unreadMessageCount = MutableLiveData<Int>()
val avatarModel: ContactAvatarModel
init {
lastUpdateTime.postValue(chatRoom.lastUpdateTime)
val address = if (chatRoom.hasCapability(Capabilities.Basic.toInt())) {
Log.i("$TAG Chat room [$id] is 'Basic'")
chatRoom.peerAddress
} else {
val firstParticipant = chatRoom.participants.firstOrNull()
if (isGroup) {
Log.i("$TAG Group chat room [$id] has [${chatRoom.nbParticipants}] participant(s)")
} else {
Log.i(
"$TAG Chat room [$id] is with participant [${firstParticipant?.address?.asStringUriOnly()}]"
)
}
firstParticipant?.address ?: chatRoom.peerAddress
}
val friend = coreContext.contactsManager.findContactByAddress(address)
if (friend != null) {
avatarModel = ContactAvatarModel(friend)
} else {
val fakeFriend = coreContext.core.createFriend()
fakeFriend.address = address
avatarModel = ContactAvatarModel(fakeFriend)
}
isMuted.postValue(chatRoom.muted)
isEphemeral.postValue(chatRoom.isEphemeralEnabled)
updateLastMessage()
updateLastUpdatedTime()
unreadMessageCount.postValue(chatRoom.unreadMessagesCount)
}
@UiThread
fun markAsRead() {
coreContext.postOnCoreThread {
chatRoom.markAsRead()
unreadMessageCount.postValue(chatRoom.unreadMessagesCount)
Log.i("$TAG Conversation [$id] has been marked as read")
}
}
@UiThread
fun toggleMute() {
coreContext.postOnCoreThread {
chatRoom.muted = !chatRoom.muted
val muted = chatRoom.muted
if (muted) {
Log.i("$TAG Conversation [$id] is now muted")
} else {
Log.i("$TAG Conversation [$id] is no longer muted")
}
isMuted.postValue(muted)
}
}
@UiThread
fun call() {
coreContext.postOnCoreThread {
val address = chatRoom.participants.firstOrNull()?.address ?: chatRoom.peerAddress
Log.i("$TAG Calling [${address.asStringUriOnly()}]")
coreContext.startCall(address)
}
}
@UiThread
fun delete() {
coreContext.postOnCoreThread { core ->
core.deleteChatRoom(chatRoom)
Log.i("$TAG Conversation [$id] has been deleted")
}
}
@UiThread
fun leaveGroup() {
coreContext.postOnCoreThread {
chatRoom.leave()
Log.i("$TAG Group conversation [$id] has been leaved")
}
}
@WorkerThread
private fun updateLastMessage() {
val message = chatRoom.lastMessageInHistory
if (message != null) {
val text = LinphoneUtils.getTextDescribingMessage(message)
lastMessage.postValue(text)
val isOutgoing = message.isOutgoing
isLastMessageOutgoing.postValue(isOutgoing)
if (isOutgoing) {
val icon = when (message.state) {
ChatMessage.State.Displayed -> {
R.drawable.checks
}
ChatMessage.State.DeliveredToUser -> {
R.drawable.check
}
ChatMessage.State.Delivered -> {
R.drawable.sent
}
ChatMessage.State.InProgress, ChatMessage.State.FileTransferInProgress -> {
R.drawable.in_progress
}
ChatMessage.State.NotDelivered, ChatMessage.State.FileTransferError -> {
R.drawable.warning_circle
}
else -> {
R.drawable.info
}
}
lastMessageIcon.postValue(icon)
}
} else {
Log.w("$TAG No last message to display for chat room [$id]")
}
}
@WorkerThread
private fun updateLastUpdatedTime() {
val timestamp = chatRoom.lastUpdateTime
val humanReadableTimestamp = when {
TimestampUtils.isToday(timestamp) -> {
TimestampUtils.timeToString(chatRoom.lastUpdateTime)
}
TimestampUtils.isYesterday(timestamp) -> {
val time = TimestampUtils.timeToString(chatRoom.lastUpdateTime)
AppUtils.getFormattedString(R.string.conversation_yesterday_timestamp, time)
}
else -> {
TimestampUtils.toString(chatRoom.lastUpdateTime, onlyDate = true)
}
}
dateTime.postValue(humanReadableTimestamp)
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.main.chat.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.contacts.ContactsManager
import org.linphone.core.CallLog
import org.linphone.core.ChatRoom.Capabilities
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.ConversationModel
import org.linphone.ui.main.model.isInSecureMode
import org.linphone.ui.main.viewmodel.AbstractTopBarViewModel
import org.linphone.utils.LinphoneUtils
class ConversationsListViewModel @UiThread constructor() : AbstractTopBarViewModel() {
companion object {
private const val TAG = "[Conversations List ViewModel]"
}
val conversations = MutableLiveData<ArrayList<ConversationModel>>()
val fetchInProgress = MutableLiveData<Boolean>()
private var currentFilter = ""
private val coreListener = object : CoreListenerStub() {
override fun onCallLogUpdated(core: Core, callLog: CallLog) {
computeChatRoomsList(currentFilter)
}
}
private val contactsListener = object : ContactsManager.ContactsListener {
@WorkerThread
override fun onContactsLoaded() {
Log.i("$TAG Contacts have been (re)loaded, updating list")
computeChatRoomsList(currentFilter)
}
}
init {
coreContext.postOnCoreThread { core ->
coreContext.contactsManager.addListener(contactsListener)
core.addListener(coreListener)
computeChatRoomsList(currentFilter)
}
}
@UiThread
override fun onCleared() {
super.onCleared()
coreContext.postOnCoreThread { core ->
coreContext.contactsManager.removeListener(contactsListener)
core.removeListener(coreListener)
}
}
@UiThread
fun applyFilter(filter: String = currentFilter) {
currentFilter = filter
coreContext.postOnCoreThread {
computeChatRoomsList(currentFilter)
}
}
@WorkerThread
private fun computeChatRoomsList(filter: String) {
if (conversations.value.orEmpty().isEmpty()) {
fetchInProgress.postValue(true)
}
val list = arrayListOf<ConversationModel>()
var count = 0
// TODO? : Add support for chat rooms in magic search
val account = LinphoneUtils.getDefaultAccount()
val chatRooms = account?.chatRooms ?: coreContext.core.chatRooms
for (chatRoom in chatRooms) {
// TODO: remove when SDK will do it automatically
if (account?.isInSecureMode() == true) {
if (!chatRoom.hasCapability(Capabilities.Encrypted.toInt())) {
Log.w(
"$TAG Skipping chat room [${LinphoneUtils.getChatRoomId(chatRoom)}] as it is not E2E encrypted and default account requires it"
)
continue
}
}
val participants = chatRoom.participants
val found = participants.find {
it.address.asStringUriOnly().contains(filter)
}
if (found != null || chatRoom.peerAddress.asStringUriOnly().contains(filter)) {
val model = ConversationModel(chatRoom)
list.add(model)
count += 1
}
if (count == 20) {
conversations.postValue(list)
fetchInProgress.postValue(false)
}
}
conversations.postValue(list)
fetchInProgress.postValue(false)
}
}

View file

@ -130,6 +130,18 @@ 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)
}
}
}
}
override fun onResume() {

View file

@ -145,7 +145,7 @@ class ContactsListFragment : AbstractTopBarFragment() {
// TopBarFragment related
setViewModelAndTitle(listViewModel, "Contacts")
setViewModelAndTitle(listViewModel, getString(R.string.bottom_navigation_contacts_label))
listViewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
listViewModel.applyFilter(filter.trim())

View file

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

View file

@ -119,7 +119,7 @@ class HistoryFragment : GenericFragment() {
sharedViewModel.navigateToContactsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.historyFragment) {
// To prevent any previously seen contact to show up when navigating back to here later
// To prevent any previously seen call log to show up when navigating back to here later
binding.historyNavContainer.findNavController().popBackStack()
val action = HistoryFragmentDirections.actionHistoryFragmentToContactsFragment()
@ -127,6 +127,18 @@ class HistoryFragment : GenericFragment() {
}
}
}
sharedViewModel.navigateToConversationsEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.historyFragment) {
// To prevent any previously seen call log to show up when navigating back to here later
binding.historyNavContainer.findNavController().popBackStack()
val action = HistoryFragmentDirections.actionHistoryFragmentToConversationsFragment()
findNavController().navigate(action)
}
}
}
}
override fun onResume() {

View file

@ -191,7 +191,7 @@ class HistoryListFragment : AbstractTopBarFragment() {
// TopBarFragment related
setViewModelAndTitle(listViewModel, "Calls")
setViewModelAndTitle(listViewModel, getString(R.string.bottom_navigation_calls_label))
listViewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
listViewModel.applyFilter(filter.trim())

View file

@ -45,6 +45,10 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
MutableLiveData<Event<Boolean>>()
}
val navigateToConversationsEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
var currentlyDisplayedFragment = MutableLiveData<Int>()
/* Top bar related */

View file

@ -33,6 +33,7 @@ import org.linphone.core.Address
import org.linphone.core.Call
import org.linphone.core.Call.Dir
import org.linphone.core.Call.Status
import org.linphone.core.ChatMessage
import org.linphone.core.ChatRoom
import org.linphone.core.Core
import org.linphone.core.tools.Log
@ -239,5 +240,29 @@ class LinphoneUtils {
}
}
}
@WorkerThread
fun getTextDescribingMessage(message: ChatMessage): String {
// If message contains text, then use that
var text = message.contents.find { content -> content.isText }?.utf8Text ?: ""
if (text.isEmpty()) {
val firstContent = message.contents.firstOrNull()
if (firstContent?.isIcalendar == true) {
text = "meeting invite" // TODO: use translated string
} else if (firstContent?.isVoiceRecording == true) {
text = "voice message" // TODO: use translated string
} else {
for (content in message.contents) {
if (text.isNotEmpty()) {
text += ", "
}
text += content.name
}
}
}
return text
}
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M168,224a8,8 0,0 1,-8 8L96,232a8,8 0,1 1,0 -16h64A8,8 0,0 1,168 224ZM221.85,192A15.8,15.8 0,0 1,208 200L48,200a16,16 0,0 1,-13.8 -24.06C39.75,166.38 48,139.34 48,104a80,80 0,1 1,160 0c0,35.33 8.26,62.38 13.81,71.94A15.89,15.89 0,0 1,221.84 192ZM208,184c-7.73,-13.27 -16,-43.95 -16,-80a64,64 0,1 0,-128 0c0,36.06 -8.28,66.74 -16,80Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M53.92,34.62A8,8 0,1 0,42.08 45.38L58.82,63.8A79.59,79.59 0,0 0,48 104c0,35.34 -8.26,62.38 -13.81,71.94A16,16 0,0 0,48 200L182.64,200l19.44,21.38a8,8 0,1 0,11.84 -10.76ZM48,184c7.7,-13.24 16,-43.92 16,-80a63.65,63.65 0,0 1,6.26 -27.62L168.09,184ZM168,224a8,8 0,0 1,-8 8L96,232a8,8 0,0 1,0 -16h64A8,8 0,0 1,168 224ZM214,179.25a8.13,8.13 0,0 1,-2.93 0.55,8 8,0 0,1 -7.44,-5.08C196.35,156.19 192,129.75 192,104A64,64 0,0 0,96.43 48.31a8,8 0,0 1,-7.9 -13.91A80,80 0,0 1,208 104c0,35.35 8.05,58.59 10.52,64.88A8,8 0,0 1,214 179.25Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M149.61,85.71l-89.6,88a8,8 0,0 1,-11.22 0L10.39,136a8,8 0,1 1,11.22 -11.41L54.4,156.79l84,-82.5a8,8 0,1 1,11.22 11.42ZM245.71,74.39a8,8 0,0 0,-11.32 -0.1l-84,82.5 -18.83,-18.5a8,8 0,0 0,-11.21 11.42l24.43,24a8,8 0,0 0,11.22 0l89.6,-88A8,8 0,0 0,245.71 74.39Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M232,136.66A104.12,104.12 0,1 1,119.34 24,8 8,0 0,1 120.66,40 88.12,88.12 0,1 0,216 135.34,8 8,0 0,1 232,136.66ZM120,72v56a8,8 0,0 0,8 8h56a8,8 0,0 0,0 -16L136,120L136,72a8,8 0,0 0,-16 0ZM160,48a12,12 0,1 0,-12 -12A12,12 0,0 0,160 48ZM196,72a12,12 0,1 0,-12 -12A12,12 0,0 0,196 72ZM220,108a12,12 0,1 0,-12 -12A12,12 0,0 0,220 108Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.692,3.692V6.462C12.692,6.645 12.619,6.821 12.49,6.951C12.36,7.081 12.184,7.154 12,7.154C11.816,7.154 11.64,7.081 11.51,6.951C11.381,6.821 11.308,6.645 11.308,6.462V3.692C11.308,3.509 11.381,3.333 11.51,3.203C11.64,3.073 11.816,3 12,3C12.184,3 12.36,3.073 12.49,3.203C12.619,3.333 12.692,3.509 12.692,3.692ZM15.916,8.776C16.007,8.776 16.097,8.758 16.181,8.724C16.265,8.689 16.341,8.637 16.406,8.573L18.364,6.616C18.494,6.486 18.567,6.309 18.567,6.126C18.567,5.942 18.494,5.766 18.364,5.636C18.234,5.506 18.058,5.433 17.874,5.433C17.691,5.433 17.514,5.506 17.384,5.636L15.427,7.594C15.33,7.691 15.264,7.814 15.237,7.949C15.21,8.083 15.224,8.222 15.276,8.349C15.329,8.475 15.417,8.583 15.531,8.66C15.645,8.736 15.779,8.776 15.916,8.776ZM20.308,11.308H17.538C17.355,11.308 17.179,11.381 17.049,11.51C16.919,11.64 16.846,11.816 16.846,12C16.846,12.184 16.919,12.36 17.049,12.49C17.179,12.619 17.355,12.692 17.538,12.692H20.308C20.491,12.692 20.667,12.619 20.797,12.49C20.927,12.36 21,12.184 21,12C21,11.816 20.927,11.64 20.797,11.51C20.667,11.381 20.491,11.308 20.308,11.308ZM16.406,15.427C16.275,15.303 16.101,15.234 15.92,15.237C15.739,15.239 15.567,15.312 15.439,15.439C15.312,15.567 15.239,15.739 15.237,15.92C15.234,16.101 15.303,16.275 15.427,16.406L17.384,18.364C17.514,18.494 17.691,18.567 17.874,18.567C18.058,18.567 18.234,18.494 18.364,18.364C18.494,18.234 18.567,18.058 18.567,17.874C18.567,17.691 18.494,17.514 18.364,17.384L16.406,15.427ZM12,16.846C11.816,16.846 11.64,16.919 11.51,17.049C11.381,17.179 11.308,17.355 11.308,17.538V20.308C11.308,20.491 11.381,20.667 11.51,20.797C11.64,20.927 11.816,21 12,21C12.184,21 12.36,20.927 12.49,20.797C12.619,20.667 12.692,20.491 12.692,20.308V17.538C12.692,17.355 12.619,17.179 12.49,17.049C12.36,16.919 12.184,16.846 12,16.846ZM7.594,15.427L5.636,17.384C5.506,17.514 5.433,17.691 5.433,17.874C5.433,18.058 5.506,18.234 5.636,18.364C5.766,18.494 5.942,18.567 6.126,18.567C6.309,18.567 6.486,18.494 6.616,18.364L8.573,16.406C8.697,16.275 8.766,16.101 8.763,15.92C8.761,15.739 8.688,15.567 8.561,15.439C8.433,15.312 8.261,15.239 8.08,15.237C7.899,15.234 7.725,15.303 7.594,15.427ZM7.154,12C7.154,11.816 7.081,11.64 6.951,11.51C6.821,11.381 6.645,11.308 6.462,11.308H3.692C3.509,11.308 3.333,11.381 3.203,11.51C3.073,11.64 3,11.816 3,12C3,12.184 3.073,12.36 3.203,12.49C3.333,12.619 3.509,12.692 3.692,12.692H6.462C6.645,12.692 6.821,12.619 6.951,12.49C7.081,12.36 7.154,12.184 7.154,12ZM6.616,5.636C6.486,5.506 6.309,5.433 6.126,5.433C5.942,5.433 5.766,5.506 5.636,5.636C5.506,5.766 5.433,5.942 5.433,6.126C5.433,6.309 5.506,6.486 5.636,6.616L7.594,8.573C7.725,8.697 7.899,8.766 8.08,8.763C8.261,8.761 8.433,8.688 8.561,8.561C8.688,8.433 8.761,8.261 8.763,8.08C8.766,7.899 8.697,7.725 8.573,7.594L6.616,5.636Z"
android:fillColor="#FE5E00"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="20dp"
android:viewportWidth="22"
android:viewportHeight="20">
<path
android:pathData="M17.808,0.5H1.192C1.009,0.5 0.833,0.573 0.703,0.703C0.573,0.833 0.5,1.009 0.5,1.192V12.962C0.5,13.329 0.646,13.681 0.906,13.941C1.165,14.2 1.517,14.346 1.885,14.346H17.115C17.483,14.346 17.835,14.2 18.094,13.941C18.354,13.681 18.5,13.329 18.5,12.962V1.192C18.5,1.009 18.427,0.833 18.297,0.703C18.167,0.573 17.991,0.5 17.808,0.5ZM16.028,1.885L9.5,7.869L2.972,1.885H16.028ZM17.115,12.962H1.885V2.766L9.032,9.318C9.16,9.436 9.327,9.501 9.5,9.501C9.673,9.501 9.84,9.436 9.968,9.318L17.115,2.766V12.962Z"
android:fillColor="#FE5E00"/>
<path
android:pathData="M15.5,13.5m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
android:fillColor="#ffffff"/>
<path
android:pathData="M15.5,8.5C14.511,8.5 13.544,8.793 12.722,9.343C11.9,9.892 11.259,10.673 10.881,11.587C10.502,12.5 10.403,13.505 10.596,14.476C10.789,15.445 11.265,16.336 11.965,17.035C12.664,17.735 13.555,18.211 14.524,18.404C15.495,18.597 16.5,18.498 17.413,18.119C18.327,17.741 19.108,17.1 19.657,16.278C20.207,15.456 20.5,14.489 20.5,13.5C20.499,12.174 19.971,10.903 19.034,9.966C18.097,9.029 16.826,8.501 15.5,8.5ZM17.695,13.772L16.157,15.311C16.085,15.383 15.987,15.423 15.885,15.423C15.783,15.423 15.685,15.383 15.613,15.311C15.54,15.238 15.5,15.141 15.5,15.038C15.5,14.936 15.54,14.839 15.613,14.766L16.495,13.885H13.577C13.475,13.885 13.377,13.844 13.305,13.772C13.233,13.7 13.192,13.602 13.192,13.5C13.192,13.398 13.233,13.3 13.305,13.228C13.377,13.156 13.475,13.115 13.577,13.115H16.495L15.613,12.234C15.54,12.161 15.5,12.064 15.5,11.962C15.5,11.859 15.54,11.762 15.613,11.689C15.685,11.617 15.783,11.577 15.885,11.577C15.987,11.577 16.085,11.617 16.157,11.689L17.695,13.228C17.731,13.264 17.759,13.306 17.779,13.353C17.798,13.399 17.808,13.45 17.808,13.5C17.808,13.55 17.798,13.601 17.779,13.647C17.759,13.694 17.731,13.736 17.695,13.772Z"
android:fillColor="#FE5E00"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M112,216a8,8 0,0 1,-8 8L48,224a16,16 0,0 1,-16 -16L32,48A16,16 0,0 1,48 32h56a8,8 0,0 1,0 16L48,48L48,208h56A8,8 0,0 1,112 216ZM221.66,122.34 L181.66,82.34a8,8 0,0 0,-11.32 11.32L196.69,120L104,120a8,8 0,0 0,0 16h92.69l-26.35,26.34a8,8 0,0 0,11.32 11.32l40,-40A8,8 0,0 0,221.66 122.34Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -79,7 +79,6 @@
style="@style/bottom_nav_bar_label_style"
android:id="@+id/conversations"
android:visibility="@{viewModel.hideConversations ? View.GONE : View.VISIBLE}"
android:enabled="false"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:drawableTop="@drawable/chat_text"

View file

@ -0,0 +1,42 @@
<?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" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.slidingpanelayout.widget.SlidingPaneLayout
android:id="@+id/sliding_pane_layout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/conversations_list"
android:name="org.linphone.ui.main.chat.fragment.ConversationsListFragment"
android:layout_width="@dimen/sliding_pane_left_fragment_with_nav_width"
android:layout_height="match_parent"
app:layout="@layout/chat_list_fragment"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/chat_nav_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:defaultNavHost="false"
app:navGraph="@navigation/chat_nav_graph"/>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,122 @@
<?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="filterClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.chat.viewmodel.ConversationsListViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/orange_main_500">
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="no_conversation_image, no_conversation_label"
android:visibility="@{viewModel.conversations.empty ? View.VISIBLE : View.GONE}" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/bottom_nav_bar"
android:name="org.linphone.ui.main.fragment.BottomNavBarFragment"
android:layout_width="75dp"
android:layout_height="0dp"
bind:layout="@layout/bottom_nav_bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<include
android:id="@+id/top_bar"
layout="@layout/top_bar"
bind:viewModel="@{viewModel}"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:layout_marginEnd="9dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/top_bar"/>
<ImageView
android:id="@+id/no_conversation_image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:src="@drawable/illu"
android:layout_margin="10dp"
app:layout_constraintHeight_max="200dp"
app:layout_constraintBottom_toTopOf="@id/no_conversation_label"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/background" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/no_conversation_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/conversations_list_empty"
app:layout_constraintBottom_toTopOf="@id/background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintTop_toBottomOf="@id/no_conversation_image" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/conversations_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/background"
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/gray_main2_700"
app:backgroundTint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.fetchInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -87,7 +87,6 @@
style="@style/bottom_nav_bar_label_style"
android:onClick="@{onConversationsClicked}"
android:visibility="@{viewModel.hideConversations ? View.GONE : View.VISIBLE}"
android:enabled="false"
android:id="@+id/conversations"
android:layout_width="0dp"
android:layout_height="wrap_content"

View file

@ -0,0 +1,32 @@
<?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" />
</data>
<androidx.slidingpanelayout.widget.SlidingPaneLayout
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/conversations_list"
android:name="org.linphone.ui.main.chat.fragment.ConversationsListFragment"
android:layout_width="@dimen/sliding_pane_left_fragment_width"
android:layout_height="match_parent"
app:layout="@layout/chat_list_fragment"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/chat_nav_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="@dimen/sliding_pane_right_fragment_width"
android:layout_height="match_parent"
android:layout_weight="1"
app:defaultNavHost="false"
app:navGraph="@navigation/chat_nav_graph"/>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
</layout>

View file

@ -0,0 +1,195 @@
<?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" />
<import type="org.linphone.core.ConsolidatedPresence" />
<import type="org.linphone.core.ChatRoom.SecurityLevel" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.ConversationModel" />
<variable
name="onClickListener"
type="View.OnClickListener" />
<variable
name="onLongClickListener"
type="View.OnLongClickListener" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{onClickListener}"
android:onLongClickListener="@{onLongClickListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/primary_cell_background">
<io.getstream.avatarview.AvatarView
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_list_cell_size"
android:layout_height="@dimen/avatar_list_cell_size"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:layout_marginStart="5dp"
android:adjustViewBounds="true"
android:background="@drawable/shape_circle_light_blue_background"
contactAvatar="@{model.avatarModel}"
app:avatarViewPlaceholder="@drawable/user_circle"
app:avatarViewInitialsBackgroundColor="@color/gray_main2_200"
app:avatarViewInitialsTextColor="@color/gray_main2_600"
app:avatarViewInitialsTextSize="16sp"
app:avatarViewInitialsTextStyle="bold"
app:avatarViewShape="circle"
app:avatarViewBorderWidth="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/presence_badge"
android:layout_width="@dimen/avatar_presence_badge_size"
android:layout_height="@dimen/avatar_presence_badge_size"
android:layout_marginEnd="@dimen/avatar_presence_badge_end_margin"
android:background="@drawable/led_background"
android:padding="@dimen/avatar_presence_badge_padding"
app:presenceIcon="@{model.avatarModel.presenceStatus}"
android:visibility="@{model.avatarModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<ImageView
android:id="@+id/trust_badge"
android:layout_width="@dimen/avatar_presence_badge_size"
android:layout_height="@dimen/avatar_presence_badge_size"
android:src="@{model.avatarModel.trust == SecurityLevel.Encrypted ? @drawable/trusted : @drawable/not_trusted, default=@drawable/trusted}"
android:visibility="@{model.avatarModel.trust == SecurityLevel.Encrypted || model.avatarModel.trust == SecurityLevel.Unsafe ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/right_border"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start"
app:constraint_referenced_ids="notifications_count, date_time" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="5dp"
android:maxLines="1"
android:ellipsize="end"
android:text="@{model.avatarModel.name, default=`John Doe`}"
android:textSize="14sp"
android:textColor="@color/gray_main2_800"
android:textStyle="@{model.unreadMessageCount > 0 ? Typeface.BOLD : Typeface.NORMAL}"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/right_border"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/last_message_or_composing"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/last_message_or_composing"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginEnd="5dp"
android:gravity="center_vertical|start"
android:maxLines="2"
android:ellipsize="end"
android:text="@{model.isComposing ? model.composingLabel : model.lastMessage, default=`Hello there!`}"
android:textSize="14sp"
android:textColor="@color/gray_main2_400"
android:textStyle="@{model.unreadMessageCount > 0 ? Typeface.BOLD : Typeface.NORMAL}"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintEnd_toStartOf="@id/right_border"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_constraintBottom_toTopOf="@id/separator" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/notifications_count"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:background="@drawable/shape_red_round"
android:text="@{String.valueOf(model.unreadMessageCount), default=`1`}"
android:textColor="@color/white"
android:textSize="13sp"
android:visibility="@{model.unreadMessageCount > 0 ? View.VISIBLE : View.GONE}"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/name"
app:layout_constraintEnd_toStartOf="@id/date_time"/>
<ImageView
android:id="@+id/last_sent_message_status"
android:layout_width="@dimen/small_icon_size"
android:layout_height="@dimen/small_icon_size"
android:layout_marginEnd="10dp"
android:src="@{model.lastMessageIcon, default=@drawable/check}"
android:visibility="@{model.isLastMessageOutgoing ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/name"
app:layout_constraintBottom_toBottomOf="@id/name"
app:tint="@color/orange_main_500" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:id="@+id/date_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@{model.dateTime, default=`16:45`}"
android:textSize="12sp"
android:textColor="@color/gray_main2_500"
app:layout_constraintEnd_toStartOf="@id/last_sent_message_status"
app:layout_constraintTop_toTopOf="@id/last_sent_message_status"
app:layout_constraintBottom_toBottomOf="@id/last_sent_message_status" />
<ImageView
android:id="@+id/muted"
android:layout_width="@dimen/small_icon_size"
android:layout_height="@dimen/small_icon_size"
android:layout_marginEnd="10dp"
android:src="@drawable/bell_simple_slash"
android:visibility="@{model.isMuted ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/date_time"
app:layout_constraintBottom_toTopOf="@id/separator"
app:tint="@color/gray_main2_400" />
<ImageView
android:id="@+id/ephemeral"
android:layout_width="@dimen/small_icon_size"
android:layout_height="@dimen/small_icon_size"
android:layout_marginEnd="10dp"
android:src="@drawable/clock_countdown"
android:visibility="@{model.isEphemeral ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toStartOf="@id/muted"
app:layout_constraintTop_toBottomOf="@id/date_time"
app:layout_constraintBottom_toTopOf="@id/separator"
app:tint="@color/gray_main2_400" />
<View
android:id="@+id/separator"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginEnd="10dp"
android:background="@color/gray_main2_200"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,122 @@
<?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="filterClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.chat.viewmodel.ConversationsListViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/orange_main_500">
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="no_conversation_image, no_conversation_label"
android:visibility="@{viewModel.conversations.empty ? View.VISIBLE : View.GONE}" />
<include
android:id="@+id/top_bar"
layout="@layout/top_bar"
bind:viewModel="@{viewModel}"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:layout_marginEnd="9dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@color/white"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar" />
<ImageView
android:id="@+id/no_conversation_image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:src="@drawable/illu"
android:layout_margin="10dp"
app:layout_constraintHeight_max="200dp"
app:layout_constraintBottom_toTopOf="@id/no_conversation_label"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/background" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/section_header_style"
android:id="@+id/no_conversation_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/conversations_list_empty"
app:layout_constraintBottom_toTopOf="@id/background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_conversation_image" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/conversations_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/background"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/bottom_nav_bar"
android:name="org.linphone.ui.main.fragment.BottomNavBarFragment"
android:layout_width="0dp"
android:layout_height="wrap_content"
bind:layout="@layout/bottom_nav_bar"
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/gray_main2_700"
app:backgroundTint="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.fetchInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,109 @@
<?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="toggleMuteClickListener"
type="View.OnClickListener" />
<variable
name="callClickListener"
type="View.OnClickListener" />
<variable
name="deleteClickListener"
type="View.OnClickListener" />
<variable
name="leaveClickListener"
type="View.OnClickListener" />
<variable
name="isMuted"
type="Boolean" />
<variable
name="isGroup"
type="Boolean" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/gray_main2_200">
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{markAsReadClickListener}"
style="@style/context_menu_action_label_style"
android:id="@+id/mark_as_read"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/conversation_action_mark_as_read"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
android:drawableStart="@drawable/envelope_simple_open"
app:layout_constraintBottom_toTopOf="@id/mute"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{toggleMuteClickListener}"
style="@style/context_menu_action_label_style"
android:id="@+id/mute"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{isMuted ? @string/conversation_action_unmute : @string/conversation_action_mute, default=@string/conversation_action_mute}"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
android:drawableStart="@{isMuted ? @drawable/bell_simple : @drawable/bell_simple_slash, default=@drawable/bell_simple_slash}"
app:layout_constraintBottom_toTopOf="@id/call"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{callClickListener}"
style="@style/context_menu_action_label_style"
android:id="@+id/call"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/conversation_action_call"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
android:drawableStart="@drawable/phone"
android:visibility="@{isGroup ? View.GONE : View.VISIBLE}"
app:layout_constraintBottom_toTopOf="@id/delete_chat_room"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{deleteClickListener}"
style="@style/context_menu_danger_action_label_style"
android:id="@+id/delete_chat_room"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/conversation_action_delete"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
android:drawableStart="@drawable/trash_simple"
app:layout_constraintBottom_toTopOf="@id/leave_group"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{leaveClickListener}"
style="@style/context_menu_action_label_style"
android:visibility="@{isGroup ? View.VISIBLE : View.GONE}"
android:id="@+id/leave_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/conversation_action_leave_group"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
android:drawableStart="@drawable/sign_out"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/chat_nav_graph"
app:startDestination="@id/emptyFragment">
<fragment
android:id="@+id/emptyFragment"
android:name="org.linphone.ui.main.fragment.EmptyFragment"
android:label="EmptyFragment"
tools:layout="@layout/empty_fragment"/>
<fragment
android:id="@+id/conversationsListFragment"
android:name="org.linphone.ui.main.chat.fragment.ConversationsListFragment"
android:label="ConversationsListFragment"
tools:layout="@layout/chat_list_fragment"/>
</navigation>

View file

@ -17,6 +17,12 @@
app:launchSingleTop="true"
app:popUpTo="@id/contactsFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_contactsFragment_to_conversationsFragment"
app:destination="@id/conversationsFragment"
app:launchSingleTop="true"
app:popUpTo="@id/contactsFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
@ -30,6 +36,12 @@
app:launchSingleTop="true"
app:popUpTo="@id/historyFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_historyFragment_to_conversationsFragment"
app:destination="@id/conversationsFragment"
app:launchSingleTop="true"
app:popUpTo="@id/historyFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
@ -168,5 +180,23 @@
android:name="accountIdentity"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/conversationsFragment"
android:name="org.linphone.ui.main.chat.fragment.ConversationsFragment"
android:label="ConversationsFragment"
tools:layout="@layout/chat_fragment" >
<action
android:id="@+id/action_conversationsFragment_to_historyFragment"
app:destination="@id/historyFragment"
app:launchSingleTop="true"
app:popUpTo="@id/conversationsFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_conversationsFragment_to_contactsFragment"
app:destination="@id/contactsFragment"
app:launchSingleTop="true"
app:popUpTo="@id/conversationsFragment"
app:popUpToInclusive="true" />
</fragment>
</navigation>

View file

@ -10,6 +10,7 @@
<dimen name="sliding_pane_left_fragment_with_nav_width">355dp</dimen>
<dimen name="sliding_pane_right_fragment_width">300dp</dimen>
<dimen name="small_icon_size">14dp</dimen>
<dimen name="icon_size">24dp</dimen>
<dimen name="welcome_icon_size">100dp</dimen>

View file

@ -317,6 +317,15 @@
<string name="call_ended">Call ended</string>
<string name="call_incoming_for_account">Incoming call for %s</string>
<string name="conversations_list_empty">No conversation for the moment…</string>
<string name="conversation_action_mark_as_read">Mark as read</string>
<string name="conversation_action_mute">Mute</string>
<string name="conversation_action_unmute">Un-mute</string>
<string name="conversation_action_call">Call</string>
<string name="conversation_action_delete">Delete conversation</string>
<string name="conversation_action_leave_group">Leave the group</string>
<string name="conversation_yesterday_timestamp">Yesterday at %s</string>
<string name="operation_in_progress_overlay">Operation in progress, please wait</string>
<string name="call_action_blind_transfer">Transfer</string>