mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-28 20:45:17 +00:00
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:
commit
0bc933dcec
@ -273,9 +273,9 @@ dependencies {
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
implementation "androidx.paging:paging-runtime-ktx:$pagingVersion"
|
||||
implementation 'androidx.activity:activity-ktx:1.5.1'
|
||||
implementation 'androidx.activity:activity-compose:1.5.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.3'
|
||||
implementation 'androidx.activity:activity-ktx:1.9.2'
|
||||
implementation 'androidx.activity:activity-compose:1.9.2'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.4'
|
||||
implementation "androidx.core:core-ktx:$coreVersion"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||
|
||||
|
@ -127,11 +127,7 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
||||
if (!isResume) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -145,6 +145,12 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
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) {
|
||||
val database = DatabaseComponent.get(context).attachmentDatabase()
|
||||
val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return
|
||||
|
@ -119,14 +119,14 @@ 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.ConversationViewModel.Commands.*
|
||||
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_REPLY
|
||||
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.dialogs.BlockedDialog
|
||||
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.groups.OpenGroupManager
|
||||
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.LinkPreviewUtil
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
@ -913,6 +915,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
|
||||
// show or hide loading indicator
|
||||
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
|
||||
|
||||
// Get the correct placeholder text for this type of empty conversation
|
||||
val isNoteToSelf = recipient.isLocalNumber
|
||||
val txtCS: CharSequence = when {
|
||||
// note to self
|
||||
recipient.isLocalNumber -> getString(R.string.noteToSelfEmpty)
|
||||
|
||||
// If this is a community which we cannot write to
|
||||
@ -1187,8 +1193,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
.format()
|
||||
}
|
||||
|
||||
recipient.isGroupRecipient -> {
|
||||
// If this is a group or community that we CAN send messages to
|
||||
// 10n1 and groups
|
||||
recipient.is1on1 || recipient.isGroupRecipient -> {
|
||||
Phrase.from(applicationContext, R.string.groupNoMessages)
|
||||
.put(GROUP_NAME_KEY, recipient.toShortString())
|
||||
.format()
|
||||
@ -1591,14 +1597,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
sendEmojiRemoval(emoji, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user is attempting to clear all instance of a specific emoji.
|
||||
*/
|
||||
override fun onClearAll(emoji: String, messageId: MessageId) {
|
||||
reactionDb.deleteEmojiReactions(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)
|
||||
viewModel.onEmojiClear(emoji, messageId)
|
||||
}
|
||||
|
||||
override fun onMicrophoneButtonMove(event: MotionEvent) {
|
||||
|
@ -41,7 +41,6 @@ import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanBanSelectedUsers
|
||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanDeleteSelectedItems
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
@ -559,10 +558,8 @@ class ConversationReactionOverlay : FrameLayout {
|
||||
items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
|
||||
}
|
||||
// Delete message
|
||||
if (userCanDeleteSelectedItems(context, message, openGroup, userPublicKey, blindedPublicKey)) {
|
||||
items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) },
|
||||
R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
|
||||
}
|
||||
items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) },
|
||||
R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
|
||||
// Ban user
|
||||
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey) && !isDeleteOnly) {
|
||||
items += ActionItem(R.attr.menu_block_icon, R.string.banUser, { handleActionItemClicked(Action.BAN_USER) })
|
||||
|
@ -14,12 +14,13 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.squareup.phrase.Phrase
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteAllDevicesDialog
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteEveryoneDialog
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedForEveryone
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedLocally
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.ShowOpenUrlDialog
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.*
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.DeleteForEveryoneDialogData
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.ui.AlertDialog
|
||||
import org.thoughtcrime.securesms.ui.DialogButtonModel
|
||||
import org.thoughtcrime.securesms.ui.GetString
|
||||
@ -49,9 +50,10 @@ fun ConversationV2Dialogs(
|
||||
)
|
||||
}
|
||||
|
||||
// delete message(s) for everyone
|
||||
// delete message(s)
|
||||
if(dialogsState.deleteEveryone != null){
|
||||
var deleteForEveryone by remember { mutableStateOf(dialogsState.deleteEveryone.defaultToEveryone)}
|
||||
val data = dialogsState.deleteEveryone
|
||||
var deleteForEveryone by remember { mutableStateOf(data.defaultToEveryone)}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
@ -60,17 +62,17 @@ fun ConversationV2Dialogs(
|
||||
},
|
||||
title = pluralStringResource(
|
||||
R.plurals.deleteMessage,
|
||||
dialogsState.deleteEveryone.messages.size,
|
||||
dialogsState.deleteEveryone.messages.size
|
||||
data.messages.size,
|
||||
data.messages.size
|
||||
),
|
||||
text = pluralStringResource(
|
||||
R.plurals.deleteMessageConfirm,
|
||||
dialogsState.deleteEveryone.messages.size,
|
||||
dialogsState.deleteEveryone.messages.size
|
||||
data.messages.size,
|
||||
data.messages.size
|
||||
),
|
||||
content = {
|
||||
// add warning text, if any
|
||||
dialogsState.deleteEveryone.warning?.let {
|
||||
data.warning?.let {
|
||||
Text(
|
||||
text = it,
|
||||
textAlign = TextAlign.Center,
|
||||
@ -104,9 +106,9 @@ fun ConversationV2Dialogs(
|
||||
),
|
||||
option = RadioOption(
|
||||
value = Unit,
|
||||
title = GetString(stringResource(R.string.deleteMessageEveryone)),
|
||||
title = GetString(data.deleteForEveryoneLabel),
|
||||
selected = deleteForEveryone,
|
||||
enabled = dialogsState.deleteEveryone.everyoneEnabled
|
||||
enabled = data.everyoneEnabled
|
||||
)
|
||||
) {
|
||||
deleteForEveryone = true
|
||||
@ -120,9 +122,9 @@ fun ConversationV2Dialogs(
|
||||
// delete messages based on chosen option
|
||||
sendCommand(
|
||||
if(deleteForEveryone) MarkAsDeletedForEveryone(
|
||||
dialogsState.deleteEveryone.copy(defaultToEveryone = deleteForEveryone)
|
||||
data.copy(defaultToEveryone = deleteForEveryone)
|
||||
)
|
||||
else MarkAsDeletedLocally(dialogsState.deleteEveryone.messages)
|
||||
else MarkAsDeletedLocally(data.messages)
|
||||
)
|
||||
}
|
||||
),
|
||||
@ -133,65 +135,24 @@ fun ConversationV2Dialogs(
|
||||
)
|
||||
}
|
||||
|
||||
// delete message(s) for all my devices
|
||||
if(dialogsState.deleteAllDevices != null){
|
||||
var deleteAllDevices by remember { mutableStateOf(dialogsState.deleteAllDevices.defaultToEveryone) }
|
||||
|
||||
// Clear emoji
|
||||
if(dialogsState.clearAllEmoji != null){
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
// hide dialog
|
||||
sendCommand(HideDeleteAllDevicesDialog)
|
||||
sendCommand(HideClearEmoji)
|
||||
},
|
||||
title = pluralStringResource(
|
||||
R.plurals.deleteMessage,
|
||||
dialogsState.deleteAllDevices.messages.size,
|
||||
dialogsState.deleteAllDevices.messages.size
|
||||
),
|
||||
text = pluralStringResource(
|
||||
R.plurals.deleteMessageConfirm,
|
||||
dialogsState.deleteAllDevices.messages.size,
|
||||
dialogsState.deleteAllDevices.messages.size
|
||||
),
|
||||
content = {
|
||||
TitledRadioButton(
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = LocalDimensions.current.xxsSpacing,
|
||||
vertical = 0.dp
|
||||
),
|
||||
option = RadioOption(
|
||||
value = Unit,
|
||||
title = GetString(stringResource(R.string.deleteMessageDeviceOnly)),
|
||||
selected = !deleteAllDevices
|
||||
)
|
||||
) {
|
||||
deleteAllDevices = false
|
||||
}
|
||||
|
||||
TitledRadioButton(
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = LocalDimensions.current.xxsSpacing,
|
||||
vertical = 0.dp
|
||||
),
|
||||
option = RadioOption(
|
||||
value = Unit,
|
||||
title = GetString(stringResource(R.string.deleteMessageDevicesAll)),
|
||||
selected = deleteAllDevices
|
||||
)
|
||||
) {
|
||||
deleteAllDevices = true
|
||||
}
|
||||
text = stringResource(R.string.emojiReactsClearAll).let { txt ->
|
||||
Phrase.from(txt).put(EMOJI_KEY, dialogsState.clearAllEmoji.emoji).format().toString()
|
||||
},
|
||||
buttons = listOf(
|
||||
DialogButtonModel(
|
||||
text = GetString(stringResource(id = R.string.delete)),
|
||||
text = GetString(stringResource(id = R.string.clear)),
|
||||
color = LocalColors.current.danger,
|
||||
onClick = {
|
||||
// delete messages based on chosen option
|
||||
// delete emoji
|
||||
sendCommand(
|
||||
if(deleteAllDevices) MarkAsDeletedForEveryone(
|
||||
dialogsState.deleteAllDevices.copy(defaultToEveryone = deleteAllDevices)
|
||||
)
|
||||
else MarkAsDeletedLocally(dialogsState.deleteAllDevices.messages)
|
||||
ClearEmoji(dialogsState.clearAllEmoji.emoji, dialogsState.clearAllEmoji.messageId)
|
||||
)
|
||||
}
|
||||
),
|
||||
@ -201,7 +162,6 @@ fun ConversationV2Dialogs(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,10 @@ import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||
import org.thoughtcrime.securesms.database.ReactionDatabase
|
||||
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.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
@ -61,6 +64,7 @@ class ConversationViewModel(
|
||||
private val messageDataProvider: MessageDataProvider,
|
||||
private val groupDb: GroupDatabase,
|
||||
private val threadDb: ThreadDatabase,
|
||||
private val reactionDb: ReactionDatabase,
|
||||
private val lokiMessageDb: LokiMessageDatabase,
|
||||
private val textSecurePreferences: TextSecurePreferences,
|
||||
private val configFactory: ConfigFactory,
|
||||
@ -339,12 +343,12 @@ class ConversationViewModel(
|
||||
|
||||
// hashes are required if wanting to delete messages from the 'storage server'
|
||||
// They are not required for communities OR if all messages are outgoing
|
||||
// also we can only delete deleted messages (marked as deleted) locally
|
||||
// also we can only delete deleted messages and control messages (marked as deleted) locally
|
||||
val canDeleteForEveryone = messages.all{ !it.isDeleted && !it.isControlMessage } && (
|
||||
messages.all { it.isOutgoing } ||
|
||||
conversationType == MessageType.COMMUNITY ||
|
||||
messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null
|
||||
})
|
||||
messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null }
|
||||
)
|
||||
|
||||
// There are three types of dialogs for deletion:
|
||||
// 1- Delete on device only OR all devices - Used for Note to self
|
||||
@ -354,11 +358,16 @@ class ConversationViewModel(
|
||||
// the conversation is a note to self
|
||||
conversationType == MessageType.NOTE_TO_SELF -> {
|
||||
_dialogsState.update {
|
||||
it.copy(deleteAllDevices = DeleteForEveryoneDialogData(
|
||||
it.copy(deleteEveryone = DeleteForEveryoneDialogData(
|
||||
messages = messages,
|
||||
defaultToEveryone = false,
|
||||
everyoneEnabled = true,
|
||||
messageType = conversationType
|
||||
everyoneEnabled = canDeleteForEveryone,
|
||||
messageType = conversationType,
|
||||
deleteForEveryoneLabel = application.getString(R.string.deleteMessageDevicesAll),
|
||||
warning = if(canDeleteForEveryone) null else
|
||||
application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageNoteToSelfWarning, messages.count(), messages.count()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -372,6 +381,7 @@ class ConversationViewModel(
|
||||
messages = messages,
|
||||
defaultToEveryone = isAdmin.value,
|
||||
everyoneEnabled = true,
|
||||
deleteForEveryoneLabel = application.getString(R.string.deleteMessageEveryone),
|
||||
messageType = conversationType
|
||||
)
|
||||
)
|
||||
@ -387,6 +397,7 @@ class ConversationViewModel(
|
||||
defaultToEveryone = false,
|
||||
everyoneEnabled = false, // disable 'delete for everyone' - can only delete locally in this case
|
||||
messageType = conversationType,
|
||||
deleteForEveryoneLabel = application.getString(R.string.deleteMessageEveryone),
|
||||
warning = application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageWarning, messages.count(), messages.count()
|
||||
)
|
||||
@ -642,7 +653,7 @@ class ConversationViewModel(
|
||||
).show()
|
||||
}
|
||||
|
||||
_dialogsState.update { it.copy(deleteAllDevices = data) }
|
||||
_dialogsState.update { it.copy(deleteEveryone = data) }
|
||||
}
|
||||
|
||||
// hide loading indicator
|
||||
@ -659,11 +670,8 @@ class ConversationViewModel(
|
||||
try {
|
||||
repository.deleteCommunityMessagesRemotely(threadId, data.messages)
|
||||
|
||||
// When this is done we simply need to remove the message locally
|
||||
repository.markAsDeletedLocally(
|
||||
messages = data.messages,
|
||||
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
|
||||
)
|
||||
// When this is done we simply need to remove the message locally (leave nothing behind)
|
||||
repository.deleteMessages(messages = data.messages, threadId = threadId)
|
||||
|
||||
// show confirmation toast
|
||||
withContext(Dispatchers.Main) {
|
||||
@ -855,9 +863,9 @@ class ConversationViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.HideDeleteAllDevicesDialog -> {
|
||||
is Commands.HideClearEmoji -> {
|
||||
_dialogsState.update {
|
||||
it.copy(deleteAllDevices = null)
|
||||
it.copy(clearAllEmoji = null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -872,6 +880,35 @@ class ConversationViewModel(
|
||||
is Commands.MarkAsDeletedForEveryone -> {
|
||||
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 groupDb: GroupDatabase,
|
||||
private val threadDb: ThreadDatabase,
|
||||
private val reactionDb: ReactionDatabase,
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
private val lokiMessageDb: LokiMessageDatabase,
|
||||
@ -939,6 +977,7 @@ class ConversationViewModel(
|
||||
messageDataProvider = messageDataProvider,
|
||||
groupDb = groupDb,
|
||||
threadDb = threadDb,
|
||||
reactionDb = reactionDb,
|
||||
lokiMessageDb = lokiMessageDb,
|
||||
textSecurePreferences = textSecurePreferences,
|
||||
configFactory = configFactory,
|
||||
@ -949,8 +988,8 @@ class ConversationViewModel(
|
||||
|
||||
data class DialogsState(
|
||||
val openLinkDialogUrl: String? = null,
|
||||
val deleteEveryone: DeleteForEveryoneDialogData? = null,
|
||||
val deleteAllDevices: DeleteForEveryoneDialogData? = null,
|
||||
val clearAllEmoji: ClearAllEmoji? = null,
|
||||
val deleteEveryone: DeleteForEveryoneDialogData? = null
|
||||
)
|
||||
|
||||
data class DeleteForEveryoneDialogData(
|
||||
@ -958,13 +997,22 @@ class ConversationViewModel(
|
||||
val messageType: MessageType,
|
||||
val defaultToEveryone: Boolean,
|
||||
val everyoneEnabled: Boolean,
|
||||
val deleteForEveryoneLabel: String,
|
||||
val warning: String? = null
|
||||
)
|
||||
|
||||
data class ClearAllEmoji(
|
||||
val emoji: String,
|
||||
val messageId: MessageId
|
||||
)
|
||||
|
||||
sealed class Commands {
|
||||
data class ShowOpenUrlDialog(val url: String?) : Commands()
|
||||
|
||||
data class ClearEmoji(val emoji:String, val messageId: MessageId) : Commands()
|
||||
|
||||
data object HideDeleteEveryoneDialog : Commands()
|
||||
data object HideDeleteAllDevicesDialog : Commands()
|
||||
data object HideClearEmoji : Commands()
|
||||
|
||||
data class MarkAsDeletedLocally(val messages: Set<MessageRecord>): Commands()
|
||||
data class MarkAsDeletedForEveryone(val data: DeleteForEveryoneDialogData): Commands()
|
||||
|
@ -42,18 +42,6 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
||||
val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes }
|
||||
?.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
|
||||
fun userCanBanSelectedUsers(): Boolean {
|
||||
if (openGroup == null) { return false }
|
||||
@ -67,7 +55,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
||||
|
||||
|
||||
// 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
|
||||
menu.findItem(R.id.menu_context_ban_user).isVisible = userCanBanSelectedUsers()
|
||||
// Ban and delete all
|
||||
|
@ -7,13 +7,6 @@ import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
|
||||
object ConversationMenuItemHelper {
|
||||
|
||||
@JvmStatic
|
||||
fun userCanDeleteSelectedItems(context: Context, message: MessageRecord, openGroup: OpenGroup?, userPublicKey: String, blindedPublicKey: String?): Boolean {
|
||||
if (openGroup == null) return message.isOutgoing || !message.isOutgoing
|
||||
if (message.isOutgoing) return true
|
||||
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun userCanBanSelectedUsers(context: Context, message: MessageRecord, openGroup: OpenGroup?, userPublicKey: String, blindedPublicKey: String?): Boolean {
|
||||
if (openGroup == null) return false
|
||||
|
@ -89,7 +89,11 @@ class ControlMessageView : LinearLayout {
|
||||
&& message.expiryMode != (MessagingModuleConfiguration.shared.storage.getExpirationConfiguration(message.threadId)?.expiryMode ?: ExpiryMode.NONE)
|
||||
&& threadRecipient?.isGroupRecipient != true
|
||||
|
||||
binding.controlContentView.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) }
|
||||
if (followSetting.isVisible) {
|
||||
binding.controlContentView.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) }
|
||||
} else {
|
||||
binding.controlContentView.setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
message.isMediaSavedNotification -> {
|
||||
|
@ -234,7 +234,7 @@ class VisibleMessageView : FrameLayout {
|
||||
showStatusMessage(message)
|
||||
|
||||
// Emoji Reactions
|
||||
if (message.reactions.isNotEmpty()) {
|
||||
if (!message.isDeleted && message.reactions.isNotEmpty()) {
|
||||
val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) }
|
||||
if (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) {
|
||||
emojiReactionsBinding.value.root.let { root ->
|
||||
|
@ -106,6 +106,23 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
.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(
|
||||
messageId: SyncMessageId,
|
||||
timestamp: Long,
|
||||
@ -913,7 +930,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
val groupReceiptDatabase = get(context).groupReceiptDatabase()
|
||||
groupReceiptDatabase.deleteRowsForMessage(messageId)
|
||||
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)
|
||||
notifyConversationListeners(threadId)
|
||||
notifyStickerListeners()
|
||||
|
@ -17,6 +17,9 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
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.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) {
|
||||
return getMessageFor(timestamp, serializedAuthor, true);
|
||||
}
|
||||
@ -342,7 +353,9 @@ public class MmsSmsDatabase extends Database {
|
||||
|
||||
public long getLastMessageTimestamp(long threadId) {
|
||||
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")) {
|
||||
if (cursor.moveToFirst()) {
|
||||
|
@ -243,6 +243,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
contentValues.put(READ, 1);
|
||||
contentValues.put(BODY, displayedMessage);
|
||||
contentValues.put(HAS_MENTION, 0);
|
||||
contentValues.put(STATUS, Status.STATUS_NONE);
|
||||
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
|
||||
|
||||
updateTypeBitmask(messageId, Types.BASE_TYPE_MASK,
|
||||
@ -299,6 +300,28 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
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
|
||||
public String getTypeColumn() {
|
||||
return TYPE;
|
||||
|
@ -1288,6 +1288,16 @@ open class Storage @Inject constructor(
|
||||
}
|
||||
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>) {
|
||||
@ -1766,10 +1776,17 @@ open class Storage @Inject constructor(
|
||||
val timestamp = reaction.timestamp
|
||||
val localId = reaction.localId
|
||||
val isMms = reaction.isMms
|
||||
|
||||
val messageId = if (localId != null && localId > 0 && isMms != null) {
|
||||
// bail early is the message is marked as deleted
|
||||
val messagingDatabase: MessagingDatabase = if (isMms == true) DatabaseComponent.get(context).mmsDatabase()
|
||||
else DatabaseComponent.get(context).smsDatabase()
|
||||
if(messagingDatabase.getMessageRecord(localId)?.isDeleted == true) return
|
||||
|
||||
MessageId(localId, isMms)
|
||||
} else if (timestamp != null && timestamp > 0) {
|
||||
val messageRecord = mmsSmsDatabase.getMessageForTimestamp(timestamp) ?: return
|
||||
if (messageRecord.isDeleted) return
|
||||
MessageId(messageRecord.id, messageRecord.isMms)
|
||||
} else return
|
||||
reactionDatabase.addReaction(
|
||||
|
@ -193,6 +193,16 @@ public class ThreadDatabase extends Database {
|
||||
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) {
|
||||
ContentValues contentValues = new ContentValues(4);
|
||||
|
||||
@ -298,7 +308,7 @@ public class ThreadDatabase extends Database {
|
||||
public void trimThreadBefore(long threadId, long timestamp) {
|
||||
Log.i("ThreadDatabase", "Trimming thread: " + threadId + " before :"+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);
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
@ -707,10 +717,7 @@ public class ThreadDatabase extends Database {
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseComponent.get(context).mmsSmsDatabase();
|
||||
long count = mmsSmsDatabase.getConversationCount(threadId);
|
||||
|
||||
MmsSmsDatabase.Reader reader = null;
|
||||
|
||||
try {
|
||||
reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId));
|
||||
try (MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId))) {
|
||||
MessageRecord record = null;
|
||||
if (reader != null) {
|
||||
record = reader.getNext();
|
||||
@ -724,7 +731,8 @@ public class ThreadDatabase extends Database {
|
||||
record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount());
|
||||
return false;
|
||||
} 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;
|
||||
}
|
||||
} finally {
|
||||
@ -772,10 +780,6 @@ public class ThreadDatabase extends Database {
|
||||
return setLastSeen(threadId, lastSeenTime);
|
||||
}
|
||||
|
||||
private boolean deleteThreadOnEmpty(long threadId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private @NonNull String getFormattedBodyFor(@NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.isMms()) {
|
||||
MmsMessageRecord record = (MmsMessageRecord) messageRecord;
|
||||
|
@ -107,10 +107,12 @@ public class ThreadRecord extends DisplayRecord {
|
||||
|
||||
@Override
|
||||
public CharSequence getDisplayBody(@NonNull Context context) {
|
||||
if (isGroupUpdateMessage()) {
|
||||
return lastMessage != null
|
||||
? lastMessage.getDisplayBody(context).toString()
|
||||
: context.getString(R.string.groupUpdated);
|
||||
// no need to display anything if there are no messages
|
||||
if(lastMessage == null){
|
||||
return "";
|
||||
}
|
||||
else if (isGroupUpdateMessage()) {
|
||||
return context.getString(R.string.groupUpdated);
|
||||
} else if (isOpenGroupInvitation()) {
|
||||
return context.getString(R.string.communityInvitation);
|
||||
} else if (MmsSmsColumns.Types.isLegacyType(type)) {
|
||||
|
@ -365,6 +365,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
binding.seedReminderView.isVisible = false
|
||||
}
|
||||
|
||||
// refresh search on resume, in case we a conversation was deleted
|
||||
if (binding.globalSearchRecycler.isVisible){
|
||||
globalSearchViewModel.refresh()
|
||||
}
|
||||
|
||||
updateLegacyConfigView()
|
||||
}
|
||||
|
||||
|
@ -134,6 +134,16 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
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 var showAvatarDialog: Boolean by mutableStateOf(false)
|
||||
@ -183,7 +193,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
|
||||
binding.composeView.setThemedContent {
|
||||
Buttons()
|
||||
val recoveryHidden by viewModel.recoveryHidden.collectAsState()
|
||||
Buttons(recoveryHidden = recoveryHidden)
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
@ -383,7 +394,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Buttons() {
|
||||
fun Buttons(
|
||||
recoveryHidden: Boolean
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = LocalDimensions.current.spacing)
|
||||
@ -445,12 +458,15 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
Divider()
|
||||
|
||||
// Only show the recovery password option if the user has not chosen to permanently hide it
|
||||
if (!prefs.getHidePassword()) {
|
||||
if (!recoveryHidden) {
|
||||
LargeItemButton(
|
||||
R.string.sessionRecoveryPassword,
|
||||
R.drawable.ic_shield_outline,
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
@ -63,6 +64,10 @@ class SettingsViewModel @Inject constructor(
|
||||
val showLoader: StateFlow<Boolean>
|
||||
get() = _showLoader
|
||||
|
||||
private val _recoveryHidden: MutableStateFlow<Boolean> = MutableStateFlow(prefs.getHidePassword())
|
||||
val recoveryHidden: StateFlow<Boolean>
|
||||
get() = _recoveryHidden
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
object NoAvatar : AvatarDialogState()
|
||||
data class UserAvatar(val address: Address) : AvatarDialogState()
|
||||
|
@ -1,16 +1,21 @@
|
||||
package org.thoughtcrime.securesms.recoverypassword
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.showSessionDialog
|
||||
import org.thoughtcrime.securesms.ui.setComposeContent
|
||||
|
||||
|
||||
class RecoveryPasswordActivity : BaseActionBarActivity() {
|
||||
|
||||
companion object {
|
||||
const val RESULT_RECOVERY_HIDDEN = "recovery_hidden"
|
||||
}
|
||||
|
||||
private val viewModel: RecoveryPasswordViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -25,7 +30,9 @@ class RecoveryPasswordActivity : BaseActionBarActivity() {
|
||||
mnemonic = mnemonic,
|
||||
seed = seed,
|
||||
confirmHideRecovery = {
|
||||
viewModel.permanentlyHidePassword()
|
||||
val returnIntent = Intent()
|
||||
returnIntent.putExtra(RESULT_RECOVERY_HIDDEN, true)
|
||||
setResult(RESULT_OK, returnIntent)
|
||||
finish()
|
||||
},
|
||||
copyMnemonic = viewModel::copyMnemonic
|
||||
|
@ -34,10 +34,6 @@ class RecoveryPasswordViewModel @Inject constructor(
|
||||
.map { MnemonicCodec { MnemonicUtilities.loadFileContents(application, it) }.encode(it, MnemonicCodec.Language.Configuration.english) }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, "")
|
||||
|
||||
fun permanentlyHidePassword() {
|
||||
prefs.setHidePassword(true)
|
||||
}
|
||||
|
||||
fun copyMnemonic() {
|
||||
prefs.setHasViewedSeed(true)
|
||||
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 |
@ -305,14 +305,14 @@
|
||||
|
||||
<TextView
|
||||
android:padding="@dimen/medium_spacing"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
style="@style/Signal.Text.Preview"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textAlignment="center"
|
||||
android:id="@+id/placeholderText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/large_spacing"
|
||||
app:layout_constraintTop_toBottomOf="@+id/outdatedDisappearingBanner"
|
||||
android:elevation="8dp"
|
||||
android:contentDescription="@string/AccessibilityId_control_message"
|
||||
tools:text="Some Control Message Text"
|
||||
/>
|
||||
@ -339,7 +339,7 @@
|
||||
android:paddingHorizontal="@dimen/massive_spacing"
|
||||
android:paddingVertical="@dimen/small_spacing"
|
||||
android:textSize="@dimen/text_size"
|
||||
android:text="@string/block"/>
|
||||
android:text="@string/deleteAfterGroupPR1BlockUser"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sendAcceptsTextView"
|
||||
@ -375,7 +375,7 @@
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
android:layout_weight="1"
|
||||
android:text="d" />
|
||||
android:text="@string/delete" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -31,7 +31,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_trash_filled_32" />
|
||||
app:tint="@color/white"
|
||||
android:src="@drawable/ic_delete" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/scribble_undo_button"
|
||||
|
@ -6,14 +6,15 @@
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/small_spacing">
|
||||
android:gravity="center_vertical"
|
||||
android:paddingVertical="@dimen/small_spacing">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/deletedMessageViewIconImageView"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginStart="@dimen/small_spacing"
|
||||
android:src="?menu_trash_icon"
|
||||
android:layout_width="19dp"
|
||||
android:layout_height="19dp"
|
||||
android:layout_marginStart="18dp"
|
||||
android:src="@drawable/ic_delete"
|
||||
app:tint="?android:textColorPrimary" />
|
||||
|
||||
<TextView
|
||||
@ -22,7 +23,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="@dimen/small_spacing"
|
||||
android:layout_marginEnd="@dimen/large_spacing"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
tools:text="This message has been deleted"
|
||||
|
@ -27,7 +27,7 @@
|
||||
app:tint="?searchIconColor"
|
||||
android:contentDescription="@string/search" />
|
||||
<EditText
|
||||
android:hint="@string/messages"
|
||||
android:hint="@string/search"
|
||||
android:imeOptions="actionSearch"
|
||||
android:id="@+id/search_input"
|
||||
android:paddingHorizontal="@dimen/small_spacing"
|
||||
|
@ -71,6 +71,12 @@
|
||||
<item name="android:textSize">@dimen/very_large_font_size</item>
|
||||
</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">
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:textSize">@dimen/medium2_font_size</item>
|
||||
|
@ -60,6 +60,8 @@
|
||||
|
||||
<item name="conversation_icon_attach_audio">@drawable/ic_audio_dark</item>
|
||||
<item name="conversation_icon_attach_video">@drawable/ic_video_dark</item>
|
||||
|
||||
<item name="android:actionMenuTextAppearance">@style/MenuTextAppearance</item>
|
||||
</style>
|
||||
|
||||
<!-- This should be the default theme for the application. -->
|
||||
|
@ -11,12 +11,6 @@
|
||||
android:title="@string/lockApp"
|
||||
android:summary="@string/lockAppDescription" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="@bool/screen_security_default"
|
||||
android:key="pref_screen_security"
|
||||
android:title="@string/screenshotNotifications"
|
||||
android:summary="@string/screenshotNotificationsDescription" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
|
@ -46,6 +46,9 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
textSecurePreferences = mock(),
|
||||
lokiMessageDb = mock(),
|
||||
application = mock(),
|
||||
reactionDb = mock(),
|
||||
configFactory = mock(),
|
||||
groupManagerV2 = mock()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,7 @@ interface MessageDataProvider {
|
||||
fun updateAudioAttachmentDuration(attachmentId: AttachmentId, durationMs: Long, threadId: Long)
|
||||
fun isMmsOutgoing(mmsMessageId: Long): Boolean
|
||||
fun isOutgoingMessage(timestamp: Long): Boolean
|
||||
fun isDeletedMessage(timestamp: Long): Boolean
|
||||
fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult)
|
||||
fun handleFailedAttachmentUpload(attachmentId: Long)
|
||||
fun getMessageForQuote(timestamp: Long, author: Address): Triple<Long, Boolean, String>?
|
||||
|
@ -39,6 +39,13 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
||||
val message = message as? VisibleMessage
|
||||
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 sender = storage.getUserPublicKey()
|
||||
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)
|
||||
val message = message as? VisibleMessage
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -511,9 +511,15 @@ object MessageSender {
|
||||
|
||||
fun handleFailedMessageSend(message: Message, error: Exception, isSyncMessage: Boolean = false) {
|
||||
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 timestamp = message.sentTimestamp!!
|
||||
val author = message.sender ?: userPublicKey
|
||||
|
||||
if (isSyncMessage) storage.markAsSyncFailed(timestamp, author, error)
|
||||
|
@ -46,7 +46,8 @@ object LocalisedTimeUtil {
|
||||
"${this.inWholeHours}h ${minutesRemaining}m"
|
||||
} else if (this.inWholeMinutes > 0) {
|
||||
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 {
|
||||
"0m ${this.inWholeSeconds}s"
|
||||
}
|
||||
|
@ -114,7 +114,6 @@ interface TextSecurePreferences {
|
||||
fun isEnterSendsEnabled(): Boolean
|
||||
fun isPasswordDisabled(): Boolean
|
||||
fun setPasswordDisabled(disabled: Boolean)
|
||||
fun isScreenSecurityEnabled(): Boolean
|
||||
fun getLastVersionCode(): Int
|
||||
fun setLastVersionCode(versionCode: Int)
|
||||
fun isPassphraseTimeoutEnabled(): Boolean
|
||||
@ -219,7 +218,6 @@ interface TextSecurePreferences {
|
||||
const val LED_BLINK_PREF_CUSTOM = "pref_led_blink_custom"
|
||||
const val PASSPHRASE_TIMEOUT_INTERVAL_PREF = "pref_timeout_interval"
|
||||
const val PASSPHRASE_TIMEOUT_PREF = "pref_timeout_passphrase"
|
||||
const val SCREEN_SECURITY_PREF = "pref_screen_security"
|
||||
const val ENTER_SENDS_PREF = "pref_enter_sends"
|
||||
const val THREAD_TRIM_ENABLED = "pref_trim_threads"
|
||||
internal const val LOCAL_NUMBER_PREF = "pref_local_number"
|
||||
@ -666,11 +664,6 @@ interface TextSecurePreferences {
|
||||
setBooleanPreference(context, DISABLE_PASSPHRASE_PREF, disabled)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isScreenSecurityEnabled(context: Context): Boolean {
|
||||
return getBooleanPreference(context, SCREEN_SECURITY_PREF, context.resources.getBoolean(R.bool.screen_security_default))
|
||||
}
|
||||
|
||||
fun getLastVersionCode(context: Context): Int {
|
||||
return getIntegerPreference(context, LAST_VERSION_CODE_PREF, 0)
|
||||
}
|
||||
@ -1298,10 +1291,6 @@ class AppTextSecurePreferences @Inject constructor(
|
||||
setBooleanPreference(TextSecurePreferences.DISABLE_PASSPHRASE_PREF, disabled)
|
||||
}
|
||||
|
||||
override fun isScreenSecurityEnabled(): Boolean {
|
||||
return getBooleanPreference(TextSecurePreferences.SCREEN_SECURITY_PREF, true)
|
||||
}
|
||||
|
||||
override fun getLastVersionCode(): Int {
|
||||
return getIntegerPreference(TextSecurePreferences.LAST_VERSION_CODE_PREF, 0)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user