588 lines
30 KiB
Swift
588 lines
30 KiB
Swift
//
|
|
// PreviewViewControllerProvider.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 10/10/18.
|
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import SafariServices
|
|
import Pachyderm
|
|
import WebURLFoundationExtras
|
|
import SwiftUI
|
|
|
|
protocol MenuActionProvider: AnyObject {
|
|
var navigationDelegate: TuskerNavigationDelegate? { get }
|
|
var toastableViewController: ToastableViewController? { get }
|
|
}
|
|
|
|
protocol MenuPreviewProvider: AnyObject {
|
|
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement])
|
|
|
|
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders?
|
|
}
|
|
|
|
protocol CustomPreviewPresenting {
|
|
func presentFromPreview(presenter: UIViewController)
|
|
}
|
|
|
|
extension MenuActionProvider where Self: TuskerNavigationDelegate {
|
|
var navigationDelegate: TuskerNavigationDelegate? { self }
|
|
}
|
|
|
|
extension MenuActionProvider where Self: ToastableViewController {
|
|
var toastableViewController: ToastableViewController? { self }
|
|
}
|
|
|
|
extension MenuActionProvider {
|
|
|
|
private var mastodonController: MastodonController? { navigationDelegate?.apiController }
|
|
|
|
func actionsForProfile(accountID: String, source: PopoverSource, fetchRelationship: Bool = true) -> [UIMenuElement] {
|
|
guard let mastodonController = mastodonController,
|
|
let account = mastodonController.persistentContainer.account(for: accountID) else { return [] }
|
|
|
|
var shareSection = [
|
|
openInSafariAction(url: account.url),
|
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
|
guard let self = self else { return }
|
|
self.navigationDelegate?.showMoreOptions(forAccount: accountID, source: source)
|
|
})
|
|
]
|
|
|
|
guard let loggedInAccountID = mastodonController.accountInfo?.id else {
|
|
// user is logged out
|
|
return shareSection
|
|
}
|
|
|
|
var actionsSection: [UIMenuElement] = [
|
|
createAction(identifier: "sendmessage", title: "Send Message", systemImageName: "envelope", handler: { [weak self] (_) in
|
|
guard let self = self else { return }
|
|
let draft = self.mastodonController!.createDraft(mentioningAcct: account.acct)
|
|
draft.visibility = .direct
|
|
self.navigationDelegate?.compose(editing: draft)
|
|
})
|
|
]
|
|
var suppressSection: [UIMenuElement] = []
|
|
|
|
if let ownAccount = mastodonController.account,
|
|
accountID != ownAccount.id {
|
|
actionsSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.followAction(for: $0, mastodonController: $1) }))
|
|
actionsSection.append(UIDeferredMenuElement.uncached({ elementHandler in
|
|
var listActions = mastodonController.lists.map { list in
|
|
UIAction(title: list.title, image: UIImage(systemName: "list.bullet")) { [unowned self] _ in
|
|
let req = List.add(list, accounts: [accountID])
|
|
mastodonController.run(req) { response in
|
|
if case .failure(let error) = response {
|
|
self.handleError(error, title: "Error Adding to List")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
listActions.append(UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in
|
|
Task { @MainActor in
|
|
let service = CreateListService(mastodonController: mastodonController, present: { [unowned self] in
|
|
self.navigationDelegate!.present($0, animated: true)
|
|
}) { list in
|
|
let req = List.add(list, accounts: [accountID])
|
|
mastodonController.run(req) { response in
|
|
if case .failure(let error) = response {
|
|
self.handleError(error, title: "Error Adding to List")
|
|
}
|
|
}
|
|
}
|
|
service.run()
|
|
}
|
|
}))
|
|
elementHandler([UIMenu(title: "Add to List", image: UIImage(systemName: "list.bullet"), children: listActions)])
|
|
}))
|
|
suppressSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.blockAction(for: $0, mastodonController: $1) }))
|
|
suppressSection.append(relationshipAction(fetchRelationship, accountID: accountID, mastodonController: mastodonController, builder: { [unowned self] in self.muteAction(for: $0, mastodonController: $1) }))
|
|
suppressSection.append(createAction(identifier: "report", title: "Report", systemImageName: "flag", handler: { [unowned self] _ in
|
|
let view = ReportView(report: EditedReport(accountID: accountID), mastodonController: mastodonController)
|
|
let host = UIHostingController(rootView: view)
|
|
self.navigationDelegate?.present(host, animated: true)
|
|
}))
|
|
}
|
|
|
|
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showProfileActivity(id: accountID, accountID: loggedInAccountID))
|
|
|
|
return [
|
|
UIMenu(options: .displayInline, children: shareSection),
|
|
UIMenu(options: .displayInline, children: actionsSection),
|
|
UIMenu(options: .displayInline, children: suppressSection),
|
|
]
|
|
}
|
|
|
|
func actionsForURL(_ url: URL, source: PopoverSource) -> [UIAction] {
|
|
return [
|
|
openInSafariAction(url: url),
|
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
|
guard let self = self else { return }
|
|
self.navigationDelegate?.showMoreOptions(forURL: url, source: source)
|
|
})
|
|
]
|
|
}
|
|
|
|
func actionsForHashtag(_ hashtag: Hashtag, source: PopoverSource) -> [UIMenuElement] {
|
|
var actionsSection: [UIMenuElement] = []
|
|
if let mastodonController = mastodonController,
|
|
mastodonController.loggedIn {
|
|
let name = hashtag.name.lowercased()
|
|
let context = mastodonController.persistentContainer.viewContext
|
|
let existing = try? context.fetch(SavedHashtag.fetchRequest(name: name, account: mastodonController.accountInfo!)).first
|
|
let saveSubtitle = "Saved hashtags appear in the Explore section of Tusker"
|
|
let saveImage = UIImage(systemName: existing != nil ? "minus" : "plus")
|
|
actionsSection = [
|
|
UIAction(title: existing != nil ? "Unsave Hashtag" : "Save Hashtag", subtitle: saveSubtitle, image: saveImage, handler: { (_) in
|
|
if let existing = existing {
|
|
context.delete(existing)
|
|
} else {
|
|
_ = SavedHashtag(hashtag: hashtag, account: mastodonController.accountInfo!, context: context)
|
|
}
|
|
mastodonController.persistentContainer.save(context: context)
|
|
})
|
|
]
|
|
if mastodonController.instanceFeatures.canFollowHashtags {
|
|
let existing = mastodonController.followedHashtags.first(where: { $0.name.lowercased() == name })
|
|
let subtitle = "Posts tagged with followed hashtags appear in your Home timeline"
|
|
let image = UIImage(systemName: existing != nil ? "person.badge.minus" : "person.badge.plus")
|
|
actionsSection.append(UIAction(title: existing != nil ? "Unfollow" : "Follow", subtitle: subtitle, image: image) { [unowned self] _ in
|
|
Task {
|
|
await ToggleFollowHashtagService(hashtag: hashtag, presenter: navigationDelegate!).toggleFollow()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
let shareSection: [UIMenuElement]
|
|
if let url = URL(hashtag.url) {
|
|
shareSection = actionsForURL(url, source: source)
|
|
} else {
|
|
shareSection = []
|
|
}
|
|
|
|
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, source: PopoverSource, includeStatusButtonActions: Bool = true) -> [UIMenuElement] {
|
|
guard let mastodonController = mastodonController else { return [] }
|
|
|
|
guard let accountID = mastodonController.accountInfo?.id,
|
|
let account = mastodonController.account else {
|
|
// user is logged out
|
|
return [
|
|
openInSafariAction(url: status.url!),
|
|
createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
|
guard let self = self else { return }
|
|
self.navigationDelegate?.showMoreOptions(forStatus: status.id, source: source)
|
|
})
|
|
]
|
|
}
|
|
let bookmarked = status.bookmarked ?? false
|
|
|
|
var toggleableSection = [
|
|
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
|
|
switch response {
|
|
case .success(let status, _):
|
|
self.mastodonController?.persistentContainer.addOrUpdate(status: status)
|
|
|
|
case .failure(let error):
|
|
self.handleError(error, title: "Error \(bookmarked ? "Unb" : "B")ookmarking")
|
|
}
|
|
}
|
|
}),
|
|
]
|
|
|
|
if #available(iOS 16.0, *),
|
|
includeStatusButtonActions {
|
|
let favorited = status.favourited
|
|
// TODO: move this color into an asset catalog or something
|
|
var favImage = UIImage(systemName: favorited ? "star.fill" : "star")!
|
|
if favorited {
|
|
favImage = favImage.withTintColor(UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1), renderingMode: .alwaysOriginal)
|
|
}
|
|
toggleableSection.insert(createAction(identifier: "favorite", title: favorited ? "Unfavorite" : "Favorite", image: favImage, handler: { [weak self] _ in
|
|
guard let self,
|
|
let navigationDelegate = self.navigationDelegate else { return }
|
|
Task { @MainActor in
|
|
await FavoriteService(status: status, mastodonController: mastodonController, presenter: navigationDelegate).toggleFavorite()
|
|
}
|
|
}), at: 0)
|
|
|
|
let reblogged = status.reblogged
|
|
var reblogImage = UIImage(systemName: "repeat")!
|
|
if reblogged {
|
|
reblogImage = reblogImage.withTintColor(UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1), renderingMode: .alwaysOriginal)
|
|
}
|
|
toggleableSection.insert(createAction(identifier: "reblog", title: reblogged ? "Unreblog" : "Reblog", image: reblogImage, handler: { [weak self] _ in
|
|
guard let self,
|
|
let navigationDelegate = self.navigationDelegate else { return }
|
|
Task { @MainActor in
|
|
await ReblogService(status: status, mastodonController: mastodonController, presenter: navigationDelegate).toggleReblog()
|
|
}
|
|
}), at: 1)
|
|
}
|
|
|
|
var actionsSection: [UIMenuElement] = []
|
|
|
|
if includeStatusButtonActions {
|
|
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)
|
|
}
|
|
|
|
// only allow muting conversations that either current user posted or is participating in (technically, is mentioned, since that's the best we can do)
|
|
if status.account.id == account.id || status.mentions.contains(where: { $0.id == account.id }) {
|
|
let muted = status.muted
|
|
toggleableSection.append(createAction(identifier: "mute", title: muted ? "Unmute Conversation" : "Mute Conversation", 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
|
|
switch response {
|
|
case .success(let status, _):
|
|
self.mastodonController?.persistentContainer.addOrUpdate(status: status)
|
|
case .failure(let error):
|
|
self.handleError(error, title: "Error \(muted ? "Unm" : "M")uting")
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
if status.poll != nil {
|
|
actionsSection.append(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
|
|
switch response {
|
|
case .success(let status, _):
|
|
// 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
|
|
DispatchQueue.main.async {
|
|
mastodonController.persistentContainer.addOrUpdate(status: status, context: mastodonController.persistentContainer.viewContext)
|
|
}
|
|
case .failure(let error):
|
|
self?.handleError(error, title: "Error Refreshing Poll")
|
|
}
|
|
})
|
|
}))
|
|
}
|
|
|
|
if account.id == status.account.id {
|
|
if mastodonController.instanceFeatures.profilePinnedStatuses {
|
|
let pinned = status.pinned ?? false
|
|
toggleableSection.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 }
|
|
switch response {
|
|
case .success(let status, _):
|
|
self.mastodonController?.persistentContainer.addOrUpdate(status: status)
|
|
case .failure(let error):
|
|
self.handleError(error, title: "Error \(pinned ? "Unp" :"P")inning")
|
|
}
|
|
})
|
|
}))
|
|
}
|
|
|
|
actionsSection.append(UIMenu(title: "Delete Post", image: UIImage(systemName: "trash"), children: [
|
|
UIAction(title: "Cancel", handler: { _ in }),
|
|
UIAction(title: "Delete Post", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { [weak self] _ in
|
|
guard let self,
|
|
let navigationDelegate = self.navigationDelegate else {
|
|
return
|
|
}
|
|
Task { @MainActor in
|
|
let service = DeleteStatusService(status: status, mastodonController: mastodonController, presenter: navigationDelegate)
|
|
await service.run()
|
|
}
|
|
})
|
|
]))
|
|
} else {
|
|
actionsSection.append(createAction(identifier: "report", title: "Report", systemImageName: "flag", handler: { [weak self] _ in
|
|
let report = EditedReport(accountID: status.account.id)
|
|
report.statusIDs = [status.id]
|
|
let view = ReportView(report: report, mastodonController: mastodonController)
|
|
let host = UIHostingController(rootView: view)
|
|
self?.navigationDelegate?.present(host, animated: true)
|
|
}))
|
|
}
|
|
|
|
var shareSection: [UIAction] = []
|
|
if let url = status.url {
|
|
shareSection.append(openInSafariAction(url: url))
|
|
} else {
|
|
Logging.general.fault("Status missing URL: id=\(status.id, privacy: .public), reblog=\((status.reblog?.id).debugDescription, privacy: .public)")
|
|
}
|
|
shareSection.append(createAction(identifier: "share", title: "Share", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
|
|
guard let self = self else { return }
|
|
self.navigationDelegate?.showMoreOptions(forStatus: status.id, source: source)
|
|
}))
|
|
|
|
addOpenInNewWindow(actions: &shareSection, activity: UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID))
|
|
|
|
if #available(iOS 16.0, *) {
|
|
let toggleableAndActions = toggleableSection + actionsSection
|
|
return [
|
|
UIMenu(options: .displayInline, preferredElementSize: toggleableAndActions.count == 1 ? .large : .medium, children: toggleableAndActions),
|
|
UIMenu(options: .displayInline, children: shareSection),
|
|
]
|
|
} else {
|
|
return [
|
|
UIMenu(options: .displayInline, children: shareSection),
|
|
UIMenu(options: .displayInline, children: toggleableSection),
|
|
UIMenu(options: .displayInline, children: actionsSection),
|
|
]
|
|
}
|
|
}
|
|
|
|
func actionsForTrendingLink(card: Card) -> [UIMenuElement] {
|
|
guard let url = URL(card.url) else {
|
|
return []
|
|
}
|
|
return [
|
|
openInSafariAction(url: url),
|
|
createAction(identifier: "postlink", title: "Post this Link", systemImageName: "square.and.pencil", handler: { [weak self] _ in
|
|
guard let self = self else { return }
|
|
let draft = self.mastodonController!.createDraft()
|
|
let title = card.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if !title.isEmpty {
|
|
draft.text += title
|
|
draft.text += ":\n"
|
|
}
|
|
draft.text += url.absoluteString
|
|
// prevents the draft from being saved automatically until the user makes a change
|
|
// also prevents it from being posted without being changed
|
|
draft.initialText = draft.text
|
|
self.navigationDelegate?.compose(editing: draft)
|
|
})
|
|
]
|
|
}
|
|
|
|
private func createAction(identifier: String, title: String, systemImageName: String?, handler: @escaping (UIAction) -> Void) -> UIAction {
|
|
let image: UIImage?
|
|
if let name = systemImageName {
|
|
image = UIImage(systemName: name)
|
|
} else {
|
|
image = nil
|
|
}
|
|
return createAction(identifier: identifier, title: title, image: image, handler: handler)
|
|
}
|
|
|
|
private func createAction(identifier: String, title: String, image: UIImage?, handler: @escaping UIActionHandler) -> UIAction {
|
|
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, allowResolveStatuses: false, allowUniversalLinks: false)
|
|
})
|
|
}
|
|
|
|
private func addOpenInNewWindow(actions: inout [UIAction], activity: @escaping @autoclosure () -> NSUserActivity) {
|
|
let options = UIWindowScene.ActivationRequestOptions()
|
|
options.preferredPresentationStyle = .automatic
|
|
actions.append(UIWindowScene.ActivationAction { (_) in
|
|
let activity = activity()
|
|
activity.displaysAuxiliaryScene = true
|
|
return .init(userActivity: activity, options: options, preview: nil)
|
|
})
|
|
}
|
|
|
|
private func handleError(_ error: Client.Error, title: String) {
|
|
if let toastable = self.toastableViewController {
|
|
let config = ToastConfiguration(from: error, with: title, in: toastable, retryAction: nil)
|
|
DispatchQueue.main.async {
|
|
toastable.showToast(configuration: config, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleSuccess(title: String) {
|
|
if let toastable = self.toastableViewController {
|
|
var config = ToastConfiguration(title: title)
|
|
config.systemImageName = "checkmark"
|
|
config.dismissAutomaticallyAfter = 2
|
|
DispatchQueue.main.async {
|
|
toastable.showToast(configuration: config, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func relationshipAction(_ fetch: Bool, accountID: String, mastodonController: MastodonController, builder: @escaping @MainActor (RelationshipMO, MastodonController) -> UIMenuElement) -> UIDeferredMenuElement {
|
|
return UIDeferredMenuElement.uncached({ @MainActor elementHandler in
|
|
// workaround for #198, may result in showing outdated relationship, so only do so where necessary
|
|
if !fetch || ProcessInfo.processInfo.isiOSAppOnMac,
|
|
let mo = mastodonController.persistentContainer.relationship(forAccount: accountID) {
|
|
elementHandler([builder(mo, mastodonController)])
|
|
} else {
|
|
let relationship = Task {
|
|
await fetchRelationship(accountID: accountID, mastodonController: mastodonController)
|
|
}
|
|
Task { @MainActor in
|
|
if let relationship = await relationship.value {
|
|
elementHandler([builder(relationship, mastodonController)])
|
|
} else {
|
|
elementHandler([])
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
@MainActor
|
|
private func followAction(for relationship: RelationshipMO, mastodonController: MastodonController) -> UIMenuElement {
|
|
let accountID = relationship.accountID
|
|
let following = relationship.following
|
|
let requested = relationship.requested
|
|
let title = following ? "Unfollow" : requested ? "Cancel Request" : "Follow"
|
|
let imageName = following || requested ? "person.badge.minus" : "person.badge.plus"
|
|
return createAction(identifier: "follow", title: title, systemImageName: imageName) { [weak self] _ in
|
|
let request = (following || requested ? Account.unfollow : Account.follow)(accountID)
|
|
mastodonController.run(request) { response in
|
|
switch response {
|
|
case .failure(let error):
|
|
self?.handleError(error, title: following ? "Error Unfollowing" : requested ? "Error Cancelinng Request" : "Error Following")
|
|
case .success(let relationship, _):
|
|
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
|
if requested { // was requested, now cancelled
|
|
self?.handleSuccess(title: "Follow Request Cancelled")
|
|
} else if following { // was following, now unfollowed
|
|
self?.handleSuccess(title: "Unfollowed")
|
|
} else if relationship.followRequested { // was not following, now requested
|
|
self?.handleSuccess(title: "Request Sent")
|
|
} else { // was not following, not now requested, assume success
|
|
self?.handleSuccess(title: "Followed")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func blockAction(for relationship: RelationshipMO, mastodonController: MastodonController) -> UIMenuElement {
|
|
let accountID = relationship.accountID
|
|
let displayName = relationship.account!.displayOrUserName
|
|
let host = relationship.account!.url.host!
|
|
let handler = { (block: Bool) in
|
|
return { [weak self] (_: UIAction) in
|
|
let req = block ? Account.block(accountID) : Account.unblock(accountID)
|
|
_ = mastodonController.run(req) { response in
|
|
switch response {
|
|
case .failure(let error):
|
|
self?.handleError(error, title: "Error \(block ? "B" : "Unb")locking")
|
|
case .success(let relationship, _):
|
|
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
|
self?.handleSuccess(title: "\(block ? "B" : "Unb")locked")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let domainHandler = { (block: Bool) in
|
|
return { [weak self] (_: UIAction) in
|
|
let req = block ? Client.block(domain: host) : Client.unblock(domain: host)
|
|
mastodonController.run(req) { response in
|
|
switch response {
|
|
case .failure(let error):
|
|
self?.handleError(error, title: "Error \(block ? "B" : "Unb")locking")
|
|
case .success(_, _):
|
|
self?.handleSuccess(title: "Domain \(block ? "B" : "Unb")locked")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if relationship.domainBlocking {
|
|
return createAction(identifier: "block", title: "Unblock \(host)", systemImageName: "circle.slash", handler: domainHandler(false))
|
|
} else if relationship.blocking {
|
|
return createAction(identifier: "block", title: "Unblock \(displayName)", systemImageName: "circle.slash", handler: handler(false))
|
|
} else {
|
|
let image = UIImage(systemName: "circle.slash")
|
|
return UIMenu(title: "Block", image: image, children: [
|
|
UIAction(title: "Cancel", handler: { _ in }),
|
|
UIAction(title: "Block \(displayName)", image: image, attributes: .destructive, handler: handler(true)),
|
|
UIAction(title: "Block \(host)", image: image, attributes: .destructive, handler: domainHandler(true))
|
|
])
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func muteAction(for relationship: RelationshipMO, mastodonController: MastodonController) -> UIMenuElement {
|
|
if relationship.muting || relationship.mutingNotifications {
|
|
return UIAction(title: "Unmute", image: UIImage(systemName: "speaker")) { [weak self] _ in
|
|
let req = Account.unmute(relationship.accountID)
|
|
mastodonController.run(req) { response in
|
|
switch response {
|
|
case .failure(let error):
|
|
self?.handleError(error, title: "Error Unmuting")
|
|
case .success(let relationship, _):
|
|
mastodonController.persistentContainer.addOrUpdate(relationship: relationship)
|
|
self?.handleSuccess(title: "Unmuted")
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return UIAction(title: "Mute", image: UIImage(systemName: "speaker.slash")) { [weak self] _ in
|
|
let view = MuteAccountView(account: relationship.account!, mastodonController: mastodonController)
|
|
let host = UIHostingController(rootView: view)
|
|
self?.navigationDelegate?.present(host, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private func fetchRelationship(accountID: String, mastodonController: MastodonController) async -> RelationshipMO? {
|
|
let req = Client.getRelationships(accounts: [accountID])
|
|
guard let (relationships, _) = try? await mastodonController.run(req),
|
|
let r = relationships.first else {
|
|
return nil
|
|
}
|
|
return await withCheckedContinuation { continuation in
|
|
mastodonController.persistentContainer.addOrUpdate(relationship: r, in: mastodonController.persistentContainer.viewContext) { mo in
|
|
continuation.resume(returning: mo)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MenuPreviewHelper {
|
|
static func willPerformPreviewAction(animator: UIContextMenuInteractionCommitAnimating, presenter: UIViewController) {
|
|
if let viewController = animator.previewViewController {
|
|
animator.preferredCommitStyle = .pop
|
|
animator.addCompletion {
|
|
if let customPresenting = viewController as? CustomPreviewPresenting {
|
|
customPresenting.presentFromPreview(presenter: presenter)
|
|
} else {
|
|
presenter.show(viewController, sender: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|