Informational UX for delete syncs
This commit is contained in:
parent
84295351e8
commit
41d2a3d1b2
@ -1862,6 +1862,8 @@
|
||||
D9E7C8772B9A4A9C005BD3B9 /* CallRecord+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E7C8762B9A4A9C005BD3B9 /* CallRecord+Sorting.swift */; };
|
||||
D9E7C8792B9B9072005BD3B9 /* IdentifierIndexedArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E7C8782B9B9072005BD3B9 /* IdentifierIndexedArray.swift */; };
|
||||
D9E8EDED2C0EAFE700923E3C /* DeleteForMeOutgoingSyncMessageManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E8EDEC2C0EAFE700923E3C /* DeleteForMeOutgoingSyncMessageManagerTest.swift */; };
|
||||
D9E8EDF12C0FCB3000923E3C /* DeleteForMeSyncMessageInfoSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E8EDF02C0FCB3000923E3C /* DeleteForMeSyncMessageInfoSheet.swift */; };
|
||||
D9E8EDF32C0FD8C800923E3C /* DeleteForMeInfoSheetCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E8EDF22C0FD8C800923E3C /* DeleteForMeInfoSheetCoordinator.swift */; };
|
||||
D9EB221E2A4B636C00C73E1D /* Bitmaps+LineDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9EB22192A4B636B00C73E1D /* Bitmaps+LineDrawing.swift */; };
|
||||
D9EB221F2A4B636C00C73E1D /* Bitmaps+Shapes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9EB221A2A4B636B00C73E1D /* Bitmaps+Shapes.swift */; };
|
||||
D9EB22202A4B636C00C73E1D /* Bitmaps+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9EB221B2A4B636B00C73E1D /* Bitmaps+Image.swift */; };
|
||||
@ -4803,6 +4805,8 @@
|
||||
D9E7C8762B9A4A9C005BD3B9 /* CallRecord+Sorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CallRecord+Sorting.swift"; sourceTree = "<group>"; };
|
||||
D9E7C8782B9B9072005BD3B9 /* IdentifierIndexedArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifierIndexedArray.swift; sourceTree = "<group>"; };
|
||||
D9E8EDEC2C0EAFE700923E3C /* DeleteForMeOutgoingSyncMessageManagerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteForMeOutgoingSyncMessageManagerTest.swift; sourceTree = "<group>"; };
|
||||
D9E8EDF02C0FCB3000923E3C /* DeleteForMeSyncMessageInfoSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteForMeSyncMessageInfoSheet.swift; sourceTree = "<group>"; };
|
||||
D9E8EDF22C0FD8C800923E3C /* DeleteForMeInfoSheetCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteForMeInfoSheetCoordinator.swift; sourceTree = "<group>"; };
|
||||
D9EB22192A4B636B00C73E1D /* Bitmaps+LineDrawing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bitmaps+LineDrawing.swift"; sourceTree = "<group>"; };
|
||||
D9EB221A2A4B636B00C73E1D /* Bitmaps+Shapes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bitmaps+Shapes.swift"; sourceTree = "<group>"; };
|
||||
D9EB221B2A4B636B00C73E1D /* Bitmaps+Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bitmaps+Image.swift"; sourceTree = "<group>"; };
|
||||
@ -9308,6 +9312,7 @@
|
||||
88D23D1323CEC0C700B0E74B /* Calls */,
|
||||
50B6BCAF2AEC4F3B0010FB3B /* Contacts */,
|
||||
3448BFC01EDF0EA7005B2D69 /* ConversationView */,
|
||||
D9E8EDEF2C0FCB0600923E3C /* DeleteForMe */,
|
||||
88C4E38124671F9D009C9B97 /* DeviceTransfer */,
|
||||
3428576F26BD8777005A2A96 /* Emoji */,
|
||||
50E7E1CC2BACBDE000A94861 /* Expiration */,
|
||||
@ -9812,6 +9817,15 @@
|
||||
path = Mocks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D9E8EDEF2C0FCB0600923E3C /* DeleteForMe */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D9E8EDF22C0FD8C800923E3C /* DeleteForMeInfoSheetCoordinator.swift */,
|
||||
D9E8EDF02C0FCB3000923E3C /* DeleteForMeSyncMessageInfoSheet.swift */,
|
||||
);
|
||||
path = DeleteForMe;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D9F399B72A9EA1EB001599EC /* Mocks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -12958,6 +12972,8 @@
|
||||
88DBDFB9263731C800C2101C /* DefaultDisappearingMessageTimerInteraction.swift in Sources */,
|
||||
66B8B28028C94C0F005EAFE0 /* DelegatingContextMenuButton.swift in Sources */,
|
||||
887B6DC925F6C3E900E677D4 /* DeleteAccountConfirmationViewController.swift in Sources */,
|
||||
D9E8EDF32C0FD8C800923E3C /* DeleteForMeInfoSheetCoordinator.swift in Sources */,
|
||||
D9E8EDF12C0FCB3000923E3C /* DeleteForMeSyncMessageInfoSheet.swift in Sources */,
|
||||
3498AC892513896400B1F315 /* Dependencies+MainApp.swift in Sources */,
|
||||
5011D1CD29400E7300064098 /* DeviceProvisioningURL.swift in Sources */,
|
||||
887CD4772472FEA500FDD265 /* DeviceTransferOperation.swift in Sources */,
|
||||
|
||||
@ -300,35 +300,67 @@ extension ConversationViewController {
|
||||
return
|
||||
}
|
||||
|
||||
let messageFormat = OWSLocalizedString("DELETE_SELECTED_MESSAGES_IN_CONVERSATION_ALERT_%d", tableName: "PluralAware",
|
||||
comment: "action sheet body. Embeds {{number of selected messages}} which will be deleted.")
|
||||
let message = String.localizedStringWithFormat(messageFormat, selectionItems.count)
|
||||
let alert = ActionSheetController(title: nil, message: message)
|
||||
alert.addAction(OWSActionSheets.cancelAction)
|
||||
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
|
||||
fromViewController: self
|
||||
) { interactionDeleteManager, _ in
|
||||
self.presentDeleteSelectedMessagesActionSheet(
|
||||
selectionItems: selectionItems,
|
||||
interactionDeleteManager: interactionDeleteManager
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let delete = ActionSheetAction(title: CommonStrings.deleteForMeButton, style: .destructive) { [weak self] _ in
|
||||
private func presentDeleteSelectedMessagesActionSheet(
|
||||
selectionItems: [CVSelectionItem],
|
||||
interactionDeleteManager: InteractionDeleteManager
|
||||
) {
|
||||
let deleteAction = ActionSheetAction(
|
||||
title: CommonStrings.deleteForMeButton,
|
||||
style: .destructive
|
||||
) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self] modalActivityIndicator in
|
||||
|
||||
ModalActivityIndicatorViewController.present(
|
||||
fromViewController: self,
|
||||
canCancel: false
|
||||
) { [weak self] modalActivityIndicator in
|
||||
guard let self = self else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
Self.deleteSelectedItems(
|
||||
selectionItems: selectionItems,
|
||||
thread: self.thread
|
||||
thread: self.thread,
|
||||
interactionDeleteManager: interactionDeleteManager
|
||||
)
|
||||
|
||||
modalActivityIndicator.dismiss {
|
||||
self.uiMode = .normal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
alert.addAction(delete)
|
||||
|
||||
let alert = ActionSheetController(
|
||||
title: nil,
|
||||
message: String.localizedStringWithFormat(
|
||||
OWSLocalizedString(
|
||||
"DELETE_SELECTED_MESSAGES_IN_CONVERSATION_ALERT_%d",
|
||||
tableName: "PluralAware",
|
||||
comment: "action sheet body. Embeds {{number of selected messages}} which will be deleted."
|
||||
),
|
||||
selectionItems.count
|
||||
)
|
||||
)
|
||||
alert.addAction(OWSActionSheets.cancelAction)
|
||||
alert.addAction(deleteAction)
|
||||
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private static func deleteSelectedItems(
|
||||
selectionItems: [CVSelectionItem],
|
||||
thread: TSThread
|
||||
thread: TSThread,
|
||||
interactionDeleteManager: InteractionDeleteManager
|
||||
) {
|
||||
databaseStorage.write { tx in
|
||||
var interactionsToDelete = [TSInteraction]()
|
||||
@ -351,7 +383,7 @@ extension ConversationViewController {
|
||||
}
|
||||
}
|
||||
|
||||
DependenciesBridge.shared.interactionDeleteManager.delete(
|
||||
interactionDeleteManager.delete(
|
||||
interactions: interactionsToDelete,
|
||||
sideEffects: .custom(
|
||||
deleteForMeSyncMessage: .sendSyncMessage(interactionsThread: thread)
|
||||
@ -462,6 +494,20 @@ extension ConversationViewController {
|
||||
}
|
||||
|
||||
func didTapDeleteAll() {
|
||||
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
|
||||
fromViewController: self
|
||||
) { [weak self] _, threadSoftDeleteManager in
|
||||
guard let self else { return }
|
||||
|
||||
self.presentDeleteAllConfirmationSheet(
|
||||
threadSoftDeleteManager: threadSoftDeleteManager
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentDeleteAllConfirmationSheet(
|
||||
threadSoftDeleteManager: any ThreadSoftDeleteManager
|
||||
) {
|
||||
let thread = self.thread
|
||||
let alert = ActionSheetController(title: nil, message: OWSLocalizedString("DELETE_ALL_MESSAGES_IN_CONVERSATION_ALERT_BODY", comment: "action sheet body"))
|
||||
alert.addAction(OWSActionSheets.cancelAction)
|
||||
@ -471,8 +517,11 @@ extension ConversationViewController {
|
||||
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self] modalActivityIndicator in
|
||||
guard let self = self else { return }
|
||||
self.databaseStorage.write {
|
||||
DependenciesBridge.shared.threadSoftDeleteManager
|
||||
.removeAllInteractions(thread: thread, sendDeleteForMeSyncMessage: true, tx: $0.asV2Write)
|
||||
threadSoftDeleteManager.removeAllInteractions(
|
||||
thread: thread,
|
||||
sendDeleteForMeSyncMessage: true,
|
||||
tx: $0.asV2Write
|
||||
)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
modalActivityIndicator.dismiss { [weak self] in
|
||||
|
||||
@ -10,6 +10,97 @@ import UIKit
|
||||
|
||||
public extension TSInteraction {
|
||||
func presentDeletionActionSheet(from fromViewController: UIViewController, forceDarkTheme: Bool = false) {
|
||||
let (associatedThread, hasLinkedDevices): (TSThread?, Bool) = databaseStorage.read { tx in
|
||||
return (
|
||||
thread(tx: tx),
|
||||
DependenciesBridge.shared.deviceStore.hasLinkedDevices(tx: tx.asV2Read)
|
||||
)
|
||||
}
|
||||
|
||||
guard let associatedThread else { return }
|
||||
|
||||
if
|
||||
// We only want the new Note to Self delete UX if we're sending
|
||||
// DeleteForMe sync messages.
|
||||
DeleteForMeSyncMessage.isSendingEnabled,
|
||||
associatedThread.isNoteToSelf
|
||||
{
|
||||
presentDeletionActionSheetForNoteToSelf(
|
||||
fromViewController: fromViewController,
|
||||
thread: associatedThread,
|
||||
hasLinkedDevices: hasLinkedDevices,
|
||||
forceDarkTheme: forceDarkTheme,
|
||||
interactionDeleteManager: DependenciesBridge.shared.interactionDeleteManager
|
||||
)
|
||||
} else {
|
||||
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
|
||||
fromViewController: fromViewController
|
||||
) { [weak self] interactionDeleteManager, _ in
|
||||
self?.presentDeletionActionSheetForNotNoteToSelf(
|
||||
fromViewController: fromViewController,
|
||||
thread: associatedThread,
|
||||
forceDarkTheme: forceDarkTheme,
|
||||
interactionDeleteManager: interactionDeleteManager
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentDeletionActionSheetForNoteToSelf(
|
||||
fromViewController: UIViewController,
|
||||
thread: TSThread,
|
||||
hasLinkedDevices: Bool,
|
||||
forceDarkTheme: Bool,
|
||||
interactionDeleteManager: any InteractionDeleteManager
|
||||
) {
|
||||
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 (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."
|
||||
),
|
||||
OWSLocalizedString(
|
||||
"DELETE_FOR_ME_NOTE_TO_SELF_LINKED_DEVICES_PRESENT_ACTION_SHEET_BUTTON_TITLE",
|
||||
comment: "Title for an action sheet button explaining that a message will be deleted."
|
||||
)
|
||||
)
|
||||
} else {
|
||||
(
|
||||
nil,
|
||||
deleteMessageHeaderText,
|
||||
OWSLocalizedString(
|
||||
"DELETE_FOR_ME_NOTE_TO_SELF_LINKED_DEVICES_NOT_PRESENT_ACTION_SHEET_BUTTON_TITLE",
|
||||
comment: "Title for an action sheet button explaining that a message will be deleted."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let actionSheet = ActionSheetController(
|
||||
title: title,
|
||||
message: message,
|
||||
theme: forceDarkTheme ? .translucentDark : .default
|
||||
)
|
||||
actionSheet.addAction(deleteForMeAction(
|
||||
title: deleteActionTitle,
|
||||
thread: thread,
|
||||
interactionDeleteManager: interactionDeleteManager
|
||||
))
|
||||
actionSheet.addAction(.cancel)
|
||||
|
||||
fromViewController.presentActionSheet(actionSheet)
|
||||
}
|
||||
|
||||
private func presentDeletionActionSheetForNotNoteToSelf(
|
||||
fromViewController: UIViewController,
|
||||
thread: TSThread,
|
||||
forceDarkTheme: Bool,
|
||||
interactionDeleteManager: any InteractionDeleteManager
|
||||
) {
|
||||
let actionSheetController = ActionSheetController(
|
||||
message: OWSLocalizedString(
|
||||
"MESSAGE_ACTION_DELETE_FOR_TITLE",
|
||||
@ -18,33 +109,11 @@ public extension TSInteraction {
|
||||
theme: forceDarkTheme ? .translucentDark : .default
|
||||
)
|
||||
|
||||
let deleteForMeAction = ActionSheetAction(
|
||||
actionSheetController.addAction(deleteForMeAction(
|
||||
title: CommonStrings.deleteForMeButton,
|
||||
style: .destructive
|
||||
) { _ in
|
||||
Self.databaseStorage.asyncWrite { tx in
|
||||
guard let freshSelf = TSInteraction.anyFetch(
|
||||
uniqueId: self.uniqueId, transaction: tx
|
||||
) else { return }
|
||||
|
||||
if let thread = freshSelf.thread(tx: tx) {
|
||||
DependenciesBridge.shared.interactionDeleteManager.delete(
|
||||
interactions: [freshSelf],
|
||||
sideEffects: .custom(
|
||||
deleteForMeSyncMessage: .sendSyncMessage(interactionsThread: thread)
|
||||
),
|
||||
tx: tx.asV2Write
|
||||
)
|
||||
} else {
|
||||
DependenciesBridge.shared.interactionDeleteManager.delete(
|
||||
interactions: [freshSelf],
|
||||
sideEffects: .default(),
|
||||
tx: tx.asV2Write
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
actionSheetController.addAction(deleteForMeAction)
|
||||
thread: thread,
|
||||
interactionDeleteManager: interactionDeleteManager
|
||||
))
|
||||
|
||||
if
|
||||
let outgoingMessage = self as? TSOutgoingMessage,
|
||||
@ -123,4 +192,32 @@ public extension TSInteraction {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteForMeAction(
|
||||
title: String,
|
||||
thread: TSThread,
|
||||
interactionDeleteManager: any InteractionDeleteManager
|
||||
) -> ActionSheetAction {
|
||||
return ActionSheetAction(
|
||||
title: CommonStrings.deleteForMeButton,
|
||||
style: .destructive
|
||||
) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
self.databaseStorage.asyncWrite { tx in
|
||||
guard
|
||||
let freshSelf = TSInteraction.anyFetch(uniqueId: self.uniqueId, transaction: tx),
|
||||
let freshThread = TSThread.anyFetch(uniqueId: thread.uniqueId, transaction: tx)
|
||||
else { return }
|
||||
|
||||
interactionDeleteManager.delete(
|
||||
interactions: [freshSelf],
|
||||
sideEffects: .custom(
|
||||
deleteForMeSyncMessage: .sendSyncMessage(interactionsThread: freshThread)
|
||||
),
|
||||
tx: tx.asV2Write
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
109
Signal/DeleteForMe/DeleteForMeInfoSheetCoordinator.swift
Normal file
109
Signal/DeleteForMe/DeleteForMeInfoSheetCoordinator.swift
Normal file
@ -0,0 +1,109 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SignalServiceKit
|
||||
|
||||
/// Responsible for optionally showing informational UX before a deletion occurs
|
||||
/// that will sync across devices.
|
||||
///
|
||||
/// As part of the introduction of "delete syncs" (powered by `DeleteForMe` sync
|
||||
/// messages), we want to show users a one-time pop-up explaining that their
|
||||
/// deletes will now sync before we do the deletion. This type handles checking
|
||||
/// if the pop-up should be shown, and if so, doing so.
|
||||
final class DeleteForMeInfoSheetCoordinator {
|
||||
typealias DeletionBlock = (InteractionDeleteManager, ThreadSoftDeleteManager) -> Void
|
||||
|
||||
private enum StoreKeys {
|
||||
static let hasShownDeleteForMeInfoSheet = "hasShownDeleteForMeInfoSheet"
|
||||
}
|
||||
|
||||
private let db: DB
|
||||
private let deviceStore: OWSDeviceStore
|
||||
private let interactionDeleteManager: InteractionDeleteManager
|
||||
private let keyValueStore: KeyValueStore
|
||||
private let threadSoftDeleteManager: ThreadSoftDeleteManager
|
||||
|
||||
init(
|
||||
db: DB,
|
||||
deviceStore: OWSDeviceStore,
|
||||
interactionDeleteManager: InteractionDeleteManager,
|
||||
keyValueStoreFactory: KeyValueStoreFactory,
|
||||
threadSoftDeleteManager: ThreadSoftDeleteManager
|
||||
) {
|
||||
self.db = db
|
||||
self.deviceStore = deviceStore
|
||||
self.interactionDeleteManager = interactionDeleteManager
|
||||
self.keyValueStore = keyValueStoreFactory.keyValueStore(collection: "DeleteForMeInfoSheetCoordinator")
|
||||
self.threadSoftDeleteManager = threadSoftDeleteManager
|
||||
}
|
||||
|
||||
static func fromGlobals() -> DeleteForMeInfoSheetCoordinator {
|
||||
return DeleteForMeInfoSheetCoordinator(
|
||||
db: DependenciesBridge.shared.db,
|
||||
deviceStore: DependenciesBridge.shared.deviceStore,
|
||||
interactionDeleteManager: DependenciesBridge.shared.interactionDeleteManager,
|
||||
keyValueStoreFactory: DependenciesBridge.shared.keyValueStoreFactory,
|
||||
threadSoftDeleteManager: DependenciesBridge.shared.threadSoftDeleteManager
|
||||
)
|
||||
}
|
||||
|
||||
func coordinateDelete(
|
||||
fromViewController: UIViewController,
|
||||
deletionBlock: @escaping DeletionBlock
|
||||
) {
|
||||
guard shouldShowInfoSheet() else {
|
||||
deletionBlock(interactionDeleteManager, threadSoftDeleteManager)
|
||||
return
|
||||
}
|
||||
|
||||
let infoSheet = DeleteForMeSyncMessage.InfoSheet(onConfirmBlock: {
|
||||
self.db.write { tx in
|
||||
self.keyValueStore.setBool(
|
||||
true,
|
||||
key: StoreKeys.hasShownDeleteForMeInfoSheet,
|
||||
transaction: tx
|
||||
)
|
||||
}
|
||||
|
||||
deletionBlock(
|
||||
self.interactionDeleteManager,
|
||||
self.threadSoftDeleteManager
|
||||
)
|
||||
})
|
||||
|
||||
fromViewController.present(infoSheet, animated: true)
|
||||
}
|
||||
|
||||
#if USE_DEBUG_UI
|
||||
func forceEnableInfoSheet(tx: any DBWriteTransaction) {
|
||||
keyValueStore.removeValue(
|
||||
forKey: StoreKeys.hasShownDeleteForMeInfoSheet,
|
||||
transaction: tx
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
private func shouldShowInfoSheet() -> Bool {
|
||||
return db.read { tx -> Bool in
|
||||
guard DeleteForMeSyncMessage.isSendingEnabled else {
|
||||
// Nothing will actually be synced!
|
||||
return false
|
||||
}
|
||||
|
||||
guard deviceStore.hasLinkedDevices(tx: tx) else {
|
||||
// No devices with which to sync!
|
||||
return false
|
||||
}
|
||||
|
||||
guard keyValueStore.getBool(StoreKeys.hasShownDeleteForMeInfoSheet, transaction: tx) != true else {
|
||||
// Already shown!
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
134
Signal/DeleteForMe/DeleteForMeSyncMessageInfoSheet.swift
Normal file
134
Signal/DeleteForMe/DeleteForMeSyncMessageInfoSheet.swift
Normal file
@ -0,0 +1,134 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SignalServiceKit
|
||||
import SignalUI
|
||||
import UIKit
|
||||
|
||||
extension DeleteForMeSyncMessage {
|
||||
final class InfoSheet: InteractiveSheetViewController {
|
||||
private let onConfirmBlock: () -> Void
|
||||
|
||||
init(onConfirmBlock: @escaping () -> Void) {
|
||||
self.onConfirmBlock = onConfirmBlock
|
||||
}
|
||||
|
||||
override var interactiveScrollViews: [UIScrollView] { [contentScrollWrapper] }
|
||||
|
||||
private lazy var contentScrollWrapper: UIScrollView = {
|
||||
let scrollView = UIScrollView(forAutoLayout: ())
|
||||
scrollView.addSubview(_contentView)
|
||||
|
||||
// Pin height to scrollable area, but width to the viewport.
|
||||
_contentView.autoPinHeightToSuperview()
|
||||
_contentView.autoPinWidth(toWidthOf: scrollView)
|
||||
|
||||
return scrollView
|
||||
}()
|
||||
|
||||
private lazy var _contentView: UIView = {
|
||||
let view = UIView()
|
||||
|
||||
let headerImageView = { () -> UIImageView in
|
||||
let imageName = Theme.isDarkThemeEnabled ? "delete-sync-dark" : "delete-sync-light"
|
||||
let imageView = UIImageView(image: UIImage(named: imageName)!)
|
||||
imageView.heightAnchor.constraint(equalToConstant: 88).isActive = true
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let titleLabel = { () -> UILabel in
|
||||
let label = UILabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.text = OWSLocalizedString(
|
||||
"DELETE_FOR_ME_SYNC_MESSAGE_INFO_SHEET_TITLE",
|
||||
comment: "Title for an info sheet explaining that deletes are now synced across devices."
|
||||
)
|
||||
label.font = .dynamicTypeTitle3.semibold()
|
||||
label.numberOfLines = 0
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
label.textAlignment = .center
|
||||
|
||||
return label
|
||||
}()
|
||||
|
||||
let subtitleLabel = { () -> UILabel in
|
||||
let label = UILabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.text = OWSLocalizedString(
|
||||
"DELETE_FOR_ME_SYNC_MESSAGE_INFO_SHEET_SUBTITLE",
|
||||
comment: "Subtitle for an info sheet explaining that deletes are now synced across devices."
|
||||
)
|
||||
label.font = .dynamicTypeBody
|
||||
label.textColor = Theme.isDarkThemeEnabled ? .ows_gray20 : .ows_gray60
|
||||
label.numberOfLines = 0
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
label.textAlignment = .center
|
||||
|
||||
return label
|
||||
}()
|
||||
|
||||
let spacer = UIView(forAutoLayout: ())
|
||||
|
||||
let gotItButton = { () -> UIButton in
|
||||
let button = OWSButton(
|
||||
title: OWSLocalizedString(
|
||||
"DELETE_FOR_ME_SYNC_MESSAGE_INFO_SHEET_BUTTON",
|
||||
comment: "Label for a button in an info sheet confirming that deletes are now synced across devices."
|
||||
),
|
||||
block: { [weak self] in
|
||||
guard let self else { return }
|
||||
self.dismiss(animated: true) {
|
||||
self.onConfirmBlock()
|
||||
}
|
||||
}
|
||||
)
|
||||
button.backgroundColor = .ows_accentBlue
|
||||
button.layer.cornerRadius = 12
|
||||
button.configureForMultilineTitle()
|
||||
button.titleLabel!.font = .dynamicTypeHeadline.semibold()
|
||||
|
||||
return button
|
||||
}()
|
||||
|
||||
view.addSubview(headerImageView)
|
||||
view.addSubview(titleLabel)
|
||||
view.addSubview(subtitleLabel)
|
||||
view.addSubview(spacer)
|
||||
view.addSubview(gotItButton)
|
||||
|
||||
headerImageView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom)
|
||||
|
||||
titleLabel.autoPinEdge(.top, to: .bottom, of: headerImageView, withOffset: 24)
|
||||
titleLabel.autoPinWidthToSuperviewMargins(withInset: 24)
|
||||
|
||||
subtitleLabel.autoPinEdge(.top, to: .bottom, of: titleLabel, withOffset: 12)
|
||||
subtitleLabel.autoPinWidthToSuperviewMargins(withInset: 24)
|
||||
|
||||
spacer.autoPinEdge(.top, to: .bottom, of: subtitleLabel)
|
||||
spacer.autoPinWidthToSuperviewMargins()
|
||||
spacer.heightAnchor.constraint(greaterThanOrEqualToConstant: 92).isActive = true
|
||||
|
||||
gotItButton.autoPinEdge(.top, to: .bottom, of: spacer)
|
||||
gotItButton.autoPinLeadingToSuperviewMargin(withInset: 48)
|
||||
gotItButton.autoPinTrailingToSuperviewMargin(withInset: 48)
|
||||
gotItButton.autoPinBottomToSuperviewMargin()
|
||||
gotItButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 50).isActive = true
|
||||
|
||||
return view
|
||||
}()
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
minimizedHeight = 487
|
||||
allowsExpansion = true
|
||||
|
||||
contentView.addSubview(contentScrollWrapper)
|
||||
contentScrollWrapper.autoPinEdgesToSuperviewMargins()
|
||||
contentScrollWrapper.alwaysBounceVertical = true
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Signal/Images.xcassets/delete-sync-dark.imageset/Contents.json
vendored
Normal file
12
Signal/Images.xcassets/delete-sync-dark.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "delete-sync-dark.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/delete-sync-dark.imageset/delete-sync-dark.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/delete-sync-dark.imageset/delete-sync-dark.pdf
vendored
Normal file
Binary file not shown.
12
Signal/Images.xcassets/delete-sync-light.imageset/Contents.json
vendored
Normal file
12
Signal/Images.xcassets/delete-sync-light.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "delete-sync-light.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/delete-sync-light.imageset/delete-sync-light.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/delete-sync-light.imageset/delete-sync-light.pdf
vendored
Normal file
Binary file not shown.
@ -171,34 +171,71 @@ class ChatsSettingsViewController: OWSTableViewController2 {
|
||||
Self.storageServiceManager.recordPendingLocalAccountUpdates()
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private func didTapClearHistory() {
|
||||
OWSActionSheets.showConfirmationAlert(
|
||||
title: OWSLocalizedString(
|
||||
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION",
|
||||
comment: "Alert message before user confirms clearing history"
|
||||
),
|
||||
proceedTitle: OWSLocalizedString(
|
||||
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_BUTTON",
|
||||
comment: "Confirmation text for button which deletes all message, calling, attachments, etc."
|
||||
),
|
||||
proceedStyle: .destructive,
|
||||
proceedAction: { [weak self] _ in self?.clearHistory() }
|
||||
)
|
||||
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
|
||||
fromViewController: self
|
||||
) { _, threadSoftDeleteManager in
|
||||
OWSActionSheets.showConfirmationAlert(
|
||||
title: OWSLocalizedString(
|
||||
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION",
|
||||
comment: "Alert message before user confirms clearing history"
|
||||
),
|
||||
proceedTitle: OWSLocalizedString(
|
||||
"SETTINGS_DELETE_HISTORYLOG_CONFIRMATION_BUTTON",
|
||||
comment: "Confirmation text for button which deletes all message, calling, attachments, etc."
|
||||
),
|
||||
proceedStyle: .destructive,
|
||||
proceedAction: { [weak self] _ in
|
||||
self?.clearHistoryBehindSpinner(threadSoftDeleteManager: threadSoftDeleteManager)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func clearHistory() {
|
||||
private func clearHistoryBehindSpinner(
|
||||
threadSoftDeleteManager: any ThreadSoftDeleteManager
|
||||
) {
|
||||
ModalActivityIndicatorViewController.present(
|
||||
fromViewController: self,
|
||||
canCancel: false,
|
||||
presentationDelay: 0.5,
|
||||
backgroundBlockQueueQos: .userInitiated,
|
||||
backgroundBlock: { modal in
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
ThreadUtil.deleteAllContentWithSneakyTransaction()
|
||||
DispatchQueue.main.async {
|
||||
modal.dismiss()
|
||||
}
|
||||
self.databaseStorage.write { tx in
|
||||
self.clearHistoryWithSneakyTransaction(
|
||||
threadSoftDeleteManager: threadSoftDeleteManager
|
||||
)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
modal.dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func clearHistoryWithSneakyTransaction(
|
||||
threadSoftDeleteManager: any ThreadSoftDeleteManager
|
||||
) {
|
||||
Logger.info("")
|
||||
|
||||
databaseStorage.write { transaction in
|
||||
threadSoftDeleteManager.softDelete(
|
||||
threads: TSThread.anyFetchAll(transaction: transaction),
|
||||
sendDeleteForMeSyncMessage: true,
|
||||
tx: transaction.asV2Write
|
||||
)
|
||||
|
||||
StoryMessage.anyRemoveAllWithInstantiation(transaction: transaction)
|
||||
TSAttachment.anyRemoveAllWithInstantiation(transaction: transaction)
|
||||
|
||||
// Deleting attachments above should be enough to remove any gallery items, but
|
||||
// we redunantly clean up *all* gallery items to be safe.
|
||||
MediaGalleryRecordManager.didRemoveAllContent(transaction: transaction)
|
||||
}
|
||||
|
||||
TSAttachmentStream.deleteAttachmentsFromDisk()
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,6 +172,13 @@ class DebugUIMisc: NSObject, DebugUIPage, Dependencies {
|
||||
let flipCamTooltipManager = FlipCameraTooltipManager(db: DependenciesBridge.shared.db)
|
||||
flipCamTooltipManager.markTooltipAsUnread()
|
||||
}),
|
||||
|
||||
OWSTableItem(title: "Enable DeleteForMeSyncMessage info sheet", actionBlock: {
|
||||
SDSDatabaseStorage.shared.write { tx in
|
||||
DeleteForMeInfoSheetCoordinator.fromGlobals()
|
||||
.forceEnableInfoSheet(tx: tx.asV2Write)
|
||||
}
|
||||
})
|
||||
]
|
||||
return OWSTableSection(title: name, items: items)
|
||||
}
|
||||
|
||||
@ -278,6 +278,16 @@ extension ChatListViewController {
|
||||
return
|
||||
}
|
||||
|
||||
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
|
||||
fromViewController: self
|
||||
) { [weak self] _, threadSoftDeleteManager in
|
||||
self?.showDeleteAllActionSheet(
|
||||
threadSoftDeleteManager: threadSoftDeleteManager
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func showDeleteAllActionSheet(threadSoftDeleteManager: any ThreadSoftDeleteManager) {
|
||||
let title: String
|
||||
let message: String
|
||||
let count = tableView.indexPathsForSelectedRows?.count ?? 0
|
||||
@ -306,7 +316,7 @@ extension ChatListViewController {
|
||||
// change as we're deleting them.
|
||||
self.databaseStorage.write { transaction in
|
||||
self.performOnAllSelectedEntries { threadViewModels in
|
||||
DependenciesBridge.shared.threadSoftDeleteManager.softDelete(
|
||||
threadSoftDeleteManager.softDelete(
|
||||
threads: threadViewModels.map { $0.threadRecord },
|
||||
sendDeleteForMeSyncMessage: true,
|
||||
tx: transaction.asV2Write
|
||||
|
||||
@ -85,8 +85,23 @@ extension ThreadSwipeHandler where Self: UIViewController {
|
||||
muteAction.accessibilityLabel = threadViewModel.isMuted ? CommonStrings.unmuteButton : CommonStrings.muteButton
|
||||
|
||||
let deleteAction = UIContextualAction(style: .destructive, title: nil) { [weak self] (_, _, completion) in
|
||||
self?.deleteThreadWithConfirmation(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock)
|
||||
completion(false)
|
||||
guard let self else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
DeleteForMeInfoSheetCoordinator.fromGlobals().coordinateDelete(
|
||||
fromViewController: self
|
||||
) { [weak self] _, threadSoftDeleteManager in
|
||||
guard let self else { return }
|
||||
|
||||
self.deleteThreadWithConfirmation(
|
||||
threadViewModel: threadViewModel,
|
||||
threadSoftDeleteManager: threadSoftDeleteManager,
|
||||
closeConversationBlock: closeConversationBlock
|
||||
)
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
deleteAction.backgroundColor = .ows_accentRed
|
||||
deleteAction.image = actionImage(name: "trash-fill", title: CommonStrings.deleteButton)
|
||||
@ -140,36 +155,40 @@ extension ThreadSwipeHandler where Self: UIViewController {
|
||||
updateUIAfterSwipeAction()
|
||||
}
|
||||
|
||||
fileprivate func deleteThreadWithConfirmation(threadViewModel: ThreadViewModel, closeConversationBlock: (() -> Void)?) {
|
||||
fileprivate func deleteThreadWithConfirmation(
|
||||
threadViewModel: ThreadViewModel,
|
||||
threadSoftDeleteManager: any ThreadSoftDeleteManager,
|
||||
closeConversationBlock: (() -> Void)?
|
||||
) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let alert = ActionSheetController(title: OWSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE",
|
||||
comment: "Title for the 'conversation delete confirmation' alert."),
|
||||
message: OWSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE",
|
||||
comment: "Message for the 'conversation delete confirmation' alert."))
|
||||
alert.addAction(ActionSheetAction(title: CommonStrings.deleteButton,
|
||||
style: .destructive) { [weak self] _ in
|
||||
self?.deleteThread(threadViewModel: threadViewModel, closeConversationBlock: closeConversationBlock)
|
||||
alert.addAction(ActionSheetAction(
|
||||
title: CommonStrings.deleteButton,
|
||||
style: .destructive
|
||||
) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
closeConversationBlock?()
|
||||
|
||||
databaseStorage.write { transaction in
|
||||
threadSoftDeleteManager.softDelete(
|
||||
threads: [threadViewModel.threadRecord],
|
||||
sendDeleteForMeSyncMessage: true,
|
||||
tx: transaction.asV2Write
|
||||
)
|
||||
}
|
||||
|
||||
updateUIAfterSwipeAction()
|
||||
})
|
||||
alert.addAction(OWSActionSheets.cancelAction)
|
||||
|
||||
presentActionSheet(alert)
|
||||
}
|
||||
|
||||
func deleteThread(threadViewModel: ThreadViewModel, closeConversationBlock: (() -> Void)?) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
closeConversationBlock?()
|
||||
databaseStorage.write { transaction in
|
||||
DependenciesBridge.shared.threadSoftDeleteManager.softDelete(
|
||||
threads: [threadViewModel.threadRecord],
|
||||
sendDeleteForMeSyncMessage: true,
|
||||
tx: transaction.asV2Write
|
||||
)
|
||||
}
|
||||
updateUIAfterSwipeAction()
|
||||
}
|
||||
|
||||
func markThreadAsRead(threadViewModel: ThreadViewModel) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
|
||||
@ -300,6 +300,8 @@ class MediaGallery: Dependencies {
|
||||
typealias Update = Sections.Update
|
||||
typealias Journal = [JournalingOrderedDictionaryChange<Sections.ItemChange>]
|
||||
|
||||
private let thread: TSThread
|
||||
|
||||
// Used for filtering.
|
||||
private(set) var mediaFilter: AllMediaFilter
|
||||
private let mediaCategory: AllMediaCategory
|
||||
@ -324,6 +326,7 @@ class MediaGallery: Dependencies {
|
||||
}
|
||||
|
||||
init(thread: TSThread, mediaCategory: AllMediaCategory, spoilerState: SpoilerRenderState) {
|
||||
self.thread = thread
|
||||
mediaFilter = AllMediaFilter.defaultMediaType(for: mediaCategory)
|
||||
let finder = MediaGalleryResourceFinder(thread: thread, filter: mediaFilter)
|
||||
self.mediaGalleryFinder = finder
|
||||
@ -837,26 +840,22 @@ class MediaGallery: Dependencies {
|
||||
return false
|
||||
}()
|
||||
if shouldDeleteMessage {
|
||||
// Refresh attachment list on the model, so deletion doesn't try to remove
|
||||
// them again. Also, this ensures we've fetched the latest message details
|
||||
// within this transaction.
|
||||
// Refresh attachment list on the message, so deletion doesn't try to remove
|
||||
// them again. We want to ensure we have the latest models within this transaction.
|
||||
message.anyReload(transaction: tx)
|
||||
self.thread.anyReload(transaction: tx)
|
||||
|
||||
if let thread = message.thread(tx: tx) {
|
||||
DependenciesBridge.shared.interactionDeleteManager.delete(
|
||||
message,
|
||||
sideEffects: .custom(
|
||||
deleteForMeSyncMessage: .sendSyncMessage(interactionsThread: thread)
|
||||
),
|
||||
tx: tx.asV2Write
|
||||
)
|
||||
} else {
|
||||
DependenciesBridge.shared.interactionDeleteManager.delete(
|
||||
message,
|
||||
sideEffects: .default(),
|
||||
tx: tx.asV2Write
|
||||
)
|
||||
}
|
||||
// Since we don't know until we're deep in the write
|
||||
// transaction whether we'll actually end up deleting an
|
||||
// interaction, we'll skip showing the one-time "delete
|
||||
// sync info sheet".
|
||||
DependenciesBridge.shared.interactionDeleteManager.delete(
|
||||
message,
|
||||
sideEffects: .custom(
|
||||
deleteForMeSyncMessage: .sendSyncMessage(interactionsThread: self.thread)
|
||||
),
|
||||
tx: tx.asV2Write
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
@ -1786,6 +1786,27 @@
|
||||
/* Toast message confirming the system contact was deleted. Embeds {{The name of the user who was deleted.}}. */
|
||||
"DELETE_CONTACT_CONFIRMATION_TOAST" = "%@ was deleted from your contacts and removed.";
|
||||
|
||||
/* Header text for an action sheet confirming deleting a message in Note to Self. */
|
||||
"DELETE_FOR_ME_NOTE_TO_SELF_ACTION_SHEET_HEADER" = "Delete message?";
|
||||
|
||||
/* Title for an action sheet button explaining that a message will be deleted. */
|
||||
"DELETE_FOR_ME_NOTE_TO_SELF_LINKED_DEVICES_NOT_PRESENT_ACTION_SHEET_BUTTON_TITLE" = "Delete";
|
||||
|
||||
/* Title for an action sheet button explaining that a message will be deleted. */
|
||||
"DELETE_FOR_ME_NOTE_TO_SELF_LINKED_DEVICES_PRESENT_ACTION_SHEET_BUTTON_TITLE" = "Delete for Me";
|
||||
|
||||
/* 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. */
|
||||
"DELETE_FOR_ME_NOTE_TO_SELF_LINKED_DEVICES_PRESENT_ACTION_SHEET_SUBHEADER" = "This message will be deleted from all your devices.";
|
||||
|
||||
/* Label for a button in an info sheet confirming that deletes are now synced across devices. */
|
||||
"DELETE_FOR_ME_SYNC_MESSAGE_INFO_SHEET_BUTTON" = "Got it";
|
||||
|
||||
/* Subtitle for an info sheet explaining that deletes are now synced across devices. */
|
||||
"DELETE_FOR_ME_SYNC_MESSAGE_INFO_SHEET_SUBTITLE" = "When you delete messages or chats, they will be deleted from your phone and all linked devices.";
|
||||
|
||||
/* Title for an info sheet explaining that deletes are now synced across devices. */
|
||||
"DELETE_FOR_ME_SYNC_MESSAGE_INFO_SHEET_TITLE" = "Deleting is now synced across all of your devices";
|
||||
|
||||
/* Label indicating a user who deleted their account. */
|
||||
"DELETED_USER" = "Deleted Account";
|
||||
|
||||
|
||||
@ -7,6 +7,19 @@ import Intents
|
||||
import LibSignalClient
|
||||
import SignalCoreKit
|
||||
|
||||
/// Responsible for "soft-deleting" threads, or removing their contents without
|
||||
/// removing the `TSThread` record itself. The app's architecture is to never\*
|
||||
/// delete the thread itself, but instead to delete all data associated with the
|
||||
/// thread, in case the thread is needed again later on.
|
||||
///
|
||||
/// \*Threads can be hard-deleted, but only in niche scenarios.
|
||||
///
|
||||
/// - SeeAlso ``ThreadRemover``.
|
||||
///
|
||||
/// - SeeAlso
|
||||
/// If you're calling this type for a user-initiated deletion, consider using
|
||||
/// ``DeleteForMeInfoSheetCoordinator`` in the Signal target instead, which
|
||||
/// handles some one-time informational UX.
|
||||
public protocol ThreadSoftDeleteManager {
|
||||
func softDelete(
|
||||
threads: [TSThread],
|
||||
@ -168,7 +181,7 @@ final class ThreadSoftDeleteManagerImpl: ThreadSoftDeleteManager {
|
||||
)
|
||||
|
||||
let callDeleteBehavior: InteractionDelete.SideEffects.AssociatedCallDeleteBehavior = {
|
||||
if deleteForMeOutgoingSyncMessageManager.isSendingEnabled() {
|
||||
if DeleteForMeSyncMessage.isSendingEnabled {
|
||||
/// If we're able to send a `DeleteForMe` sync
|
||||
/// message, we don't need to send `CallEvent`s...
|
||||
return .localDeleteOnly
|
||||
|
||||
@ -118,6 +118,10 @@ public extension OWSDevice {
|
||||
var isPrimaryDevice: Bool {
|
||||
deviceId == Self.primaryDeviceId
|
||||
}
|
||||
|
||||
var isLinkedDevice: Bool {
|
||||
!isPrimaryDevice
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Replace all
|
||||
|
||||
@ -7,6 +7,12 @@ public protocol OWSDeviceStore {
|
||||
func fetchAll(tx: DBReadTransaction) -> [OWSDevice]
|
||||
}
|
||||
|
||||
public extension OWSDeviceStore {
|
||||
func hasLinkedDevices(tx: DBReadTransaction) -> Bool {
|
||||
return fetchAll(tx: tx).contains { $0.isLinkedDevice }
|
||||
}
|
||||
}
|
||||
|
||||
class OWSDeviceStoreImpl: OWSDeviceStore {
|
||||
func fetchAll(tx: DBReadTransaction) -> [OWSDevice] {
|
||||
return OWSDevice.anyFetchAll(transaction: SDSDB.shimOnlyBridge(tx))
|
||||
|
||||
@ -58,9 +58,9 @@ public protocol DeleteForMeOutgoingSyncMessageManager {
|
||||
) -> Outgoing.ThreadDeletionContext?
|
||||
}
|
||||
|
||||
extension DeleteForMeOutgoingSyncMessageManager {
|
||||
extension DeleteForMeSyncMessage {
|
||||
/// Is sending a `DeleteForMe` sync message enabled at all?
|
||||
func isSendingEnabled() -> Bool {
|
||||
public static var isSendingEnabled: Bool {
|
||||
// [DeleteForMe] TODO: We can remove this 90d after release.
|
||||
return FeatureFlags.shouldSendDeleteForMeSyncMessages
|
||||
|| RemoteConfig.shouldSendDeleteForMeSyncMessages
|
||||
@ -231,7 +231,7 @@ final class DeleteForMeOutgoingSyncMessageManagerImpl: DeleteForMeOutgoingSyncMe
|
||||
contents: DeleteForMeOutgoingSyncMessage.Contents,
|
||||
tx: any DBWriteTransaction
|
||||
) {
|
||||
guard isSendingEnabled() else {
|
||||
guard DeleteForMeSyncMessage.isSendingEnabled else {
|
||||
logger.warn("Skipping delete-for-me sync message, feature not enabled!")
|
||||
return
|
||||
}
|
||||
|
||||
@ -86,6 +86,11 @@ public enum InteractionDelete {
|
||||
/// delete call records alongside their associated interactions. This may seem
|
||||
/// counterintuitive, but avoids a circular dependency between interaction and
|
||||
/// call record deletion.
|
||||
///
|
||||
/// - SeeAlso
|
||||
/// If you're calling this type for a user-initiated deletion, consider using
|
||||
/// ``DeleteForMeInfoSheetCoordinator`` in the Signal target instead, which
|
||||
/// handles some one-time informational UX.
|
||||
public protocol InteractionDeleteManager {
|
||||
typealias SideEffects = InteractionDelete.SideEffects
|
||||
|
||||
|
||||
@ -252,31 +252,6 @@ extension ThreadUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delete all content
|
||||
|
||||
extension ThreadUtil {
|
||||
public static func deleteAllContentWithSneakyTransaction() {
|
||||
Logger.info("")
|
||||
|
||||
databaseStorage.write { transaction in
|
||||
DependenciesBridge.shared.threadSoftDeleteManager.softDelete(
|
||||
threads: TSThread.anyFetchAll(transaction: transaction),
|
||||
sendDeleteForMeSyncMessage: true,
|
||||
tx: transaction.asV2Write
|
||||
)
|
||||
|
||||
StoryMessage.anyRemoveAllWithInstantiation(transaction: transaction)
|
||||
TSAttachment.anyRemoveAllWithInstantiation(transaction: transaction)
|
||||
|
||||
// Deleting attachments above should be enough to remove any gallery items, but
|
||||
// we redunantly clean up *all* gallery items to be safe.
|
||||
MediaGalleryRecordManager.didRemoveAllContent(transaction: transaction)
|
||||
}
|
||||
|
||||
TSAttachmentStream.deleteAttachmentsFromDisk()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sharing Suggestions
|
||||
|
||||
import Intents
|
||||
|
||||
@ -39,6 +39,14 @@ public extension UIView {
|
||||
return autoPinEdge(toSuperviewMargin: .trailing, withInset: inset)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func autoPinWidthToSuperviewMargins(withInset inset: CGFloat) -> [NSLayoutConstraint] {
|
||||
return [
|
||||
autoPinEdge(toSuperviewMargin: .leading, withInset: inset),
|
||||
autoPinEdge(toSuperviewMargin: .trailing, withInset: inset)
|
||||
]
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func autoPinWidthToSuperviewMargins(relation: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] {
|
||||
// We invert the relation because of the weird grammar switch when talking about
|
||||
|
||||
@ -9,6 +9,9 @@ import SignalServiceKit
|
||||
// A modal view that be used during blocking interactions (e.g. waiting on response from
|
||||
// service or on the completion of a long-running local operation).
|
||||
public class ModalActivityIndicatorViewController: OWSViewController {
|
||||
public enum Constants {
|
||||
public static let defaultPresentationDelay: TimeInterval = 0.05
|
||||
}
|
||||
|
||||
let canCancel: Bool
|
||||
|
||||
@ -27,7 +30,6 @@ public class ModalActivityIndicatorViewController: OWSViewController {
|
||||
|
||||
var wasDimissed: Bool = false
|
||||
|
||||
private static let kPresentationDelayDefault: TimeInterval = 0.05
|
||||
private let presentationDelay: TimeInterval
|
||||
|
||||
// MARK: Initializers
|
||||
@ -47,21 +49,8 @@ public class ModalActivityIndicatorViewController: OWSViewController {
|
||||
public class func present(
|
||||
fromViewController: UIViewController,
|
||||
canCancel: Bool,
|
||||
backgroundBlock: @escaping (ModalActivityIndicatorViewController) -> Void
|
||||
) {
|
||||
present(
|
||||
fromViewController: fromViewController,
|
||||
canCancel: canCancel,
|
||||
presentationDelay: kPresentationDelayDefault,
|
||||
isInvisible: false,
|
||||
backgroundBlock: backgroundBlock
|
||||
)
|
||||
}
|
||||
|
||||
public class func present(
|
||||
fromViewController: UIViewController,
|
||||
canCancel: Bool,
|
||||
presentationDelay: TimeInterval,
|
||||
presentationDelay: TimeInterval = Constants.defaultPresentationDelay,
|
||||
backgroundBlockQueueQos: DispatchQoS = .default,
|
||||
backgroundBlock: @escaping (ModalActivityIndicatorViewController) -> Void
|
||||
) {
|
||||
present(
|
||||
@ -69,6 +58,7 @@ public class ModalActivityIndicatorViewController: OWSViewController {
|
||||
canCancel: canCancel,
|
||||
presentationDelay: presentationDelay,
|
||||
isInvisible: false,
|
||||
backgroundBlockQueueQos: backgroundBlockQueueQos,
|
||||
backgroundBlock: backgroundBlock
|
||||
)
|
||||
}
|
||||
@ -80,17 +70,19 @@ public class ModalActivityIndicatorViewController: OWSViewController {
|
||||
present(
|
||||
fromViewController: fromViewController,
|
||||
canCancel: false,
|
||||
presentationDelay: kPresentationDelayDefault,
|
||||
presentationDelay: Constants.defaultPresentationDelay,
|
||||
isInvisible: true,
|
||||
backgroundBlockQueueQos: .default,
|
||||
backgroundBlock: backgroundBlock
|
||||
)
|
||||
}
|
||||
|
||||
public class func present(
|
||||
private class func present(
|
||||
fromViewController: UIViewController,
|
||||
canCancel: Bool,
|
||||
presentationDelay: TimeInterval,
|
||||
isInvisible: Bool,
|
||||
backgroundBlockQueueQos: DispatchQoS,
|
||||
backgroundBlock: @escaping (ModalActivityIndicatorViewController) -> Void
|
||||
) {
|
||||
AssertIsOnMainThread()
|
||||
@ -103,7 +95,7 @@ public class ModalActivityIndicatorViewController: OWSViewController {
|
||||
// Present this modal _over_ the current view contents.
|
||||
view.modalPresentationStyle = .overFullScreen
|
||||
fromViewController.present(view, animated: false) {
|
||||
DispatchQueue.global().async {
|
||||
DispatchQueue.global(qos: backgroundBlockQueueQos.qosClass).async {
|
||||
backgroundBlock(view)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user