// // PreviewViewControllerProvider.swift // Tusker // // Created by Shadowfacts on 10/10/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import UIKit import SafariServices import Pachyderm protocol MenuPreviewProvider: AnyObject { typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement]) var navigationDelegate: TuskerNavigationDelegate? { get } func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? } protocol CustomPreviewPresenting { func presentFromPreview(presenter: UIViewController) } extension MenuPreviewProvider { private var mastodonController: MastodonController? { navigationDelegate?.apiController } // Default no-op implementation func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { return nil } func actionsForProfile(accountID: String, sourceView: UIView?) -> [UIMenuElement] { guard let mastodonController = mastodonController, let account = mastodonController.persistentContainer.account(for: accountID) else { return [] } guard let loggedInAccountID = mastodonController.accountInfo?.id else { // user is logged out return [ openInSafariAction(url: account.url), createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in guard let self = self else { return } self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView) }) ] } var actionsSection: [UIMenuElement] = [ createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { [weak self] (_) in guard let self = self else { return } self.navigationDelegate?.compose(mentioningAcct: account.acct) }), ] if accountID != mastodonController.account.id { actionsSection.append(UIDeferredMenuElement({ (elementHandler) in guard let mastodonController = self.mastodonController else { elementHandler([]) return } let request = Client.getRelationships(accounts: [account.id]) // talk about callback hell :/ mastodonController.run(request) { [weak self] (response) in guard let self = self, case let .success(results, _) = response, let relationship = results.first else { elementHandler([]) return } let following = relationship.following DispatchQueue.main.async { let action = self.createAction(identifier: "follow", title: following ? "Unfollow" : "Follow", systemImageName: following ? "person.badge.minus" : "person.badge.plus", handler: { (_) in let request = (following ? Account.unfollow : Account.follow)(accountID) mastodonController.run(request) { (response) in switch response { case .failure(_): fatalError() case let .success(relationship, _): mastodonController.persistentContainer.addOrUpdate(relationship: relationship) } } }) elementHandler([ action ]) } } })) } var shareSection = [ openInSafariAction(url: account.url), createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in guard let self = self else { return } self.navigationDelegate?.showMoreOptions(forAccount: accountID, sourceView: sourceView) }) ] addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showProfileActivity(id: accountID, accountID: loggedInAccountID)) return [ UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection), UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection), ] } func actionsForURL(_ url: URL, sourceView: UIView?) -> [UIAction] { return [ openInSafariAction(url: url), createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in guard let self = self else { return } self.navigationDelegate?.showMoreOptions(forURL: url, sourceView: sourceView) }) ] } func actionsForHashtag(_ hashtag: Hashtag, sourceView: UIView?) -> [UIMenuElement] { let account = mastodonController!.accountInfo! let saved = SavedDataManager.shared.isSaved(hashtag: hashtag, for: account) let actionsSection = [ createAction(identifier: "save", title: saved ? "Unsave Hashtag" : "Save Hashtag", systemImageName: "number", handler: { (_) in if saved { SavedDataManager.shared.remove(hashtag: hashtag, for: account) } else { SavedDataManager.shared.add(hashtag: hashtag, for: account) } }) ] let shareSection = actionsForURL(hashtag.url, sourceView: sourceView) return [ UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection), UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection), ] } func actionsForStatus(_ status: StatusMO, sourceView: UIView?, includeReply: Bool = true) -> [UIMenuElement] { guard let mastodonController = mastodonController else { return [] } guard let accountID = mastodonController.accountInfo?.id else { // user is logged out return [ openInSafariAction(url: status.url!), createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in guard let self = self else { return } self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView) }) ] } let bookmarked = status.bookmarked ?? false let muted = status.muted var actionsSection = [ createAction(identifier: "bookmark", title: bookmarked ? "Unbookmark" : "Bookmark", systemImageName: bookmarked ? "bookmark.fill" : "bookmark", handler: { [weak self] (_) in guard let self = self else { return } let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id) self.mastodonController?.run(request) { (response) in if case let .success(status, _) = response { self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false) } } }), createAction(identifier: "mute", title: muted ? "Unmute" : "Mute", systemImageName: muted ? "speaker" : "speaker.slash", handler: { [weak self] (_) in guard let self = self else { return } let request = (muted ? Status.unmuteConversation : Status.muteConversation)(status.id) self.mastodonController?.run(request) { (response) in if case let .success(status, _) = response { self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false) } } }) ] if includeReply { actionsSection.insert(createAction(identifier: "reply", title: "Reply", systemImageName: "arrowshape.turn.up.left", handler: { [weak self] (_) in guard let self = self else { return } self.navigationDelegate?.compose(inReplyToID: status.id) }), at: 0) } if mastodonController.account != nil && mastodonController.account.id == status.account.id { let pinned = status.pinned ?? false actionsSection.append(createAction(identifier: "pin", title: pinned ? "Unpin from Profile" : "Pin to Profile", systemImageName: pinned ? "pin.slash" : "pin", handler: { [weak self] (_) in guard let self = self else { return } let request = (pinned ? Status.unpin : Status.pin)(status.id) self.mastodonController?.run(request, completion: { [weak self] (response) in guard let self = self else { return } if case let .success(status, _) = response { self.mastodonController?.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false) } }) })) } if status.poll != nil { actionsSection.insert(createAction(identifier: "refresh", title: "Refresh Poll", systemImageName: "arrow.clockwise", handler: { [weak self] (_) in guard let mastodonController = self?.mastodonController else { return } let request = Client.getStatus(id: status.id) mastodonController.run(request, completion: { (response) in if case let .success(status, _) = response { // todo: this shouldn't really use the viewContext, but for some reason saving the // backgroundContext with the new version of the status isn't updating the viewContext mastodonController.persistentContainer.addOrUpdate(status: status, incrementReferenceCount: false, context: mastodonController.persistentContainer.viewContext) } }) }), at: 0) } var shareSection = [ openInSafariAction(url: status.url!), createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self, weak sourceView] (_) in guard let self = self else { return } self.navigationDelegate?.showMoreOptions(forStatus: status.id, sourceView: sourceView) }), ] addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID)) return [ UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection), UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection), ] } private func createAction(identifier: String, title: String, systemImageName: String?, handler: @escaping UIActionHandler) -> UIAction { let image: UIImage? if let name = systemImageName { image = UIImage(systemName: name) } else { image = nil } return UIAction(title: title, image: image, identifier: UIAction.Identifier(identifier), discoverabilityTitle: nil, attributes: [], state: .off, handler: handler) } private func openInSafariAction(url: URL) -> UIAction { return createAction(identifier: "openinsafari", title: "Open in Safari", systemImageName: "safari", handler: { [weak self] (_) in self?.navigationDelegate?.selected(url: url, allowUniversalLinks: false) }) } private func addOpenInNewWindow(actions: inout [UIAction], activity: @escaping @autoclosure () -> NSUserActivity) { #if SDK_IOS_15 if #available(iOS 15.0, *) { let options = UIWindowScene.ActivationRequestOptions() options.preferredPresentationStyle = .automatic actions.append(UIWindowScene.ActivationAction { (_) in return .init(userActivity: activity(), options: options, preview: nil) }) } else if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity(), options: nil, errorHandler: nil) })) } #else if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { actions.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "rectangle.badge.plus", handler: { (_) in UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity(), options: nil, errorHandler: nil) })) } #endif } } extension LargeImageViewController: CustomPreviewPresenting { func presentFromPreview(presenter: UIViewController) { presenter.present(self, animated: true) } } extension GalleryViewController: CustomPreviewPresenting { func presentFromPreview(presenter: UIViewController) { presenter.present(self, animated: true) } } extension SFSafariViewController: CustomPreviewPresenting { func presentFromPreview(presenter: UIViewController) { presenter.present(self, animated: true) } }