From ed38e507ae67749aff2c5008442fead59cfc1093 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 26 Sep 2024 10:30:06 +1000 Subject: [PATCH] Recreating xml dialogs in Compose and moved logic in VM --- .../conversation/v2/ConversationActivityV2.kt | 150 +----------- .../conversation/v2/ConversationV2Dialogs.kt | 217 ++++++++++++++++++ .../conversation/v2/ConversationViewModel.kt | 205 ++++++++++++++++- .../dialogs/DeleteMessageDeviceOnlyDialog.kt | 39 ---- .../v2/dialogs/DeleteMessageDialog.kt | 80 ------- .../v2/dialogs/DeleteNoteToSelfDialog.kt | 78 ------- .../securesms/ui/components/RadioButton.kt | 9 +- 7 files changed, 430 insertions(+), 348 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteMessageDeviceOnlyDialog.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteMessageDialog.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteNoteToSelfDialog.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index f30a103779..44c92144f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -33,12 +33,10 @@ import android.widget.Toast import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.content.ContextCompat -import androidx.core.view.drawToBitmap import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment @@ -110,6 +108,7 @@ import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity +import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.* import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP @@ -119,9 +118,6 @@ import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companio import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_COPY import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_SAVE import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog -import org.thoughtcrime.securesms.conversation.v2.dialogs.DeleteMessageDeviceOnlyDialog -import org.thoughtcrime.securesms.conversation.v2.dialogs.DeleteMessageDialog -import org.thoughtcrime.securesms.conversation.v2.dialogs.DeleteNoteToSelfDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate @@ -176,14 +172,13 @@ import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment import org.thoughtcrime.securesms.showSessionDialog -import org.thoughtcrime.securesms.ui.OpenURLAlertDialog -import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.NetworkUtils import org.thoughtcrime.securesms.util.SaveAttachmentTask +import org.thoughtcrime.securesms.util.drawToBitmap import org.thoughtcrime.securesms.util.isScrolledToBottom import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom import org.thoughtcrime.securesms.util.push @@ -246,8 +241,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe .get(LinkPreviewViewModel::class.java) } - private var openLinkDialogUrl: String? by mutableStateOf(null) - private val threadId: Long by lazy { var threadId = intent.getLongExtra(THREAD_ID, -1L) if (threadId == -1L) { @@ -412,7 +405,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // endregion fun showOpenUrlDialog(url: String){ - openLinkDialogUrl = url + viewModel.onCommand(ShowOpenUrlDialog(url)) } // region Lifecycle @@ -425,16 +418,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe binding.dialogOpenUrl.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - SessionMaterialTheme { - if(!openLinkDialogUrl.isNullOrEmpty()){ - OpenURLAlertDialog( - url = openLinkDialogUrl!!, - onDismissRequest = { - openLinkDialogUrl = null - } - ) - } - } + val dialogsState by viewModel.dialogsState.collectAsState() + ConversationV2Dialogs( + dialogsState = dialogsState, + sendCommand = viewModel::onCommand + ) } } @@ -1668,11 +1656,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } - private fun isUserCommunityManager() = viewModel.openGroup?.let { openGroup -> - val userPublicKey = textSecurePreferences.getLocalNumber() ?: return@let false - OpenGroupManager.isUserModerator(this, openGroup.id, userPublicKey, viewModel.blindedPublicKey) - } ?: false - override fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) { if (!textSecurePreferences.autoplayAudioMessages()) return @@ -2069,120 +2052,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // Note: The messages in the provided set may be a single message, or multiple if there are a // group of selected messages. override fun deleteMessages(messages: Set) { - val conversation = viewModel.recipient - if (conversation == null) { - Log.w("ConversationActivityV2", "Asked to delete messages but could not obtain viewModel recipient - aborting.") - return - } - - // 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 check attachments deleted - - //todo DELETION check links deleted - - //todo DELETION check notifications deleted - - //todo DELETION handle errors: Toasts for errors, or deleting messages not fully sent yet - - - - val allSentByCurrentUser = messages.all { it.isOutgoing } - // hashes are required if wanting to delete messages from the 'storage server' - they are not required for communities - val canDeleteForEveryone = conversation.isCommunityRecipient || messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null } - // Determining is the current user is an admin will depend on the kind of conversation we are in - val isAdmin = when { - //todo GROUPS V2 add logic where code is commented to determine if user is an admin - CAREFUL in the current old code: - // isClosedGroup refers to the existing legacy groups. - // With the groupsV2 changes, isClosedGroup refers to groupsV2 and isLegacyClosedGroup is a new property to refer to old groups - - // for Groups V2 - // conversation: check if it is a GroupsV2 conversation - then check if user is an admin - - // for legacy groups, check if the user created the group - conversation.isClosedGroupRecipient -> { //todo GROUPS V2 this property will change for groups v2. Check for legacyGroup here - // for legacy groups, we check if the current user is the one who created the group - run { - val localUserAddress = textSecurePreferences.getLocalNumber() ?: return@run false - val group = storage.getGroup(conversation.address.toGroupString()) - group?.admins?.contains(fromSerialized(localUserAddress)) ?: false - } - } - - // for communities the the `isUserModerator` field - conversation.isCommunityRecipient -> isUserCommunityManager() - - // false in other cases - else -> false - } - - // creating a reusable callback - val deleteDeviceOnly = { - // delete the message locally - viewModel.markAsDeletedLocally( - messages = messages, - displayedMessage = resources.getString(R.string.deleteMessageDeletedLocally) - ) - endActionMode() - - // show confirmation toast - Toast.makeText( - this, - resources.getQuantityString(R.plurals.deleteMessageDeleted, messages.count(), messages.count()), - Toast.LENGTH_SHORT - ).show() - } - - // There are three types of dialogs for deletion: - // 1- Delete on device only OR all devices - Used for Note to self - // 2- Delete on device only OR for everyone - Used for 'admins' or a user's own messages, as long as the message have a server hash - // 3- Delete on device only - Used otherwise - when{ - // the conversation is a note to self - conversation.isLocalNumber -> { - DeleteNoteToSelfDialog( - messageCount = messages.size, - onDeleteDeviceOnly = deleteDeviceOnly, - onDeleteAllDevices = { - endActionMode() - }, - onCancel = { endActionMode() } - ).show(supportFragmentManager, "DeleteNoteToSelfDialog") - } - - // If the user is an admin or is interacting with their own message And are allowed to delete for everyone - (isAdmin || allSentByCurrentUser) && canDeleteForEveryone -> { - DeleteMessageDialog( - messageCount = messages.size, - defaultToEveryone = isAdmin, - onDeleteDeviceOnly = deleteDeviceOnly, - onDeleteForEveryone = { - endActionMode() - }, - onCancel = { endActionMode() } - ).show(supportFragmentManager, "DeleteMessageDialog") - } + endActionMode() - // for non admins, users interacting with someone else's message, or control messages - else -> { - //todo DELETION this should also happen for ControlMessages - DeleteMessageDeviceOnlyDialog( - messageCount = messages.size, - onDeleteDeviceOnly = deleteDeviceOnly, - onCancel = { endActionMode() } - ).show(supportFragmentManager, "DeleteMessageDeviceOnlyDialog") - } - } + viewModel.handleMessagesDeletion(messages) /* // If the recipient is a community OR a Note-to-Self then we delete the message for everyone diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt new file mode 100644 index 0000000000..587b078c67 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt @@ -0,0 +1,217 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.ui.AlertDialog +import org.thoughtcrime.securesms.ui.DialogButtonModel +import org.thoughtcrime.securesms.ui.GetString +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 + +@Composable +fun ConversationV2Dialogs( + dialogsState: ConversationViewModel.DialogsState, + sendCommand: (ConversationViewModel.Commands) -> Unit +){ + SessionMaterialTheme { + // open link confirmation + if(!dialogsState.openLinkDialogUrl.isNullOrEmpty()){ + OpenURLAlertDialog( + url = dialogsState.openLinkDialogUrl, + onDismissRequest = { + // hide dialog + sendCommand(ShowOpenUrlDialog(null)) + } + ) + } + + // delete message(s) on device only + if(dialogsState.deleteDeviceOnly != null){ + AlertDialog( + onDismissRequest = { + // hide dialog + sendCommand(HideDeleteDeviceOnlyDialog) + }, + title = pluralStringResource( + R.plurals.deleteMessage, + dialogsState.deleteDeviceOnly.size, + dialogsState.deleteDeviceOnly.size + ), + text = stringResource(R.string.deleteMessageDescriptionDevice), //todo DELETION we need the plural version of this here, which currently is not set up in strings + buttons = listOf( + DialogButtonModel( + text = GetString(stringResource(id = R.string.delete)), + color = LocalColors.current.danger, + onClick = { + sendCommand(MarkAsDeletedLocally(dialogsState.deleteDeviceOnly)) + } + ), + DialogButtonModel( + GetString(stringResource(R.string.cancel)) + ) + ) + ) + } + + // delete message(s) for everyone + if(dialogsState.deleteEveryone != null){ + var deleteForEveryone by remember { mutableStateOf(dialogsState.deleteEveryone.defaultToEveryone)} + + AlertDialog( + onDismissRequest = { + // hide dialog + sendCommand(HideDeleteEveryoneDialog) + }, + title = pluralStringResource( + R.plurals.deleteMessage, + dialogsState.deleteEveryone.messages.size, + dialogsState.deleteEveryone.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 = { + TitledRadioButton( + contentPadding = PaddingValues( + horizontal = LocalDimensions.current.xxsSpacing, + vertical = 0.dp + ), + option = RadioOption( + value = Unit, + title = GetString(stringResource(R.string.deleteMessageDeviceOnly)), + selected = !deleteForEveryone + ) + ) { + deleteForEveryone = false + } + + TitledRadioButton( + contentPadding = PaddingValues( + horizontal = LocalDimensions.current.xxsSpacing, + vertical = 0.dp + ), + option = RadioOption( + value = Unit, + title = GetString(stringResource(R.string.deleteMessageEveryone)), + selected = deleteForEveryone + ) + ) { + deleteForEveryone = true + } + }, + buttons = listOf( + DialogButtonModel( + text = GetString(stringResource(id = R.string.delete)), + color = LocalColors.current.danger, + onClick = { + // delete messages based on chosen option + sendCommand( + if(deleteForEveryone) MarkAsDeletedForEveryone(dialogsState.deleteEveryone.messages) + else MarkAsDeletedLocally(dialogsState.deleteEveryone.messages) + ) + } + ), + DialogButtonModel( + GetString(stringResource(R.string.cancel)) + ) + ) + ) + } + + // delete message(s) for all my devices + if(dialogsState.deleteAllDevices != null){ + var deleteAllDevices by remember { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = { + // hide dialog + sendCommand(HideDeleteAllDevicesDialog) + }, + title = pluralStringResource( + R.plurals.deleteMessage, + dialogsState.deleteAllDevices.size, + dialogsState.deleteAllDevices.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 = { + 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) + else MarkAsDeletedLocally(dialogsState.deleteAllDevices) + ) + } + ), + DialogButtonModel( + GetString(stringResource(R.string.cancel)) + ) + ) + ) + } + + } +} + +@Preview +@Composable +fun PreviewURLDialog(){ + PreviewTheme { + ConversationV2Dialogs( + dialogsState = ConversationViewModel.DialogsState( + openLinkDialogUrl = "https://google.com" + ), + sendCommand = {} + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index eebb291f04..97d1c9af0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.conversation.v2 +import android.app.Application import android.content.Context +import android.widget.Toast import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -10,13 +12,13 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import network.loki.messenger.R import org.session.libsession.database.MessageDataProvider import org.session.libsession.messaging.messages.ExpirationConfiguration import org.session.libsession.messaging.open_groups.OpenGroup @@ -25,25 +27,30 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt import org.session.libsession.messaging.utilities.AccountId import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.utilities.Address +import org.session.libsession.utilities.Address.Companion.fromSerialized +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.audio.AudioSlidePlayer +import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord -import org.thoughtcrime.securesms.mms.AudioSlide import org.thoughtcrime.securesms.groups.OpenGroupManager +import org.thoughtcrime.securesms.mms.AudioSlide import org.thoughtcrime.securesms.repository.ConversationRepository import java.util.UUID class ConversationViewModel( val threadId: Long, val edKeyPair: KeyPair?, + private val application: Application, private val repository: ConversationRepository, private val storage: Storage, - private val messageDataProvider: MessageDataProvider + private val messageDataProvider: MessageDataProvider, + private val lokiMessageDb: LokiMessageDatabase, + private val textSecurePreferences: TextSecurePreferences ) : ViewModel() { val showSendAfterApprovalText: Boolean @@ -52,6 +59,9 @@ class ConversationViewModel( private val _uiState = MutableStateFlow(ConversationUiState(conversationExists = true)) val uiState: StateFlow = _uiState + private val _dialogsState = MutableStateFlow(DialogsState()) + val dialogsState: StateFlow = _dialogsState + private var _recipient: RetrieveOnce = RetrieveOnce { repository.maybeGetRecipientForThreadId(threadId) } @@ -180,6 +190,104 @@ class ConversationViewModel( repository.deleteThread(threadId) } + fun handleMessagesDeletion(messages: Set){ + val conversation = recipient + if (conversation == null) { + Log.w("ConversationActivityV2", "Asked to delete messages but could not obtain viewModel recipient - aborting.") + return + } + + // 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 + + viewModelScope.launch(Dispatchers.IO) { + val allSentByCurrentUser = messages.all { it.isOutgoing } + // hashes are required if wanting to delete messages from the 'storage server' - they are not required for communities + val canDeleteForEveryone = conversation.isCommunityRecipient || messages.all { + lokiMessageDb.getMessageServerHash( + it.id, + it.isMms + ) != null + } + // Determining is the current user is an admin will depend on the kind of conversation we are in + val isAdmin = when { + //todo GROUPS V2 add logic where code is commented to determine if user is an admin - CAREFUL in the current old code: + // isClosedGroup refers to the existing legacy groups. + // With the groupsV2 changes, isClosedGroup refers to groupsV2 and isLegacyClosedGroup is a new property to refer to old groups + + // for Groups V2 + // conversation: check if it is a GroupsV2 conversation - then check if user is an admin + + // for legacy groups, check if the user created the group + conversation.isClosedGroupRecipient -> { //todo GROUPS V2 this property will change for groups v2. Check for legacyGroup here + // for legacy groups, we check if the current user is the one who created the group + run { + val localUserAddress = + textSecurePreferences.getLocalNumber() ?: return@run false + val group = storage.getGroup(conversation.address.toGroupString()) + group?.admins?.contains(fromSerialized(localUserAddress)) ?: false + } + } + + // for communities the the `isUserModerator` field + conversation.isCommunityRecipient -> isUserCommunityManager() + + // false in other cases + else -> false + } + + + // There are three types of dialogs for deletion: + // 1- Delete on device only OR all devices - Used for Note to self + // 2- Delete on device only OR for everyone - Used for 'admins' or a user's own messages, as long as the message have a server hash + // 3- Delete on device only - Used otherwise + when { + // the conversation is a note to self + conversation.isLocalNumber -> { + _dialogsState.update { + it.copy(deleteAllDevices = messages) + } + } + + // If the user is an admin or is interacting with their own message And are allowed to delete for everyone + (isAdmin || allSentByCurrentUser) && canDeleteForEveryone -> { + _dialogsState.update { + it.copy( + deleteEveryone = DeleteForEveryoneDialogData( + messages = messages, + defaultToEveryone = isAdmin + ) + ) + } + } + + // for non admins, users interacting with someone else's message, or control messages + else -> { + //todo DELETION this should also happen for ControlMessages + _dialogsState.update { + it.copy(deleteDeviceOnly = messages) + } + } + } + } + } + + 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' @@ -194,17 +302,25 @@ class ConversationViewModel( * but the messages themselves won't be removed from the db. * Instead they will appear as a special type of message * that says something like "This message was deleted" - * - * @displayedMessage is the message that will be displayed in place of the deleted message. */ - fun markAsDeletedLocally(messages: Set, displayedMessage: String) { + private fun markAsDeletedLocally(messages: Set) { // make sure to stop audio messages, if any messages.filterIsInstance() .mapNotNull { it.slideDeck.audioSlide } .forEach(::stopMessageAudio) - repository.markAsDeletedLocally(messages, displayedMessage) + repository.markAsDeletedLocally( + messages = messages, + displayedMessage = application.getString(R.string.deleteMessageDeletedLocally) + ) + + // show confirmation toast + Toast.makeText( + application, + application.resources.getQuantityString(R.plurals.deleteMessageDeleted, messages.count(), messages.count()), + Toast.LENGTH_SHORT + ).show() } /** @@ -231,7 +347,7 @@ class ConversationViewModel( * Instead they will appear as a special type of message * that says something like "This message was deleted" */ - fun markAsDeletedForEveryone(message: MessageRecord) = viewModelScope.launch { + private fun markAsDeletedForEveryone(message: MessageRecord) = viewModelScope.launch { val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for delete for everyone - aborting delete operation.") stopMessageAudio(message) @@ -329,6 +445,47 @@ class ConversationViewModel( attachmentDownloadHandler.onAttachmentDownloadRequest(attachment) } + fun onCommand(command: Commands) { + when (command) { + is Commands.ShowOpenUrlDialog -> { + _dialogsState.update { + it.copy(openLinkDialogUrl = command.url) + } + } + + is Commands.HideDeleteDeviceOnlyDialog -> { + _dialogsState.update { + it.copy(deleteDeviceOnly = null) + } + } + + is Commands.HideDeleteEveryoneDialog -> { + _dialogsState.update { + it.copy(deleteEveryone = null) + } + } + + is Commands.HideDeleteAllDevicesDialog -> { + _dialogsState.update { + it.copy(deleteAllDevices = null) + } + } + + is Commands.MarkAsDeletedLocally -> { + // hide dialog first + _dialogsState.update { + it.copy(deleteDeviceOnly = null) + } + + markAsDeletedLocally(command.messages) + } + is Commands.MarkAsDeletedForEveryone -> { + //todo DELETION mark as deleted for everyone here + //markAsDeletedForEveryone(command.messages) + } + } + } + @dagger.assisted.AssistedFactory interface AssistedFactory { fun create(threadId: Long, edKeyPair: KeyPair?): Factory @@ -338,21 +495,49 @@ class ConversationViewModel( class Factory @AssistedInject constructor( @Assisted private val threadId: Long, @Assisted private val edKeyPair: KeyPair?, + private val application: Application, private val repository: ConversationRepository, private val storage: Storage, private val messageDataProvider: MessageDataProvider, + private val lokiMessageDb: LokiMessageDatabase, + private val textSecurePreferences: TextSecurePreferences ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return ConversationViewModel( threadId = threadId, edKeyPair = edKeyPair, + application = application, repository = repository, storage = storage, - messageDataProvider = messageDataProvider + messageDataProvider = messageDataProvider, + lokiMessageDb = lokiMessageDb, + textSecurePreferences = textSecurePreferences ) as T } } + + data class DialogsState( + val openLinkDialogUrl: String? = null, + val deleteDeviceOnly: Set? = null, + val deleteEveryone: DeleteForEveryoneDialogData? = null, + val deleteAllDevices: Set? = null, + ) + + data class DeleteForEveryoneDialogData( + val messages: Set, + val defaultToEveryone: Boolean + ) + + sealed class Commands { + data class ShowOpenUrlDialog(val url: String?) : Commands() + data object HideDeleteDeviceOnlyDialog : Commands() + data object HideDeleteEveryoneDialog : Commands() + data object HideDeleteAllDevicesDialog : Commands() + + data class MarkAsDeletedLocally(val messages: Set): Commands() + data class MarkAsDeletedForEveryone(val messages: Set): Commands() + } } data class UiMessage(val id: Long, val message: String) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteMessageDeviceOnlyDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteMessageDeviceOnlyDialog.kt deleted file mode 100644 index 43e30cc12c..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteMessageDeviceOnlyDialog.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.dialogs - -import android.app.Dialog -import android.os.Bundle -import android.util.TypedValue -import androidx.annotation.ColorInt -import androidx.fragment.app.DialogFragment -import network.loki.messenger.R -import org.thoughtcrime.securesms.createSessionDialog - -/** - * Shown when deleting a message can only be deleted locally - * - * @param messageCount The number of messages to be deleted. - * @param onDeleteDeviceOnly Callback to be executed when the user chooses to delete only on their device. - * @param onCancel Callback to be executed when cancelling the dialog. - */ -class DeleteMessageDeviceOnlyDialog( - private val messageCount: Int, - private val onDeleteDeviceOnly: () -> Unit, - private val onCancel: () -> Unit -) : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { - val typedValue = TypedValue() - val theme = context.theme - theme.resolveAttribute(R.attr.danger, typedValue, true) - @ColorInt val deleteColor = typedValue.data - - title(resources.getQuantityString(R.plurals.deleteMessage, messageCount, messageCount)) - text(resources.getString(R.string.deleteMessageDescriptionDevice)) //todo DELETION we need the plural version of this here, which currently is not set up in strings - button( - text = R.string.delete, - textColor = deleteColor, - listener = onDeleteDeviceOnly - ) - cancelButton(onCancel) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteMessageDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteMessageDialog.kt deleted file mode 100644 index 2dafd8d9ee..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteMessageDialog.kt +++ /dev/null @@ -1,80 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.dialogs - -import android.app.Dialog -import android.content.Context -import android.os.Bundle -import android.util.TypedValue -import androidx.annotation.ColorInt -import androidx.fragment.app.DialogFragment -import network.loki.messenger.R -import org.thoughtcrime.securesms.createSessionDialog - -/** - * Shown when deleting a message that can be removed both locally and for everyone - * - * @param messageCount The number of messages to be deleted. - * @param defaultToEveryone Whether the dialog should default to deleting for everyone. - * @param onDeleteDeviceOnly Callback to be executed when the user chooses to delete only on their device. - * @param onDeleteForEveryone Callback to be executed when the user chooses to delete for everyone. - * @param onCancel Callback to be executed when cancelling the dialog. - */ -class DeleteMessageDialog( - private val messageCount: Int, - private val defaultToEveryone: Boolean, - private val onDeleteDeviceOnly: () -> Unit, - private val onDeleteForEveryone: () -> Unit, - private val onCancel: () -> Unit -) : DialogFragment() { - - // tracking the user choice from the radio buttons - private var deleteForEveryone = defaultToEveryone - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { - val typedValue = TypedValue() - val theme = context.theme - theme.resolveAttribute(R.attr.danger, typedValue, true) - @ColorInt val deleteColor = typedValue.data - - title(resources.getQuantityString(R.plurals.deleteMessage, messageCount, messageCount)) - text(resources.getString(R.string.deleteMessageConfirm)) //todo DELETION we need the plural version of this here, which currently is not set up in strings - singleChoiceItems( - options = deleteOptions.map { it.label }, - currentSelected = if (defaultToEveryone) 1 else 0, // some cases require the second option, "delete for everyone", to be the default selected - dismissOnRadioSelect = false - ) { index -> - deleteForEveryone = (deleteOptions[index] is DeleteOption.DeleteForEveryone) // we delete for everyone if the selected index is 1 - } - button( - text = R.string.delete, - textColor = deleteColor, - listener = { - if (deleteForEveryone) { - onDeleteForEveryone() - } else { - onDeleteDeviceOnly() - } - } - ) - cancelButton(onCancel) - } - - private val deleteOptions: List by lazy { - listOf( - DeleteOption.DeleteDeviceOnly(requireContext()), DeleteOption.DeleteForEveryone(requireContext()) - ) - } - - private sealed class DeleteOption( - open val label: String - ){ - data class DeleteDeviceOnly( - val context: Context, - override val label: String = context.getString(R.string.deleteMessageDeviceOnly), - ): DeleteOption(label) - - data class DeleteForEveryone( - val context: Context, - override val label: String = context.getString(R.string.deleteMessageEveryone), - ): DeleteOption(label) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteNoteToSelfDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteNoteToSelfDialog.kt deleted file mode 100644 index 464cdd0f14..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DeleteNoteToSelfDialog.kt +++ /dev/null @@ -1,78 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.dialogs - -import android.app.Dialog -import android.content.Context -import android.os.Bundle -import android.util.TypedValue -import androidx.annotation.ColorInt -import androidx.fragment.app.DialogFragment -import network.loki.messenger.R -import org.thoughtcrime.securesms.createSessionDialog - -/** - * Shown when deleting a 'note to self' - * - * @param messageCount The number of messages to be deleted. - * @param onDeleteDeviceOnly Callback to be executed when the user chooses to delete only on their device. - * @param onDeleteAllDevices Callback to be executed when the user chooses to delete for everyone. - * @param onCancel Callback to be executed when cancelling the dialog. - */ -class DeleteNoteToSelfDialog( - private val messageCount: Int, - private val onDeleteDeviceOnly: () -> Unit, - private val onDeleteAllDevices: () -> Unit, - private val onCancel: () -> Unit -) : DialogFragment() { - - // tracking the user choice from the radio buttons - private var deleteOnAllDevices = false - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { - val typedValue = TypedValue() - val theme = context.theme - theme.resolveAttribute(R.attr.danger, typedValue, true) - @ColorInt val deleteColor = typedValue.data - - title(resources.getQuantityString(R.plurals.deleteMessage, messageCount, messageCount)) - text(resources.getString(R.string.deleteMessageConfirm)) //todo DELETION we need the plural version of this here, which currently is not set up in strings - singleChoiceItems( - options = deleteOptions.map { it.label }, - currentSelected = 0, - dismissOnRadioSelect = false - ) { index -> - deleteOnAllDevices = (deleteOptions[index] is DeleteOption.DeleteOnAllMyDevices) // we delete for everyone if the selected index is 1 - } - button( - text = R.string.delete, - textColor = deleteColor, - listener = { - if (deleteOnAllDevices) { - onDeleteAllDevices() - } else { - onDeleteDeviceOnly() - } - } - ) - cancelButton(onCancel) - } - - private val deleteOptions: List by lazy { - listOf( - DeleteOption.DeleteDeviceOnly(requireContext()), DeleteOption.DeleteOnAllMyDevices(requireContext()) - ) - } - - private sealed class DeleteOption( - open val label: String - ){ - data class DeleteDeviceOnly( - val context: Context, - override val label: String = context.getString(R.string.deleteMessageDeviceOnly), - ): DeleteOption(label) - - data class DeleteOnAllMyDevices( - val context: Context, - override val label: String = context.getString(R.string.deleteMessageDevicesAll), - ): DeleteOption(label) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/RadioButton.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/RadioButton.kt index cabe536767..d272a0d4f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/RadioButton.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/RadioButton.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import network.loki.messenger.libsession_util.util.ExpiryMode import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType @@ -116,16 +117,20 @@ private fun RadioButtonIndicator( @Composable fun TitledRadioButton( modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues( + horizontal = LocalDimensions.current.spacing, + vertical = LocalDimensions.current.smallSpacing + ), option: RadioOption, onClick: () -> Unit ) { RadioButton( - modifier = modifier.heightIn(min = 60.dp) + modifier = modifier .contentDescription(option.contentDescription), onClick = onClick, selected = option.selected, enabled = option.enabled, - contentPadding = PaddingValues(horizontal = LocalDimensions.current.spacing), + contentPadding = contentPadding, content = { Column( modifier = Modifier