Add context menu to group replies

This commit is contained in:
Nora Trapp 2022-03-29 15:31:02 -07:00
parent b1dc925e48
commit 03bdb55e21
14 changed files with 217 additions and 108 deletions

View File

@ -941,6 +941,7 @@
88C4E38024635337009C9B97 /* DeviceTransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C4E37F24635337009C9B97 /* DeviceTransferService.swift */; };
88C659B024688335002AC115 /* SelfSignedIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C659AF24688335002AC115 /* SelfSignedIdentity.swift */; };
88C7597324B7EAA600DB03EA /* AdvancedPinSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7597224B7EAA600DB03EA /* AdvancedPinSettingsTableViewController.swift */; };
88C980D427F3AD2C009750C0 /* TSMessage+SignalUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C980D327F3AD2C009750C0 /* TSMessage+SignalUI.swift */; };
88CB462225843758001900F2 /* GroupCallTooltip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CB462125843758001900F2 /* GroupCallTooltip.swift */; };
88D1BCB924F73C05009A1738 /* PhoneNumberSharingSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D1BCB824F73C04009A1738 /* PhoneNumberSharingSettingsTableViewController.swift */; };
88D1BCBB24F73C15009A1738 /* PhoneNumberDiscoverabilitySettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D1BCBA24F73C15009A1738 /* PhoneNumberDiscoverabilitySettingsTableViewController.swift */; };
@ -2247,6 +2248,7 @@
88C4E37F24635337009C9B97 /* DeviceTransferService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceTransferService.swift; sourceTree = "<group>"; };
88C659AF24688335002AC115 /* SelfSignedIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSignedIdentity.swift; sourceTree = "<group>"; };
88C7597224B7EAA600DB03EA /* AdvancedPinSettingsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPinSettingsTableViewController.swift; sourceTree = "<group>"; };
88C980D327F3AD2C009750C0 /* TSMessage+SignalUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSMessage+SignalUI.swift"; sourceTree = "<group>"; };
88CB462125843758001900F2 /* GroupCallTooltip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallTooltip.swift; sourceTree = "<group>"; };
88D1BCB824F73C04009A1738 /* PhoneNumberSharingSettingsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberSharingSettingsTableViewController.swift; sourceTree = "<group>"; };
88D1BCBA24F73C15009A1738 /* PhoneNumberDiscoverabilitySettingsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberDiscoverabilitySettingsTableViewController.swift; sourceTree = "<group>"; };
@ -3189,6 +3191,7 @@
342FFE63271DB66E000AC89F /* OWSProfileManager+SignalUI.h */,
342FFE64271DB66E000AC89F /* OWSProfileManager+SignalUI.m */,
342FFE56271DA8C9000AC89F /* OWSSounds+SignalUI.swift */,
88C980D327F3AD2C009750C0 /* TSMessage+SignalUI.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -5827,6 +5830,7 @@
34A955AE271B533000B05242 /* FullTextSearcher.swift in Sources */,
3402AA58271D9DCD0084CBAE /* OWSNavigationController.m in Sources */,
3402A9FF271D9D7B0084CBAE /* OWSAnyTouchGestureRecognizer.m in Sources */,
88C980D427F3AD2C009750C0 /* TSMessage+SignalUI.swift in Sources */,
342FFE66271DB66E000AC89F /* OWSProfileManager+SignalUI.m in Sources */,
3402AAB2271D9E180084CBAE /* ContactTableViewCell.swift in Sources */,
3402AA74271D9E180084CBAE /* OWSBubbleShapeView.swift in Sources */,

View File

@ -17,11 +17,12 @@ public class ContextMenuActionsAccessory: ContextMenuTargetedPreviewAccessory, C
public init(
menu: ContextMenu,
accessoryAlignment: AccessoryAlignment
accessoryAlignment: AccessoryAlignment,
forceDarkTheme: Bool = false
) {
self.menu = menu
menuView = ContextMenuActionsView(menu: menu)
menuView = ContextMenuActionsView(menu: menu, forceDarkTheme: forceDarkTheme)
menuView.isHidden = true
super.init(accessoryView: menuView, accessoryAlignment: accessoryAlignment)
menuView.delegate = self
@ -134,6 +135,7 @@ public class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScro
private class ContextMenuActionRow: UIView {
let attributes: ContextMenuAction.Attributes
let hostEffect: UIBlurEffect
let forceDarkTheme: Bool
let titleLabel: UILabel
let iconView: UIImageView
let separatorView: UIVisualEffectView
@ -147,8 +149,8 @@ public class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScro
vibrancyView.frame = bounds
let view = UIView(frame: bounds)
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray80 : .ows_gray12
if Theme.isDarkThemeEnabled {
view.backgroundColor = forceDarkTheme || Theme.isDarkThemeEnabled ? .ows_gray80 : .ows_gray12
if forceDarkTheme || Theme.isDarkThemeEnabled {
vibrancyView.backgroundColor = UIColor.ows_whiteAlpha20
}
view.alpha = 0.3
@ -175,7 +177,8 @@ public class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScro
title: String,
icon: UIImage?,
attributes: ContextMenuAction.Attributes,
hostBlurEffect: UIBlurEffect
hostBlurEffect: UIBlurEffect,
forceDarkTheme: Bool
) {
titleLabel = UILabel(frame: CGRect.zero)
titleLabel.text = title
@ -184,13 +187,14 @@ public class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScro
self.attributes = attributes
hostEffect = hostBlurEffect
self.forceDarkTheme = forceDarkTheme
if attributes.contains(.destructive) {
titleLabel.textColor = Theme.ActionSheet.default.destructiveButtonTextColor
} else if attributes.contains(.disabled) {
titleLabel.textColor = Theme.secondaryTextAndIconColor
titleLabel.textColor = forceDarkTheme ? Theme.darkThemeSecondaryTextAndIconColor : Theme.secondaryTextAndIconColor
} else {
titleLabel.textColor = Theme.primaryTextColor
titleLabel.textColor = forceDarkTheme ? Theme.darkThemePrimaryColor : Theme.primaryTextColor
}
iconView = UIImageView(image: icon)
@ -198,12 +202,12 @@ public class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScro
iconView.tintColor = titleLabel.textColor
separatorView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: hostBlurEffect))
if Theme.isDarkThemeEnabled {
if forceDarkTheme || Theme.isDarkThemeEnabled {
separatorView.backgroundColor = UIColor.ows_whiteAlpha20
}
let separator = UIView(frame: separatorView.bounds)
separator.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray75 : .ows_gray22
separator.backgroundColor = forceDarkTheme || Theme.isDarkThemeEnabled ? .ows_gray75 : .ows_gray22
separator.autoresizingMask = [.flexibleWidth, .flexibleHeight]
separatorView.contentView.addSubview(separator)
isHighlighted = false
@ -263,6 +267,7 @@ public class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScro
weak var delegate: ContextMenuActionsViewDelegate?
public let menu: ContextMenu
public let forceDarkTheme: Bool
private let actionViews: [ContextMenuActionRow]
private let scrollView: UIScrollView
@ -284,9 +289,11 @@ public class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScro
let cornerRadius: CGFloat = 12
public init(
menu: ContextMenu
menu: ContextMenu,
forceDarkTheme: Bool = false
) {
self.menu = menu
self.forceDarkTheme = forceDarkTheme
scrollView = UIScrollView(frame: CGRect.zero)
let effect = UIBlurEffect(style: UIBlurEffect.Style.prominent)
@ -294,7 +301,7 @@ public class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScro
var actionViews: [ContextMenuActionRow] = []
for action in menu.children {
let actionView = ContextMenuActionRow(title: action.title, icon: action.image, attributes: action.attributes, hostBlurEffect: effect)
let actionView = ContextMenuActionRow(title: action.title, icon: action.image, attributes: action.attributes, hostBlurEffect: effect, forceDarkTheme: forceDarkTheme)
actionViews.append(actionView)
}

View File

@ -160,6 +160,7 @@ public class ContextMenuTargetedPreview {
public let snapshot: UIView?
public var auxiliarySnapshot: UIView?
public let alignment: Alignment
public var alignmentOffset: CGPoint?
public let accessoryViews: [ContextMenuTargetedPreviewAccessory]
/// Default targeted preview initializer
@ -196,9 +197,11 @@ public typealias ContextMenuActionProvider = ([ContextMenuAction]) -> ContextMen
public class ContextMenuConfiguration {
public let identifier: NSCopying
public let actionProvider: ContextMenuActionProvider?
public let forceDarkTheme: Bool
public init (
identifier: NSCopying?,
forceDarkTheme: Bool = false,
actionProvider: ContextMenuActionProvider?
) {
if let ident = identifier {
@ -207,6 +210,7 @@ public class ContextMenuConfiguration {
self.identifier = UUID() as NSCopying
}
self.forceDarkTheme = forceDarkTheme
self.actionProvider = actionProvider
}
}

View File

@ -386,6 +386,7 @@ class ContextMenuController: UIViewController, ContextMenuViewDelegate, UIGestur
self.menuAccessory = menuAccessory
self.presentImmediately = presentImmediately
super.init(nibName: nil, bundle: nil)
if #available(iOS 13, *), configuration.forceDarkTheme { overrideUserInterfaceStyle = .dark }
}
required init?(coder: NSCoder) {
@ -448,7 +449,7 @@ class ContextMenuController: UIViewController, ContextMenuViewDelegate, UIGestur
UIView.animate(withDuration: animationDuration / 2.0) {
if !UIDevice.current.isIPad {
self.blurView.effect = UIBlurEffect(style: UIBlurEffect.Style.regular)
self.blurView.backgroundColor = Theme.isDarkThemeEnabled ? UIColor.ows_whiteAlpha20 : UIColor.ows_blackAlpha20
self.blurView.backgroundColor = self.contextMenuConfiguration.forceDarkTheme || Theme.isDarkThemeEnabled ? UIColor.ows_whiteAlpha20 : UIColor.ows_blackAlpha20
} else {
self.blurView.backgroundColor = UIColor.ows_blackAlpha40
}

View File

@ -155,7 +155,7 @@ public class ContextMenuInteraction: NSObject, UIInteraction {
public func presentMenu(window: UIWindow, contextMenuConfiguration: ContextMenuConfiguration, targetedPreview: ContextMenuTargetedPreview, presentImmediately: Bool) {
let menuAccessory = menuAccessory(configuration: contextMenuConfiguration, previewAlignment: targetedPreview.alignment)
let menuAccessory = menuAccessory(configuration: contextMenuConfiguration, targetedPreview: targetedPreview)
let contextMenuController = ContextMenuController(configuration: contextMenuConfiguration, preview: targetedPreview, initiatingGestureRecognizer: initiatingGestureRecognizer(), menuAccessory: menuAccessory, presentImmediately: presentImmediately)
contextMenuController.delegate = self
self.contextMenuController = contextMenuController
@ -171,11 +171,11 @@ public class ContextMenuInteraction: NSObject, UIInteraction {
return longPressGestureRecognizer
}
public func menuAccessory(configuration: ContextMenuConfiguration, previewAlignment: ContextMenuTargetedPreview.Alignment) -> ContextMenuActionsAccessory {
public func menuAccessory(configuration: ContextMenuConfiguration, targetedPreview: ContextMenuTargetedPreview) -> ContextMenuActionsAccessory {
var alignments: [(ContextMenuTargetedPreviewAccessory.AccessoryAlignment.Edge, ContextMenuTargetedPreviewAccessory.AccessoryAlignment.Origin)] = [(.bottom, .exterior)]
switch previewAlignment {
switch targetedPreview.alignment {
case .left:
alignments.append((CurrentAppContext().isRTL ? .trailing : .leading, .interior))
case .right:
@ -185,8 +185,8 @@ public class ContextMenuInteraction: NSObject, UIInteraction {
}
let menu = configuration.actionProvider?([]) ?? ContextMenu([])
let alignment = ContextMenuTargetedPreviewAccessory.AccessoryAlignment(alignments: alignments, alignmentOffset: CGPoint(x: 0, y: 12))
let accessory = ContextMenuActionsAccessory(menu: menu, accessoryAlignment: alignment)
let alignment = ContextMenuTargetedPreviewAccessory.AccessoryAlignment(alignments: alignments, alignmentOffset: targetedPreview.alignmentOffset ?? CGPoint(x: 0, y: 12))
let accessory = ContextMenuActionsAccessory(menu: menu, accessoryAlignment: alignment, forceDarkTheme: configuration.forceDarkTheme)
accessory.delegate = self
return accessory
}
@ -327,7 +327,7 @@ public class ChatHistoryContextMenuInteraction: ContextMenuInteraction {
return chatHistoryLongPressGesture
}
public override func menuAccessory(configuration: ContextMenuConfiguration, previewAlignment: ContextMenuTargetedPreview.Alignment) -> ContextMenuActionsAccessory {
public override func menuAccessory(configuration: ContextMenuConfiguration, targetedPreview: ContextMenuTargetedPreview) -> ContextMenuActionsAccessory {
let isRTL = CurrentAppContext().isRTL
let menu = configuration.actionProvider?([]) ?? ContextMenu([])
let isIncomingMessage = itemViewModel.interaction.interactionType == .incomingMessage

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
//
import Foundation
@ -227,13 +227,6 @@ extension CVItemViewModelImpl {
return !hasUnloadedAttachments
}
}
func deleteAction() {
let interaction = self.interaction
databaseStorage.asyncWrite { transaction in
interaction.anyRemove(transaction: transaction)
}
}
}
// MARK: -

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
//
extension ConversationViewController: MessageActionsDelegate {
@ -95,49 +95,7 @@ extension ConversationViewController: MessageActionsDelegate {
}
func messageActionsDeleteItem(_ itemViewModel: CVItemViewModelImpl) {
let actionSheetController = ActionSheetController(message: NSLocalizedString(
"MESSAGE_ACTION_DELETE_FOR_TITLE",
comment: "The title for the action sheet asking who the user wants to delete the message for."
))
let deleteForMeAction = ActionSheetAction(
title: CommonStrings.deleteForMeButton,
style: .destructive
) { _ in
itemViewModel.deleteAction()
}
actionSheetController.addAction(deleteForMeAction)
if canBeRemotelyDeleted(item: itemViewModel),
let message = itemViewModel.interaction as? TSOutgoingMessage {
let deleteForEveryoneAction = ActionSheetAction(
title: NSLocalizedString(
"MESSAGE_ACTION_DELETE_FOR_EVERYONE",
comment: "The title for the action that deletes a message for all users in the conversation."
),
style: .destructive
) { [weak self] _ in
self?.showDeleteForEveryoneConfirmationIfNecessary {
guard let self = self else { return }
let deleteMessage = TSOutgoingDeleteMessage(thread: self.thread, message: message)
self.databaseStorage.write { transaction in
// Reset the sending states, so we can render the sending state of the deleted message.
// TSOutgoingDeleteMessage will automatically pass through it's send state to the message
// record that it is deleting.
message.updateWith(recipientAddressStates: deleteMessage.recipientAddressStates, transaction: transaction)
message.updateWithRemotelyDeletedAndRemoveRenderableContent(with: transaction)
Self.messageSenderJobQueue.add(message: deleteMessage.asPreparer, transaction: transaction)
}
}
}
actionSheetController.addAction(deleteForEveryoneAction)
}
actionSheetController.addAction(OWSActionSheets.cancelAction)
presentActionSheet(actionSheetController)
guard let message = itemViewModel.interaction as? TSMessage else { return }
message.presentDeletionActionSheet(from: self)
}
}

View File

@ -236,42 +236,6 @@ extension ConversationViewController: ForwardMessageDelegate {
}
}
// MARK: -
extension ConversationViewController {
// A message can be remotely deleted iff:
// * the feature flag is enabled
// * you sent this message
// * you haven't already remotely deleted this message
// * it has been less than 3 hours since you sent the message
func canBeRemotelyDeleted(item: CVItemViewModel) -> Bool {
guard let outgoingMessage = item.interaction as? TSOutgoingMessage else { return false }
guard !outgoingMessage.wasRemotelyDeleted else { return false }
guard Date.ows_millisecondTimestamp() - outgoingMessage.timestamp <= (kHourInMs * 3) else { return false }
return true
}
func showDeleteForEveryoneConfirmationIfNecessary(completion: @escaping () -> Void) {
guard !Self.preferences.wasDeleteForEveryoneConfirmationShown() else { return completion() }
OWSActionSheets.showConfirmationAlert(
title: NSLocalizedString(
"MESSAGE_ACTION_DELETE_FOR_EVERYONE_CONFIRMATION",
comment: "A one-time confirmation that you want to delete for everyone"
),
proceedTitle: NSLocalizedString(
"MESSAGE_ACTION_DELETE_FOR_EVERYONE",
comment: "The title for the action that deletes a message for all users in the conversation."
),
proceedStyle: .destructive) { _ in
Self.preferences.setWasDeleteForEveryoneConfirmationShown()
completion()
}
}
}
// MARK: - MessageActionsToolbarDelegate
extension ConversationViewController: MessageActionsToolbarDelegate {

View File

@ -12,6 +12,7 @@ class StoryGroupReplySheet: InteractiveSheetViewController {
private lazy var tableView = UITableView()
private lazy var inputToolbar = StoryReplyInputToolbar()
private lazy var inputToolbarBottomConstraint = inputToolbar.autoPinEdge(toSuperviewEdge: .bottom)
private lazy var contextMenu = ContextMenuInteraction(delegate: self)
private lazy var inputAccessoryPlaceholder: InputAccessoryViewPlaceholder = {
let placeholder = InputAccessoryViewPlaceholder()
@ -54,6 +55,7 @@ class StoryGroupReplySheet: InteractiveSheetViewController {
tableView.rowHeight = UITableView.automaticDimension
tableView.keyboardDismissMode = .interactive
tableView.contentInset = UIEdgeInsets(top: 30, left: 0, bottom: 0, right: 0)
tableView.addInteraction(contextMenu)
contentView.addSubview(tableView)
tableView.autoPinEdgesToSuperviewEdges()
@ -358,3 +360,73 @@ extension StoryGroupReplySheet: MessageReactionPickerDelegate {
}
}
}
extension StoryGroupReplySheet: ContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: ContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> ContextMenuConfiguration? {
guard let indexPath = tableView.indexPathForRow(at: location),
let item = replyLoader?.replyItem(for: indexPath) else { return nil }
return .init(identifier: indexPath as NSCopying, forceDarkTheme: true) { _ in
var actions = [ContextMenuAction]()
actions.append(.init(
title: NSLocalizedString(
"STORIES_PRIVATE_REPLY_ACTION",
comment: "Context menu action to privately reply to the selected story reply"),
image: Theme.iconImage(.messageActionReply, isDarkThemeEnabled: true),
handler: { _ in
OWSActionSheets.showActionSheet(title: LocalizationNotNeeded("Private replies are not yet implemented."))
}))
if item.cellType != .reaction {
actions.append(.init(
title: NSLocalizedString(
"STORIES_COPY_REPLY_ACTION",
comment: "Context menu action to copy the selected story reply"),
image: Theme.iconImage(.messageActionCopy, isDarkThemeEnabled: true),
handler: { _ in
guard let displayableText = item.displayableText else { return }
MentionTextView.copyAttributedStringToPasteboard(displayableText.fullAttributedText)
}))
}
actions.append(.init(
title: NSLocalizedString(
"STORIES_DELETE_REPLY_ACTION",
comment: "Context menu action to delete the selected story reply"),
image: Theme.iconImage(.messageActionDelete, isDarkThemeEnabled: true),
attributes: .destructive,
handler: { [weak self] _ in
guard let self = self else { return }
guard let message = Self.databaseStorage.read(
block: { TSMessage.anyFetchMessage(uniqueId: item.interactionUniqueId, transaction: $0) }
) else { return }
message.presentDeletionActionSheet(from: self)
}))
return .init(actions)
}
}
func contextMenuInteraction(_ interaction: ContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: ContextMenuConfiguration) -> ContextMenuTargetedPreview? {
guard let indexPath = configuration.identifier as? IndexPath else { return nil }
guard let cell = tableView.cellForRow(at: indexPath) else { return nil }
let targetedPreview = ContextMenuTargetedPreview(
view: cell,
alignment: .leading,
accessoryViews: nil
)
targetedPreview.alignmentOffset = CGPoint(x: 52, y: 12)
return targetedPreview
}
func contextMenuInteraction(_ interaction: ContextMenuInteraction, willDisplayMenuForConfiguration: ContextMenuConfiguration) {}
func contextMenuInteraction(_ interaction: ContextMenuInteraction, willEndForConfiguration: ContextMenuConfiguration) {}
func contextMenuInteraction(_ interaction: ContextMenuInteraction, didEndForConfiguration configuration: ContextMenuConfiguration) {}
}

View File

@ -7,6 +7,7 @@ import SignalServiceKit
import UIKit
class StoryGroupReplyViewItem: Dependencies {
let interactionUniqueId: String
let displayableText: DisplayableText?
let reactionEmoji: String?
let wasRemotelyDeleted: Bool
@ -26,6 +27,8 @@ class StoryGroupReplyViewItem: Dependencies {
authorColor: UIColor,
transaction: SDSAnyReadTransaction
) {
self.interactionUniqueId = message.uniqueId
if !message.wasRemotelyDeleted {
self.displayableText = DisplayableText.displayableText(
withMessageBody: .init(text: message.body ?? "", ranges: message.bodyRanges ?? .empty),

View File

@ -6091,6 +6091,12 @@
/* Label for the 'uninstall sticker pack' button. */
"STICKERS_UNINSTALL_BUTTON" = "Uninstall";
/* Context menu action to copy the selected story reply */
"STORIES_COPY_REPLY_ACTION" = "Copy";
/* Context menu action to delete the selected story reply */
"STORIES_DELETE_REPLY_ACTION" = "Delete";
/* Context menu action to forward the selected story */
"STORIES_FORWARD_STORY_ACTION" = "Forward";
@ -6100,6 +6106,9 @@
/* Context menu action to hide the selected story */
"STORIES_HIDE_STORY_ACTION" = "Hide Story";
/* Context menu action to privately reply to the selected story reply */
"STORIES_PRIVATE_REPLY_ACTION" = "Private Reply";
/* Context menu action to share the selected story */
"STORIES_SHARE_STORY_ACTION" = "Share";

View File

@ -134,6 +134,18 @@ public extension TSMessage {
// MARK: - Remote Delete
// A message can be remotely deleted iff:
// * you sent this message
// * you haven't already remotely deleted this message
// * it has been less than 3 hours since you sent the message
var canBeRemotelyDeleted: Bool {
guard let outgoingMessage = self as? TSOutgoingMessage else { return false }
guard !outgoingMessage.wasRemotelyDeleted else { return false }
guard Date.ows_millisecondTimestamp() - outgoingMessage.timestamp <= (kHourInMs * 3) else { return false }
return true
}
@objc(OWSRemoteDeleteProcessingResult)
enum RemoteDeleteProcessingResult: Int, Error {
case deletedMessageMissing

View File

@ -168,7 +168,11 @@ public extension Theme {
@objc
public extension Theme {
class func iconImage(_ icon: ThemeIcon) -> UIImage {
let name = iconName(icon)
iconImage(icon, isDarkThemeEnabled: isDarkThemeEnabled)
}
class func iconImage(_ icon: ThemeIcon, isDarkThemeEnabled: Bool) -> UIImage {
let name = iconName(icon, isDarkThemeEnabled: isDarkThemeEnabled)
guard let image = UIImage(named: name) else {
owsFailDebug("image was unexpectedly nil: \(name)")
return UIImage()
@ -178,6 +182,10 @@ public extension Theme {
}
class func iconName(_ icon: ThemeIcon) -> String {
iconName(icon, isDarkThemeEnabled: isDarkThemeEnabled)
}
class func iconName(_ icon: ThemeIcon, isDarkThemeEnabled: Bool) -> String {
switch icon {
case .settingsUserInContacts:
return isDarkThemeEnabled ? "profile-circle-solid-24" : "profile-circle-outline-24"

View File

@ -0,0 +1,74 @@
//
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
//
import Foundation
import UIKit
public extension TSMessage {
func presentDeletionActionSheet(from fromViewController: UIViewController) {
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."
))
let deleteForMeAction = ActionSheetAction(
title: CommonStrings.deleteForMeButton,
style: .destructive
) { _ in
Self.databaseStorage.asyncWrite { self.anyRemove(transaction: $0) }
}
actionSheetController.addAction(deleteForMeAction)
if canBeRemotelyDeleted, let outgoingMessage = self as? TSOutgoingMessage {
let deleteForEveryoneAction = ActionSheetAction(
title: NSLocalizedString(
"MESSAGE_ACTION_DELETE_FOR_EVERYONE",
comment: "The title for the action that deletes a message for all users in the conversation."
),
style: .destructive
) { [weak self] _ in
self?.showDeleteForEveryoneConfirmationIfNecessary {
guard let self = self else { return }
self.databaseStorage.write { transaction in
let deleteMessage = TSOutgoingDeleteMessage(
thread: outgoingMessage.thread(transaction: transaction),
message: outgoingMessage
)
// Reset the sending states, so we can render the sending state of the deleted message.
// TSOutgoingDeleteMessage will automatically pass through it's send state to the message
// record that it is deleting.
outgoingMessage.updateWith(recipientAddressStates: deleteMessage.recipientAddressStates, transaction: transaction)
outgoingMessage.updateWithRemotelyDeletedAndRemoveRenderableContent(with: transaction)
Self.messageSenderJobQueue.add(message: deleteMessage.asPreparer, transaction: transaction)
}
}
}
actionSheetController.addAction(deleteForEveryoneAction)
}
actionSheetController.addAction(OWSActionSheets.cancelAction)
fromViewController.presentActionSheet(actionSheetController)
}
private func showDeleteForEveryoneConfirmationIfNecessary(completion: @escaping () -> Void) {
guard !Self.preferences.wasDeleteForEveryoneConfirmationShown() else { return completion() }
OWSActionSheets.showConfirmationAlert(
title: NSLocalizedString(
"MESSAGE_ACTION_DELETE_FOR_EVERYONE_CONFIRMATION",
comment: "A one-time confirmation that you want to delete for everyone"
),
proceedTitle: NSLocalizedString(
"MESSAGE_ACTION_DELETE_FOR_EVERYONE",
comment: "The title for the action that deletes a message for all users in the conversation."
),
proceedStyle: .destructive) { _ in
Self.preferences.setWasDeleteForEveryoneConfirmationShown()
completion()
}
}
}