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-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"
|
||||||
|
|
||||||
|
@ -127,11 +127,7 @@ public abstract class BaseActionBarActivity extends AppCompatActivity {
|
|||||||
if (!isResume) {
|
if (!isResume) {
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||||
} else {
|
} else {
|
||||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
|
||||||
} else {
|
|
||||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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) })
|
||||||
|
@ -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(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
binding.controlContentView.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) }
|
if (followSetting.isVisible) {
|
||||||
|
binding.controlContentView.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) }
|
||||||
|
} else {
|
||||||
|
binding.controlContentView.setOnClickListener(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message.isMediaSavedNotification -> {
|
message.isMediaSavedNotification -> {
|
||||||
|
@ -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 ->
|
||||||
|
@ -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()
|
||||||
|
@ -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()) {
|
||||||
|
@ -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;
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
@ -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)) {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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 |
@ -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>
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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. -->
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>?
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user