mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Started to display conversations list with long press menu
This commit is contained in:
parent
3f24e73978
commit
0d880dda50
33 changed files with 1611 additions and 32 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
app/src/main/res/drawable/bell_simple.xml
Normal file
9
app/src/main/res/drawable/bell_simple.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/bell_simple_slash.xml
Normal file
9
app/src/main/res/drawable/bell_simple_slash.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/checks.xml
Normal file
9
app/src/main/res/drawable/checks.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/clock_countdown.xml
Normal file
9
app/src/main/res/drawable/clock_countdown.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/in_progress.xml
Normal file
9
app/src/main/res/drawable/in_progress.xml
Normal 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>
|
||||
15
app/src/main/res/drawable/sent.xml
Normal file
15
app/src/main/res/drawable/sent.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/sign_out.xml
Normal file
9
app/src/main/res/drawable/sign_out.xml
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
42
app/src/main/res/layout-land/chat_fragment.xml
Normal file
42
app/src/main/res/layout-land/chat_fragment.xml
Normal 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>
|
||||
122
app/src/main/res/layout-land/chat_list_fragment.xml
Normal file
122
app/src/main/res/layout-land/chat_list_fragment.xml
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
32
app/src/main/res/layout/chat_fragment.xml
Normal file
32
app/src/main/res/layout/chat_fragment.xml
Normal 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>
|
||||
195
app/src/main/res/layout/chat_list_cell.xml
Normal file
195
app/src/main/res/layout/chat_list_cell.xml
Normal 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>
|
||||
122
app/src/main/res/layout/chat_list_fragment.xml
Normal file
122
app/src/main/res/layout/chat_list_fragment.xml
Normal 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>
|
||||
109
app/src/main/res/layout/chat_long_press_menu.xml
Normal file
109
app/src/main/res/layout/chat_long_press_menu.xml
Normal 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>
|
||||
19
app/src/main/res/navigation/chat_nav_graph.xml
Normal file
19
app/src/main/res/navigation/chat_nav_graph.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue