349 lines
15 KiB
Swift
349 lines
15 KiB
Swift
//
|
|
// Copyright 2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
public import SignalServiceKit
|
|
import LibSignalClient
|
|
import SignalUI
|
|
public import UIKit
|
|
|
|
public extension TSInteraction {
|
|
func presentDeletionActionSheet(from fromViewController: UIViewController, forceDarkTheme: Bool = false) {
|
|
let (
|
|
associatedThread,
|
|
hasLinkedDevices,
|
|
): (
|
|
TSThread?,
|
|
Bool,
|
|
) = SSKEnvironment.shared.databaseStorageRef.read { tx in
|
|
return (
|
|
thread(tx: tx),
|
|
DependenciesBridge.shared.deviceStore.hasLinkedDevices(tx: tx),
|
|
)
|
|
}
|
|
|
|
guard let associatedThread else { return }
|
|
|
|
if associatedThread.isNoteToSelf {
|
|
presentDeletionActionSheetForNoteToSelf(
|
|
fromViewController: fromViewController,
|
|
thread: associatedThread,
|
|
hasLinkedDevices: hasLinkedDevices,
|
|
forceDarkTheme: forceDarkTheme,
|
|
)
|
|
} else {
|
|
presentDeletionActionSheetForNotNoteToSelf(
|
|
fromViewController: fromViewController,
|
|
thread: associatedThread,
|
|
forceDarkTheme: forceDarkTheme,
|
|
)
|
|
}
|
|
}
|
|
|
|
private func presentDeletionActionSheetForNoteToSelf(
|
|
fromViewController: UIViewController,
|
|
thread: TSThread,
|
|
hasLinkedDevices: Bool,
|
|
forceDarkTheme: Bool,
|
|
) {
|
|
let deleteMessageHeaderText = OWSLocalizedString(
|
|
"DELETE_FOR_ME_NOTE_TO_SELF_ACTION_SHEET_HEADER",
|
|
comment: "Header text for an action sheet confirming deleting a message in Note to Self.",
|
|
)
|
|
let deleteActionSheetButtonTitle = OWSLocalizedString(
|
|
"DELETE_FOR_ME_NOTE_TO_SELF_ACTION_SHEET_BUTTON_TITLE",
|
|
comment: "Title for an action sheet button explaining that a message will be deleted.",
|
|
)
|
|
let (title, message, deleteActionTitle): (String?, String, String) = if hasLinkedDevices {
|
|
(
|
|
deleteMessageHeaderText,
|
|
OWSLocalizedString(
|
|
"DELETE_FOR_ME_NOTE_TO_SELF_LINKED_DEVICES_PRESENT_ACTION_SHEET_SUBHEADER",
|
|
comment: "Subheader for an action sheet explaining that a Note to Self deleted on this device will be deleted on the user's other devices as well.",
|
|
),
|
|
deleteActionSheetButtonTitle,
|
|
)
|
|
} else {
|
|
(
|
|
nil,
|
|
deleteMessageHeaderText,
|
|
deleteActionSheetButtonTitle,
|
|
)
|
|
}
|
|
|
|
let actionSheet = ActionSheetController(
|
|
title: title,
|
|
message: message,
|
|
)
|
|
if forceDarkTheme {
|
|
actionSheet.overrideUserInterfaceStyle = .dark
|
|
}
|
|
actionSheet.addAction(deleteForMeAction(
|
|
title: deleteActionTitle,
|
|
thread: thread,
|
|
))
|
|
actionSheet.addAction(.cancel)
|
|
|
|
fromViewController.presentActionSheet(actionSheet)
|
|
}
|
|
|
|
/// If the local user is an admin, we prefer regular remote delete. We only fallback to
|
|
/// admin delete if the remote delete timeframe has expired and admin delete timeframe has not.
|
|
class func buildDeleteMessage(
|
|
thread: TSThread,
|
|
message: TSMessage,
|
|
localIdentifiers: LocalIdentifiers,
|
|
canAdminDelete: Bool,
|
|
tx: DBReadTransaction,
|
|
) -> TransientOutgoingMessage? {
|
|
if
|
|
message.canBeRemotelyDeletedByNonAdmin,
|
|
let outgoingMessage = message as? TSOutgoingMessage
|
|
{
|
|
return OutgoingDeleteMessage(thread: thread, message: outgoingMessage, tx: tx)
|
|
}
|
|
|
|
guard canAdminDelete else {
|
|
owsFailDebug("Unable to admin-delete incoming message")
|
|
return nil
|
|
}
|
|
|
|
return OutgoingAdminDeleteMessage(
|
|
thread: thread,
|
|
message: message,
|
|
localIdentifiers: localIdentifiers,
|
|
tx: tx,
|
|
)
|
|
}
|
|
|
|
private func buildDeleteForEveryoneAction(thread: TSThread) -> ActionSheetAction? {
|
|
let adminDeleteManager = DependenciesBridge.shared.adminDeleteManager
|
|
let db = DependenciesBridge.shared.db
|
|
|
|
guard let message = self as? TSMessage else {
|
|
return nil
|
|
}
|
|
|
|
let canAdminDelete = db.read { tx in adminDeleteManager.canAdminDeleteMessage(message: message, thread: thread, tx: tx) }
|
|
if !thread.isTerminatedGroup, message.canBeRemotelyDeletedByNonAdmin || canAdminDelete {
|
|
return ActionSheetAction(
|
|
title: CommonStrings.deleteForEveryoneButton,
|
|
style: .destructive,
|
|
) { [weak self] _ in
|
|
guard self != nil else { return }
|
|
|
|
var deleteType: AdminDeleteManager.DeleteType
|
|
if message.isIncoming {
|
|
deleteType = .admin
|
|
} else {
|
|
deleteType = .regular
|
|
}
|
|
|
|
Self.showDeleteForEveryoneConfirmationIfNecessary(
|
|
deleteType: deleteType,
|
|
completion: {
|
|
SSKEnvironment.shared.databaseStorageRef.write { tx in
|
|
let latestMessage = TSMessage.fetchMessageViaCache(
|
|
uniqueId: message.uniqueId,
|
|
transaction: tx,
|
|
)
|
|
guard let latestMessage else {
|
|
ToastViewHelper.presentToastOnFrontmostViewController(
|
|
text: OWSLocalizedString(
|
|
"REMOTE_DELETE_DISAPPEARED_MESSAGE_TOAST",
|
|
comment: "Toast that appears when local user tried to delete a message that has disappeared",
|
|
),
|
|
)
|
|
Logger.warn("User tried to delete a message that no longer exists")
|
|
return
|
|
}
|
|
guard let latestThread = latestMessage.thread(tx: tx) else {
|
|
// We can't reach this point in the UI if a message doesn't have a thread.
|
|
return owsFailDebug("Trying to delete a message without a thread.")
|
|
}
|
|
guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx) else {
|
|
return owsFailDebug("LocalIdentifiers missing during message deletion.")
|
|
}
|
|
|
|
guard
|
|
let deleteMessage = Self.buildDeleteMessage(
|
|
thread: latestThread,
|
|
message: latestMessage,
|
|
localIdentifiers: localIdentifiers,
|
|
canAdminDelete: canAdminDelete,
|
|
tx: tx,
|
|
)
|
|
else {
|
|
return owsFailDebug("Failure to build outgoing delete for everyone.")
|
|
}
|
|
// Reset the sending states, so we can render the sending state of the
|
|
// deleted message. OutgoingDeleteMessage will automatically pass through
|
|
// it's send state to the message record that it is deleting.
|
|
// TODO: support sending state animation for incoming messages.
|
|
(latestMessage as? TSOutgoingMessage)?.updateWithRecipientAddressStates(deleteMessage.recipientAddressStates, tx: tx)
|
|
|
|
if message.canBeRemotelyDeletedByNonAdmin {
|
|
do {
|
|
try TSMessage.tryToRemotelyDeleteMessageAsNonAdmin(
|
|
fromAuthor: localIdentifiers.aci,
|
|
sentAtTimestamp: latestMessage.timestamp,
|
|
threadUniqueId: latestThread.uniqueId,
|
|
serverTimestamp: 0, // TSOutgoingMessage won't have server timestamp.
|
|
transaction: tx,
|
|
)
|
|
} catch {
|
|
return owsFailDebug("Unable to remotely delete message")
|
|
}
|
|
} else if
|
|
canAdminDelete,
|
|
let groupThread = thread as? TSGroupThread
|
|
{
|
|
let originalMessageAuthorAci: Aci?
|
|
if let incomingMessage = (latestMessage as? TSIncomingMessage) {
|
|
originalMessageAuthorAci = incomingMessage.authorAddress.aci
|
|
} else {
|
|
originalMessageAuthorAci = localIdentifiers.aci
|
|
}
|
|
|
|
guard let originalMessageAuthorAci else {
|
|
owsFailDebug("Unable to admin delete without original message author")
|
|
return
|
|
}
|
|
|
|
do {
|
|
try DependenciesBridge.shared.adminDeleteManager.tryToAdminDeleteMessage(
|
|
originalMessageAuthorAci: originalMessageAuthorAci,
|
|
deleteAuthorAci: localIdentifiers.aci,
|
|
sentAtTimestamp: latestMessage.timestamp,
|
|
groupThread: groupThread,
|
|
threadUniqueId: latestThread.uniqueId,
|
|
serverTimestamp: 0, // TSOutgoingMessage won't have server timestamp.
|
|
transaction: tx,
|
|
)
|
|
} catch {
|
|
return owsFailDebug("Unable to remotely delete message")
|
|
}
|
|
} else {
|
|
owsFailDebug("Unable to delete as admin or as non-admin")
|
|
return
|
|
}
|
|
|
|
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
|
transientMessageWithoutAttachments: deleteMessage,
|
|
)
|
|
|
|
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: tx)
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func presentDeletionActionSheetForNotNoteToSelf(
|
|
fromViewController: UIViewController,
|
|
thread: TSThread,
|
|
forceDarkTheme: Bool,
|
|
) {
|
|
let actionSheetController = ActionSheetController(
|
|
message: OWSLocalizedString(
|
|
"MESSAGE_ACTION_DELETE_FOR_TITLE",
|
|
comment: "The title for the action sheet asking who the user wants to delete the message for.",
|
|
),
|
|
)
|
|
if forceDarkTheme {
|
|
actionSheetController.overrideUserInterfaceStyle = .dark
|
|
}
|
|
|
|
actionSheetController.addAction(deleteForMeAction(
|
|
title: CommonStrings.deleteForMeButton,
|
|
thread: thread,
|
|
))
|
|
|
|
if let deleteForEveryoneAction = buildDeleteForEveryoneAction(thread: thread) {
|
|
actionSheetController.addAction(deleteForEveryoneAction)
|
|
}
|
|
|
|
actionSheetController.addAction(OWSActionSheets.cancelAction)
|
|
|
|
fromViewController.presentActionSheet(actionSheetController)
|
|
}
|
|
|
|
static func showDeleteForEveryoneConfirmationIfNecessary(deleteType: AdminDeleteManager.DeleteType, completion: @escaping () -> Void) {
|
|
let adminDeleteManager = DependenciesBridge.shared.adminDeleteManager
|
|
let db = DependenciesBridge.shared.db
|
|
|
|
let shouldShowAdminDeleteConfirmation = deleteType.contains(.admin) && db.read { tx in adminDeleteManager.adminDeleteEducationReadStatus(tx: tx) == false }
|
|
let shouldShowRegularDeleteConfirmation = deleteType.contains(.regular) && !SSKEnvironment.shared.preferencesRef.wasDeleteForEveryoneConfirmationShown
|
|
|
|
guard shouldShowAdminDeleteConfirmation || shouldShowRegularDeleteConfirmation else { return completion() }
|
|
|
|
let title: String
|
|
if shouldShowAdminDeleteConfirmation {
|
|
title = OWSLocalizedString("MESSAGE_ACTION_ADMIN_DELETE_FOR_EVERYONE_CONFIRMATION", comment: "A one-time confirmation that you, as an admin, want to delete for everyone")
|
|
} else {
|
|
title = OWSLocalizedString(
|
|
"MESSAGE_ACTION_DELETE_FOR_EVERYONE_CONFIRMATION",
|
|
comment: "A one-time confirmation that you want to delete for everyone",
|
|
)
|
|
}
|
|
|
|
OWSActionSheets.showConfirmationAlert(
|
|
title: title,
|
|
proceedTitle: CommonStrings.deleteForEveryoneButton,
|
|
proceedStyle: .destructive,
|
|
) { _ in
|
|
if shouldShowAdminDeleteConfirmation {
|
|
db.write { tx in
|
|
adminDeleteManager.setAdminDeleteEducationRead(tx: tx, updateStorageService: true)
|
|
}
|
|
} else {
|
|
SSKEnvironment.shared.preferencesRef.setWasDeleteForEveryoneConfirmationShown()
|
|
}
|
|
completion()
|
|
}
|
|
}
|
|
|
|
private func deleteForMeAction(
|
|
title: String,
|
|
thread: TSThread,
|
|
) -> ActionSheetAction {
|
|
let db = DependenciesBridge.shared.db
|
|
let interactionDeleteManager = DependenciesBridge.shared.interactionDeleteManager
|
|
|
|
return ActionSheetAction(
|
|
title: CommonStrings.deleteForMeButton,
|
|
style: .destructive,
|
|
) { [weak self] _ in
|
|
guard let self else { return }
|
|
|
|
db.asyncWrite { tx in
|
|
guard
|
|
let freshSelf = TSInteraction.fetchViaCache(uniqueId: self.uniqueId, transaction: tx),
|
|
let freshThread = TSThread.fetchViaCache(uniqueId: thread.uniqueId, transaction: tx)
|
|
else { return }
|
|
|
|
interactionDeleteManager.delete(
|
|
interactions: [freshSelf],
|
|
sideEffects: .custom(
|
|
deleteForMeSyncMessage: .sendSyncMessage(interactionsThread: freshThread),
|
|
),
|
|
tx: tx,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension CommonStrings {
|
|
public static var deleteForEveryoneButton: String {
|
|
OWSLocalizedString(
|
|
"MESSAGE_ACTION_DELETE_FOR_EVERYONE",
|
|
comment: "The title for the action that deletes a message for all users in the conversation.",
|
|
)
|
|
}
|
|
}
|