diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt
index 80a4749f0..e436f810a 100644
--- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt
+++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt
@@ -233,6 +233,28 @@ class CallActivity : GenericActivity() {
}
}
+ override fun onStart() {
+ super.onStart()
+
+ findNavController(R.id.call_nav_container).addOnDestinationChangedListener { _, destination, _ ->
+ val showTopBar = when (destination.id) {
+ R.id.inCallConversationFragment, R.id.transferCallFragment, R.id.newCallFragment -> true
+ else -> false
+ }
+ callsViewModel.showTopBar.postValue(showTopBar)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ val isInPipMode = isInPictureInPictureMode
+ if (::callViewModel.isInitialized) {
+ Log.i("$TAG onResume: is in PiP mode? $isInPipMode")
+ callViewModel.pipMode.value = isInPipMode
+ }
+ }
+
override fun onPause() {
super.onPause()
@@ -249,16 +271,6 @@ class CallActivity : GenericActivity() {
}
}
- override fun onResume() {
- super.onResume()
-
- val isInPipMode = isInPictureInPictureMode
- if (::callViewModel.isInitialized) {
- Log.i("$TAG onResume: is in PiP mode? $isInPipMode")
- callViewModel.pipMode.value = isInPipMode
- }
- }
-
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
@@ -323,7 +335,7 @@ class CallActivity : GenericActivity() {
)
}
- private fun showRedToast(
+ fun showRedToast(
message: String,
@DrawableRes icon: Int,
duration: Long = 4000,
diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt
index eca76502b..f992ee69d 100644
--- a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt
+++ b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt
@@ -330,6 +330,33 @@ class ActiveCallFragment : GenericCallFragment() {
}
}
}
+
+ callViewModel.chatRoomCreationErrorEvent.observe(viewLifecycleOwner) {
+ it.consume { error ->
+ (requireActivity() as CallActivity).showRedToast(
+ error,
+ R.drawable.x
+ )
+ }
+ }
+
+ callViewModel.goToConversationEvent.observe(viewLifecycleOwner) {
+ it.consume { pair ->
+ if (findNavController().currentDestination?.id == R.id.activeCallFragment) {
+ val localSipUri = pair.first
+ val remoteSipUri = pair.second
+ Log.i(
+ "$TAG Display conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
+ )
+ val action =
+ ActiveCallFragmentDirections.actionActiveCallFragmentToInCallConversationFragment(
+ localSipUri,
+ remoteSipUri
+ )
+ findNavController().navigate(action)
+ }
+ }
+ }
}
@SuppressLint("ClickableViewAccessibility")
diff --git a/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt
index fdec459db..4edac46e2 100644
--- a/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt
+++ b/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt
@@ -99,7 +99,7 @@ class CallsListFragment : GenericCallFragment() {
}
binding.setMergeCallsClickListener {
- viewModel.mergeCallsIntoLocalConference()
+ viewModel.mergeCallsIntoConference()
}
viewModel.calls.observe(viewLifecycleOwner) {
diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ConversationFragment.kt
new file mode 100644
index 000000000..59f70b25e
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/call/fragment/ConversationFragment.kt
@@ -0,0 +1,780 @@
+/*
+ * Copyright (c) 2010-2024 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 .
+ */
+package org.linphone.ui.call.fragment
+
+import android.app.Dialog
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager
+import androidx.annotation.UiThread
+import androidx.core.view.doOnPreDraw
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import com.google.android.material.tabs.TabLayout
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.compatibility.Compatibility
+import org.linphone.core.ChatMessage
+import org.linphone.core.tools.Log
+import org.linphone.databinding.ChatBubbleLongPressMenuBinding
+import org.linphone.databinding.ChatConversationFragmentBinding
+import org.linphone.ui.call.CallActivity
+import org.linphone.ui.main.chat.ConversationScrollListener
+import org.linphone.ui.main.chat.adapter.ConversationEventAdapter
+import org.linphone.ui.main.chat.adapter.MessageBottomSheetAdapter
+import org.linphone.ui.main.chat.fragment.ConversationFragmentArgs
+import org.linphone.ui.main.chat.fragment.EndToEndEncryptionDetailsDialogFragment
+import org.linphone.ui.main.chat.model.MessageDeliveryModel
+import org.linphone.ui.main.chat.model.MessageModel
+import org.linphone.ui.main.chat.model.MessageReactionsModel
+import org.linphone.ui.main.chat.view.RichEditText
+import org.linphone.ui.main.chat.viewmodel.ConversationViewModel
+import org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel
+import org.linphone.utils.RecyclerViewHeaderDecoration
+import org.linphone.utils.RecyclerViewSwipeUtils
+import org.linphone.utils.RecyclerViewSwipeUtilsCallback
+import org.linphone.utils.addCharacterAtPosition
+import org.linphone.utils.hideKeyboard
+import org.linphone.utils.setKeyboardInsetListener
+import org.linphone.utils.showKeyboard
+
+class ConversationFragment : GenericCallFragment() {
+ companion object {
+ private const val TAG = "[In-call Conversation Fragment]"
+ }
+
+ private lateinit var binding: ChatConversationFragmentBinding
+
+ private lateinit var viewModel: ConversationViewModel
+
+ private lateinit var sendMessageViewModel: SendMessageInConversationViewModel
+
+ private lateinit var adapter: ConversationEventAdapter
+
+ private lateinit var bottomSheetAdapter: MessageBottomSheetAdapter
+
+ private var messageLongPressDialog: Dialog? = null
+
+ private val args: ConversationFragmentArgs by navArgs()
+
+ private var bottomSheetDialog: BottomSheetDialogFragment? = null
+
+ private val dataObserver = object : RecyclerView.AdapterDataObserver() {
+ override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
+ if (positionStart > 0) {
+ adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
+ }
+
+ if (viewModel.isUserScrollingUp.value == true) {
+ Log.i(
+ "$TAG [$itemCount] events have been loaded but user was scrolling up in conversation, do not scroll"
+ )
+ return
+ }
+
+ if (positionStart == 0 && adapter.itemCount == itemCount) {
+ // First time we fill the list with messages
+ Log.i(
+ "$TAG [$itemCount] events have been loaded"
+ )
+ } else {
+ Log.i(
+ "$TAG [$itemCount] new events have been loaded, scrolling to first unread message"
+ )
+ scrollToFirstUnreadMessageOrBottom()
+ }
+ }
+ }
+
+ private lateinit var scrollListener: ConversationScrollListener
+
+ private lateinit var headerItemDecoration: RecyclerViewHeaderDecoration
+
+ private val listItemTouchListener = object : RecyclerView.OnItemTouchListener {
+ override fun onInterceptTouchEvent(
+ rv: RecyclerView,
+ e: MotionEvent
+ ): Boolean {
+ // Following code is only to detect click on header at position 0
+ if (::headerItemDecoration.isInitialized) {
+ if (e.action == MotionEvent.ACTION_UP) {
+ if ((rv.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() == 0) {
+ if (e.y >= 0 && e.y <= headerItemDecoration.getDecorationHeight(0)) {
+ showEndToEndEncryptionDetailsBottomSheet()
+ return true
+ }
+ }
+ }
+ }
+ return false
+ }
+
+ override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) { }
+
+ override fun onRequestDisallowInterceptTouchEvent(
+ disallowIntercept: Boolean
+ ) { }
+ }
+
+ private var currentChatMessageModelForBottomSheet: MessageModel? = null
+
+ private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
+ currentChatMessageModelForBottomSheet?.isSelected?.value = false
+ }
+ }
+
+ override fun onSlide(bottomSheet: View, slideOffset: Float) { }
+ }
+
+ private var bottomSheetDeliveryModel: MessageDeliveryModel? = null
+
+ private var bottomSheetReactionsModel: MessageReactionsModel? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ adapter = ConversationEventAdapter()
+ headerItemDecoration = RecyclerViewHeaderDecoration(
+ requireContext(),
+ adapter,
+ false
+ )
+ bottomSheetAdapter = MessageBottomSheetAdapter()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = ChatConversationFragmentBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ postponeEnterTransition()
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.lifecycleOwner = viewLifecycleOwner
+
+ viewModel = ViewModelProvider(this)[ConversationViewModel::class.java]
+ sendMessageViewModel =
+ ViewModelProvider(this)[SendMessageInConversationViewModel::class.java]
+
+ viewModel.isInCallConversation.value = true
+ binding.viewModel = viewModel
+
+ sendMessageViewModel.isInCallConversation.value = true
+ binding.sendMessageViewModel = sendMessageViewModel
+
+ binding.setBackClickListener {
+ findNavController().popBackStack()
+ }
+
+ binding.eventsList.setHasFixedSize(true)
+ val layoutManager = LinearLayoutManager(requireContext())
+ layoutManager.stackFromEnd = true
+ binding.eventsList.layoutManager = layoutManager
+
+ if (binding.eventsList.adapter != adapter) {
+ binding.eventsList.adapter = adapter
+ }
+
+ val callbacks = RecyclerViewSwipeUtilsCallback(
+ R.drawable.reply,
+ ConversationEventAdapter.EventViewHolder::class.java
+ ) { viewHolder ->
+ val index = viewHolder.bindingAdapterPosition
+ if (index < 0 || index >= adapter.currentList.size) {
+ Log.e("$TAG Swipe viewHolder index [$index] is out of bounds!")
+ } else {
+ adapter.notifyItemChanged(index)
+ if (viewModel.isReadOnly.value == true || viewModel.isDisabledBecauseNotSecured.value == true) {
+ Log.w("$TAG Do not handle swipe action because conversation is read only")
+ return@RecyclerViewSwipeUtilsCallback
+ }
+
+ val chatMessageEventLog = adapter.currentList[index]
+ val chatMessageModel = (chatMessageEventLog.model as? MessageModel)
+ if (chatMessageModel != null) {
+ sendMessageViewModel.replyToMessage(chatMessageModel)
+ // Open keyboard & focus edit text
+ binding.sendArea.messageToSend.showKeyboard()
+ } else {
+ Log.e(
+ "$TAG Can't reply, failed to get a ChatMessageModel from adapter item #[$index]"
+ )
+ }
+ }
+ }
+ RecyclerViewSwipeUtils(callbacks).attachToRecyclerView(binding.eventsList)
+
+ val localSipUri = args.localSipUri
+ val remoteSipUri = args.remoteSipUri
+ Log.i(
+ "$TAG Looking up for conversation with local SIP URI [$localSipUri] and remote SIP URI [$remoteSipUri]"
+ )
+ viewModel.findChatRoom(null, localSipUri, remoteSipUri)
+
+ viewModel.chatRoomFoundEvent.observe(viewLifecycleOwner) {
+ it.consume { found ->
+ if (!found) {
+ (view.parent as? ViewGroup)?.doOnPreDraw {
+ Log.e("$TAG Failed to find conversation, going back")
+ findNavController().popBackStack()
+ val message = getString(R.string.toast_cant_find_conversation_to_display)
+ (requireActivity() as CallActivity).showRedToast(message, R.drawable.x)
+ }
+ } else {
+ sendMessageViewModel.configureChatRoom(viewModel.chatRoom)
+ (view.parent as? ViewGroup)?.doOnPreDraw {
+ startPostponedEnterTransition()
+ }
+ }
+ }
+ }
+
+ viewModel.updateEvents.observe(viewLifecycleOwner) {
+ val items = viewModel.eventsList
+ adapter.submitList(items)
+ Log.i("$TAG Events (messages) list updated, contains [${items.size}] items")
+ }
+
+ viewModel.isEndToEndEncrypted.observe(viewLifecycleOwner) { encrypted ->
+ if (encrypted) {
+ binding.eventsList.addItemDecoration(headerItemDecoration)
+ binding.eventsList.addOnItemTouchListener(listItemTouchListener)
+ }
+ }
+ binding.messageBottomSheet.bottomSheetList.setHasFixedSize(true)
+ val bottomSheetLayoutManager = LinearLayoutManager(requireContext())
+ binding.messageBottomSheet.bottomSheetList.layoutManager = bottomSheetLayoutManager
+
+ adapter.chatMessageLongPressEvent.observe(viewLifecycleOwner) {
+ it.consume { model ->
+ showChatMessageLongPressMenu(model)
+ }
+ }
+
+ adapter.showDeliveryForChatMessageModelEvent.observe(viewLifecycleOwner) {
+ it.consume { model ->
+ showBottomSheetDialog(model, showDelivery = true)
+ }
+ }
+
+ adapter.showReactionForChatMessageModelEvent.observe(viewLifecycleOwner) {
+ it.consume { model ->
+ showBottomSheetDialog(model, showReactions = true)
+ }
+ }
+
+ adapter.scrollToRepliedMessageEvent.observe(viewLifecycleOwner) {
+ it.consume { model ->
+ val repliedMessageId = model.replyToMessageId
+ if (repliedMessageId.isNullOrEmpty()) {
+ Log.w("$TAG Message [${model.id}] doesn't have a reply to ID!")
+ } else {
+ val originalMessage = adapter.currentList.find { eventLog ->
+ !eventLog.isEvent && (eventLog.model as MessageModel).id == repliedMessageId
+ }
+ if (originalMessage != null) {
+ val position = adapter.currentList.indexOf(originalMessage)
+ Log.i("$TAG Scrolling to position [$position]")
+ binding.eventsList.scrollToPosition(position)
+ } else {
+ Log.w("$TAG Failed to find matching message in adapter's items!")
+ }
+ }
+ }
+ }
+
+ binding.setScrollToBottomClickListener {
+ scrollToFirstUnreadMessageOrBottom()
+ }
+
+ binding.setEndToEndEncryptedEventClickListener {
+ showEndToEndEncryptionDetailsBottomSheet()
+ }
+
+ sendMessageViewModel.emojiToAddEvent.observe(viewLifecycleOwner) {
+ it.consume { emoji ->
+ binding.sendArea.messageToSend.addCharacterAtPosition(emoji)
+ }
+ }
+
+ sendMessageViewModel.participantUsernameToAddEvent.observe(viewLifecycleOwner) {
+ it.consume { username ->
+ Log.i("$TAG Adding username [$username] after '@'")
+ // Also add a space for convenience
+ binding.sendArea.messageToSend.addCharacterAtPosition("$username ")
+ }
+ }
+
+ sendMessageViewModel.requestKeyboardHidingEvent.observe(viewLifecycleOwner) {
+ it.consume {
+ binding.search.hideKeyboard()
+ }
+ }
+
+ sendMessageViewModel.showRedToastEvent.observe(viewLifecycleOwner) {
+ it.consume { pair ->
+ val message = pair.first
+ val icon = pair.second
+ (requireActivity() as CallActivity).showRedToast(message, icon)
+ }
+ }
+
+ viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
+ viewModel.applyFilter(filter.trim())
+ }
+
+ viewModel.focusSearchBarEvent.observe(viewLifecycleOwner) {
+ it.consume { show ->
+ if (show) {
+ // To automatically open keyboard
+ binding.search.showKeyboard()
+ } else {
+ binding.search.hideKeyboard()
+ }
+ }
+ }
+
+ viewModel.openWebBrowserEvent.observe(viewLifecycleOwner) {
+ it.consume { url ->
+ if (messageLongPressDialog != null) return@consume
+ Log.i("$TAG Requesting to open web browser on page [$url]")
+ try {
+ val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+ startActivity(browserIntent)
+ } catch (e: Exception) {
+ Log.e(
+ "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e"
+ )
+ }
+ }
+ }
+
+ viewModel.showRedToastEvent.observe(viewLifecycleOwner) {
+ it.consume { pair ->
+ val message = pair.first
+ val icon = pair.second
+ (requireActivity() as CallActivity).showRedToast(message, icon)
+ }
+ }
+
+ viewModel.messageDeletedEvent.observe(viewLifecycleOwner) {
+ it.consume {
+ val message = getString(R.string.conversation_message_deleted_toast)
+ val icon = R.drawable.x
+ (requireActivity() as CallActivity).showGreenToast(message, icon)
+ }
+ }
+ binding.sendArea.messageToSend.setControlEnterListener(object :
+ RichEditText.RichEditTextSendListener {
+ override fun onControlEnterPressedAndReleased() {
+ Log.i("$TAG Detected left control + enter key presses, sending message")
+ sendMessageViewModel.sendMessage()
+ }
+ })
+
+ binding.root.setKeyboardInsetListener { keyboardVisible ->
+ sendMessageViewModel.isKeyboardOpen.value = keyboardVisible
+ if (keyboardVisible) {
+ sendMessageViewModel.isEmojiPickerOpen.value = false
+ }
+ }
+
+ scrollListener = object : ConversationScrollListener(layoutManager) {
+ @UiThread
+ override fun onLoadMore(totalItemsCount: Int) {
+ viewModel.loadMoreData(totalItemsCount)
+ }
+
+ @UiThread
+ override fun onScrolledUp() {
+ viewModel.isUserScrollingUp.value = true
+ }
+
+ @UiThread
+ override fun onScrolledToEnd() {
+ if (viewModel.isUserScrollingUp.value == true) {
+ viewModel.isUserScrollingUp.value = false
+ Log.i("$TAG Last message is visible, considering conversation as read")
+ viewModel.markAsRead()
+ }
+ }
+ }
+ binding.eventsList.addOnScrollListener(scrollListener)
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ viewModel.updateCurrentlyDisplayedConversation()
+
+ try {
+ adapter.registerAdapterDataObserver(dataObserver)
+ } catch (e: IllegalStateException) {
+ Log.e("$TAG Failed to register data observer to adapter: $e")
+ }
+
+ val bottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root)
+ bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback)
+ }
+
+ override fun onPause() {
+ super.onPause()
+
+ bottomSheetDialog?.dismiss()
+ bottomSheetDialog = null
+
+ if (::scrollListener.isInitialized) {
+ binding.eventsList.removeOnScrollListener(scrollListener)
+ }
+
+ coreContext.postOnCoreThread {
+ bottomSheetReactionsModel?.destroy()
+ bottomSheetDeliveryModel?.destroy()
+ coreContext.notificationsManager.resetCurrentlyDisplayedChatRoomId()
+ }
+
+ try {
+ adapter.unregisterAdapterDataObserver(dataObserver)
+ } catch (e: IllegalStateException) {
+ Log.e("$TAG Failed to unregister data observer to adapter: $e")
+ }
+
+ val bottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root)
+ bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback)
+ currentChatMessageModelForBottomSheet = null
+ }
+
+ private fun scrollToFirstUnreadMessageOrBottom() {
+ if (adapter.itemCount == 0) return
+
+ val recyclerView = binding.eventsList
+ // Scroll to first unread message if any, unless we are already on it
+ val firstUnreadMessagePosition = adapter.getFirstUnreadMessagePosition()
+ val currentPosition = (recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
+ val indexToScrollTo = if (firstUnreadMessagePosition != -1 && firstUnreadMessagePosition != currentPosition) {
+ firstUnreadMessagePosition
+ } else {
+ adapter.itemCount - 1
+ }
+
+ Log.i(
+ "$TAG Scrolling to position $indexToScrollTo, first unread message is at $firstUnreadMessagePosition"
+ )
+ recyclerView.scrollToPosition(indexToScrollTo)
+
+ if (indexToScrollTo == adapter.itemCount - 1) {
+ viewModel.isUserScrollingUp.postValue(false)
+ viewModel.markAsRead()
+ }
+ }
+
+ private fun dismissDialog() {
+ messageLongPressDialog?.dismiss()
+ messageLongPressDialog = null
+ }
+
+ private fun showChatMessageLongPressMenu(chatMessageModel: MessageModel) {
+ Compatibility.setBlurRenderEffect(binding.root)
+
+ val dialog = Dialog(requireContext(), R.style.Theme_LinphoneDialog)
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+
+ val layout: ChatBubbleLongPressMenuBinding = DataBindingUtil.inflate(
+ LayoutInflater.from(context),
+ R.layout.chat_bubble_long_press_menu,
+ null,
+ false
+ )
+ layout.hideForward = true
+
+ layout.root.setOnClickListener {
+ dismissDialog()
+ }
+
+ layout.setDeleteClickListener {
+ Log.i("$TAG Deleting message")
+ viewModel.deleteChatMessage(chatMessageModel)
+ dismissDialog()
+ }
+
+ layout.setCopyClickListener {
+ Log.i("$TAG Copying message text into clipboard")
+ val text = chatMessageModel.text.value?.toString()
+ val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val label = "Message"
+ clipboard.setPrimaryClip(ClipData.newPlainText(label, text))
+
+ dismissDialog()
+ }
+
+ layout.setPickEmojiClickListener {
+ Log.i("$TAG Opening emoji-picker for reaction")
+ val emojiSheetBehavior = BottomSheetBehavior.from(layout.emojiPickerBottomSheet.root)
+ emojiSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+
+ layout.setResendClickListener {
+ Log.i("$TAG Re-sending message in error state")
+ chatMessageModel.resend()
+ dismissDialog()
+ }
+
+ layout.setReplyClickListener {
+ Log.i("$TAG Updating sending area to reply to selected message")
+ sendMessageViewModel.replyToMessage(chatMessageModel)
+ dismissDialog()
+
+ // Open keyboard & focus edit text
+ binding.sendArea.messageToSend.showKeyboard()
+ }
+
+ layout.model = chatMessageModel
+ chatMessageModel.dismissLongPressMenuEvent.observe(viewLifecycleOwner) {
+ dismissDialog()
+ }
+
+ dialog.setContentView(layout.root)
+ dialog.setOnDismissListener {
+ Compatibility.removeBlurRenderEffect(binding.root)
+ }
+
+ dialog.window
+ ?.setLayout(
+ WindowManager.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.MATCH_PARENT
+ )
+ val d: Drawable = ColorDrawable(
+ requireContext().getColor(R.color.grey_300)
+ )
+ d.alpha = 102
+ dialog.window?.setBackgroundDrawable(d)
+ dialog.show()
+ messageLongPressDialog = dialog
+ }
+
+ @UiThread
+ private fun showBottomSheetDialog(
+ chatMessageModel: MessageModel,
+ showDelivery: Boolean = false,
+ showReactions: Boolean = false
+ ) {
+ val bottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root)
+
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
+ binding.messageBottomSheet.setHandleClickedListener {
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
+ }
+
+ if (binding.messageBottomSheet.bottomSheetList.adapter != bottomSheetAdapter) {
+ binding.messageBottomSheet.bottomSheetList.adapter = bottomSheetAdapter
+ }
+
+ currentChatMessageModelForBottomSheet?.isSelected?.value = false
+ currentChatMessageModelForBottomSheet = chatMessageModel
+ chatMessageModel.isSelected.value = true
+
+ lifecycleScope.launch {
+ withContext(Dispatchers.IO) {
+ // Wait for previous bottom sheet to go away
+ delay(200)
+
+ withContext(Dispatchers.Main) {
+ if (showDelivery) {
+ prepareBottomSheetForDeliveryStatus(chatMessageModel)
+ } else if (showReactions) {
+ prepareBottomSheetForReactions(chatMessageModel)
+ }
+
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+ }
+ }
+ }
+
+ @UiThread
+ private fun prepareBottomSheetForDeliveryStatus(chatMessageModel: MessageModel) {
+ coreContext.postOnCoreThread {
+ bottomSheetDeliveryModel?.destroy()
+
+ val model = MessageDeliveryModel(chatMessageModel.chatMessage) { deliveryModel ->
+ coreContext.postOnMainThread {
+ displayDeliveryStatuses(deliveryModel)
+ }
+ }
+ bottomSheetDeliveryModel = model
+ }
+ }
+
+ @UiThread
+ private fun prepareBottomSheetForReactions(chatMessageModel: MessageModel) {
+ coreContext.postOnCoreThread {
+ bottomSheetReactionsModel?.destroy()
+
+ val model = MessageReactionsModel(chatMessageModel.chatMessage) { reactionsModel ->
+ coreContext.postOnMainThread {
+ if (reactionsModel.allReactions.isEmpty()) {
+ Log.i("$TAG No reaction to display, closing bottom sheet")
+ val bottomSheetBehavior = BottomSheetBehavior.from(
+ binding.messageBottomSheet.root
+ )
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
+ } else {
+ displayReactions(reactionsModel)
+ }
+ }
+ }
+ bottomSheetReactionsModel = model
+ }
+ }
+
+ @UiThread
+ private fun displayDeliveryStatuses(model: MessageDeliveryModel) {
+ val tabs = binding.messageBottomSheet.tabs
+ tabs.removeAllTabs()
+ tabs.addTab(
+ tabs.newTab().setText(model.readLabel.value).setId(
+ ChatMessage.State.Displayed.toInt()
+ )
+ )
+ tabs.addTab(
+ tabs.newTab().setText(
+ model.receivedLabel.value
+ ).setId(
+ ChatMessage.State.DeliveredToUser.toInt()
+ )
+ )
+ tabs.addTab(
+ tabs.newTab().setText(model.sentLabel.value).setId(
+ ChatMessage.State.Delivered.toInt()
+ )
+ )
+ tabs.addTab(
+ tabs.newTab().setText(
+ model.errorLabel.value
+ ).setId(
+ ChatMessage.State.NotDelivered.toInt()
+ )
+ )
+
+ tabs.setOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+ override fun onTabSelected(tab: TabLayout.Tab?) {
+ val state = tab?.id ?: ChatMessage.State.Displayed.toInt()
+ bottomSheetAdapter.submitList(
+ model.computeListForState(ChatMessage.State.fromInt(state))
+ )
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab?) {
+ }
+
+ override fun onTabReselected(tab: TabLayout.Tab?) {
+ }
+ })
+
+ val initialList = model.displayedModels
+ bottomSheetAdapter.submitList(initialList)
+ Log.i("$TAG Submitted [${initialList.size}] items for default delivery status list")
+ }
+
+ @UiThread
+ private fun displayReactions(model: MessageReactionsModel) {
+ val totalCount = model.allReactions.size
+ val label = getString(R.string.message_reactions_info_all_title, totalCount.toString())
+
+ val tabs = binding.messageBottomSheet.tabs
+ tabs.removeAllTabs()
+ tabs.addTab(
+ tabs.newTab().setText(label).setId(0).setTag("")
+ )
+
+ var index = 1
+ for (reaction in model.differentReactions.value.orEmpty()) {
+ val count = model.reactionsMap[reaction]
+ val tabLabel = getString(
+ R.string.message_reactions_info_emoji_title,
+ reaction,
+ count.toString()
+ )
+ tabs.addTab(
+ tabs.newTab().setText(tabLabel).setId(index).setTag(reaction)
+ )
+ index += 1
+ }
+
+ tabs.setOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+ override fun onTabSelected(tab: TabLayout.Tab?) {
+ val filter = tab?.tag.toString()
+ if (filter.isEmpty()) {
+ bottomSheetAdapter.submitList(model.allReactions)
+ } else {
+ bottomSheetAdapter.submitList(model.filterReactions(filter))
+ }
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab?) {
+ }
+
+ override fun onTabReselected(tab: TabLayout.Tab?) {
+ }
+ })
+
+ val initialList = model.allReactions
+ bottomSheetAdapter.submitList(initialList)
+ Log.i("$TAG Submitted [${initialList.size}] items for default reactions list")
+ }
+
+ private fun showEndToEndEncryptionDetailsBottomSheet() {
+ val e2eEncryptionDetailsBottomSheet = EndToEndEncryptionDetailsDialogFragment()
+ e2eEncryptionDetailsBottomSheet.show(
+ requireActivity().supportFragmentManager,
+ EndToEndEncryptionDetailsDialogFragment.TAG
+ )
+ bottomSheetDialog = e2eEncryptionDetailsBottomSheet
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt
index 0fdbefdcf..774de120c 100644
--- a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt
@@ -43,6 +43,8 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
val callsCount = MutableLiveData()
+ val showTopBar = MutableLiveData()
+
val goToActiveCallEvent = MutableLiveData>()
val showIncomingCallEvent = MutableLiveData>()
@@ -51,9 +53,11 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
val noCallFoundEvent = MutableLiveData>()
- val otherCallsLabel = MutableLiveData()
+ val callsTopBarLabel = MutableLiveData()
- val otherCallsStatus = MutableLiveData()
+ val callsTopBarIcon = MutableLiveData()
+
+ val callsTopBarStatus = MutableLiveData()
val goToCallsListEvent: MutableLiveData> by lazy {
MutableLiveData>()
@@ -149,6 +153,8 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
}
init {
+ showTopBar.value = false
+
coreContext.postOnCoreThread { core ->
core.addListener(coreListener)
@@ -198,12 +204,18 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
}
@UiThread
- fun goToCallsList() {
- goToCallsListEvent.value = Event(true)
+ fun topBarClicked() {
+ coreContext.postOnCoreThread { core ->
+ if (core.callsNb == 1) {
+ goToActiveCallEvent.postValue(Event(core.calls.first().conference == null))
+ } else {
+ goToCallsListEvent.postValue(Event(true))
+ }
+ }
}
@UiThread
- fun mergeCallsIntoLocalConference() {
+ fun mergeCallsIntoConference() {
// TODO FIXME: implement local conferences merge
}
@@ -212,31 +224,46 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
val core = coreContext.core
if (core.callsNb > 1) {
+ showTopBar.postValue(true)
if (core.callsNb == 2) {
val found = core.calls.find {
it.state == Call.State.Paused
}
+ callsTopBarIcon.postValue(R.drawable.phone_pause)
if (found != null) {
val contact = coreContext.contactsManager.findContactByAddress(
found.remoteAddress
)
- otherCallsLabel.postValue(
+ callsTopBarLabel.postValue(
contact?.name ?: LinphoneUtils.getDisplayName(found.remoteAddress)
)
- otherCallsStatus.postValue(LinphoneUtils.callStateToString(found.state))
+ callsTopBarStatus.postValue(LinphoneUtils.callStateToString(found.state))
} else {
Log.e("$TAG Failed to find a paused call")
}
} else {
- otherCallsLabel.postValue(
+ callsTopBarLabel.postValue(
AppUtils.getFormattedString(R.string.calls_paused_count_label, core.callsNb - 1)
)
- otherCallsStatus.postValue("") // TODO: improve ?
+ callsTopBarStatus.postValue("") // TODO: improve ?
}
Log.i("$TAG At least one other call, asking activity to change status bar color")
changeSystemTopBarColorToMultipleCallsEvent.postValue(Event(true))
} else {
+ if (core.callsNb == 1) {
+ callsTopBarIcon.postValue(R.drawable.phone)
+
+ val call = core.calls.first()
+ val contact = coreContext.contactsManager.findContactByAddress(
+ call.remoteAddress
+ )
+ callsTopBarLabel.postValue(
+ contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress)
+ )
+ callsTopBarStatus.postValue(LinphoneUtils.callStateToString(call.state))
+ }
+
Log.i(
"$TAG No more than one call, asking activity to change status bar color back to primary"
)
diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt
index 33d406e67..8892964cb 100644
--- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt
@@ -41,6 +41,9 @@ import org.linphone.core.AudioDevice
import org.linphone.core.Call
import org.linphone.core.CallListenerStub
import org.linphone.core.CallStats
+import org.linphone.core.ChatRoom
+import org.linphone.core.ChatRoomListenerStub
+import org.linphone.core.ChatRoomParams
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.MediaDirection
@@ -54,6 +57,7 @@ import org.linphone.ui.call.model.CallStatsModel
import org.linphone.ui.call.model.ConferenceModel
import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.ui.main.history.model.NumpadModel
+import org.linphone.ui.main.model.isInSecureMode
import org.linphone.utils.AppUtils
import org.linphone.utils.AudioUtils
import org.linphone.utils.Event
@@ -155,6 +159,18 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
MutableLiveData>>()
}
+ // Chat
+
+ val operationInProgress = MutableLiveData()
+
+ val goToConversationEvent: MutableLiveData>> by lazy {
+ MutableLiveData>>()
+ }
+
+ val chatRoomCreationErrorEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
// Conference
val conferenceModel = ConferenceModel()
@@ -284,6 +300,34 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
}
}
+ private val chatRoomListener = object : ChatRoomListenerStub() {
+ @WorkerThread
+ override fun onStateChanged(chatRoom: ChatRoom, newState: ChatRoom.State?) {
+ val state = chatRoom.state
+ val id = LinphoneUtils.getChatRoomId(chatRoom)
+ Log.i("$TAG Conversation [$id] (${chatRoom.subject}) state changed: [$state]")
+
+ if (state == ChatRoom.State.Created) {
+ Log.i("$TAG Conversation [$id] successfully created")
+ chatRoom.removeListener(this)
+ operationInProgress.postValue(false)
+ goToConversationEvent.postValue(
+ Event(
+ Pair(
+ chatRoom.localAddress.asStringUriOnly(),
+ chatRoom.peerAddress.asStringUriOnly()
+ )
+ )
+ )
+ } else if (state == ChatRoom.State.CreationFailed) {
+ Log.e("$TAG Conversation [$id] creation has failed!")
+ chatRoom.removeListener(this)
+ operationInProgress.postValue(false)
+ chatRoomCreationErrorEvent.postValue(Event("Error!")) // TODO: use translated string
+ }
+ }
+ }
+
private val coreListener = object : CoreListenerStub() {
override fun onCallStateChanged(
core: Core,
@@ -331,6 +375,7 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
init {
fullScreenMode.value = false
+ operationInProgress.value = false
coreContext.postOnCoreThread { core ->
core.addListener(coreListener)
@@ -669,6 +714,115 @@ class CurrentCallViewModel @UiThread constructor() : ViewModel() {
}
}
+ @UiThread
+ fun createConversation() {
+ coreContext.postOnCoreThread { core ->
+ val account = core.defaultAccount
+ val localSipUri = account?.params?.identityAddress?.asStringUriOnly()
+ val remote = currentCall.remoteAddress
+ if (!localSipUri.isNullOrEmpty()) {
+ val remoteSipUri = remote.asStringUriOnly()
+ Log.i(
+ "$TAG Looking for existing conversation between [$localSipUri] and [$remoteSipUri]"
+ )
+
+ val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
+ params.isGroupEnabled = false
+ params.subject = AppUtils.getString(R.string.conversation_one_to_one_hidden_subject)
+ params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
+
+ val sameDomain =
+ remote.domain == corePreferences.defaultDomain && remote.domain == account.params.domain
+ if (account.isInSecureMode() && sameDomain) {
+ Log.i(
+ "$TAG Account is in secure mode & domain matches, creating a E2E conversation"
+ )
+ params.backend = ChatRoom.Backend.FlexisipChat
+ params.isEncryptionEnabled = true
+ } else if (!account.isInSecureMode()) {
+ if (LinphoneUtils.isEndToEndEncryptedChatAvailable(core)) {
+ Log.i(
+ "$TAG Account is in interop mode but LIME is available, creating a E2E conversation"
+ )
+ params.backend = ChatRoom.Backend.FlexisipChat
+ params.isEncryptionEnabled = true
+ } else {
+ Log.i(
+ "$TAG Account is in interop mode but LIME isn't available, creating a SIP simple conversation"
+ )
+ params.backend = ChatRoom.Backend.Basic
+ params.isEncryptionEnabled = false
+ }
+ } else {
+ Log.e(
+ "$TAG Account is in secure mode, can't chat with SIP address of different domain [${remote.asStringUriOnly()}]"
+ )
+ return@postOnCoreThread
+ }
+
+ val participants = arrayOf(remote)
+ val localAddress = account.params.identityAddress
+ val existingChatRoom = core.searchChatRoom(params, localAddress, null, participants)
+ if (existingChatRoom != null) {
+ Log.i(
+ "$TAG Found existing conversation [${
+ LinphoneUtils.getChatRoomId(
+ existingChatRoom
+ )
+ }], going to it"
+ )
+ goToConversationEvent.postValue(
+ Event(Pair(localSipUri, existingChatRoom.peerAddress.asStringUriOnly()))
+ )
+ } else {
+ Log.i(
+ "$TAG No existing conversation between [$localSipUri] and [$remoteSipUri] was found, let's create it"
+ )
+ operationInProgress.postValue(true)
+ val chatRoom = core.createChatRoom(params, localAddress, participants)
+ if (chatRoom != null) {
+ if (params.backend == ChatRoom.Backend.FlexisipChat) {
+ if (chatRoom.state == ChatRoom.State.Created) {
+ val id = LinphoneUtils.getChatRoomId(chatRoom)
+ Log.i("$TAG 1-1 conversation [$id] has been created")
+ operationInProgress.postValue(false)
+ goToConversationEvent.postValue(
+ Event(
+ Pair(
+ chatRoom.localAddress.asStringUriOnly(),
+ chatRoom.peerAddress.asStringUriOnly()
+ )
+ )
+ )
+ } else {
+ Log.i("$TAG Conversation isn't in Created state yet, wait for it")
+ chatRoom.addListener(chatRoomListener)
+ }
+ } else {
+ val id = LinphoneUtils.getChatRoomId(chatRoom)
+ Log.i("$TAG Conversation successfully created [$id]")
+ operationInProgress.postValue(false)
+ goToConversationEvent.postValue(
+ Event(
+ Pair(
+ chatRoom.localAddress.asStringUriOnly(),
+ chatRoom.peerAddress.asStringUriOnly()
+ )
+ )
+ )
+ }
+ } else {
+ Log.e(
+ "$TAG Failed to create 1-1 conversation with [${remote.asStringUriOnly()}]!"
+ )
+ operationInProgress.postValue(false)
+ chatRoomCreationErrorEvent.postValue(Event("Error!")) // TODO: use translated string
+ }
+ }
+ }
+ }
+ }
+
@WorkerThread
fun blindTransferCallTo(to: Address) {
if (::currentCall.isInitialized) {
diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt
index 21faecaf3..f3aeaf88d 100644
--- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt
@@ -261,6 +261,7 @@ class ConversationFragment : SlidingPaneChildFragment() {
}
private var currentChatMessageModelForBottomSheet: MessageModel? = null
+
private val bottomSheetCallback = object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
@@ -805,6 +806,7 @@ class ConversationFragment : SlidingPaneChildFragment() {
if (indexToScrollTo == adapter.itemCount - 1) {
viewModel.isUserScrollingUp.postValue(false)
+ viewModel.markAsRead()
}
}
diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt
index 0c6a74023..e62d70d4d 100644
--- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt
@@ -57,6 +57,8 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
val showBackButton = MutableLiveData()
+ val isInCallConversation = MutableLiveData()
+
val avatarModel = MutableLiveData()
val isEmpty = MutableLiveData()
diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt
index defc6019b..7367395e0 100644
--- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt
@@ -83,6 +83,8 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() {
val isKeyboardOpen = MutableLiveData()
+ val isInCallConversation = MutableLiveData()
+
val isVoiceRecording = MutableLiveData()
val isVoiceRecordingInProgress = MutableLiveData()
@@ -149,6 +151,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() {
isEmojiPickerOpen.value = false
isPlayingVoiceRecord.value = false
+ isInCallConversation.value = false
}
override fun onCleared() {
diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt
index 6b7239e86..49cdf1ec9 100644
--- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt
+++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt
@@ -254,7 +254,11 @@ fun ImageView.loadImageForChatBubbleGrid(file: String?) {
}
private fun loadImageForChatBubble(imageView: ImageView, file: String?, grid: Boolean) {
- if (!file.isNullOrEmpty()) {
+ if (file.isNullOrEmpty()) return
+
+ val isImage = FileUtils.isExtensionImage((file))
+ val isVideo = FileUtils.isExtensionVideo(file)
+ if (isImage || isVideo) {
val dimen = if (grid) {
imageView.resources.getDimension(R.dimen.chat_bubble_grid_image_size).toInt()
} else {
@@ -266,7 +270,7 @@ private fun loadImageForChatBubble(imageView: ImageView, file: String?, grid: Bo
R.dimen.chat_bubble_images_rounded_corner_radius
)
- if (FileUtils.isExtensionVideo(file)) {
+ if (isVideo) {
imageView.load(file) {
placeholder(R.drawable.image_square)
videoFrameMillis(0)
diff --git a/app/src/main/res/drawable/pause_call.xml b/app/src/main/res/drawable/pause_call.xml
deleted file mode 100644
index b380c2569..000000000
--- a/app/src/main/res/drawable/pause_call.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
diff --git a/app/src/main/res/layout-land/call_actions_bottom_sheet.xml b/app/src/main/res/layout-land/call_actions_bottom_sheet.xml
index da98ae2dc..bb0d85459 100644
--- a/app/src/main/res/layout-land/call_actions_bottom_sheet.xml
+++ b/app/src/main/res/layout-land/call_actions_bottom_sheet.xml
@@ -118,17 +118,32 @@
+ app:tint="@color/in_call_button_tint_color" />
+
+
diff --git a/app/src/main/res/layout/call_actions_bottom_sheet.xml b/app/src/main/res/layout/call_actions_bottom_sheet.xml
index c9c593ffe..3f5536784 100644
--- a/app/src/main/res/layout/call_actions_bottom_sheet.xml
+++ b/app/src/main/res/layout/call_actions_bottom_sheet.xml
@@ -118,18 +118,33 @@
+
+
diff --git a/app/src/main/res/layout/call_activity.xml b/app/src/main/res/layout/call_activity.xml
index 772c7ad6c..53e2fe6a6 100644
--- a/app/src/main/res/layout/call_activity.xml
+++ b/app/src/main/res/layout/call_activity.xml
@@ -28,7 +28,7 @@
layout="@layout/call_activity_other_calls_top_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:visibility="@{callsViewModel.callsCount >= 2 ? View.VISIBLE : View.GONE, default=gone}"
+ android:visibility="@{callsViewModel.callsCount > 1 || callsViewModel.showTopBar ? View.VISIBLE : View.GONE, default=gone}"
app:viewModel="@{callsViewModel}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
diff --git a/app/src/main/res/layout/call_activity_other_calls_top_bar.xml b/app/src/main/res/layout/call_activity_other_calls_top_bar.xml
index d23af3eda..a25e07a18 100644
--- a/app/src/main/res/layout/call_activity_other_calls_top_bar.xml
+++ b/app/src/main/res/layout/call_activity_other_calls_top_bar.xml
@@ -14,7 +14,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/color_success_500"
- android:onClick="@{() -> viewModel.goToCallsList()}">
+ android:onClick="@{() -> viewModel.topBarClicked()}">
+
+
@@ -44,7 +53,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:gravity="center_vertical"
- android:text="@{viewModel.otherCallsStatus, default=`Paused`}"
+ android:text="@{viewModel.callsTopBarStatus, default=`Paused`}"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/call_list_cell.xml b/app/src/main/res/layout/call_list_cell.xml
index 55c00f10a..20a108db5 100644
--- a/app/src/main/res/layout/call_list_cell.xml
+++ b/app/src/main/res/layout/call_list_cell.xml
@@ -55,7 +55,7 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="5dp"
- android:src="@{model.isPaused ? @drawable/pause_call : @drawable/phone_call, default=@drawable/pause_call}"
+ android:src="@{model.isPaused ? @drawable/phone_pause : @drawable/phone_call, default=@drawable/phone_pause}"
app:tint="?attr/color_main2_500"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
diff --git a/app/src/main/res/layout/calls_list_long_press_menu.xml b/app/src/main/res/layout/calls_list_long_press_menu.xml
index 6c03a7d58..c28cd5ad6 100644
--- a/app/src/main/res/layout/calls_list_long_press_menu.xml
+++ b/app/src/main/res/layout/calls_list_long_press_menu.xml
@@ -30,7 +30,7 @@
style="@style/context_menu_action_label_style"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
- android:drawableStart="@drawable/pause_call"
+ android:drawableStart="@drawable/phone_pause"
app:layout_constraintBottom_toTopOf="@id/hang_up"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
@@ -45,7 +45,7 @@
style="@style/context_menu_action_label_style"
android:background="@drawable/menu_item_background"
android:layout_marginBottom="1dp"
- android:drawableStart="@drawable/pause_call"
+ android:drawableStart="@drawable/phone_pause"
app:layout_constraintBottom_toTopOf="@id/hang_up"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
diff --git a/app/src/main/res/layout/chat_bubble_long_press_menu.xml b/app/src/main/res/layout/chat_bubble_long_press_menu.xml
index 5e5ef02cd..1ee1d32f1 100644
--- a/app/src/main/res/layout/chat_bubble_long_press_menu.xml
+++ b/app/src/main/res/layout/chat_bubble_long_press_menu.xml
@@ -26,6 +26,9 @@
+
diff --git a/app/src/main/res/layout/chat_conversation_fragment.xml b/app/src/main/res/layout/chat_conversation_fragment.xml
index ef3951b9a..198a34b27 100644
--- a/app/src/main/res/layout/chat_conversation_fragment.xml
+++ b/app/src/main/res/layout/chat_conversation_fragment.xml
@@ -69,7 +69,7 @@
android:padding="15dp"
android:adjustViewBounds="true"
android:onClick="@{backClickListener}"
- android:visibility="@{viewModel.showBackButton && !viewModel.searchBarVisible ? View.VISIBLE : View.GONE}"
+ android:visibility="@{viewModel.isInCallConversation || viewModel.showBackButton && !viewModel.searchBarVisible ? View.VISIBLE : View.GONE}"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
app:tint="?attr/color_main1_500"
@@ -148,6 +148,7 @@
android:padding="15dp"
android:adjustViewBounds="true"
android:src="@drawable/dots_three_vertical"
+ android:visibility="@{viewModel.isInCallConversation ? View.GONE : View.VISIBLE}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:tint="?attr/color_main2_500"/>
@@ -159,7 +160,7 @@
android:layout_height="@dimen/top_bar_height"
android:padding="15dp"
android:src="@drawable/phone"
- android:visibility="@{viewModel.isReadOnly || viewModel.searchBarVisible ? View.GONE : View.VISIBLE}"
+ android:visibility="@{viewModel.isInCallConversation || viewModel.isReadOnly || viewModel.searchBarVisible ? View.GONE : View.VISIBLE}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/show_menu"
app:tint="?attr/color_main2_500" />
diff --git a/app/src/main/res/layout/chat_conversation_send_area.xml b/app/src/main/res/layout/chat_conversation_send_area.xml
index 50fe20af6..fcdc5b2e4 100644
--- a/app/src/main/res/layout/chat_conversation_send_area.xml
+++ b/app/src/main/res/layout/chat_conversation_send_area.xml
@@ -33,7 +33,7 @@
android:id="@+id/extra_actions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:visibility="@{viewModel.isVoiceRecording ? View.INVISIBLE : viewModel.isKeyboardOpen ? View.GONE : View.VISIBLE}"
+ android:visibility="@{viewModel.isVoiceRecording ? View.INVISIBLE : (viewModel.isKeyboardOpen || viewModel.isInCallConversation || !viewModel.isFileTransferServerAvailable) ? View.GONE : View.VISIBLE}"
app:constraint_referenced_ids="attach_file, capture_image" />
+
+
+
+
+
+
\ No newline at end of file