mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-20 11:08:31 +00:00
Improve the pinned messages bottom sheet.
This commit is contained in:
committed by
jeffrey-signal
parent
c9a0fb30b0
commit
d2c3861ac7
@@ -9,4 +9,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
|||||||
interface ConversationBottomSheetCallback {
|
interface ConversationBottomSheetCallback {
|
||||||
fun getConversationAdapterListener(): ConversationAdapter.ItemClickListener
|
fun getConversationAdapterListener(): ConversationAdapter.ItemClickListener
|
||||||
fun jumpToMessage(messageRecord: MessageRecord)
|
fun jumpToMessage(messageRecord: MessageRecord)
|
||||||
|
fun unpin(conversationMessage: ConversationMessage)
|
||||||
|
fun copy(conversationMessage: ConversationMessage)
|
||||||
|
fun delete(conversationMessage: ConversationMessage)
|
||||||
|
fun save(conversationMessage: ConversationMessage)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<ActionItem>().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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import androidx.fragment.app.FragmentManager
|
|||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
@@ -81,7 +82,6 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment()
|
|||||||
val list: RecyclerView = view.findViewById<RecyclerView>(R.id.pinned_list).apply {
|
val list: RecyclerView = view.findViewById<RecyclerView>(R.id.pinned_list).apply {
|
||||||
layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
|
layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
|
||||||
adapter = messageAdapter
|
adapter = messageAdapter
|
||||||
itemAnimator = null
|
|
||||||
|
|
||||||
doOnNextLayout {
|
doOnNextLayout {
|
||||||
// Adding this without waiting for a layout pass would result in an indeterminate amount of padding added to the top of the view
|
// Adding this without waiting for a layout pass would result in an indeterminate amount of padding added to the top of the view
|
||||||
@@ -96,11 +96,14 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment()
|
|||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val currentMessages = messageAdapter.currentList
|
||||||
|
if (currentMessages != messages) {
|
||||||
messageAdapter.submitList(messages) {
|
messageAdapter.submitList(messages) {
|
||||||
if (!list.canScrollVertically(1)) {
|
if (!list.canScrollVertically(1)) {
|
||||||
list.layoutManager?.scrollToPosition(0)
|
list.layoutManager?.scrollToPosition(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
recyclerViewColorizer.setChatColors(conversationRecipient.chatColors)
|
recyclerViewColorizer.setChatColors(conversationRecipient.chatColors)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,12 +114,18 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment()
|
|||||||
|
|
||||||
initializeGiphyMp4(view.findViewById(R.id.video_container)!!, list)
|
initializeGiphyMp4(view.findViewById(R.id.video_container)!!, list)
|
||||||
|
|
||||||
// TODO(michelle): Check with design about a confirmation dialog here
|
|
||||||
val unpinAll = view.findViewById<TextView>(R.id.unpin_all)
|
val unpinAll = view.findViewById<TextView>(R.id.unpin_all)
|
||||||
unpinAll.setOnClickListener {
|
unpinAll.setOnClickListener {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.PinnedMessage__unpin_title)
|
||||||
|
.setMessage(getString(R.string.PinnedMessage__unpin_body))
|
||||||
|
.setPositiveButton(R.string.PinnedMessage__unpin) { dialog, which ->
|
||||||
viewModel.unpinMessage()
|
viewModel.unpinMessage()
|
||||||
dismissAllowingStateLoss()
|
dismissAllowingStateLoss()
|
||||||
}
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { dialog, which -> dialog.dismiss() }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
unpinAll.visible = requireArguments().getBoolean(KEY_CAN_UNPIN)
|
unpinAll.visible = requireArguments().getBoolean(KEY_CAN_UNPIN)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +153,6 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment()
|
|||||||
return getCallback().getConversationAdapterListener()
|
return getCallback().getConversationAdapterListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(michelle): Allow for more interactions from the pinned messages sheet
|
|
||||||
private inner class ConversationAdapterListener : ConversationAdapter.ItemClickListener by getAdapterListener() {
|
private inner class ConversationAdapterListener : ConversationAdapter.ItemClickListener by getAdapterListener() {
|
||||||
override fun onItemClick(item: MultiselectPart) {
|
override fun onItemClick(item: MultiselectPart) {
|
||||||
dismiss()
|
dismiss()
|
||||||
@@ -152,8 +160,27 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment()
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemLongClick(itemView: View, item: MultiselectPart) {
|
override fun onItemLongClick(itemView: View, item: MultiselectPart) {
|
||||||
|
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()
|
dismiss()
|
||||||
getCallback().jumpToMessage(item.getMessageRecord())
|
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) {
|
override fun onQuoteClicked(messageRecord: MmsMessageRecord) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.signal.core.util.logging.Log
|
|||||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
|
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
|
||||||
import org.thoughtcrime.securesms.conversation.v2.data.AttachmentHelper
|
import org.thoughtcrime.securesms.conversation.v2.data.AttachmentHelper
|
||||||
import org.thoughtcrime.securesms.conversation.v2.data.ReactionHelper
|
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.SignalDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
@@ -22,6 +23,11 @@ class PinnedMessagesRepository {
|
|||||||
|
|
||||||
fun getPinnedMessage(application: Application, threadId: Long): Observable<List<ConversationMessage>> {
|
fun getPinnedMessage(application: Application, threadId: Long): Observable<List<ConversationMessage>> {
|
||||||
return Observable.create { emitter ->
|
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))
|
emitter.onNext(getPinnedMessages(application, threadId))
|
||||||
}.subscribeOn(Schedulers.io())
|
}.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -820,6 +820,22 @@ class ConversationFragment :
|
|||||||
.addTo(disposables)
|
.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() {
|
override fun onReactWithAnyEmojiDialogDismissed() {
|
||||||
reactionDelegate.hide()
|
reactionDelegate.hide()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,17 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pinned_message_header"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/anchor"
|
||||||
|
android:layout_marginVertical="16dp"
|
||||||
|
android:text="@string/PinnedMessage__pinned_messages"
|
||||||
|
android:textAppearance="@style/Signal.Text.TitleMedium" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/video_container"
|
android:id="@+id/video_container"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@@ -34,23 +45,21 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingBottom="40dp"
|
android:paddingBottom="40dp"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
app:layout_constraintTop_toBottomOf="@id/anchor"
|
app:layout_constraintTop_toBottomOf="@id/pinned_message_header"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/unpin_all" />
|
||||||
|
|
||||||
<TextView
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/unpin_all"
|
android:id="@+id/unpin_all"
|
||||||
|
style="@style/Signal.Widget.Button.Large.Tonal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:paddingVertical="8dp"
|
|
||||||
android:background="@color/signal_colorSurface"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:text="@string/PinnedMessage__unpin_all_messages"
|
android:layout_marginVertical="8dp"
|
||||||
android:textAppearance="@style/Signal.Text.LabelLarge"
|
android:layout_marginHorizontal="32dp"
|
||||||
android:textColor="@color/conversation_ultramarine"
|
android:text="@string/PinnedMessage__unpin_all_messages" />
|
||||||
android:textAlignment="center"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -9062,6 +9062,14 @@
|
|||||||
<string name="PinnedMessage__got_it">Got it</string>
|
<string name="PinnedMessage__got_it">Got it</string>
|
||||||
<!-- Content description of disappearing timer icon -->
|
<!-- Content description of disappearing timer icon -->
|
||||||
<string name="PinnedMessage__disappearing_message_content_description">Disappearing message</string>
|
<string name="PinnedMessage__disappearing_message_content_description">Disappearing message</string>
|
||||||
|
<!-- Title of bottom sheet -->
|
||||||
|
<string name="PinnedMessage__pinned_messages">Pinned messages</string>
|
||||||
|
<!-- Context menu option to unpin message -->
|
||||||
|
<string name="PinnedMessage__unpin">Unpin</string>
|
||||||
|
<!-- Title of dialog to unpin all messages -->
|
||||||
|
<string name="PinnedMessage__unpin_title">Unpin all messages?</string>
|
||||||
|
<!-- Body of dialog to unpin all messages -->
|
||||||
|
<string name="PinnedMessage__unpin_body">Messages will be unpinned for all members.</string>
|
||||||
|
|
||||||
<!-- EOF -->
|
<!-- EOF -->
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user