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 + +