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 75747b4c9..e701d3613 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
@@ -21,15 +21,9 @@ package org.linphone.ui.main.chat.fragment
import android.Manifest
import android.app.Activity
-import android.app.Dialog
import android.content.ActivityNotFoundException
-import android.content.ClipData
-import android.content.ClipboardManager
-import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
-import android.graphics.drawable.ColorDrawable
-import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.text.Editable
@@ -40,18 +34,13 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
-import android.view.Window
-import android.view.WindowManager
import android.widget.PopupWindow
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.UiThread
import androidx.core.app.ActivityCompat
import androidx.core.content.FileProvider
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnPreDraw
-import androidx.core.view.updatePadding
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@@ -75,7 +64,6 @@ 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.databinding.ChatConversationPopupMenuBinding
import org.linphone.ui.GenericActivity
@@ -88,6 +76,7 @@ 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.ChatMessageLongPressViewModel
import org.linphone.ui.main.chat.viewmodel.ConversationViewModel
import org.linphone.ui.main.chat.viewmodel.ConversationViewModel.Companion.SCROLLING_POSITION_NOT_SET
import org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel
@@ -119,12 +108,12 @@ open class ConversationFragment : SlidingPaneChildFragment() {
protected lateinit var sendMessageViewModel: SendMessageInConversationViewModel
+ protected lateinit var messageLongPressViewModel: ChatMessageLongPressViewModel
+
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
@@ -364,6 +353,11 @@ open class ConversationFragment : SlidingPaneChildFragment() {
binding.sendMessageViewModel = sendMessageViewModel
observeToastEvents(sendMessageViewModel)
+ messageLongPressViewModel = ViewModelProvider(this)[ChatMessageLongPressViewModel::class.java]
+ binding.messageLongPressViewModel = messageLongPressViewModel
+ observeToastEvents(messageLongPressViewModel)
+ messageLongPressViewModel.setupEmojiPicker(binding.longPressMenu.emojiPickerBottomSheet)
+
binding.setBackClickListener {
goBack()
}
@@ -617,7 +611,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
viewModel.fileToDisplayEvent.observe(viewLifecycleOwner) {
it.consume { model ->
- if (messageLongPressDialog != null) return@consume
+ if (messageLongPressViewModel.visible.value == true) return@consume
Log.i("$TAG User clicked on file [${model.path}], let's display it in file viewer")
goToFileViewer(model)
}
@@ -625,7 +619,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
viewModel.conferenceToJoinEvent.observe(viewLifecycleOwner) {
it.consume { conferenceUri ->
- if (messageLongPressDialog != null) return@consume
+ if (messageLongPressViewModel.visible.value == true) return@consume
Log.i("$TAG Requesting to go to waiting room for conference URI [$conferenceUri]")
sharedViewModel.goToMeetingWaitingRoomEvent.value = Event(conferenceUri)
}
@@ -633,7 +627,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
viewModel.openWebBrowserEvent.observe(viewLifecycleOwner) {
it.consume { url ->
- if (messageLongPressDialog != null) return@consume
+ if (messageLongPressViewModel.visible.value == true) return@consume
Log.i("$TAG Requesting to open web browser on page [$url]")
try {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
@@ -648,7 +642,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
viewModel.contactToDisplayEvent.observe(viewLifecycleOwner) {
it.consume { friendRefKey ->
- if (messageLongPressDialog != null) return@consume
+ if (messageLongPressViewModel.visible.value == true) return@consume
Log.i("$TAG Navigating to contact with ref key [$friendRefKey]")
sharedViewModel.navigateToContactsEvent.value = Event(true)
sharedViewModel.showContactEvent.value = Event(friendRefKey)
@@ -671,6 +665,49 @@ open class ConversationFragment : SlidingPaneChildFragment() {
}
}
+ messageLongPressViewModel.replyToMessageEvent.observe(viewLifecycleOwner) {
+ it.consume {
+ val model = messageLongPressViewModel.messageModel.value
+ if (model != null) {
+ sendMessageViewModel.replyToMessage(model)
+ // Open keyboard & focus edit text
+ binding.sendArea.messageToSend.showKeyboard()
+ }
+ }
+ }
+
+ messageLongPressViewModel.deleteMessageEvent.observe(viewLifecycleOwner) {
+ it.consume {
+ val model = messageLongPressViewModel.messageModel.value
+ if (model != null) {
+ viewModel.deleteChatMessage(model)
+ }
+ }
+ }
+
+ messageLongPressViewModel.forwardMessageEvent.observe(viewLifecycleOwner) {
+ it.consume {
+ val model = messageLongPressViewModel.messageModel.value
+ if (model != null) {
+ // Remove observer before setting the message to forward
+ // as we don't want to forward it in this chat room
+ sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner)
+ sharedViewModel.messageToForwardEvent.postValue(Event(model))
+
+ if (findNavController().currentDestination?.id == R.id.conversationFragment) {
+ val action = ConversationFragmentDirections.actionConversationFragmentToConversationForwardMessageFragment()
+ findNavController().navigate(action)
+ }
+ }
+ }
+ }
+
+ messageLongPressViewModel.onDismissedEvent.observe(viewLifecycleOwner) {
+ it.consume {
+ Compatibility.removeBlurRenderEffect(binding.coordinatorLayout)
+ }
+ }
+
sharedViewModel.richContentUri.observe(
viewLifecycleOwner
) {
@@ -1008,116 +1045,16 @@ open class ConversationFragment : SlidingPaneChildFragment() {
popupWindow.showAsDropDown(view, 0, 0, Gravity.BOTTOM)
}
- private fun dismissDialog() {
- messageLongPressDialog?.dismiss()
- messageLongPressDialog = null
- }
-
+ @UiThread
private fun showChatMessageLongPressMenu(chatMessageModel: MessageModel) {
- Compatibility.setBlurRenderEffect(binding.root)
-
- val layout: ChatBubbleLongPressMenuBinding = DataBindingUtil.inflate(
- LayoutInflater.from(context),
- R.layout.chat_bubble_long_press_menu,
- null,
- false
- )
- val emojiSheetBehavior = BottomSheetBehavior.from(layout.emojiPickerBottomSheet.root)
- emojiSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
- emojiSheetBehavior.skipCollapsed = 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")
- emojiSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
- }
-
- layout.setResendClickListener {
- Log.i("$TAG Re-sending message in error state")
- chatMessageModel.resend()
- dismissDialog()
- }
-
- layout.setForwardClickListener {
- Log.i("$TAG Forwarding message")
- // Remove observer before setting the message to forward
- // as we don't want to forward it in this chat room
- sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner)
- sharedViewModel.messageToForwardEvent.postValue(Event(chatMessageModel))
- dismissDialog()
-
- if (findNavController().currentDestination?.id == R.id.conversationFragment) {
- val action = ConversationFragmentDirections.actionConversationFragmentToConversationForwardMessageFragment()
- findNavController().navigate(action)
- }
- }
-
- 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
+ Compatibility.setBlurRenderEffect(binding.coordinatorLayout)
+ messageLongPressViewModel.setMessage(chatMessageModel)
chatMessageModel.dismissLongPressMenuEvent.observe(viewLifecycleOwner) {
- dismissDialog()
- }
-
- val dialog = Dialog(requireContext(), R.style.Theme_LinphoneDialog)
- dialog.apply {
- setOnDismissListener {
- Compatibility.removeBlurRenderEffect(binding.root)
- }
- requestWindowFeature(Window.FEATURE_NO_TITLE)
-
- window?.apply {
- setLayout(
- WindowManager.LayoutParams.MATCH_PARENT,
- WindowManager.LayoutParams.MATCH_PARENT
- )
-
- setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
-
- val d: Drawable = ColorDrawable(
- requireContext().getColor(R.color.grey_300)
- )
- d.alpha = 102
- setBackgroundDrawable(d)
- }
-
- setContentView(layout.root)
-
- ViewCompat.setOnApplyWindowInsetsListener(layout.root) { v, windowInsets ->
- val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
- v.updatePadding(0, 0, 0, insets.bottom)
- WindowInsetsCompat.CONSUMED
+ it.consume {
+ messageLongPressViewModel.dismiss()
}
}
-
- dialog.show()
- messageLongPressDialog = dialog
+ messageLongPressViewModel.visible.value = true
}
@UiThread
diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ChatMessageLongPressViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ChatMessageLongPressViewModel.kt
new file mode 100644
index 000000000..3c0d061c1
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ChatMessageLongPressViewModel.kt
@@ -0,0 +1,146 @@
+/*
+ * 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.main.chat.viewmodel
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.view.View
+import androidx.annotation.UiThread
+import androidx.lifecycle.MutableLiveData
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.tools.Log
+import org.linphone.databinding.ChatBubbleEmojiPickerBottomSheetBinding
+import org.linphone.ui.GenericViewModel
+import org.linphone.ui.main.chat.model.MessageModel
+import org.linphone.utils.Event
+
+class ChatMessageLongPressViewModel : GenericViewModel() {
+ companion object {
+ const val TAG = "[Chat Message LongPress ViewModel]"
+ }
+
+ val visible = MutableLiveData()
+
+ val hideForward = MutableLiveData()
+
+ val horizontalBias = MutableLiveData()
+
+ val isChatRoomReadOnly = MutableLiveData()
+
+ val messageModel = MutableLiveData()
+
+ val isMessageOutgoing = MutableLiveData()
+
+ val isMessageInError = MutableLiveData()
+
+ val replyToMessageEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val forwardMessageEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val deleteMessageEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val onDismissedEvent = MutableLiveData>()
+
+ private lateinit var emojiBottomSheet: ChatBubbleEmojiPickerBottomSheetBinding
+ private lateinit var emojiBottomSheetBehavior: BottomSheetBehavior
+
+ init {
+ visible.value = false
+ }
+
+ @UiThread
+ fun setupEmojiPicker(bottomSheet: ChatBubbleEmojiPickerBottomSheetBinding) {
+ emojiBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet.root)
+ emojiBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
+ emojiBottomSheetBehavior.skipCollapsed = true
+ }
+
+ @UiThread
+ fun setMessage(model: MessageModel) {
+ isChatRoomReadOnly.value = model.chatRoomIsReadOnly
+ isMessageOutgoing.value = model.isOutgoing
+ isMessageInError.value = model.isInError
+ horizontalBias.value = if (model.isOutgoing) 1f else 0f
+ messageModel.value = model
+
+ emojiBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
+ }
+
+ @UiThread
+ fun dismiss() {
+ onDismissedEvent.value = Event(true)
+ emojiBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
+ visible.value = false
+ }
+
+ @UiThread
+ fun resend() {
+ Log.i("$TAG Re-sending message in error state")
+ messageModel.value?.resend()
+ dismiss()
+ }
+
+ @UiThread
+ fun reply() {
+ Log.i("$TAG Replying to message")
+ replyToMessageEvent.value = Event(true)
+ dismiss()
+ }
+
+ @UiThread
+ fun copyClickListener() {
+ Log.i("$TAG Copying message text into clipboard")
+
+ val text = messageModel.value?.text?.value?.toString()
+ val clipboard = coreContext.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val label = "Message"
+ clipboard.setPrimaryClip(ClipData.newPlainText(label, text))
+
+ dismiss()
+ }
+
+ @UiThread
+ fun forwardClickListener() {
+ Log.i("$TAG Forwarding message")
+ forwardMessageEvent.value = Event(true)
+ dismiss()
+ }
+
+ @UiThread
+ fun deleteClickListener() {
+ Log.i("$TAG Deleting message")
+ deleteMessageEvent.value = Event(true)
+ dismiss()
+ }
+
+ @UiThread
+ fun pickEmoji() {
+ Log.i("$TAG Opening emoji-picker for reaction")
+ emojiBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+}
diff --git a/app/src/main/res/layout/chat_bubble_incoming.xml b/app/src/main/res/layout/chat_bubble_incoming.xml
index 43adf4171..2fcf098f2 100644
--- a/app/src/main/res/layout/chat_bubble_incoming.xml
+++ b/app/src/main/res/layout/chat_bubble_incoming.xml
@@ -22,6 +22,12 @@
+
+
+ android:layout_marginBottom="@{skipGroupMargins || model.groupedWithNextMessage ? @dimen/chat_bubble_grouped_bottom_margin : @dimen/chat_bubble_bottom_margin, default=@dimen/chat_bubble_bottom_margin}"
+ android:layout_marginTop="@{skipGroupMargins || model.groupedWithPreviousMessage ? @dimen/chat_bubble_grouped_top_margin : @dimen/chat_bubble_top_margin, default=@dimen/chat_bubble_top_margin}"
+ android:visibility="@{inflatedVisibility == View.VISIBLE ? View.VISIBLE : View.GONE}"
+ inflatedLifecycleOwner="@{true}">
-
-
-
-
-
-
-
+ name="viewModel"
+ type="org.linphone.ui.main.chat.viewmodel.ChatMessageLongPressViewModel" />
+ android:layout_height="match_parent"
+ android:onClick="@{() -> viewModel.dismiss()}">
-
-
-
-
-
-
-
-
+ android:layout_height="match_parent"
+ android:background="@color/gray_300_alpha_40">
-
-
-
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/actions">
-
+
+
+
+
+
+
-
-
-
-
-
-
+ app:layout_constraintEnd_toEndOf="parent">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ bind:model="@{viewModel.messageModel}"/>
diff --git a/app/src/main/res/layout/chat_bubble_outgoing.xml b/app/src/main/res/layout/chat_bubble_outgoing.xml
index 351249a33..4e007c55c 100644
--- a/app/src/main/res/layout/chat_bubble_outgoing.xml
+++ b/app/src/main/res/layout/chat_bubble_outgoing.xml
@@ -22,6 +22,9 @@
+
+ android:layout_marginTop="@{model.groupedWithPreviousMessage ? @dimen/chat_bubble_grouped_top_margin : @dimen/chat_bubble_top_margin, default=@dimen/chat_bubble_top_margin}"
+ android:visibility="@{inflatedVisibility == View.VISIBLE ? View.VISIBLE : View.GONE}"
+ inflatedLifecycleOwner="@{true}">
+
-
-
+ android:layout_height="match_parent">
-
+
-
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:textColor="?attr/color_main2_600"
+ android:gravity="center_vertical"
+ app:layout_constraintEnd_toStartOf="@id/start_call"
+ app:layout_constraintStart_toEndOf="@id/avatar"
+ app:layout_constraintTop_toTopOf="@id/avatar"
+ app:layout_constraintBottom_toTopOf="@id/subtitle_barrier"/>
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:id="@+id/message_bottom_sheet"
+ layout="@layout/chat_message_bottom_sheet" />
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ android:id="@+id/long_press_menu"
+ android:visibility="@{messageLongPressViewModel.visible ? View.VISIBLE : View.GONE, default=gone}"
+ bind:viewModel="@{messageLongPressViewModel}"
+ layout="@layout/chat_bubble_long_press_menu" />
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 1c15fab01..a9d02bb19 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -25,6 +25,7 @@
#EDEDED
#1F1F1F
#C9C9C9
+ #66C9C9C9
#949494
#4E4E4E
#2E3030
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 7515dcd14..8a32c532d 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -117,6 +117,26 @@
- ?attr/color_danger_500
- 8dp
+
+