Updated message deletion logic

Still need to finalise "note to self" and "legacy groups"
pull/1518/head
ThomasSession 7 months ago
parent 2861b3a02d
commit 28498c4ff2

@ -241,7 +241,6 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
//todo DELETION can this be batched?
messages.forEach { message ->
messagingDatabase.markAsDeleted(message.messageId, message.isOutgoing, displayedMessage)
}

@ -845,7 +845,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
finish()
}
// show or hide the text input
binding.inputBar.isGone = uiState.hideInputBar
// show or hide loading indicator
binding.loader.isVisible = uiState.showLoader
}
}
}

@ -10,24 +10,24 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.squareup.phrase.Phrase
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.*
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteAllDevicesDialog
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteDeviceOnlyDialog
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteEveryoneDialog
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedForEveryone
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedLocally
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.ShowOpenUrlDialog
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.DeleteForEveryoneMessageType.NoteToSelf
import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.DialogButtonModel
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
import org.thoughtcrime.securesms.ui.RadioOption
import org.thoughtcrime.securesms.ui.components.TitledRadioButton
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import kotlin.time.Duration.Companion.days
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
@Composable
fun ConversationV2Dialogs(
@ -125,7 +125,9 @@ fun ConversationV2Dialogs(
onClick = {
// delete messages based on chosen option
sendCommand(
if(deleteForEveryone) MarkAsDeletedForEveryone(dialogsState.deleteEveryone.messages)
if(deleteForEveryone) MarkAsDeletedForEveryone(
dialogsState.deleteEveryone.copy(defaultToEveryone = deleteForEveryone)
)
else MarkAsDeletedLocally(dialogsState.deleteEveryone.messages)
)
}
@ -139,7 +141,7 @@ fun ConversationV2Dialogs(
// delete message(s) for all my devices
if(dialogsState.deleteAllDevices != null){
var deleteAllDevices by remember { mutableStateOf(false) }
var deleteAllDevices by remember { mutableStateOf(dialogsState.deleteAllDevices.defaultToEveryone) }
AlertDialog(
onDismissRequest = {
@ -148,8 +150,8 @@ fun ConversationV2Dialogs(
},
title = pluralStringResource(
R.plurals.deleteMessage,
dialogsState.deleteAllDevices.size,
dialogsState.deleteAllDevices.size
dialogsState.deleteAllDevices.messages.size,
dialogsState.deleteAllDevices.messages.size
),
text = stringResource(R.string.deleteMessageConfirm), //todo DELETION we need the plural version of this here, which currently is not set up in strings
content = {
@ -188,8 +190,10 @@ fun ConversationV2Dialogs(
onClick = {
// delete messages based on chosen option
sendCommand(
if(deleteAllDevices) MarkAsDeletedForEveryone(dialogsState.deleteAllDevices)
else MarkAsDeletedLocally(dialogsState.deleteAllDevices)
if(deleteAllDevices) MarkAsDeletedForEveryone(
dialogsState.deleteAllDevices.copy(defaultToEveryone = deleteAllDevices)
)
else MarkAsDeletedLocally(dialogsState.deleteAllDevices.messages)
)
}
),

@ -199,17 +199,13 @@ class ConversationViewModel(
// Refer to our figma document for info on message deletion [https://www.figma.com/design/kau6LggVcMMWmZRMibEo8F/Standardise-Message-Deletion?node-id=0-1&t=dEPcU0SZ9G2s4gh2-0]
//todo DELETION delete for everyone
//todo DELETION delete all my devices
//todo DELETION handle control messages deletion ( and make clickable )
//todo DELETION handle multi select scenarios
//todo DELETION check that the unread status works as expected when deleting a message
//todo DELETION handle errors: Toasts for errors, or deleting messages not fully sent yet
//todo DELETION handle deleting messages not fully sent yet (failed or sending states)
viewModelScope.launch(Dispatchers.IO) {
val allSentByCurrentUser = messages.all { it.isOutgoing }
@ -255,7 +251,12 @@ class ConversationViewModel(
// the conversation is a note to self
conversation.isLocalNumber -> {
_dialogsState.update {
it.copy(deleteAllDevices = messages)
it.copy(deleteAllDevices = DeleteForEveryoneDialogData(
messages = messages,
defaultToEveryone = false,
messageType = DeleteForEveryoneMessageType.NoteToSelf
)
)
}
}
@ -265,7 +266,14 @@ class ConversationViewModel(
it.copy(
deleteEveryone = DeleteForEveryoneDialogData(
messages = messages,
defaultToEveryone = isAdmin
defaultToEveryone = isAdmin,
messageType = when{
conversation.isLocalNumber -> DeleteForEveryoneMessageType.NoteToSelf
conversation.isCommunityRecipient -> DeleteForEveryoneMessageType.Community
conversation.isClosedGroupRecipient -> DeleteForEveryoneMessageType.LegacyGroup //todo GROUPS V2 this property will change for groups v2. Check for legacyGroup here
//conversation.isClosedGroup -> DeleteForEveryoneMessageType.GroupV2(isAdmin) //todo GROUPS V2 properly check for GroupV2 type here once available
else -> DeleteForEveryoneMessageType.OneOnOne
}
)
)
}
@ -282,11 +290,6 @@ class ConversationViewModel(
}
}
private fun isUserCommunityManager() = openGroup?.let { openGroup ->
val userPublicKey = textSecurePreferences.getLocalNumber() ?: return@let false
OpenGroupManager.isUserModerator(application, openGroup.id, userPublicKey, blindedPublicKey)
} ?: false
/**
* This will delete these messages from the db
* Not to be confused with 'marking messages as deleted'
@ -322,23 +325,6 @@ class ConversationViewModel(
).show()
}
/**
* Stops audio player if its current playing is the one given in the message.
*/
private fun stopMessageAudio(message: MessageRecord) {
val mmsMessage = message as? MmsMessageRecord ?: return
val audioSlide = mmsMessage.slideDeck.audioSlide ?: return
stopMessageAudio(audioSlide)
}
private fun stopMessageAudio(audioSlide: AudioSlide) {
AudioSlidePlayer.getInstance()?.takeIf { it.audioSlide == audioSlide }?.stop()
}
fun setRecipientApproved() {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for set approved action")
repository.setApproved(recipient, true)
}
/**
* This will mark the messages as deleted, for everyone.
* Attachments and other related data will be removed from the db,
@ -346,24 +332,237 @@ class ConversationViewModel(
* Instead they will appear as a special type of message
* that says something like "This message was deleted"
*/
private fun markAsDeletedForEveryone(messages: Set<MessageRecord>) = viewModelScope.launch {
private fun markAsDeletedForEveryone(
data: DeleteForEveryoneDialogData
) = viewModelScope.launch {
val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for delete for everyone - aborting delete operation.")
// make sure to stop audio messages, if any
messages.filterIsInstance<MmsMessageRecord>()
data.messages.filterIsInstance<MmsMessageRecord>()
.mapNotNull { it.slideDeck.audioSlide }
.forEach(::stopMessageAudio)
/*repository.markAsDeletedForEveryone(threadId, recipient, messages)
.onSuccess {
Log.d("Loki", "Deleted messages $messages ")
// the exact logic for this will depend on the messages type
when(data.messageType){
is DeleteForEveryoneMessageType.NoteToSelf -> markAsDeletedForEveryoneNoteToSelf(data)
is DeleteForEveryoneMessageType.OneOnOne -> markAsDeletedForEveryone1On1(data)
is DeleteForEveryoneMessageType.LegacyGroup -> markAsDeletedForEveryoneLegacyGroup(data.messages)
is DeleteForEveryoneMessageType.GroupV2 -> markAsDeletedForEveryoneGroupsV2(data)
is DeleteForEveryoneMessageType.Community -> markAsDeletedForEveryoneCommunity(data)
}
}
private fun markAsDeletedForEveryoneNoteToSelf(data: DeleteForEveryoneDialogData){
viewModelScope.launch(Dispatchers.IO) {
// show a loading indicator
_uiState.update { it.copy(showLoader = true) }
// delete remotely
try {
//todo DELETION need to delete remotely for note to self
repository.deleteCommunityMessagesRemotely(threadId, data.messages)
//todo DELETION send unsendRequest to own swarm
// When this is done we simply need to remove the message locally
repository.markAsDeletedLocally(
messages = data.messages,
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
)
// show confirmation toast
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
_dialogsState.update { it.copy(deleteEveryone = data) }
}
.onFailure {
Log.w("Loki", "FAILED TO delete messages $messages ")
showMessage(
application.resources.getQuantityString(R.plurals.deleteMessageFailed, messages.size, messages.size)
// hide loading indicator
_uiState.update { it.copy(showLoader = false) }
}
}
private fun markAsDeletedForEveryone1On1(data: DeleteForEveryoneDialogData){
if(recipient == null) return showMessage(application.getString(R.string.errorUnknown))
viewModelScope.launch(Dispatchers.IO) {
// show a loading indicator
_uiState.update { it.copy(showLoader = true) }
// delete remotely
try {
repository.delete1on1MessagesRemotely(threadId, recipient!!, 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)
)
// show confirmation toast
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
_dialogsState.update { it.copy(deleteEveryone = data) }
}
// hide loading indicator
_uiState.update { it.copy(showLoader = false) }
}
}
private fun markAsDeletedForEveryoneLegacyGroup(messages: Set<MessageRecord>){
}
private fun markAsDeletedForEveryoneGroupsV2(data: DeleteForEveryoneDialogData){
viewModelScope.launch(Dispatchers.IO) {
// show a loading indicator
_uiState.update { it.copy(showLoader = true) }
//todo GROUPS V2 - uncomment below and use Fanchao's method to delete a group V2
try {
//repository.callMethodFromFanchao(threadId, recipient, data.messages)
// the repo will handle the internal logic (calling `/delete` on the swarm
// and sending 'GroupUpdateDeleteMemberContentMessage'
// When this is done we simply need to remove the message locally
repository.markAsDeletedLocally(
messages = data.messages,
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
)
// show confirmation toast
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(), data.messages.count()
),
Toast.LENGTH_SHORT
).show()
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
_dialogsState.update { it.copy(deleteAllDevices = data) }
}
// hide loading indicator
_uiState.update { it.copy(showLoader = false) }
}
}
private fun markAsDeletedForEveryoneCommunity(data: DeleteForEveryoneDialogData){
viewModelScope.launch(Dispatchers.IO) {
// show a loading indicator
_uiState.update { it.copy(showLoader = true) }
// delete remotely
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)
)
}*/
// show confirmation toast
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
_dialogsState.update { it.copy(deleteEveryone = data) }
}
// hide loading indicator
_uiState.update { it.copy(showLoader = false) }
}
}
private fun isUserCommunityManager() = openGroup?.let { openGroup ->
val userPublicKey = textSecurePreferences.getLocalNumber() ?: return@let false
OpenGroupManager.isUserModerator(application, openGroup.id, userPublicKey, blindedPublicKey)
} ?: false
/**
* Stops audio player if its current playing is the one given in the message.
*/
private fun stopMessageAudio(message: MessageRecord) {
val mmsMessage = message as? MmsMessageRecord ?: return
val audioSlide = mmsMessage.slideDeck.audioSlide ?: return
stopMessageAudio(audioSlide)
}
private fun stopMessageAudio(audioSlide: AudioSlide) {
AudioSlidePlayer.getInstance()?.takeIf { it.audioSlide == audioSlide }?.stop()
}
fun setRecipientApproved() {
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for set approved action")
repository.setApproved(recipient, true)
}
fun banUser(recipient: Recipient) = viewModelScope.launch {
@ -483,8 +682,7 @@ class ConversationViewModel(
markAsDeletedLocally(command.messages)
}
is Commands.MarkAsDeletedForEveryone -> {
//todo DELETION mark as deleted for everyone here
//markAsDeletedForEveryone(command.messages)
markAsDeletedForEveryone(command.data)
}
}
}
@ -524,14 +722,23 @@ class ConversationViewModel(
val openLinkDialogUrl: String? = null,
val deleteDeviceOnly: Set<MessageRecord>? = null,
val deleteEveryone: DeleteForEveryoneDialogData? = null,
val deleteAllDevices: Set<MessageRecord>? = null,
val deleteAllDevices: DeleteForEveryoneDialogData? = null,
)
data class DeleteForEveryoneDialogData(
val messages: Set<MessageRecord>,
val messageType: DeleteForEveryoneMessageType,
val defaultToEveryone: Boolean
)
sealed class DeleteForEveryoneMessageType {
data object NoteToSelf: DeleteForEveryoneMessageType()
data object OneOnOne: DeleteForEveryoneMessageType()
data object LegacyGroup: DeleteForEveryoneMessageType()
data object GroupV2: DeleteForEveryoneMessageType()
data object Community: DeleteForEveryoneMessageType()
}
sealed class Commands {
data class ShowOpenUrlDialog(val url: String?) : Commands()
data object HideDeleteDeviceOnlyDialog : Commands()
@ -539,7 +746,7 @@ class ConversationViewModel(
data object HideDeleteAllDevicesDialog : Commands()
data class MarkAsDeletedLocally(val messages: Set<MessageRecord>): Commands()
data class MarkAsDeletedForEveryone(val messages: Set<MessageRecord>): Commands()
data class MarkAsDeletedForEveryone(val data: DeleteForEveryoneDialogData): Commands()
}
}
@ -549,7 +756,8 @@ data class ConversationUiState(
val uiMessages: List<UiMessage> = emptyList(),
val isMessageRequestAccepted: Boolean? = null,
val conversationExists: Boolean,
val hideInputBar: Boolean = false
val hideInputBar: Boolean = false,
val showLoader: Boolean = false
)
data class RetrieveOnce<T>(val retrieval: () -> T?) {

@ -22,11 +22,13 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.get
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.DraftDatabase
@ -61,7 +63,12 @@ interface ConversationRepository {
fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord)
fun setApproved(recipient: Recipient, isApproved: Boolean)
suspend fun markAsDeletedForEveryone(threadId: Long, recipient: Recipient, message: MessageRecord): Result<Unit>
suspend fun deleteCommunityMessagesRemotely(threadId: Long, messages: Set<MessageRecord>)
suspend fun delete1on1MessagesRemotely(
threadId: Long,
recipient: Recipient,
messages: Set<MessageRecord>
)
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit>
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result<Unit>
@ -221,7 +228,49 @@ class DefaultConversationRepository @Inject constructor(
storage.setRecipientApproved(recipient, isApproved)
}
override suspend fun markAsDeletedForEveryone(
override suspend fun deleteCommunityMessagesRemotely(
threadId: Long,
messages: Set<MessageRecord>
) {
val community = lokiThreadDb.getOpenGroupChat(threadId) ?:
throw Error("Not a Community")
messages.forEach { message ->
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
OpenGroupApi.deleteMessage(messageServerID, community.room, community.server).await()
}
}
}
override suspend fun delete1on1MessagesRemotely(
threadId: Long,
recipient: Recipient,
messages: Set<MessageRecord>
) {
// delete the messages remotely
val publicKey = recipient.address.serialize()
val userAddress: Address? = textSecurePreferences.getLocalNumber()?.let { Address.fromSerialized(it) }
messages.forEach { message ->
// delete from swarm
messageDataProvider.getServerHashForMessage(message.id, message.isMms)
?.let { serverHash ->
SnodeAPI.deleteMessage(publicKey, listOf(serverHash)).await()
}
// send an UnsendRequest to user's swarm
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
userAddress?.let { MessageSender.send(unsendRequest, it) }
}
// send an UnsendRequest to recipient's swarm
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
MessageSender.send(unsendRequest, recipient.address)
}
}
}
/* override suspend fun markAsDeletedForEveryone(
threadId: Long,
recipient: Recipient,
message: MessageRecord
@ -276,7 +325,7 @@ class DefaultConversationRepository @Inject constructor(
}
}
}
}
}*/
override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? {
if (recipient.isCommunityRecipient) return null

@ -351,4 +351,23 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<RelativeLayout
android:id="@+id/loader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#A4000000"
android:focusable="true"
android:clickable="true"
android:visibility="gone">
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.Large.ThreeBounce"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="8dp"
app:SpinKit_Color="@android:color/white" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

Loading…
Cancel
Save