Fix/message deletion issues (#1696)

* SES-2810 - Removing the screenshot privacy toggle

* SES-2813 - clickable only when there is a 'follow settings'

* SES-2815 - proper icon and spacing for deleted messages

* Simplified deletion dialog to be reused for note to self and the rest as only the labels change

* SES-2819 - Do not show a reaction on a deleted message

* Fixing up deletion details

Message view hides reactions completely if the message is marked as deleted
All  messages can now show the 'Delete' long press option
Community messages should be removed completely not marked as deleted

* Revert "SES-2819 - Do not show a reaction on a deleted message"

This reverts commit 711e31a43a889187ec3be189ad4aa78f18c217d7.

* Avoiding adding reactions if the message is marked as deleted
This commit is contained in:
ThomasSession 2024-10-21 07:46:41 +11:00 committed by GitHub
parent f6d50ac858
commit 74939da01f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 57 additions and 144 deletions

View File

@ -127,11 +127,7 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
if (!isResume) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}
}

View File

@ -41,7 +41,6 @@ import org.thoughtcrime.securesms.components.emoji.EmojiImageView
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanBanSelectedUsers
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanDeleteSelectedItems
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
@ -559,10 +558,8 @@ class ConversationReactionOverlay : FrameLayout {
items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
}
// Delete message
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) },
R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
}
items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) },
R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
// Ban user
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey) && !isDeleteOnly) {
items += ActionItem(R.attr.menu_block_icon, R.string.banUser, { handleActionItemClicked(Action.BAN_USER) })

View File

@ -19,6 +19,8 @@ import network.loki.messenger.R
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.*
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.DeleteForEveryoneDialogData
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.DialogButtonModel
import org.thoughtcrime.securesms.ui.GetString
@ -48,9 +50,10 @@ fun ConversationV2Dialogs(
)
}
// delete message(s) for everyone
// delete message(s)
if(dialogsState.deleteEveryone != null){
var deleteForEveryone by remember { mutableStateOf(dialogsState.deleteEveryone.defaultToEveryone)}
val data = dialogsState.deleteEveryone
var deleteForEveryone by remember { mutableStateOf(data.defaultToEveryone)}
AlertDialog(
onDismissRequest = {
@ -59,17 +62,17 @@ fun ConversationV2Dialogs(
},
title = pluralStringResource(
R.plurals.deleteMessage,
dialogsState.deleteEveryone.messages.size,
dialogsState.deleteEveryone.messages.size
data.messages.size,
data.messages.size
),
text = pluralStringResource(
R.plurals.deleteMessageConfirm,
dialogsState.deleteEveryone.messages.size,
dialogsState.deleteEveryone.messages.size
data.messages.size,
data.messages.size
),
content = {
// add warning text, if any
dialogsState.deleteEveryone.warning?.let {
data.warning?.let {
Text(
text = it,
textAlign = TextAlign.Center,
@ -103,9 +106,9 @@ fun ConversationV2Dialogs(
),
option = RadioOption(
value = Unit,
title = GetString(stringResource(R.string.deleteMessageEveryone)),
title = GetString(data.deleteForEveryoneLabel),
selected = deleteForEveryone,
enabled = dialogsState.deleteEveryone.everyoneEnabled
enabled = data.everyoneEnabled
)
) {
deleteForEveryone = true
@ -119,9 +122,9 @@ fun ConversationV2Dialogs(
// delete messages based on chosen option
sendCommand(
if(deleteForEveryone) MarkAsDeletedForEveryone(
dialogsState.deleteEveryone.copy(defaultToEveryone = deleteForEveryone)
data.copy(defaultToEveryone = deleteForEveryone)
)
else MarkAsDeletedLocally(dialogsState.deleteEveryone.messages)
else MarkAsDeletedLocally(data.messages)
)
}
),
@ -132,76 +135,6 @@ fun ConversationV2Dialogs(
)
}
// delete message(s) for all my devices
if(dialogsState.deleteAllDevices != null){
var deleteAllDevices by remember { mutableStateOf(dialogsState.deleteAllDevices.defaultToEveryone) }
AlertDialog(
onDismissRequest = {
// hide dialog
sendCommand(HideDeleteAllDevicesDialog)
},
title = pluralStringResource(
R.plurals.deleteMessage,
dialogsState.deleteAllDevices.messages.size,
dialogsState.deleteAllDevices.messages.size
),
text = pluralStringResource(
R.plurals.deleteMessageConfirm,
dialogsState.deleteAllDevices.messages.size,
dialogsState.deleteAllDevices.messages.size
),
content = {
TitledRadioButton(
contentPadding = PaddingValues(
horizontal = LocalDimensions.current.xxsSpacing,
vertical = 0.dp
),
option = RadioOption(
value = Unit,
title = GetString(stringResource(R.string.deleteMessageDeviceOnly)),
selected = !deleteAllDevices
)
) {
deleteAllDevices = false
}
TitledRadioButton(
contentPadding = PaddingValues(
horizontal = LocalDimensions.current.xxsSpacing,
vertical = 0.dp
),
option = RadioOption(
value = Unit,
title = GetString(stringResource(R.string.deleteMessageDevicesAll)),
selected = deleteAllDevices
)
) {
deleteAllDevices = true
}
},
buttons = listOf(
DialogButtonModel(
text = GetString(stringResource(id = R.string.delete)),
color = LocalColors.current.danger,
onClick = {
// delete messages based on chosen option
sendCommand(
if(deleteAllDevices) MarkAsDeletedForEveryone(
dialogsState.deleteAllDevices.copy(defaultToEveryone = deleteAllDevices)
)
else MarkAsDeletedLocally(dialogsState.deleteAllDevices.messages)
)
}
),
DialogButtonModel(
GetString(stringResource(R.string.cancel))
)
)
)
}
// Clear emoji
if(dialogsState.clearAllEmoji != null){
AlertDialog(

View File

@ -245,12 +245,12 @@ class ConversationViewModel(
// hashes are required if wanting to delete messages from the 'storage server'
// They are not required for communities OR if all messages are outgoing
// also we can only delete deleted messages (marked as deleted) locally
// also we can only delete deleted messages and control messages (marked as deleted) locally
val canDeleteForEveryone = messages.all{ !it.isDeleted && !it.isControlMessage } && (
messages.all { it.isOutgoing } ||
conversationType == MessageType.COMMUNITY ||
messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null
})
messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null }
)
// There are three types of dialogs for deletion:
// 1- Delete on device only OR all devices - Used for Note to self
@ -260,11 +260,16 @@ class ConversationViewModel(
// the conversation is a note to self
conversationType == MessageType.NOTE_TO_SELF -> {
_dialogsState.update {
it.copy(deleteAllDevices = DeleteForEveryoneDialogData(
it.copy(deleteEveryone = DeleteForEveryoneDialogData(
messages = messages,
defaultToEveryone = false,
everyoneEnabled = true,
messageType = conversationType
everyoneEnabled = canDeleteForEveryone,
messageType = conversationType,
deleteForEveryoneLabel = application.getString(R.string.deleteMessageDevicesAll),
warning = if(canDeleteForEveryone) null else
application.resources.getQuantityString(
R.plurals.deleteMessageNoteToSelfWarning, messages.count(), messages.count()
)
)
)
}
@ -278,6 +283,7 @@ class ConversationViewModel(
messages = messages,
defaultToEveryone = isAdmin.value,
everyoneEnabled = true,
deleteForEveryoneLabel = application.getString(R.string.deleteMessageEveryone),
messageType = conversationType
)
)
@ -293,6 +299,7 @@ class ConversationViewModel(
defaultToEveryone = false,
everyoneEnabled = false, // disable 'delete for everyone' - can only delete locally in this case
messageType = conversationType,
deleteForEveryoneLabel = application.getString(R.string.deleteMessageEveryone),
warning = application.resources.getQuantityString(
R.plurals.deleteMessageWarning, messages.count(), messages.count()
)
@ -548,7 +555,7 @@ class ConversationViewModel(
).show()
}
_dialogsState.update { it.copy(deleteAllDevices = data) }
_dialogsState.update { it.copy(deleteEveryone = data) }
}
// hide loading indicator
@ -565,11 +572,8 @@ class ConversationViewModel(
try {
repository.deleteCommunityMessagesRemotely(threadId, data.messages)
// When this is done we simply need to remove the message locally
repository.markAsDeletedLocally(
messages = data.messages,
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
)
// When this is done we simply need to remove the message locally (leave nothing behind)
repository.deleteMessages(messages = data.messages, threadId = threadId)
// show confirmation toast
withContext(Dispatchers.Main) {
@ -731,12 +735,6 @@ class ConversationViewModel(
}
}
is Commands.HideDeleteAllDevicesDialog -> {
_dialogsState.update {
it.copy(deleteAllDevices = null)
}
}
is Commands.MarkAsDeletedLocally -> {
// hide dialog first
_dialogsState.update {
@ -818,8 +816,7 @@ class ConversationViewModel(
data class DialogsState(
val openLinkDialogUrl: String? = null,
val clearAllEmoji: ClearAllEmoji? = null,
val deleteEveryone: DeleteForEveryoneDialogData? = null,
val deleteAllDevices: DeleteForEveryoneDialogData? = null,
val deleteEveryone: DeleteForEveryoneDialogData? = null
)
data class DeleteForEveryoneDialogData(
@ -827,6 +824,7 @@ class ConversationViewModel(
val messageType: MessageType,
val defaultToEveryone: Boolean,
val everyoneEnabled: Boolean,
val deleteForEveryoneLabel: String,
val warning: String? = null
)
@ -841,7 +839,6 @@ class ConversationViewModel(
data class ClearEmoji(val emoji:String, val messageId: MessageId) : Commands()
data object HideDeleteEveryoneDialog : Commands()
data object HideDeleteAllDevicesDialog : Commands()
data object HideClearEmoji : Commands()
data class MarkAsDeletedLocally(val messages: Set<MessageRecord>): Commands()

View File

@ -7,13 +7,6 @@ import org.thoughtcrime.securesms.groups.OpenGroupManager
object ConversationMenuItemHelper {
@JvmStatic
fun userCanDeleteSelectedItems(context: Context, message: MessageRecord, openGroup: OpenGroup?, userPublicKey: String, blindedPublicKey: String?): Boolean {
if (openGroup == null) return message.isOutgoing || !message.isOutgoing
if (message.isOutgoing) return true
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
}
@JvmStatic
fun userCanBanSelectedUsers(context: Context, message: MessageRecord, openGroup: OpenGroup?, userPublicKey: String, blindedPublicKey: String?): Boolean {
if (openGroup == null) return false

View File

@ -87,7 +87,11 @@ class ControlMessageView : LinearLayout {
&& message.expiryMode != (MessagingModuleConfiguration.shared.storage.getExpirationConfiguration(message.threadId)?.expiryMode ?: ExpiryMode.NONE)
&& threadRecipient?.isGroupRecipient != true
binding.controlContentView.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) }
if (followSetting.isVisible) {
binding.controlContentView.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) }
} else {
binding.controlContentView.setOnClickListener(null)
}
}
}
message.isMediaSavedNotification -> {

View File

@ -234,7 +234,7 @@ class VisibleMessageView : FrameLayout {
showStatusMessage(message)
// Emoji Reactions
if (message.reactions.isNotEmpty()) {
if (!message.isDeleted && message.reactions.isNotEmpty()) {
val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) }
if (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) {
emojiReactionsBinding.value.root.let { root ->

View File

@ -1686,12 +1686,21 @@ open class Storage(
val timestamp = reaction.timestamp
val localId = reaction.localId
val isMms = reaction.isMms
val messageId = if (localId != null && localId > 0 && isMms != null) {
// bail early is the message is marked as deleted
val messagingDatabase: MessagingDatabase = if (isMms == true) DatabaseComponent.get(context).mmsDatabase()
else DatabaseComponent.get(context).smsDatabase()
if(messagingDatabase.getMessageRecord(localId)?.isDeleted == true) return
MessageId(localId, isMms)
} else if (timestamp != null && timestamp > 0) {
val messageRecord = DatabaseComponent.get(context).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: return
if (messageRecord.isDeleted) return
MessageId(messageRecord.id, messageRecord.isMms)
} else return
DatabaseComponent.get(context).reactionDatabase().addReaction(
messageId,
ReactionRecord(

View File

@ -6,14 +6,15 @@
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:padding="@dimen/small_spacing">
android:gravity="center_vertical"
android:paddingVertical="@dimen/small_spacing">
<ImageView
android:id="@+id/deletedMessageViewIconImageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="@dimen/small_spacing"
android:src="?menu_trash_icon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_marginStart="18dp"
android:src="@drawable/ic_delete"
app:tint="?android:textColorPrimary" />
<TextView
@ -22,7 +23,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="@dimen/small_spacing"
android:layout_marginEnd="@dimen/large_spacing"
android:textSize="@dimen/very_small_font_size"
android:textColor="?android:textColorPrimary"
tools:text="This message has been deleted"

View File

@ -11,12 +11,6 @@
android:title="@string/lockApp"
android:summary="@string/lockAppDescription" />
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:defaultValue="@bool/screen_security_default"
android:key="pref_screen_security"
android:title="@string/screenshotNotifications"
android:summary="@string/screenshotNotificationsDescription" />
</PreferenceCategory>
<PreferenceCategory

View File

@ -115,7 +115,6 @@ interface TextSecurePreferences {
fun isEnterSendsEnabled(): Boolean
fun isPasswordDisabled(): Boolean
fun setPasswordDisabled(disabled: Boolean)
fun isScreenSecurityEnabled(): Boolean
fun getLastVersionCode(): Int
fun setLastVersionCode(versionCode: Int)
fun isPassphraseTimeoutEnabled(): Boolean
@ -216,7 +215,6 @@ interface TextSecurePreferences {
const val LED_BLINK_PREF_CUSTOM = "pref_led_blink_custom"
const val PASSPHRASE_TIMEOUT_INTERVAL_PREF = "pref_timeout_interval"
const val PASSPHRASE_TIMEOUT_PREF = "pref_timeout_passphrase"
const val SCREEN_SECURITY_PREF = "pref_screen_security"
const val ENTER_SENDS_PREF = "pref_enter_sends"
const val THREAD_TRIM_ENABLED = "pref_trim_threads"
const val LOCAL_NUMBER_PREF = "pref_local_number"
@ -687,11 +685,6 @@ interface TextSecurePreferences {
setBooleanPreference(context, DISABLE_PASSPHRASE_PREF, disabled)
}
@JvmStatic
fun isScreenSecurityEnabled(context: Context): Boolean {
return getBooleanPreference(context, SCREEN_SECURITY_PREF, context.resources.getBoolean(R.bool.screen_security_default))
}
fun getLastVersionCode(context: Context): Int {
return getIntegerPreference(context, LAST_VERSION_CODE_PREF, 0)
}
@ -1329,10 +1322,6 @@ class AppTextSecurePreferences @Inject constructor(
setBooleanPreference(TextSecurePreferences.DISABLE_PASSPHRASE_PREF, disabled)
}
override fun isScreenSecurityEnabled(): Boolean {
return getBooleanPreference(TextSecurePreferences.SCREEN_SECURITY_PREF, true)
}
override fun getLastVersionCode(): Int {
return getIntegerPreference(TextSecurePreferences.LAST_VERSION_CODE_PREF, 0)
}