Recreating xml dialogs in Compose and moved logic in VM

pull/1518/head
ThomasSession 7 months ago
parent f73e022cfd
commit ed38e507ae

@ -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<MessageRecord>) {
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

@ -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 = {}
)
}
}

@ -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<ConversationUiState> = _uiState
private val _dialogsState = MutableStateFlow(DialogsState())
val dialogsState: StateFlow<DialogsState> = _dialogsState
private var _recipient: RetrieveOnce<Recipient> = RetrieveOnce {
repository.maybeGetRecipientForThreadId(threadId)
}
@ -180,6 +190,104 @@ class ConversationViewModel(
repository.deleteThread(threadId)
}
fun handleMessagesDeletion(messages: Set<MessageRecord>){
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<MessageRecord>, displayedMessage: String) {
private fun markAsDeletedLocally(messages: Set<MessageRecord>) {
// make sure to stop audio messages, if any
messages.filterIsInstance<MmsMessageRecord>()
.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 <T : ViewModel> create(modelClass: Class<T>): 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<MessageRecord>? = null,
val deleteEveryone: DeleteForEveryoneDialogData? = null,
val deleteAllDevices: Set<MessageRecord>? = null,
)
data class DeleteForEveryoneDialogData(
val messages: Set<MessageRecord>,
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<MessageRecord>): Commands()
data class MarkAsDeletedForEveryone(val messages: Set<MessageRecord>): Commands()
}
}
data class UiMessage(val id: Long, val message: String)

@ -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)
}
}

@ -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<DeleteOption> 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)
}
}

@ -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<DeleteOption> 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)
}
}

@ -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 <T> TitledRadioButton(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(
horizontal = LocalDimensions.current.spacing,
vertical = LocalDimensions.current.smallSpacing
),
option: RadioOption<T>,
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

Loading…
Cancel
Save