Add context menu to group replies
This commit is contained in:
parent
b1dc925e48
commit
03bdb55e21
@ -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 */,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: -
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
74
SignalUI/Utils/TSMessage+SignalUI.swift
Normal file
74
SignalUI/Utils/TSMessage+SignalUI.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user