mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Started to add chat during call
This commit is contained in:
parent
e805fbc7f3
commit
d19f08cf86
21 changed files with 1122 additions and 60 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class CallsListFragment : GenericCallFragment() {
|
|||
}
|
||||
|
||||
binding.setMergeCallsClickListener {
|
||||
viewModel.mergeCallsIntoLocalConference()
|
||||
viewModel.mergeCallsIntoConference()
|
||||
}
|
||||
|
||||
viewModel.calls.observe(viewLifecycleOwner) {
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,8 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
|
|||
|
||||
val callsCount = MutableLiveData<Int>()
|
||||
|
||||
val showTopBar = MutableLiveData<Boolean>()
|
||||
|
||||
val goToActiveCallEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val showIncomingCallEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
|
@ -51,9 +53,11 @@ class CallsViewModel @UiThread constructor() : ViewModel() {
|
|||
|
||||
val noCallFoundEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val otherCallsLabel = MutableLiveData<String>()
|
||||
val callsTopBarLabel = MutableLiveData<String>()
|
||||
|
||||
val otherCallsStatus = MutableLiveData<String>()
|
||||
val callsTopBarIcon = MutableLiveData<Int>()
|
||||
|
||||
val callsTopBarStatus = MutableLiveData<String>()
|
||||
|
||||
val goToCallsListEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<Event<Pair<String, String>>>()
|
||||
}
|
||||
|
||||
// Chat
|
||||
|
||||
val operationInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val goToConversationEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String>>>()
|
||||
}
|
||||
|
||||
val chatRoomCreationErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
|
|||
|
||||
val showBackButton = MutableLiveData<Boolean>()
|
||||
|
||||
val isInCallConversation = MutableLiveData<Boolean>()
|
||||
|
||||
val avatarModel = MutableLiveData<ContactAvatarModel>()
|
||||
|
||||
val isEmpty = MutableLiveData<Boolean>()
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() {
|
|||
|
||||
val isKeyboardOpen = MutableLiveData<Boolean>()
|
||||
|
||||
val isInCallConversation = MutableLiveData<Boolean>()
|
||||
|
||||
val isVoiceRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val isVoiceRecordingInProgress = MutableLiveData<Boolean>()
|
||||
|
|
@ -149,6 +151,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() {
|
|||
|
||||
isEmojiPickerOpen.value = false
|
||||
isPlayingVoiceRecord.value = false
|
||||
isInCallConversation.value = false
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:name="vector"
|
||||
android:width="28dp"
|
||||
android:height="28dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 7.63 5.833 C 7.7 6.86 7.875 7.875 8.143 8.843 L 6.743 10.255 C 6.277 8.843 5.973 7.373 5.868 5.833 L 7.63 5.833 Z M 19.133 19.845 C 20.125 20.125 21.14 20.3 22.167 20.37 L 22.167 22.12 C 20.627 22.015 19.133 21.712 17.733 21.233 L 19.133 19.845 Z M 8.75 3.5 L 4.667 3.5 C 4.025 3.5 3.5 4.025 3.5 4.667 C 3.5 15.622 12.378 24.5 23.333 24.5 C 23.975 24.5 24.5 23.975 24.5 23.333 L 24.5 19.25 C 24.5 18.608 23.975 18.083 23.333 18.083 C 21.875 18.083 20.475 17.85 19.168 17.418 C 19.052 17.383 18.923 17.36 18.807 17.36 C 18.503 17.36 18.212 17.477 17.978 17.698 L 15.412 20.265 C 12.11 18.585 9.403 15.89 7.723 12.577 L 10.29 9.998 C 10.617 9.695 10.71 9.24 10.582 8.832 C 10.15 7.525 9.917 6.125 9.917 4.667 C 9.917 4.025 9.392 3.5 8.75 3.5 Z M 17.5 3.5 L 19.833 3.5 L 19.833 11.667 L 17.5 11.667 L 17.5 3.5 Z M 22.167 3.5 L 24.5 3.5 L 24.5 11.667 L 22.167 11.667 L 22.167 3.5 Z"
|
||||
android:fillColor="#c0d1d9"
|
||||
android:strokeWidth="1"/>
|
||||
</vector>
|
||||
|
|
@ -118,17 +118,32 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/chat"
|
||||
android:onClick="@{() -> viewModel.createConversation()}"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/call_button_size"
|
||||
android:layout_marginTop="@dimen/call_extra_button_top_margin"
|
||||
android:background="@drawable/shape_round_in_call_disabled_button_background"
|
||||
android:background="@drawable/in_call_button_background_red"
|
||||
android:padding="@dimen/call_button_icon_padding"
|
||||
android:src="@drawable/chat_teardrop_text"
|
||||
android:enabled="@{!viewModel.operationInProgress}"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="@id/chat_label"
|
||||
app:layout_constraintStart_toStartOf="@id/chat_label"
|
||||
app:layout_constraintTop_toBottomOf="@id/main_actions"
|
||||
app:tint="@color/gray_500" />
|
||||
app:tint="@color/in_call_button_tint_color" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/chat_room_creation_in_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="@{viewModel.operationInProgress ? View.VISIBLE : View.GONE}"
|
||||
app:indicatorColor="@color/main1_500"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintTop_toTopOf="@id/chat"
|
||||
app:layout_constraintStart_toStartOf="@id/chat"
|
||||
app:layout_constraintEnd_toEndOf="@id/chat"
|
||||
app:layout_constraintBottom_toBottomOf="@id/chat"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pause_call"
|
||||
|
|
@ -216,11 +231,12 @@
|
|||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/chat_label"
|
||||
style="@style/in_call_extra_action_label_style"
|
||||
android:onClick="@{() -> viewModel.createConversation()}"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="15dp"
|
||||
android:text="@string/call_action_show_messages"
|
||||
android:enabled="false"
|
||||
android:enabled="@{!viewModel.operationInProgress}"
|
||||
app:layout_constraintEnd_toStartOf="@id/pause_call_label"
|
||||
app:layout_constraintStart_toEndOf="@id/numpad_label"
|
||||
app:layout_constraintTop_toBottomOf="@id/chat" />
|
||||
|
|
|
|||
|
|
@ -118,18 +118,33 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/chat"
|
||||
android:onClick="@{() -> viewModel.createConversation()}"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/call_button_size"
|
||||
android:layout_marginTop="@dimen/call_extra_button_top_margin"
|
||||
android:padding="@dimen/call_button_icon_padding"
|
||||
android:src="@drawable/chat_teardrop_text"
|
||||
android:background="@drawable/shape_round_in_call_disabled_button_background"
|
||||
app:tint="?attr/color_grey_500"
|
||||
android:background="@drawable/in_call_button_background_red"
|
||||
android:enabled="@{!viewModel.operationInProgress}"
|
||||
app:tint="@color/in_call_button_tint_color"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintTop_toBottomOf="@id/transfer_label"
|
||||
app:layout_constraintStart_toStartOf="@id/transfer"
|
||||
app:layout_constraintEnd_toEndOf="@id/transfer"/>
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/chat_room_creation_in_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="@{viewModel.operationInProgress ? View.VISIBLE : View.GONE}"
|
||||
app:indicatorColor="@color/main1_500"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintTop_toTopOf="@id/chat"
|
||||
app:layout_constraintStart_toStartOf="@id/chat"
|
||||
app:layout_constraintEnd_toEndOf="@id/chat"
|
||||
app:layout_constraintBottom_toBottomOf="@id/chat"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pause_call"
|
||||
android:onClick="@{() -> viewModel.togglePause()}"
|
||||
|
|
@ -212,11 +227,12 @@
|
|||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/in_call_extra_action_label_style"
|
||||
android:id="@+id/chat_label"
|
||||
android:onClick="@{() -> viewModel.createConversation()}"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="15dp"
|
||||
android:text="@string/call_action_show_messages"
|
||||
android:enabled="false"
|
||||
android:enabled="@{!viewModel.operationInProgress}"
|
||||
app:layout_constraintTop_toBottomOf="@id/chat"
|
||||
app:layout_constraintStart_toStartOf="@id/transfer_label"
|
||||
app:layout_constraintEnd_toEndOf="@id/transfer_label" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()}">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/call_icon"
|
||||
android:layout_width="@dimen/icon_size"
|
||||
android:layout_height="@dimen/icon_size"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:src="@{viewModel.callsTopBarIcon, default=@drawable/phone_pause}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/call_display_name"
|
||||
app:layout_constraintBottom_toBottomOf="@id/call_display_name"
|
||||
app:tint="@color/white" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/default_text_style_800"
|
||||
|
|
@ -26,14 +38,11 @@
|
|||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@{viewModel.otherCallsLabel, default=`John Doe`}"
|
||||
android:text="@{viewModel.callsTopBarLabel, default=`John Doe`}"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:drawableStart="@drawable/pause_call"
|
||||
android:drawablePadding="10dp"
|
||||
android:drawableTint="@color/white"
|
||||
app:layout_constraintEnd_toStartOf="@id/call_time"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/call_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@
|
|||
<variable
|
||||
name="model"
|
||||
type="org.linphone.ui.main.chat.model.MessageModel" />
|
||||
<variable
|
||||
name="hideForward"
|
||||
type="Boolean" />
|
||||
</data>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
|
|
@ -156,6 +159,7 @@
|
|||
android:background="@drawable/menu_item_background"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:drawableStart="@drawable/forward"
|
||||
android:visibility="@{hideForward ? View.GONE : View.VISIBLE}"
|
||||
app:layout_constraintBottom_toTopOf="@id/delete"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
<include
|
||||
|
|
@ -98,7 +98,6 @@
|
|||
android:onClick="@{openFilePickerClickListener}"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/paperclip"
|
||||
android:visibility="@{viewModel.isFileTransferServerAvailable ? View.VISIBLE : View.GONE}"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
|
||||
app:layout_constraintEnd_toStartOf="@id/capture_image"
|
||||
app:layout_constraintStart_toEndOf="@id/emoji_picker_toggle"
|
||||
|
|
@ -113,7 +112,6 @@
|
|||
android:onClick="@{openCameraClickListener}"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/camera"
|
||||
android:visibility="@{viewModel.isFileTransferServerAvailable ? View.VISIBLE : View.GONE}"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
|
||||
app:layout_constraintEnd_toStartOf="@id/message_area_background"
|
||||
app:layout_constraintStart_toEndOf="@id/attach_file"
|
||||
|
|
@ -168,7 +166,8 @@
|
|||
android:layout_width="40dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:visibility="@{viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0 ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:enabled="@{viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0}"
|
||||
android:visibility="@{viewModel.isInCallConversation || viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0 ? View.VISIBLE : View.GONE, default=gone}"
|
||||
android:onClick="@{() -> viewModel.sendMessage()}"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/paper_plane_right"
|
||||
|
|
@ -182,7 +181,7 @@
|
|||
android:layout_width="40dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:visibility="@{viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0 || viewModel.isVoiceRecording || !viewModel.isFileTransferServerAvailable ? View.GONE : View.VISIBLE}"
|
||||
android:visibility="@{viewModel.isInCallConversation || viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0 || viewModel.isVoiceRecording || !viewModel.isFileTransferServerAvailable ? View.GONE : View.VISIBLE}"
|
||||
android:onClick="@{() -> viewModel.startVoiceMessageRecording()}"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/microphone"
|
||||
|
|
|
|||
|
|
@ -84,6 +84,12 @@
|
|||
app:popUpTo="@id/activeCallFragment"
|
||||
app:popUpToInclusive="true"
|
||||
app:launchSingleTop="true" />
|
||||
<action
|
||||
android:id="@+id/action_activeCallFragment_to_inCallConversationFragment"
|
||||
app:destination="@id/inCallConversationFragment"
|
||||
app:enterAnim="@anim/slide_in"
|
||||
app:popExitAnim="@anim/slide_out"
|
||||
app:launchSingleTop="true" />
|
||||
</fragment>
|
||||
|
||||
<action android:id="@+id/action_global_activeCallFragment"
|
||||
|
|
@ -159,4 +165,17 @@
|
|||
android:label="ConferenceParticipantsListFragment"
|
||||
tools:layout="@layout/call_conference_participants_list_fragment"/>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/inCallConversationFragment"
|
||||
android:name="org.linphone.ui.call.fragment.ConversationFragment"
|
||||
android:label="ConversationFragment"
|
||||
tools:layout="@layout/chat_conversation_fragment">
|
||||
<argument
|
||||
android:name="localSipUri"
|
||||
app:argType="string" />
|
||||
<argument
|
||||
android:name="remoteSipUri"
|
||||
app:argType="string" />
|
||||
</fragment>
|
||||
|
||||
</navigation>
|
||||
Loading…
Add table
Reference in a new issue