Merge remote-tracking branch 'origin/dev' into closed_groups

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java
#	app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt
#	app/src/main/res/layout/activity_conversation_v2.xml
#	app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt
This commit is contained in:
SessionHero01 2024-10-22 15:57:22 +11:00
commit 0bc933dcec
No known key found for this signature in database
40 changed files with 309 additions and 189 deletions

View File

@ -273,9 +273,9 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.paging:paging-runtime-ktx:$pagingVersion" implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
implementation 'androidx.activity:activity-ktx:1.5.1' implementation 'androidx.activity:activity-ktx:1.9.2'
implementation 'androidx.activity:activity-compose:1.5.1' implementation 'androidx.activity:activity-compose:1.9.2'
implementation 'androidx.fragment:fragment-ktx:1.5.3' implementation 'androidx.fragment:fragment-ktx:1.8.4'
implementation "androidx.core:core-ktx:$coreVersion" implementation "androidx.core:core-ktx:$coreVersion"
implementation "androidx.work:work-runtime-ktx:2.7.1" implementation "androidx.work:work-runtime-ktx:2.7.1"

View File

@ -126,12 +126,8 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
private void initializeScreenshotSecurity(boolean isResume) { private void initializeScreenshotSecurity(boolean isResume) {
if (!isResume) { if (!isResume) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else { } else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
} }
} }
}
} }

View File

@ -145,6 +145,12 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
return smsDatabase.isOutgoingMessage(timestamp) || mmsDatabase.isOutgoingMessage(timestamp) return smsDatabase.isOutgoingMessage(timestamp) || mmsDatabase.isOutgoingMessage(timestamp)
} }
override fun isDeletedMessage(timestamp: Long): Boolean {
val smsDatabase = DatabaseComponent.get(context).smsDatabase()
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
return smsDatabase.isDeletedMessage(timestamp) || mmsDatabase.isDeletedMessage(timestamp)
}
override fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) { override fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) {
val database = DatabaseComponent.get(context).attachmentDatabase() val database = DatabaseComponent.get(context).attachmentDatabase()
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return

View File

@ -119,14 +119,14 @@ import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity 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.OnActionSelectedListener
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.*
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_COPY
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND
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.MessageDetailActivity.Companion.ON_SAVE
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
@ -168,6 +168,8 @@ import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.giph.ui.GiphyActivity import org.thoughtcrime.securesms.giph.ui.GiphyActivity
import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.home.search.getSearchName import org.thoughtcrime.securesms.home.search.getSearchName
import org.thoughtcrime.securesms.home.HomeActivity
import org.thoughtcrime.securesms.home.startHomeActivity
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
@ -913,6 +915,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// show or hide loading indicator // show or hide loading indicator
binding.loader.isVisible = state.showLoader binding.loader.isVisible = state.showLoader
if (state.isMessageRequestAccepted == true) {
binding.messageRequestBar.visibility = View.GONE
}
} }
} }
} }
@ -1169,8 +1175,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val openGroup = viewModel.openGroup val openGroup = viewModel.openGroup
// Get the correct placeholder text for this type of empty conversation // Get the correct placeholder text for this type of empty conversation
val isNoteToSelf = recipient.isLocalNumber
val txtCS: CharSequence = when { val txtCS: CharSequence = when {
// note to self
recipient.isLocalNumber -> getString(R.string.noteToSelfEmpty) recipient.isLocalNumber -> getString(R.string.noteToSelfEmpty)
// If this is a community which we cannot write to // If this is a community which we cannot write to
@ -1187,8 +1193,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
.format() .format()
} }
recipient.isGroupRecipient -> { // 10n1 and groups
// If this is a group or community that we CAN send messages to recipient.is1on1 || recipient.isGroupRecipient -> {
Phrase.from(applicationContext, R.string.groupNoMessages) Phrase.from(applicationContext, R.string.groupNoMessages)
.put(GROUP_NAME_KEY, recipient.toShortString()) .put(GROUP_NAME_KEY, recipient.toShortString())
.format() .format()
@ -1591,14 +1597,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
sendEmojiRemoval(emoji, message) sendEmojiRemoval(emoji, message)
} }
/**
* Called when the user is attempting to clear all instance of a specific emoji.
*/
override fun onClearAll(emoji: String, messageId: MessageId) { override fun onClearAll(emoji: String, messageId: MessageId) {
reactionDb.deleteEmojiReactions(emoji, messageId) viewModel.onEmojiClear(emoji, messageId)
viewModel.openGroup?.let { openGroup ->
lokiMessageDb.getServerID(messageId.id, !messageId.mms)?.let { serverId ->
OpenGroupApi.deleteAllReactions(openGroup.room, openGroup.server, serverId, emoji)
}
}
threadDb.notifyThreadUpdated(viewModel.threadId)
} }
override fun onMicrophoneButtonMove(event: MotionEvent) { override fun onMicrophoneButtonMove(event: MotionEvent) {

View File

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

View File

@ -14,12 +14,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteAllDevicesDialog import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteEveryoneDialog import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedForEveryone import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.*
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedLocally import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.DeleteForEveryoneDialogData
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.ShowOpenUrlDialog import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.ui.AlertDialog import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.DialogButtonModel import org.thoughtcrime.securesms.ui.DialogButtonModel
import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.GetString
@ -49,9 +50,10 @@ fun ConversationV2Dialogs(
) )
} }
// delete message(s) for everyone // delete message(s)
if(dialogsState.deleteEveryone != null){ if(dialogsState.deleteEveryone != null){
var deleteForEveryone by remember { mutableStateOf(dialogsState.deleteEveryone.defaultToEveryone)} val data = dialogsState.deleteEveryone
var deleteForEveryone by remember { mutableStateOf(data.defaultToEveryone)}
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest = {
@ -60,17 +62,17 @@ fun ConversationV2Dialogs(
}, },
title = pluralStringResource( title = pluralStringResource(
R.plurals.deleteMessage, R.plurals.deleteMessage,
dialogsState.deleteEveryone.messages.size, data.messages.size,
dialogsState.deleteEveryone.messages.size data.messages.size
), ),
text = pluralStringResource( text = pluralStringResource(
R.plurals.deleteMessageConfirm, R.plurals.deleteMessageConfirm,
dialogsState.deleteEveryone.messages.size, data.messages.size,
dialogsState.deleteEveryone.messages.size data.messages.size
), ),
content = { content = {
// add warning text, if any // add warning text, if any
dialogsState.deleteEveryone.warning?.let { data.warning?.let {
Text( Text(
text = it, text = it,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -104,9 +106,9 @@ fun ConversationV2Dialogs(
), ),
option = RadioOption( option = RadioOption(
value = Unit, value = Unit,
title = GetString(stringResource(R.string.deleteMessageEveryone)), title = GetString(data.deleteForEveryoneLabel),
selected = deleteForEveryone, selected = deleteForEveryone,
enabled = dialogsState.deleteEveryone.everyoneEnabled enabled = data.everyoneEnabled
) )
) { ) {
deleteForEveryone = true deleteForEveryone = true
@ -120,9 +122,9 @@ fun ConversationV2Dialogs(
// delete messages based on chosen option // delete messages based on chosen option
sendCommand( sendCommand(
if(deleteForEveryone) MarkAsDeletedForEveryone( if(deleteForEveryone) MarkAsDeletedForEveryone(
dialogsState.deleteEveryone.copy(defaultToEveryone = deleteForEveryone) data.copy(defaultToEveryone = deleteForEveryone)
) )
else MarkAsDeletedLocally(dialogsState.deleteEveryone.messages) else MarkAsDeletedLocally(data.messages)
) )
} }
), ),
@ -133,65 +135,24 @@ fun ConversationV2Dialogs(
) )
} }
// delete message(s) for all my devices // Clear emoji
if(dialogsState.deleteAllDevices != null){ if(dialogsState.clearAllEmoji != null){
var deleteAllDevices by remember { mutableStateOf(dialogsState.deleteAllDevices.defaultToEveryone) }
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest = {
// hide dialog // hide dialog
sendCommand(HideDeleteAllDevicesDialog) sendCommand(HideClearEmoji)
}, },
title = pluralStringResource( text = stringResource(R.string.emojiReactsClearAll).let { txt ->
R.plurals.deleteMessage, Phrase.from(txt).put(EMOJI_KEY, dialogsState.clearAllEmoji.emoji).format().toString()
dialogsState.deleteAllDevices.messages.size,
dialogsState.deleteAllDevices.messages.size
),
text = pluralStringResource(
R.plurals.deleteMessageConfirm,
dialogsState.deleteAllDevices.messages.size,
dialogsState.deleteAllDevices.messages.size
),
content = {
TitledRadioButton(
contentPadding = PaddingValues(
horizontal = LocalDimensions.current.xxsSpacing,
vertical = 0.dp
),
option = RadioOption(
value = Unit,
title = GetString(stringResource(R.string.deleteMessageDeviceOnly)),
selected = !deleteAllDevices
)
) {
deleteAllDevices = false
}
TitledRadioButton(
contentPadding = PaddingValues(
horizontal = LocalDimensions.current.xxsSpacing,
vertical = 0.dp
),
option = RadioOption(
value = Unit,
title = GetString(stringResource(R.string.deleteMessageDevicesAll)),
selected = deleteAllDevices
)
) {
deleteAllDevices = true
}
}, },
buttons = listOf( buttons = listOf(
DialogButtonModel( DialogButtonModel(
text = GetString(stringResource(id = R.string.delete)), text = GetString(stringResource(id = R.string.clear)),
color = LocalColors.current.danger, color = LocalColors.current.danger,
onClick = { onClick = {
// delete messages based on chosen option // delete emoji
sendCommand( sendCommand(
if(deleteAllDevices) MarkAsDeletedForEveryone( ClearEmoji(dialogsState.clearAllEmoji.emoji, dialogsState.clearAllEmoji.messageId)
dialogsState.deleteAllDevices.copy(defaultToEveryone = deleteAllDevices)
)
else MarkAsDeletedLocally(dialogsState.deleteAllDevices.messages)
) )
} }
), ),
@ -201,7 +162,6 @@ fun ConversationV2Dialogs(
) )
) )
} }
} }
} }

View File

@ -43,7 +43,10 @@ import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.LokiMessageDatabase
import org.thoughtcrime.securesms.database.ReactionDatabase
import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
@ -61,6 +64,7 @@ class ConversationViewModel(
private val messageDataProvider: MessageDataProvider, private val messageDataProvider: MessageDataProvider,
private val groupDb: GroupDatabase, private val groupDb: GroupDatabase,
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
private val reactionDb: ReactionDatabase,
private val lokiMessageDb: LokiMessageDatabase, private val lokiMessageDb: LokiMessageDatabase,
private val textSecurePreferences: TextSecurePreferences, private val textSecurePreferences: TextSecurePreferences,
private val configFactory: ConfigFactory, private val configFactory: ConfigFactory,
@ -339,12 +343,12 @@ class ConversationViewModel(
// hashes are required if wanting to delete messages from the 'storage server' // hashes are required if wanting to delete messages from the 'storage server'
// They are not required for communities OR if all messages are outgoing // They are not required for communities OR if all messages are outgoing
// also we can only delete deleted messages (marked as deleted) locally // also we can only delete deleted messages and control messages (marked as deleted) locally
val canDeleteForEveryone = messages.all{ !it.isDeleted && !it.isControlMessage } && ( val canDeleteForEveryone = messages.all{ !it.isDeleted && !it.isControlMessage } && (
messages.all { it.isOutgoing } || messages.all { it.isOutgoing } ||
conversationType == MessageType.COMMUNITY || conversationType == MessageType.COMMUNITY ||
messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null }
}) )
// There are three types of dialogs for deletion: // There are three types of dialogs for deletion:
// 1- Delete on device only OR all devices - Used for Note to self // 1- Delete on device only OR all devices - Used for Note to self
@ -354,11 +358,16 @@ class ConversationViewModel(
// the conversation is a note to self // the conversation is a note to self
conversationType == MessageType.NOTE_TO_SELF -> { conversationType == MessageType.NOTE_TO_SELF -> {
_dialogsState.update { _dialogsState.update {
it.copy(deleteAllDevices = DeleteForEveryoneDialogData( it.copy(deleteEveryone = DeleteForEveryoneDialogData(
messages = messages, messages = messages,
defaultToEveryone = false, defaultToEveryone = false,
everyoneEnabled = true, everyoneEnabled = canDeleteForEveryone,
messageType = conversationType messageType = conversationType,
deleteForEveryoneLabel = application.getString(R.string.deleteMessageDevicesAll),
warning = if(canDeleteForEveryone) null else
application.resources.getQuantityString(
R.plurals.deleteMessageNoteToSelfWarning, messages.count(), messages.count()
)
) )
) )
} }
@ -372,6 +381,7 @@ class ConversationViewModel(
messages = messages, messages = messages,
defaultToEveryone = isAdmin.value, defaultToEveryone = isAdmin.value,
everyoneEnabled = true, everyoneEnabled = true,
deleteForEveryoneLabel = application.getString(R.string.deleteMessageEveryone),
messageType = conversationType messageType = conversationType
) )
) )
@ -387,6 +397,7 @@ class ConversationViewModel(
defaultToEveryone = false, defaultToEveryone = false,
everyoneEnabled = false, // disable 'delete for everyone' - can only delete locally in this case everyoneEnabled = false, // disable 'delete for everyone' - can only delete locally in this case
messageType = conversationType, messageType = conversationType,
deleteForEveryoneLabel = application.getString(R.string.deleteMessageEveryone),
warning = application.resources.getQuantityString( warning = application.resources.getQuantityString(
R.plurals.deleteMessageWarning, messages.count(), messages.count() R.plurals.deleteMessageWarning, messages.count(), messages.count()
) )
@ -642,7 +653,7 @@ class ConversationViewModel(
).show() ).show()
} }
_dialogsState.update { it.copy(deleteAllDevices = data) } _dialogsState.update { it.copy(deleteEveryone = data) }
} }
// hide loading indicator // hide loading indicator
@ -659,11 +670,8 @@ class ConversationViewModel(
try { try {
repository.deleteCommunityMessagesRemotely(threadId, data.messages) repository.deleteCommunityMessagesRemotely(threadId, data.messages)
// When this is done we simply need to remove the message locally // When this is done we simply need to remove the message locally (leave nothing behind)
repository.markAsDeletedLocally( repository.deleteMessages(messages = data.messages, threadId = threadId)
messages = data.messages,
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
)
// show confirmation toast // show confirmation toast
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -855,9 +863,9 @@ class ConversationViewModel(
} }
} }
is Commands.HideDeleteAllDevicesDialog -> { is Commands.HideClearEmoji -> {
_dialogsState.update { _dialogsState.update {
it.copy(deleteAllDevices = null) it.copy(clearAllEmoji = null)
} }
} }
@ -872,6 +880,35 @@ class ConversationViewModel(
is Commands.MarkAsDeletedForEveryone -> { is Commands.MarkAsDeletedForEveryone -> {
markAsDeletedForEveryone(command.data) markAsDeletedForEveryone(command.data)
} }
is Commands.ClearEmoji -> {
clearEmoji(command.emoji, command.messageId)
}
}
}
private fun clearEmoji(emoji: String, messageId: MessageId){
viewModelScope.launch(Dispatchers.Default) {
reactionDb.deleteEmojiReactions(emoji, messageId)
openGroup?.let { openGroup ->
lokiMessageDb.getServerID(messageId.id, !messageId.mms)?.let { serverId ->
OpenGroupApi.deleteAllReactions(
openGroup.room,
openGroup.server,
serverId,
emoji
)
}
}
threadDb.notifyThreadUpdated(threadId)
}
}
fun onEmojiClear(emoji: String, messageId: MessageId) {
// show a confirmation dialog
_dialogsState.update {
it.copy(clearAllEmoji = ClearAllEmoji(emoji, messageId))
} }
} }
@ -921,6 +958,7 @@ class ConversationViewModel(
private val messageDataProvider: MessageDataProvider, private val messageDataProvider: MessageDataProvider,
private val groupDb: GroupDatabase, private val groupDb: GroupDatabase,
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
private val reactionDb: ReactionDatabase,
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
private val lokiMessageDb: LokiMessageDatabase, private val lokiMessageDb: LokiMessageDatabase,
@ -939,6 +977,7 @@ class ConversationViewModel(
messageDataProvider = messageDataProvider, messageDataProvider = messageDataProvider,
groupDb = groupDb, groupDb = groupDb,
threadDb = threadDb, threadDb = threadDb,
reactionDb = reactionDb,
lokiMessageDb = lokiMessageDb, lokiMessageDb = lokiMessageDb,
textSecurePreferences = textSecurePreferences, textSecurePreferences = textSecurePreferences,
configFactory = configFactory, configFactory = configFactory,
@ -949,8 +988,8 @@ class ConversationViewModel(
data class DialogsState( data class DialogsState(
val openLinkDialogUrl: String? = null, val openLinkDialogUrl: String? = null,
val deleteEveryone: DeleteForEveryoneDialogData? = null, val clearAllEmoji: ClearAllEmoji? = null,
val deleteAllDevices: DeleteForEveryoneDialogData? = null, val deleteEveryone: DeleteForEveryoneDialogData? = null
) )
data class DeleteForEveryoneDialogData( data class DeleteForEveryoneDialogData(
@ -958,13 +997,22 @@ class ConversationViewModel(
val messageType: MessageType, val messageType: MessageType,
val defaultToEveryone: Boolean, val defaultToEveryone: Boolean,
val everyoneEnabled: Boolean, val everyoneEnabled: Boolean,
val deleteForEveryoneLabel: String,
val warning: String? = null val warning: String? = null
) )
data class ClearAllEmoji(
val emoji: String,
val messageId: MessageId
)
sealed class Commands { sealed class Commands {
data class ShowOpenUrlDialog(val url: String?) : Commands() data class ShowOpenUrlDialog(val url: String?) : Commands()
data class ClearEmoji(val emoji:String, val messageId: MessageId) : Commands()
data object HideDeleteEveryoneDialog : Commands() data object HideDeleteEveryoneDialog : Commands()
data object HideDeleteAllDevicesDialog : Commands() data object HideClearEmoji : Commands()
data class MarkAsDeletedLocally(val messages: Set<MessageRecord>): Commands() data class MarkAsDeletedLocally(val messages: Set<MessageRecord>): Commands()
data class MarkAsDeletedForEveryone(val data: DeleteForEveryoneDialogData): Commands() data class MarkAsDeletedForEveryone(val data: DeleteForEveryoneDialogData): Commands()

View File

@ -42,18 +42,6 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes } val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes }
?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString ?.let { AccountId(IdPrefix.BLINDED, it) }?.hexString
// Embedded function
fun userCanDeleteSelectedItems(): Boolean {
// admin can delete all combinations
if(adapter.isAdmin) return true
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
if (openGroup == null) { return allSentByCurrentUser || allReceivedByCurrentUser }
if (allSentByCurrentUser) { return true }
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
}
// Embedded function // Embedded function
fun userCanBanSelectedUsers(): Boolean { fun userCanBanSelectedUsers(): Boolean {
if (openGroup == null) { return false } if (openGroup == null) { return false }
@ -67,7 +55,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
// Delete message // Delete message
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems() menu.findItem(R.id.menu_context_delete_message).isVisible = true // can always delete since delete logic will be handled by the VM
// Ban user // Ban user
menu.findItem(R.id.menu_context_ban_user).isVisible = userCanBanSelectedUsers() menu.findItem(R.id.menu_context_ban_user).isVisible = userCanBanSelectedUsers()
// Ban and delete all // Ban and delete all

View File

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

View File

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

View File

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

View File

@ -106,6 +106,23 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
.any { MmsSmsColumns.Types.isOutgoingMessageType(it) } .any { MmsSmsColumns.Types.isOutgoingMessageType(it) }
} }
fun isDeletedMessage(timestamp: Long): Boolean =
databaseHelper.writableDatabase.query(
TABLE_NAME,
arrayOf(ID, THREAD_ID, MESSAGE_BOX, ADDRESS),
DATE_SENT + " = ?",
arrayOf(timestamp.toString()),
null,
null,
null,
null
).use { cursor ->
cursor.asSequence()
.map { cursor.getColumnIndexOrThrow(MESSAGE_BOX) }
.map(cursor::getLong)
.any { MmsSmsColumns.Types.isDeletedMessage(it) }
}
fun incrementReceiptCount( fun incrementReceiptCount(
messageId: SyncMessageId, messageId: SyncMessageId,
timestamp: Long, timestamp: Long,
@ -913,7 +930,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
val groupReceiptDatabase = get(context).groupReceiptDatabase() val groupReceiptDatabase = get(context).groupReceiptDatabase()
groupReceiptDatabase.deleteRowsForMessage(messageId) groupReceiptDatabase.deleteRowsForMessage(messageId)
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
database.delete(TABLE_NAME, ID_WHERE, arrayOf(messageId.toString())) database!!.delete(TABLE_NAME, ID_WHERE, arrayOf(messageId.toString()))
val threadDeleted = get(context).threadDatabase().update(threadId, false) val threadDeleted = get(context).threadDatabase().update(threadId, false)
notifyConversationListeners(threadId) notifyConversationListeners(threadId)
notifyStickerListeners() notifyStickerListeners()

View File

@ -17,6 +17,9 @@
package org.thoughtcrime.securesms.database; package org.thoughtcrime.securesms.database;
import static org.thoughtcrime.securesms.database.MmsDatabase.MESSAGE_BOX; import static org.thoughtcrime.securesms.database.MmsDatabase.MESSAGE_BOX;
import static org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_DELETED_INCOMING_TYPE;
import static org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_DELETED_OUTGOING_TYPE;
import static org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_TYPE_MASK;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
@ -98,6 +101,14 @@ public class MmsSmsDatabase extends Database {
} }
} }
public @Nullable MessageRecord getNonDeletedMessageForTimestamp(long timestamp) {
String selection = MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp;
try (Cursor cursor = queryTables(PROJECTION, selection, null, null)) {
MmsSmsDatabase.Reader reader = readerFor(cursor);
return reader.getNext();
}
}
public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) { public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) {
return getMessageFor(timestamp, serializedAuthor, true); return getMessageFor(timestamp, serializedAuthor, true);
} }
@ -342,7 +353,9 @@ public class MmsSmsDatabase extends Database {
public long getLastMessageTimestamp(long threadId) { public long getLastMessageTimestamp(long threadId) {
String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC"; String order = MmsSmsColumns.NORMALIZED_DATE_SENT + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; // make sure the last message isn't marked as deleted
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " +
"(ifnull("+SmsDatabase.TYPE+", "+MmsDatabase.MESSAGE_BOX+") & "+BASE_TYPE_MASK+") NOT IN ("+BASE_DELETED_OUTGOING_TYPE+", "+BASE_DELETED_INCOMING_TYPE+")"; // this ugly line checks whether the type is deleted (incoming or outgoing) for either the sms table or the mms table
try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) { try (Cursor cursor = queryTables(PROJECTION, selection, order, "1")) {
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {

View File

@ -243,6 +243,7 @@ public class SmsDatabase extends MessagingDatabase {
contentValues.put(READ, 1); contentValues.put(READ, 1);
contentValues.put(BODY, displayedMessage); contentValues.put(BODY, displayedMessage);
contentValues.put(HAS_MENTION, 0); contentValues.put(HAS_MENTION, 0);
contentValues.put(STATUS, Status.STATUS_NONE);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)}); database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
updateTypeBitmask(messageId, Types.BASE_TYPE_MASK, updateTypeBitmask(messageId, Types.BASE_TYPE_MASK,
@ -299,6 +300,28 @@ public class SmsDatabase extends MessagingDatabase {
return isOutgoing; return isOutgoing;
} }
public boolean isDeletedMessage(long timestamp) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
Cursor cursor = null;
boolean isDeleted = false;
try {
cursor = database.query(TABLE_NAME, new String[] { ID, THREAD_ID, ADDRESS, TYPE },
DATE_SENT + " = ?", new String[] { String.valueOf(timestamp) },
null, null, null, null);
while (cursor.moveToNext()) {
if (Types.isDeletedMessage(cursor.getLong(cursor.getColumnIndexOrThrow(TYPE)))) {
isDeleted = true;
}
}
} finally {
if (cursor != null) cursor.close();
}
return isDeleted;
}
@Override @Override
public String getTypeColumn() { public String getTypeColumn() {
return TYPE; return TYPE;

View File

@ -1288,6 +1288,16 @@ open class Storage @Inject constructor(
} }
setRecipientHash(recipient, contact.hashCode().toString()) setRecipientHash(recipient, contact.hashCode().toString())
} }
// if we have contacts locally but that are missing from the config, remove their corresponding thread
val removedContacts = getAllContacts().filter { localContact ->
moreContacts.firstOrNull {
it.id == localContact.accountID
} == null
}
removedContacts.forEach {
getThreadId(fromSerialized(it.accountID))?.let(::deleteConversation)
}
} }
override fun addContacts(contacts: List<ConfigurationMessage.Contact>) { override fun addContacts(contacts: List<ConfigurationMessage.Contact>) {
@ -1766,10 +1776,17 @@ open class Storage @Inject constructor(
val timestamp = reaction.timestamp val timestamp = reaction.timestamp
val localId = reaction.localId val localId = reaction.localId
val isMms = reaction.isMms val isMms = reaction.isMms
val messageId = if (localId != null && localId > 0 && isMms != null) { val messageId = if (localId != null && localId > 0 && isMms != null) {
// bail early is the message is marked as deleted
val messagingDatabase: MessagingDatabase = if (isMms == true) DatabaseComponent.get(context).mmsDatabase()
else DatabaseComponent.get(context).smsDatabase()
if(messagingDatabase.getMessageRecord(localId)?.isDeleted == true) return
MessageId(localId, isMms) MessageId(localId, isMms)
} else if (timestamp != null && timestamp > 0) { } else if (timestamp != null && timestamp > 0) {
val messageRecord = mmsSmsDatabase.getMessageForTimestamp(timestamp) ?: return val messageRecord = mmsSmsDatabase.getMessageForTimestamp(timestamp) ?: return
if (messageRecord.isDeleted) return
MessageId(messageRecord.id, messageRecord.isMms) MessageId(messageRecord.id, messageRecord.isMms)
} else return } else return
reactionDatabase.addReaction( reactionDatabase.addReaction(

View File

@ -193,6 +193,16 @@ public class ThreadDatabase extends Database {
notifyConversationListListeners(); notifyConversationListListeners();
} }
public void clearSnippet(long threadId){
ContentValues contentValues = new ContentValues(1);
contentValues.put(SNIPPET, "");
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""});
notifyConversationListListeners();
}
public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) { public void updateSnippet(long threadId, String snippet, @Nullable Uri attachment, long date, long type, boolean unarchive) {
ContentValues contentValues = new ContentValues(4); ContentValues contentValues = new ContentValues(4);
@ -298,7 +308,7 @@ public class ThreadDatabase extends Database {
public void trimThreadBefore(long threadId, long timestamp) { public void trimThreadBefore(long threadId, long timestamp) {
Log.i("ThreadDatabase", "Trimming thread: " + threadId + " before :"+timestamp); Log.i("ThreadDatabase", "Trimming thread: " + threadId + " before :"+timestamp);
DatabaseComponent.get(context).smsDatabase().deleteMessagesInThreadBeforeDate(threadId, timestamp); DatabaseComponent.get(context).smsDatabase().deleteMessagesInThreadBeforeDate(threadId, timestamp);
DatabaseComponent.get(context).mmsDatabase().deleteMessagesInThreadBeforeDate(threadId, timestamp, false); DatabaseComponent.get(context).mmsDatabase().deleteMessagesInThreadBeforeDate(threadId, timestamp);
update(threadId, false); update(threadId, false);
notifyConversationListeners(threadId); notifyConversationListeners(threadId);
} }
@ -707,10 +717,7 @@ public class ThreadDatabase extends Database {
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase(); MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
long count = mmsSmsDatabase.getConversationCount(threadId); long count = mmsSmsDatabase.getConversationCount(threadId);
MmsSmsDatabase.Reader reader = null; try (MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId))) {
try {
reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId));
MessageRecord record = null; MessageRecord record = null;
if (reader != null) { if (reader != null) {
record = reader.getNext(); record = reader.getNext();
@ -724,7 +731,8 @@ public class ThreadDatabase extends Database {
record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount()); record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount());
return false; return false;
} else { } else {
updateThread(threadId, 0, "", null, System.currentTimeMillis(), 0, 0, 0, false, 0, 0); // for empty threads or if there is only deleted messages, show an empty snippet
clearSnippet(threadId);
return false; return false;
} }
} finally { } finally {
@ -772,10 +780,6 @@ public class ThreadDatabase extends Database {
return setLastSeen(threadId, lastSeenTime); return setLastSeen(threadId, lastSeenTime);
} }
private boolean deleteThreadOnEmpty(long threadId) {
return false;
}
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) { private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
if (messageRecord.isMms()) { if (messageRecord.isMms()) {
MmsMessageRecord record = (MmsMessageRecord) messageRecord; MmsMessageRecord record = (MmsMessageRecord) messageRecord;

View File

@ -107,10 +107,12 @@ public class ThreadRecord extends DisplayRecord {
@Override @Override
public CharSequence getDisplayBody(@NonNull Context context) { public CharSequence getDisplayBody(@NonNull Context context) {
if (isGroupUpdateMessage()) { // no need to display anything if there are no messages
return lastMessage != null if(lastMessage == null){
? lastMessage.getDisplayBody(context).toString() return "";
: context.getString(R.string.groupUpdated); }
else if (isGroupUpdateMessage()) {
return context.getString(R.string.groupUpdated);
} else if (isOpenGroupInvitation()) { } else if (isOpenGroupInvitation()) {
return context.getString(R.string.communityInvitation); return context.getString(R.string.communityInvitation);
} else if (MmsSmsColumns.Types.isLegacyType(type)) { } else if (MmsSmsColumns.Types.isLegacyType(type)) {

View File

@ -365,6 +365,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
binding.seedReminderView.isVisible = false binding.seedReminderView.isVisible = false
} }
// refresh search on resume, in case we a conversation was deleted
if (binding.globalSearchRecycler.isVisible){
globalSearchViewModel.refresh()
}
updateLegacyConfigView() updateLegacyConfigView()
} }

View File

@ -134,6 +134,16 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
cropImage(inputFile, outputFile) cropImage(inputFile, outputFile)
} }
private val hideRecoveryLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
if(result.data?.getBooleanExtra(RecoveryPasswordActivity.RESULT_RECOVERY_HIDDEN, false) == true){
viewModel.permanentlyHidePassword()
}
}
private val avatarSelection = AvatarSelection(this, onAvatarCropped, onPickImage) private val avatarSelection = AvatarSelection(this, onAvatarCropped, onPickImage)
private var showAvatarDialog: Boolean by mutableStateOf(false) private var showAvatarDialog: Boolean by mutableStateOf(false)
@ -183,7 +193,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
binding.composeView.setThemedContent { binding.composeView.setThemedContent {
Buttons() val recoveryHidden by viewModel.recoveryHidden.collectAsState()
Buttons(recoveryHidden = recoveryHidden)
} }
lifecycleScope.launch { lifecycleScope.launch {
@ -383,7 +394,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
@Composable @Composable
fun Buttons() { fun Buttons(
recoveryHidden: Boolean
) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = LocalDimensions.current.spacing) .padding(horizontal = LocalDimensions.current.spacing)
@ -445,12 +458,15 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
Divider() Divider()
// Only show the recovery password option if the user has not chosen to permanently hide it // Only show the recovery password option if the user has not chosen to permanently hide it
if (!prefs.getHidePassword()) { if (!recoveryHidden) {
LargeItemButton( LargeItemButton(
R.string.sessionRecoveryPassword, R.string.sessionRecoveryPassword,
R.drawable.ic_shield_outline, R.drawable.ic_shield_outline,
Modifier.contentDescription(R.string.AccessibilityId_sessionRecoveryPasswordMenuItem) Modifier.contentDescription(R.string.AccessibilityId_sessionRecoveryPasswordMenuItem)
) { push<RecoveryPasswordActivity>() } ) {
hideRecoveryLauncher.launch(Intent(baseContext, RecoveryPasswordActivity::class.java))
overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
}
Divider() Divider()
} }

View File

@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
@ -63,6 +64,10 @@ class SettingsViewModel @Inject constructor(
val showLoader: StateFlow<Boolean> val showLoader: StateFlow<Boolean>
get() = _showLoader get() = _showLoader
private val _recoveryHidden: MutableStateFlow<Boolean> = MutableStateFlow(prefs.getHidePassword())
val recoveryHidden: StateFlow<Boolean>
get() = _recoveryHidden
/** /**
* Refreshes the avatar on the main settings page * Refreshes the avatar on the main settings page
*/ */
@ -228,6 +233,12 @@ class SettingsViewModel @Inject constructor(
} }
} }
fun permanentlyHidePassword() {
//todo we can simplify this once we expose all our sharedPrefs as flows
prefs.setHidePassword(true)
_recoveryHidden.update { true }
}
sealed class AvatarDialogState() { sealed class AvatarDialogState() {
object NoAvatar : AvatarDialogState() object NoAvatar : AvatarDialogState()
data class UserAvatar(val address: Address) : AvatarDialogState() data class UserAvatar(val address: Address) : AvatarDialogState()

View File

@ -1,16 +1,21 @@
package org.thoughtcrime.securesms.recoverypassword package org.thoughtcrime.securesms.recoverypassword
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.setComposeContent import org.thoughtcrime.securesms.ui.setComposeContent
class RecoveryPasswordActivity : BaseActionBarActivity() { class RecoveryPasswordActivity : BaseActionBarActivity() {
companion object {
const val RESULT_RECOVERY_HIDDEN = "recovery_hidden"
}
private val viewModel: RecoveryPasswordViewModel by viewModels() private val viewModel: RecoveryPasswordViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -25,7 +30,9 @@ class RecoveryPasswordActivity : BaseActionBarActivity() {
mnemonic = mnemonic, mnemonic = mnemonic,
seed = seed, seed = seed,
confirmHideRecovery = { confirmHideRecovery = {
viewModel.permanentlyHidePassword() val returnIntent = Intent()
returnIntent.putExtra(RESULT_RECOVERY_HIDDEN, true)
setResult(RESULT_OK, returnIntent)
finish() finish()
}, },
copyMnemonic = viewModel::copyMnemonic copyMnemonic = viewModel::copyMnemonic

View File

@ -34,10 +34,6 @@ class RecoveryPasswordViewModel @Inject constructor(
.map { MnemonicCodec { MnemonicUtilities.loadFileContents(application, it) }.encode(it, MnemonicCodec.Language.Configuration.english) } .map { MnemonicCodec { MnemonicUtilities.loadFileContents(application, it) }.encode(it, MnemonicCodec.Language.Configuration.english) }
.stateIn(viewModelScope, SharingStarted.Eagerly, "") .stateIn(viewModelScope, SharingStarted.Eagerly, "")
fun permanentlyHidePassword() {
prefs.setHidePassword(true)
}
fun copyMnemonic() { fun copyMnemonic() {
prefs.setHasViewedSeed(true) prefs.setHasViewedSeed(true)
ClipData.newPlainText("Seed", mnemonic.value) ClipData.newPlainText("Seed", mnemonic.value)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -305,14 +305,14 @@
<TextView <TextView
android:padding="@dimen/medium_spacing" android:padding="@dimen/medium_spacing"
android:textSize="@dimen/small_font_size" style="@style/Signal.Text.Preview"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorTertiary"
android:textAlignment="center" android:textAlignment="center"
android:id="@+id/placeholderText" android:id="@+id/placeholderText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/large_spacing"
app:layout_constraintTop_toBottomOf="@+id/outdatedDisappearingBanner" app:layout_constraintTop_toBottomOf="@+id/outdatedDisappearingBanner"
android:elevation="8dp"
android:contentDescription="@string/AccessibilityId_control_message" android:contentDescription="@string/AccessibilityId_control_message"
tools:text="Some Control Message Text" tools:text="Some Control Message Text"
/> />
@ -339,7 +339,7 @@
android:paddingHorizontal="@dimen/massive_spacing" android:paddingHorizontal="@dimen/massive_spacing"
android:paddingVertical="@dimen/small_spacing" android:paddingVertical="@dimen/small_spacing"
android:textSize="@dimen/text_size" android:textSize="@dimen/text_size"
android:text="@string/block"/> android:text="@string/deleteAfterGroupPR1BlockUser"/>
<TextView <TextView
android:id="@+id/sendAcceptsTextView" android:id="@+id/sendAcceptsTextView"
@ -375,7 +375,7 @@
android:layout_height="@dimen/medium_button_height" android:layout_height="@dimen/medium_button_height"
android:layout_marginStart="@dimen/medium_spacing" android:layout_marginStart="@dimen/medium_spacing"
android:layout_weight="1" android:layout_weight="1"
android:text="d" /> android:text="@string/delete" />
</LinearLayout> </LinearLayout>

View File

@ -31,7 +31,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp" android:padding="8dp"
android:src="@drawable/ic_trash_filled_32" /> app:tint="@color/white"
android:src="@drawable/ic_delete" />
<ImageView <ImageView
android:id="@+id/scribble_undo_button" android:id="@+id/scribble_undo_button"

View File

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

View File

@ -27,7 +27,7 @@
app:tint="?searchIconColor" app:tint="?searchIconColor"
android:contentDescription="@string/search" /> android:contentDescription="@string/search" />
<EditText <EditText
android:hint="@string/messages" android:hint="@string/search"
android:imeOptions="actionSearch" android:imeOptions="actionSearch"
android:id="@+id/search_input" android:id="@+id/search_input"
android:paddingHorizontal="@dimen/small_spacing" android:paddingHorizontal="@dimen/small_spacing"

View File

@ -71,6 +71,12 @@
<item name="android:textSize">@dimen/very_large_font_size</item> <item name="android:textSize">@dimen/very_large_font_size</item>
</style> </style>
<style name="MenuTextAppearance" parent="TextAppearance.AppCompat.Widget.ActionBar.Menu">
<item name="android:textAllCaps">false</item>
<item name="android:textSize">@dimen/small2_font_size</item>
<item name="android:textStyle">bold</item>
</style>
<style name="TextAppearance.Session.Dialog.Title" parent="TextAppearance.AppCompat.Title"> <style name="TextAppearance.Session.Dialog.Title" parent="TextAppearance.AppCompat.Title">
<item name="android:textStyle">bold</item> <item name="android:textStyle">bold</item>
<item name="android:textSize">@dimen/medium2_font_size</item> <item name="android:textSize">@dimen/medium2_font_size</item>

View File

@ -60,6 +60,8 @@
<item name="conversation_icon_attach_audio">@drawable/ic_audio_dark</item> <item name="conversation_icon_attach_audio">@drawable/ic_audio_dark</item>
<item name="conversation_icon_attach_video">@drawable/ic_video_dark</item> <item name="conversation_icon_attach_video">@drawable/ic_video_dark</item>
<item name="android:actionMenuTextAppearance">@style/MenuTextAppearance</item>
</style> </style>
<!-- This should be the default theme for the application. --> <!-- This should be the default theme for the application. -->

View File

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

View File

@ -46,6 +46,9 @@ class ConversationViewModelTest: BaseViewModelTest() {
textSecurePreferences = mock(), textSecurePreferences = mock(),
lokiMessageDb = mock(), lokiMessageDb = mock(),
application = mock(), application = mock(),
reactionDb = mock(),
configFactory = mock(),
groupManagerV2 = mock()
) )
} }

View File

@ -38,6 +38,7 @@ interface MessageDataProvider {
fun updateAudioAttachmentDuration(attachmentId: AttachmentId, durationMs: Long, threadId: Long) fun updateAudioAttachmentDuration(attachmentId: AttachmentId, durationMs: Long, threadId: Long)
fun isMmsOutgoing(mmsMessageId: Long): Boolean fun isMmsOutgoing(mmsMessageId: Long): Boolean
fun isOutgoingMessage(timestamp: Long): Boolean fun isOutgoingMessage(timestamp: Long): Boolean
fun isDeletedMessage(timestamp: Long): Boolean
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult)
fun handleFailedAttachmentUpload(attachmentId: Long) fun handleFailedAttachmentUpload(attachmentId: Long)
fun getMessageForQuote(timestamp: Long, author: Address): Triple<Long, Boolean, String>? fun getMessageForQuote(timestamp: Long, author: Address): Triple<Long, Boolean, String>?

View File

@ -39,6 +39,13 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
val message = message as? VisibleMessage val message = message as? VisibleMessage
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
// do not attempt to send if the message is marked as deleted
message?.sentTimestamp?.let{
if(messageDataProvider.isDeletedMessage(it)){
return@execute
}
}
val sentTimestamp = this.message.sentTimestamp val sentTimestamp = this.message.sentTimestamp
val sender = storage.getUserPublicKey() val sender = storage.getUserPublicKey()
if (sentTimestamp != null && sender != null) { if (sentTimestamp != null && sender != null) {
@ -97,7 +104,10 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
Log.w(TAG, "Failed to send $message::class.simpleName.", error) Log.w(TAG, "Failed to send $message::class.simpleName.", error)
val message = message as? VisibleMessage val message = message as? VisibleMessage
if (message != null) { if (message != null) {
if (!MessagingModuleConfiguration.shared.messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) { if (
MessagingModuleConfiguration.shared.messageDataProvider.isDeletedMessage(message.sentTimestamp!!) ||
!MessagingModuleConfiguration.shared.messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)
) {
return // The message has been deleted return // The message has been deleted
} }
} }

View File

@ -511,9 +511,15 @@ object MessageSender {
fun handleFailedMessageSend(message: Message, error: Exception, isSyncMessage: Boolean = false) { fun handleFailedMessageSend(message: Message, error: Exception, isSyncMessage: Boolean = false) {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val timestamp = message.sentTimestamp!!
// no need to handle if message is marked as deleted
if(MessagingModuleConfiguration.shared.messageDataProvider.isDeletedMessage(message.sentTimestamp!!)){
return
}
val userPublicKey = storage.getUserPublicKey()!! val userPublicKey = storage.getUserPublicKey()!!
val timestamp = message.sentTimestamp!!
val author = message.sender ?: userPublicKey val author = message.sender ?: userPublicKey
if (isSyncMessage) storage.markAsSyncFailed(timestamp, author, error) if (isSyncMessage) storage.markAsSyncFailed(timestamp, author, error)

View File

@ -46,7 +46,8 @@ object LocalisedTimeUtil {
"${this.inWholeHours}h ${minutesRemaining}m" "${this.inWholeHours}h ${minutesRemaining}m"
} else if (this.inWholeMinutes > 0) { } else if (this.inWholeMinutes > 0) {
val secondsRemaining = this.minus(1.minutes.times(this.inWholeMinutes.toInt())).inWholeSeconds val secondsRemaining = this.minus(1.minutes.times(this.inWholeMinutes.toInt())).inWholeSeconds
"${this.inWholeMinutes}m ${secondsRemaining}s" if(secondsRemaining > 0) "${this.inWholeMinutes}m ${secondsRemaining}s"
else "${this.inWholeMinutes}m"
} else { } else {
"0m ${this.inWholeSeconds}s" "0m ${this.inWholeSeconds}s"
} }

View File

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