diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBottomSheetCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBottomSheetCallback.kt index 0ac3f09a6c..fdbc76c939 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBottomSheetCallback.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBottomSheetCallback.kt @@ -9,4 +9,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord interface ConversationBottomSheetCallback { fun getConversationAdapterListener(): ConversationAdapter.ItemClickListener fun jumpToMessage(messageRecord: MessageRecord) + fun unpin(conversationMessage: ConversationMessage) + fun copy(conversationMessage: ConversationMessage) + fun delete(conversationMessage: ConversationMessage) + fun save(conversationMessage: ConversationMessage) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedContextMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedContextMenu.kt new file mode 100644 index 0000000000..fbe1af5e6d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedContextMenu.kt @@ -0,0 +1,114 @@ +package org.thoughtcrime.securesms.conversation + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import org.signal.core.util.DimensionUnit +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.menu.ActionItem +import org.thoughtcrime.securesms.components.menu.SignalContextMenu +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.util.hasGiftBadge +import org.thoughtcrime.securesms.util.isPoll +import org.thoughtcrime.securesms.util.isViewOnceMessage + +/** + * A context menu shown when long pressing on a pinned messages + */ +object PinnedContextMenu { + + fun show( + context: Context, + anchorView: View, + rootView: ViewGroup = anchorView.rootView as ViewGroup, + message: MmsMessageRecord, + canUnpin: Boolean, + onUnpin: () -> Unit = {}, + onCopy: () -> Unit = {}, + onDelete: () -> Unit = {}, + onSave: () -> Unit = {} + ) { + show( + context = context, + anchorView = anchorView, + rootView = rootView, + message = message, + canUnpin = canUnpin, + callbacks = object : Callbacks { + override fun onUnpin() = onUnpin() + override fun onCopy() = onCopy() + override fun onDelete() = onDelete() + override fun onSave() = onSave() + } + ) + } + + private fun show( + context: Context, + anchorView: View, + rootView: ViewGroup, + message: MmsMessageRecord, + canUnpin: Boolean, + callbacks: Callbacks + ) { + val actions = mutableListOf().apply { + if (canUnpin) { + add( + ActionItem(R.drawable.symbol_pin_slash_24, context.getString(R.string.PinnedMessage__unpin)) { + callbacks.onUnpin() + } + ) + } + + if (message.body.isNotEmpty() && + !message.isRemoteDelete && + !message.isPaymentNotification && + !message.isPoll() && + !message.hasGiftBadge() + ) { + add( + ActionItem(R.drawable.symbol_copy_android_24, context.getString(R.string.conversation_selection__menu_copy)) { + callbacks.onCopy() + } + ) + } + + if (!message.isRemoteDelete) { + add( + ActionItem(R.drawable.symbol_trash_24, context.getString(R.string.conversation_selection__menu_delete)) { + callbacks.onDelete() + } + ) + } + + if ( + !message.isViewOnceMessage() && + !message.isMediaPending && + !message.hasGiftBadge() && + message.containsMediaSlide() && + message.slideDeck.getStickerSlide() == null + ) { + add( + ActionItem(R.drawable.symbol_save_android_24, context.getString(R.string.conversation_selection__menu_save)) { + callbacks.onSave() + } + ) + } + } + + val horizontalPosition = if (message.isOutgoing) SignalContextMenu.HorizontalPosition.END else SignalContextMenu.HorizontalPosition.START + SignalContextMenu.Builder(anchorView, rootView) + .preferredHorizontalPosition(horizontalPosition) + .preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW) + .offsetX(DimensionUnit.DP.toPixels(16f).toInt()) + .offsetY(DimensionUnit.DP.toPixels(4f).toInt()) + .show(actions) + } + + private interface Callbacks { + fun onUnpin() + fun onCopy() + fun onDelete() + fun onSave() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesBottomSheet.kt index 2ab944940b..b8622c7d29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesBottomSheet.kt @@ -12,6 +12,7 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R @@ -81,7 +82,6 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() val list: RecyclerView = view.findViewById(R.id.pinned_list).apply { layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true) adapter = messageAdapter - itemAnimator = null doOnNextLayout { // Adding this without waiting for a layout pass would result in an indeterminate amount of padding added to the top of the view @@ -96,9 +96,12 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() dismiss() } - messageAdapter.submitList(messages) { - if (!list.canScrollVertically(1)) { - list.layoutManager?.scrollToPosition(0) + val currentMessages = messageAdapter.currentList + if (currentMessages != messages) { + messageAdapter.submitList(messages) { + if (!list.canScrollVertically(1)) { + list.layoutManager?.scrollToPosition(0) + } } } recyclerViewColorizer.setChatColors(conversationRecipient.chatColors) @@ -111,11 +114,17 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() initializeGiphyMp4(view.findViewById(R.id.video_container)!!, list) - // TODO(michelle): Check with design about a confirmation dialog here val unpinAll = view.findViewById(R.id.unpin_all) unpinAll.setOnClickListener { - viewModel.unpinMessage() - dismissAllowingStateLoss() + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.PinnedMessage__unpin_title) + .setMessage(getString(R.string.PinnedMessage__unpin_body)) + .setPositiveButton(R.string.PinnedMessage__unpin) { dialog, which -> + viewModel.unpinMessage() + dismissAllowingStateLoss() + } + .setNegativeButton(android.R.string.cancel) { dialog, which -> dialog.dismiss() } + .show() } unpinAll.visible = requireArguments().getBoolean(KEY_CAN_UNPIN) } @@ -144,7 +153,6 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() return getCallback().getConversationAdapterListener() } - // TODO(michelle): Allow for more interactions from the pinned messages sheet private inner class ConversationAdapterListener : ConversationAdapter.ItemClickListener by getAdapterListener() { override fun onItemClick(item: MultiselectPart) { dismiss() @@ -152,8 +160,27 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() } override fun onItemLongClick(itemView: View, item: MultiselectPart) { - dismiss() - getCallback().jumpToMessage(item.getMessageRecord()) + PinnedContextMenu.show( + context = itemView.context, + anchorView = itemView, + rootView = itemView.rootView as ViewGroup, + message = item.conversationMessage.messageRecord as MmsMessageRecord, + canUnpin = requireArguments().getBoolean(KEY_CAN_UNPIN), + onUnpin = { + dismiss() + getCallback().unpin(item.conversationMessage) + }, + onCopy = { + getCallback().copy(item.conversationMessage) + }, + onDelete = { + dismiss() + getCallback().delete(item.conversationMessage) + }, + onSave = { + getCallback().save(item.conversationMessage) + } + ) } override fun onQuoteClicked(messageRecord: MmsMessageRecord) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesRepository.kt index 75e4f5307a..7342455c3b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesRepository.kt @@ -7,6 +7,7 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory import org.thoughtcrime.securesms.conversation.v2.data.AttachmentHelper import org.thoughtcrime.securesms.conversation.v2.data.ReactionHelper +import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.dependencies.AppDependencies @@ -22,6 +23,11 @@ class PinnedMessagesRepository { fun getPinnedMessage(application: Application, threadId: Long): Observable> { return Observable.create { emitter -> + val databaseObserver: DatabaseObserver = AppDependencies.databaseObserver + val observer = DatabaseObserver.Observer { emitter.onNext(getPinnedMessages(application, threadId)) } + databaseObserver.registerConversationObserver(threadId, observer) + emitter.setCancellable { databaseObserver.unregisterObserver(observer) } + emitter.onNext(getPinnedMessages(application, threadId)) }.subscribeOn(Schedulers.io()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 325aac5251..d714c22f62 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -820,6 +820,22 @@ class ConversationFragment : .addTo(disposables) } + override fun unpin(conversationMessage: ConversationMessage) { + handleUnpinMessage(conversationMessage.messageRecord.id) + } + + override fun copy(conversationMessage: ConversationMessage) { + handleCopyMessage(conversationMessage.multiselectCollection.toSet()) + } + + override fun delete(conversationMessage: ConversationMessage) { + handleDeleteMessages(conversationMessage.multiselectCollection.toSet()) + } + + override fun save(conversationMessage: ConversationMessage) { + handleSaveAttachment(conversationMessage.messageRecord as MmsMessageRecord) + } + override fun onReactWithAnyEmojiDialogDismissed() { reactionDelegate.hide() } diff --git a/app/src/main/res/layout/pinned_messages_bottom_sheet.xml b/app/src/main/res/layout/pinned_messages_bottom_sheet.xml index 5d636c1187..9993e75b06 100644 --- a/app/src/main/res/layout/pinned_messages_bottom_sheet.xml +++ b/app/src/main/res/layout/pinned_messages_bottom_sheet.xml @@ -19,6 +19,17 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> + + - - + app:layout_constraintBottom_toTopOf="@+id/unpin_all" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 17ef8c179f..6e47c179b4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9062,6 +9062,14 @@ Got it Disappearing message + + Pinned messages + + Unpin + + Unpin all messages? + + Messages will be unpinned for all members.