Improve remote delete handling in group story threads.
This commit is contained in:
parent
8b1552952c
commit
944c8530d8
8 changed files with 246 additions and 33 deletions
|
@ -36,13 +36,22 @@ class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSour
|
|||
}
|
||||
|
||||
private fun readRowFromRecord(record: MmsMessageRecord): StoryGroupReplyItemData {
|
||||
return if (MmsSmsColumns.Types.isStoryReaction(record.type)) {
|
||||
readReactionFromRecord(record)
|
||||
} else {
|
||||
readTextFromRecord(record)
|
||||
return when {
|
||||
record.isRemoteDelete -> readRemoteDeleteFromRecord(record)
|
||||
MmsSmsColumns.Types.isStoryReaction(record.type) -> readReactionFromRecord(record)
|
||||
else -> readTextFromRecord(record)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readRemoteDeleteFromRecord(record: MmsMessageRecord): StoryGroupReplyItemData {
|
||||
return StoryGroupReplyItemData(
|
||||
key = StoryGroupReplyItemData.Key.RemoteDelete(record.id),
|
||||
sender = if (record.isOutgoing) Recipient.self() else record.individualRecipient.resolve(),
|
||||
sentAtMillis = record.dateSent,
|
||||
replyBody = StoryGroupReplyItemData.ReplyBody.RemoteDelete(record)
|
||||
)
|
||||
}
|
||||
|
||||
private fun readReactionFromRecord(record: MmsMessageRecord): StoryGroupReplyItemData {
|
||||
return StoryGroupReplyItemData(
|
||||
key = StoryGroupReplyItemData.Key.Reaction(record.id),
|
||||
|
|
|
@ -181,9 +181,6 @@ class StoryGroupReplyFragment :
|
|||
requireContext(),
|
||||
it.sender
|
||||
),
|
||||
onPrivateReplyClick = { model ->
|
||||
requireListener<Callback>().onStartDirectReply(model.storyGroupReplyItemData.sender.id)
|
||||
},
|
||||
onCopyClick = { model ->
|
||||
val clipData = ClipData.newPlainText(requireContext().getString(R.string.app_name), model.text.message.getDisplayBody(requireContext()))
|
||||
ServiceUtil.getClipboardManager(requireContext()).setPrimaryClip(clipData)
|
||||
|
@ -216,6 +213,25 @@ class StoryGroupReplyFragment :
|
|||
)
|
||||
)
|
||||
}
|
||||
is StoryGroupReplyItemData.ReplyBody.RemoteDelete -> {
|
||||
customPref(
|
||||
StoryGroupReplyItem.RemoteDeleteModel(
|
||||
storyGroupReplyItemData = it,
|
||||
remoteDelete = it.replyBody,
|
||||
nameColor = colorizer.getIncomingGroupSenderColor(
|
||||
requireContext(),
|
||||
it.sender
|
||||
),
|
||||
onDeleteClick = { model ->
|
||||
lifecycleDisposable += DeleteDialog.show(requireActivity(), setOf(model.remoteDelete.messageRecord)).subscribe { didDeleteThread ->
|
||||
if (didDeleteThread) {
|
||||
throw AssertionError("We should never end up deleting a Group Thread like this.")
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.util.DateUtils
|
|||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.util.Locale
|
||||
|
@ -38,13 +39,13 @@ object StoryGroupReplyItem {
|
|||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(TextModel::class.java, LayoutFactory(::TextViewHolder, R.layout.stories_group_text_reply_item))
|
||||
mappingAdapter.registerFactory(ReactionModel::class.java, LayoutFactory(::ReactionViewHolder, R.layout.stories_group_reaction_reply_item))
|
||||
mappingAdapter.registerFactory(RemoteDeleteModel::class.java, LayoutFactory(::RemoteDeleteViewHolder, R.layout.stories_group_remote_delete_item))
|
||||
}
|
||||
|
||||
class TextModel(
|
||||
val storyGroupReplyItemData: StoryGroupReplyItemData,
|
||||
val text: StoryGroupReplyItemData.ReplyBody.Text,
|
||||
@ColorInt val nameColor: Int,
|
||||
val onPrivateReplyClick: (TextModel) -> Unit,
|
||||
val onCopyClick: (TextModel) -> Unit,
|
||||
val onDeleteClick: (TextModel) -> Unit,
|
||||
val onMentionClick: (RecipientId) -> Unit
|
||||
|
@ -74,6 +75,35 @@ object StoryGroupReplyItem {
|
|||
}
|
||||
}
|
||||
|
||||
class RemoteDeleteModel(
|
||||
val storyGroupReplyItemData: StoryGroupReplyItemData,
|
||||
val remoteDelete: StoryGroupReplyItemData.ReplyBody.RemoteDelete,
|
||||
val onDeleteClick: (RemoteDeleteModel) -> Unit,
|
||||
@ColorInt val nameColor: Int
|
||||
) : MappingModel<RemoteDeleteModel> {
|
||||
override fun areItemsTheSame(newItem: RemoteDeleteModel): Boolean {
|
||||
return storyGroupReplyItemData.sender == newItem.storyGroupReplyItemData.sender &&
|
||||
storyGroupReplyItemData.sentAtMillis == newItem.storyGroupReplyItemData.sentAtMillis
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: RemoteDeleteModel): Boolean {
|
||||
return storyGroupReplyItemData == newItem.storyGroupReplyItemData &&
|
||||
storyGroupReplyItemData.sender.hasSameContent(newItem.storyGroupReplyItemData.sender) &&
|
||||
nameColor == newItem.nameColor
|
||||
}
|
||||
|
||||
override fun getChangePayload(newItem: RemoteDeleteModel): Any? {
|
||||
return if (nameColor != newItem.nameColor &&
|
||||
storyGroupReplyItemData == newItem.storyGroupReplyItemData &&
|
||||
storyGroupReplyItemData.sender.hasSameContent(newItem.storyGroupReplyItemData.sender)
|
||||
) {
|
||||
NAME_COLOR_CHANGED
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ReactionModel(
|
||||
val storyGroupReplyItemData: StoryGroupReplyItemData,
|
||||
val reaction: StoryGroupReplyItemData.ReplyBody.Reaction,
|
||||
|
@ -104,13 +134,12 @@ object StoryGroupReplyItem {
|
|||
}
|
||||
}
|
||||
|
||||
private class TextViewHolder(itemView: View) : MappingViewHolder<TextModel>(itemView) {
|
||||
|
||||
private val avatar: AvatarImageView = itemView.findViewById(R.id.avatar)
|
||||
private val name: FromTextView = itemView.findViewById(R.id.name)
|
||||
private val body: EmojiTextView = itemView.findViewById(R.id.body)
|
||||
private val date: TextView = itemView.findViewById(R.id.viewed_at)
|
||||
private val dateBelow: TextView = itemView.findViewById(R.id.viewed_at_below)
|
||||
private abstract class BaseViewHolder<T>(itemView: View) : MappingViewHolder<T>(itemView) {
|
||||
protected val avatar: AvatarImageView = itemView.findViewById(R.id.avatar)
|
||||
protected val name: FromTextView = itemView.findViewById(R.id.name)
|
||||
protected val body: EmojiTextView = itemView.findViewById(R.id.body)
|
||||
protected val date: TextView = itemView.findViewById(R.id.viewed_at)
|
||||
protected val dateBelow: TextView = itemView.findViewById(R.id.viewed_at_below)
|
||||
|
||||
init {
|
||||
body.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
|
@ -123,6 +152,9 @@ object StoryGroupReplyItem {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TextViewHolder(itemView: View) : BaseViewHolder<TextModel>(itemView) {
|
||||
|
||||
override fun bind(model: TextModel) {
|
||||
itemView.setOnLongClickListener {
|
||||
|
@ -179,6 +211,39 @@ object StoryGroupReplyItem {
|
|||
}
|
||||
}
|
||||
|
||||
private class RemoteDeleteViewHolder(itemView: View) : BaseViewHolder<RemoteDeleteModel>(itemView) {
|
||||
|
||||
override fun bind(model: RemoteDeleteModel) {
|
||||
itemView.setOnLongClickListener {
|
||||
displayContextMenu(model)
|
||||
true
|
||||
}
|
||||
|
||||
name.setTextColor(model.nameColor)
|
||||
if (payload.contains(NAME_COLOR_CHANGED)) {
|
||||
return
|
||||
}
|
||||
|
||||
AvatarUtil.loadIconIntoImageView(model.storyGroupReplyItemData.sender, avatar, DimensionUnit.DP.toPixels(28f).toInt())
|
||||
name.text = resolveName(context, model.storyGroupReplyItemData.sender)
|
||||
|
||||
date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.storyGroupReplyItemData.sentAtMillis)
|
||||
dateBelow.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.storyGroupReplyItemData.sentAtMillis)
|
||||
}
|
||||
|
||||
private fun displayContextMenu(model: RemoteDeleteModel) {
|
||||
itemView.isSelected = true
|
||||
SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup)
|
||||
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START)
|
||||
.onDismiss { itemView.isSelected = false }
|
||||
.show(
|
||||
listOf(
|
||||
ActionItem(R.drawable.ic_trash_24_solid_tinted, context.getString(R.string.StoryGroupReplyItem__delete)) { model.onDeleteClick(model) }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class ReactionViewHolder(itemView: View) : MappingViewHolder<ReactionModel>(itemView) {
|
||||
private val avatar: AvatarImageView = itemView.findViewById(R.id.avatar)
|
||||
private val name: FromTextView = itemView.findViewById(R.id.name)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.thoughtcrime.securesms.stories.viewer.reply.group
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
data class StoryGroupReplyItemData(
|
||||
|
@ -12,10 +13,12 @@ data class StoryGroupReplyItemData(
|
|||
sealed class ReplyBody {
|
||||
data class Text(val message: ConversationMessage) : ReplyBody()
|
||||
data class Reaction(val emoji: CharSequence) : ReplyBody()
|
||||
data class RemoteDelete(val messageRecord: MessageRecord) : ReplyBody()
|
||||
}
|
||||
|
||||
sealed class Key {
|
||||
data class Text(val messageId: Long) : Key()
|
||||
data class Reaction(val reactionId: Long) : Key()
|
||||
data class RemoteDelete(val messageId: Long) : Key()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ class StoryGroupReplyRepository {
|
|||
|
||||
val threadId = SignalDatabase.mms.getThreadIdForMessage(parentStoryId)
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageObserver)
|
||||
ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(threadId, messageObserver)
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(threadId, observer)
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.database.model.ParentStoryId
|
|||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.mediasend.v2.UntrustedRecords
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
|
||||
/**
|
||||
|
@ -40,24 +41,26 @@ object StoryGroupReplySender {
|
|||
Completable.create {
|
||||
MessageSender.send(
|
||||
context,
|
||||
OutgoingMediaMessage(
|
||||
recipient,
|
||||
body.toString(),
|
||||
emptyList(),
|
||||
System.currentTimeMillis(),
|
||||
0,
|
||||
0L,
|
||||
false,
|
||||
0,
|
||||
StoryType.NONE,
|
||||
ParentStoryId.GroupReply(message.id),
|
||||
isReaction,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
mentions,
|
||||
emptySet(),
|
||||
emptySet()
|
||||
OutgoingSecureMediaMessage(
|
||||
OutgoingMediaMessage(
|
||||
recipient,
|
||||
body.toString(),
|
||||
emptyList(),
|
||||
System.currentTimeMillis(),
|
||||
0,
|
||||
0L,
|
||||
false,
|
||||
0,
|
||||
StoryType.NONE,
|
||||
ParentStoryId.GroupReply(message.id),
|
||||
isReaction,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
mentions,
|
||||
emptySet(),
|
||||
emptySet()
|
||||
)
|
||||
),
|
||||
message.threadId,
|
||||
false,
|
||||
|
|
|
@ -15,6 +15,17 @@ import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask
|
|||
|
||||
object DeleteDialog {
|
||||
|
||||
/**
|
||||
* Displays a deletion dialog for the given set of message records.
|
||||
*
|
||||
* @param context Android Context
|
||||
* @param messageRecords The message records to delete
|
||||
* @param title The dialog title
|
||||
* @param message The dialog message, or null
|
||||
* @param forceRemoteDelete Allow remote deletion, even if it would normally be disallowed
|
||||
*
|
||||
* @return a Single, who's value notes whether or not a thread deletion occurred.
|
||||
*/
|
||||
fun show(
|
||||
context: Context,
|
||||
messageRecords: Set<MessageRecord>,
|
||||
|
|
105
app/src/main/res/layout/stories_group_remote_delete_item.xml
Normal file
105
app/src/main/res/layout/stories_group_remote_delete_item.xml
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:background="@drawable/selectable_list_item_background"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="6dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
app:fallbackImageSize="small"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/bubble"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@drawable/rounded_rectangle_secondary_18"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/avatar"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<org.thoughtcrime.securesms.components.FromTextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Subtitle.Bold"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Miles Morales" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="1dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:text="@string/ThreadRecord_this_message_was_deleted"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textStyle="italic"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/viewed_at_below"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/name"
|
||||
app:layout_goneMarginBottom="7dp"
|
||||
app:measureLastLine="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/viewed_at"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
android:textColor="@color/transparent_white_60"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1"
|
||||
app:layout_constraintStart_toEndOf="@id/body"
|
||||
tools:text="15m"
|
||||
tools:textColor="@color/signal_text_secondary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/viewed_at_below"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
android:textColor="@color/transparent_white_60"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/bubble"
|
||||
app:layout_constraintEnd_toEndOf="@id/bubble"
|
||||
tools:text="15m"
|
||||
tools:textColor="@color/signal_text_secondary" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Add table
Reference in a new issue