From 17b39da316abe386de0ed32a98427430cb2cd422 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 3 Dec 2022 13:38:32 -0500 Subject: [PATCH] =?UTF-8?q?Add=20=E2=80=9CCopy,=E2=80=9D=20=E2=80=9CShare,?= =?UTF-8?q?=E2=80=9D=20and=20=E2=80=9CShare=20Link=20in=20Post=E2=80=9D=20?= =?UTF-8?q?actions=20to=20cards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../input/Base.lproj/app.json | 3 + Localization/app.json | 3 + ...Provider+StatusTableViewCellDelegate.swift | 96 +++++++++++++++++++ .../StatusTableViewCellDelegate.swift | 10 ++ .../Generated/Strings.swift | 8 ++ .../Resources/Base.lproj/Localizable.strings | 3 + .../View/Content/NotificationView.swift | 9 ++ .../View/Content/StatusCardControl.swift | 25 ++++- .../View/Content/StatusView+ViewModel.swift | 6 -- .../MastodonUI/View/Content/StatusView.swift | 14 +++ 10 files changed, 169 insertions(+), 8 deletions(-) diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index e09ab983c..4788a99d4 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -78,6 +78,7 @@ "sign_up": "Create account", "see_more": "See More", "preview": "Preview", + "copy": "Copy", "share": "Share", "share_user": "Share %s", "share_post": "Share Post", @@ -133,6 +134,7 @@ "media_content_warning": "Tap anywhere to reveal", "tap_to_reveal": "Tap to reveal", "load_embed": "Load Embed", + "link_via_user": "%s via %s", "poll": { "vote": "Vote", "closed": "Closed" @@ -154,6 +156,7 @@ "show_image": "Show image", "show_gif": "Show GIF", "show_video_player": "Show video player", + "share_link_in_post": "Share Link in Post", "tap_then_hold_to_show_menu": "Tap then hold to show menu" }, "tag": { diff --git a/Localization/app.json b/Localization/app.json index e09ab983c..4788a99d4 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -78,6 +78,7 @@ "sign_up": "Create account", "see_more": "See More", "preview": "Preview", + "copy": "Copy", "share": "Share", "share_user": "Share %s", "share_post": "Share Post", @@ -133,6 +134,7 @@ "media_content_warning": "Tap anywhere to reveal", "tap_to_reveal": "Tap to reveal", "load_embed": "Load Embed", + "link_via_user": "%s via %s", "poll": { "vote": "Vote", "closed": "Closed" @@ -154,6 +156,7 @@ "show_image": "Show image", "show_gif": "Show GIF", "show_video_player": "Show video player", + "share_link_in_post": "Share Link in Post", "tap_then_hold_to_show_menu": "Tap then hold to show menu" }, "tag": { diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift index 721263f30..0521a3486 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewCellDelegate.swift @@ -10,6 +10,9 @@ import CoreDataStack import MetaTextKit import MastodonCore import MastodonUI +import MastodonLocalization +import MastodonAsset +import LinkPresentation // MARK: - header extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthContextProvider { @@ -150,6 +153,99 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider & AuthConte } } + func tableViewCell( + _ cell: UITableViewCell, + statusView: StatusView, + cardControl: StatusCardControl, + didTapURL url: URL + ) { + Task { + let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil) + guard let item = await item(from: source) else { + assertionFailure() + return + } + guard case let .status(status) = item else { + assertionFailure("only works for status data provider") + return + } + + await DataSourceFacade.responseToURLAction( + provider: self, + status: status, + url: url + ) + } + } + + func tableViewCell( + _ cell: UITableViewCell, + statusView: StatusView, + cardControlMenu statusCardControl: StatusCardControl + ) -> UIMenu? { + guard let card = statusView.viewModel.card, + let url = card.url else { + return nil + } + + return UIMenu(children: [ + UIAction( + title: L10n.Common.Controls.Actions.copy, + image: UIImage(systemName: "doc.on.doc") + ) { _ in + UIPasteboard.general.url = url + }, + UIAction( + title: L10n.Common.Controls.Actions.share, + image: Asset.Arrow.squareAndArrowUp.image.withRenderingMode(.alwaysTemplate) + ) { _ in + Task { + await MainActor.run { + let activityViewController = UIActivityViewController( + activityItems: [ + URLActivityItemWithMetadata(url: url) { metadata in + metadata.title = card.title + + if let image = card.imageURL { + metadata.iconProvider = ImageProvider(url: image, filter: nil).itemProvider + } + } + ], + applicationActivities: [] + ) + self.coordinator.present( + scene: .activityViewController( + activityViewController: activityViewController, + sourceView: statusCardControl, barButtonItem: nil + ), + from: self, + transition: .activityViewControllerPresent(animated: true) + ) + } + } + }, + UIAction( + title: L10n.Common.Controls.Status.Actions.shareLinkInPost, + image: Asset.ObjectsAndTools.squareAndPencil.image.withRenderingMode(.alwaysTemplate) + ) { _ in + Task { + await MainActor.run { + self.coordinator.present( + scene: .compose(viewModel: ComposeViewModel( + context: self.context, + authContext: self.authContext, + destination: .topLevel, + initialContent: L10n.Common.Controls.Status.linkViaUser(url.absoluteString, "@" + (statusView.viewModel.authorUsername ?? "")) + )), + from: self, + transition: .modal(animated: true) + ) + } + } + } + ]) + } + } // MARK: - media diff --git a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift index e76ba5006..f21e0573c 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift +++ b/Mastodon/Scene/Share/View/TableviewCell/StatusTableViewCellDelegate.swift @@ -37,6 +37,8 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate { func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton) func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton) + func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) + func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) // sourcery:end } @@ -102,6 +104,14 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell { delegate?.tableViewCell(self, statusView: statusView, statusMetricView: statusMetricView, favoriteButtonDidPressed: button) } + func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) { + delegate?.tableViewCell(self, statusView: statusView, cardControl: cardControl, didTapURL: url) + } + + func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? { + return delegate?.tableViewCell(self, statusView: statusView, cardControlMenu: cardControlMenu) + } + func statusView(_ statusView: StatusView, accessibilityActivate: Void) { delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: accessibilityActivate) } diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 335638030..a2d2faf73 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -112,6 +112,8 @@ public enum L10n { public static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm", fallback: "Confirm") /// Continue public static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue", fallback: "Continue") + /// Copy + public static let copy = L10n.tr("Localizable", "Common.Controls.Actions.Copy", fallback: "Copy") /// Copy Photo public static let copyPhoto = L10n.tr("Localizable", "Common.Controls.Actions.CopyPhoto", fallback: "Copy Photo") /// Delete @@ -272,6 +274,10 @@ public enum L10n { public enum Status { /// Content Warning public static let contentWarning = L10n.tr("Localizable", "Common.Controls.Status.ContentWarning", fallback: "Content Warning") + /// %@ via %@ + public static func linkViaUser(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.LinkViaUser", String(describing: p1), String(describing: p2), fallback: "%@ via %@") + } /// Load Embed public static let loadEmbed = L10n.tr("Localizable", "Common.Controls.Status.LoadEmbed", fallback: "Load Embed") /// Tap anywhere to reveal @@ -303,6 +309,8 @@ public enum L10n { public static let reblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reblog", fallback: "Reblog") /// Reply public static let reply = L10n.tr("Localizable", "Common.Controls.Status.Actions.Reply", fallback: "Reply") + /// Share Link in Post + public static let shareLinkInPost = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShareLinkInPost", fallback: "Share Link in Post") /// Show GIF public static let showGif = L10n.tr("Localizable", "Common.Controls.Status.Actions.ShowGif", fallback: "Show GIF") /// Show image diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 3c3e16ca3..88a67ac1d 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -31,6 +31,7 @@ Please check your internet connection."; "Common.Controls.Actions.Compose" = "Compose"; "Common.Controls.Actions.Confirm" = "Confirm"; "Common.Controls.Actions.Continue" = "Continue"; +"Common.Controls.Actions.Copy" = "Copy"; "Common.Controls.Actions.CopyPhoto" = "Copy Photo"; "Common.Controls.Actions.Delete" = "Delete"; "Common.Controls.Actions.Discard" = "Discard"; @@ -100,6 +101,7 @@ Please check your internet connection."; "Common.Controls.Status.Actions.Menu" = "Menu"; "Common.Controls.Status.Actions.Reblog" = "Reblog"; "Common.Controls.Status.Actions.Reply" = "Reply"; +"Common.Controls.Status.Actions.ShareLinkInPost" = "Share Link in Post"; "Common.Controls.Status.Actions.ShowGif" = "Show GIF"; "Common.Controls.Status.Actions.ShowImage" = "Show image"; "Common.Controls.Status.Actions.ShowVideoPlayer" = "Show video player"; @@ -107,6 +109,7 @@ Please check your internet connection."; "Common.Controls.Status.Actions.Unfavorite" = "Unfavorite"; "Common.Controls.Status.Actions.Unreblog" = "Undo reblog"; "Common.Controls.Status.ContentWarning" = "Content Warning"; +"Common.Controls.Status.LinkViaUser" = "%@ via %@"; "Common.Controls.Status.LoadEmbed" = "Load Embed"; "Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal"; "Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift index eb077d6db..c44a06b7d 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NotificationView.swift @@ -611,6 +611,15 @@ extension NotificationView: StatusViewDelegate { assertionFailure() } + public func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) { + assertionFailure() + } + + public func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? { + assertionFailure() + return nil + } + } // MARK: - MastodonMenuDelegate diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift index 51c125a87..433719a06 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusCardControl.swift @@ -14,8 +14,13 @@ import CoreDataStack import UIKit import WebKit +public protocol StatusCardControlDelegate: AnyObject { + func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL) + func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> UIMenu? +} + public final class StatusCardControl: UIControl { - public var urlToOpen = PassthroughSubject() + public weak var delegate: StatusCardControlDelegate? private var disposeBag = Set() @@ -130,6 +135,8 @@ public final class StatusCardControl: UIControl { showEmbedButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), showEmbedButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), ]) + + addInteraction(UIContextMenuInteraction(delegate: self)) } required init?(coder: NSCoder) { @@ -238,6 +245,7 @@ public final class StatusCardControl: UIControl { } } +// MARK: WKWebView delegates extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { fileprivate func showWebView() { let webView = setupWebView() @@ -279,7 +287,7 @@ extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { navigationAction.navigationType == .linkActivated || navigationAction.navigationType == .other, let url = navigationAction.request.url, url.absoluteString != "about:blank" { - urlToOpen.send(url) + delegate?.statusCardControl(self, didTapURL: url) return .cancel } return .allow @@ -291,6 +299,19 @@ extension StatusCardControl: WKNavigationDelegate, WKUIDelegate { } } +// MARK: UIContextMenuInteractionDelegate +extension StatusCardControl { + public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { elements in + self.delegate?.statusCardControlMenu(self) + } + } + + public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + UITargetedPreview(view: self) + } +} + private extension StatusCardControl { enum Layout: Equatable { case compact diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift index 634473a65..77de106b1 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+ViewModel.swift @@ -494,12 +494,6 @@ extension StatusView.ViewModel { statusView.setStatusCardControlDisplay() } .store(in: &disposeBag) - - statusView.statusCardControl.urlToOpen - .sink { url in - statusView.delegate?.statusView(statusView, didTapCardWithURL: url) - } - .store(in: &disposeBag) } private func bindToolbar(statusView: StatusView) { diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift index 249e2e1ec..539276f41 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView.swift @@ -33,6 +33,8 @@ public protocol StatusViewDelegate: AnyObject { func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, reblogButtonDidPressed button: UIButton) func statusView(_ statusView: StatusView, statusMetricView: StatusMetricView, favoriteButtonDidPressed button: UIButton) + func statusView(_ statusView: StatusView, cardControl: StatusCardControl, didTapURL url: URL) + func statusView(_ statusView: StatusView, cardControlMenu: StatusCardControl) -> UIMenu? // a11y func statusView(_ statusView: StatusView, accessibilityActivate: Void) @@ -264,6 +266,7 @@ extension StatusView { // card statusCardControl.addTarget(self, action: #selector(statusCardControlPressed), for: .touchUpInside) + statusCardControl.delegate = self // media mediaGridContainerView.delegate = self @@ -667,6 +670,17 @@ extension StatusView: MastodonMenuDelegate { } } +// MARK: StatusCardControlDelegate +extension StatusView: StatusCardControlDelegate { + public func statusCardControl(_ statusCardControl: StatusCardControl, didTapURL url: URL) { + delegate?.statusView(self, cardControl: statusCardControl, didTapURL: url) + } + + public func statusCardControlMenu(_ statusCardControl: StatusCardControl) -> UIMenu? { + delegate?.statusView(self, cardControlMenu: statusCardControl) + } +} + #if DEBUG import SwiftUI