mastodon-ios/Mastodon/Protocol/Provider/DataSourceProvider+UITableV...

275 lines
13 KiB
Swift

//
// DataSourceProvider+UITableViewDelegate.swift
// Mastodon
//
// Created by MainasuK on 2022-1-17.
//
import os.log
import UIKit
import CoreDataStack
import MastodonLocalization
extension UITableViewDelegate where Self: DataSourceProvider {
func aspectTableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): indexPath: \(indexPath.debugDescription)")
Task {
let source = DataSourceItem.Source(tableViewCell: nil, indexPath: indexPath)
guard let item = await item(from: source) else {
return
}
switch item {
case .status(let status):
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
status: status
)
case .user(let user):
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: user
)
case .hashtag(let tag):
await DataSourceFacade.coordinateToHashtagScene(
provider: self,
tag: tag
)
case .notification(let notification):
let managedObjectContext = context.managedObjectContext
let _status: ManagedObjectRecord<Status>? = try await managedObjectContext.perform {
guard let notification = notification.object(in: managedObjectContext) else { return nil }
guard let status = notification.status else { return nil }
return .init(objectID: status.objectID)
}
if let status = _status {
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
status: status
)
} else {
let _author: ManagedObjectRecord<MastodonUser>? = try await managedObjectContext.perform {
guard let notification = notification.object(in: managedObjectContext) else { return nil }
return .init(objectID: notification.account.objectID)
}
if let author = _author {
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: author
)
}
}
}
} // end Task
} // end func
}
extension UITableViewDelegate where Self: DataSourceProvider & MediaPreviewableViewController {
func aspectTableView(
_ tableView: UITableView,
contextMenuConfigurationForRowAt
indexPath: IndexPath, point: CGPoint
) -> UIContextMenuConfiguration? {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
guard let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell else { return nil }
let mediaViews = cell.statusView.mediaGridContainerView.mediaViews
// if cell.statusView.mediaGridContainerView.viewModel.isContentWarningOverlayDisplay == true {
// return nil
// }
for (i, mediaView) in mediaViews.enumerated() {
let pointInMediaView = mediaView.convert(point, from: tableView)
guard mediaView.point(inside: pointInMediaView, with: nil) else {
continue
}
guard let image = mediaView.thumbnail(),
let assetURLString = mediaView.configuration?.assetURL,
let assetURL = URL(string: assetURLString),
let resourceType = mediaView.configuration?.resourceType
else {
// not provide preview unless thumbnail ready
return nil
}
let contextMenuImagePreviewViewModel = ContextMenuImagePreviewViewModel(
assetURL: assetURL,
thumbnail: image,
aspectRatio: image.size
)
let configuration = TimelineTableViewCellContextMenuConfiguration(identifier: nil) { () -> UIViewController? in
if UIDevice.current.userInterfaceIdiom == .pad && mediaViews.count == 1 {
return nil
}
let previewProvider = ContextMenuImagePreviewViewController()
previewProvider.viewModel = contextMenuImagePreviewViewModel
return previewProvider
} actionProvider: { _ -> UIMenu? in
return UIMenu(
title: "",
image: nil,
identifier: nil,
options: [],
children: [
UIAction(
title: L10n.Common.Controls.Actions.savePhoto,
image: UIImage(systemName: "square.and.arrow.down"),
attributes: [],
state: .off
) { [weak self] _ in
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: save photo", ((#file as NSString).lastPathComponent), #line, #function)
guard let self = self else { return }
Task { @MainActor in
do {
try await self.context.photoLibraryService.save(
imageSource: .url(assetURL)
).singleOutput()
} catch {
guard let error = error as? PhotoLibraryService.PhotoLibraryError,
case .noPermission = error
else { return }
let alertController = SettingService.openSettingsAlertController(
title: L10n.Common.Alerts.SavePhotoFailure.title,
message: L10n.Common.Alerts.SavePhotoFailure.message
)
self.coordinator.present(
scene: .alertController(alertController: alertController),
from: self,
transition: .alertController(animated: true, completion: nil)
)
}
} // end Task
},
UIAction(
title: L10n.Common.Controls.Actions.copyPhoto,
image: UIImage(systemName: "doc.on.doc"),
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: .off
) { [weak self] _ in
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: copy photo", ((#file as NSString).lastPathComponent), #line, #function)
guard let self = self else { return }
Task {
try await self.context.photoLibraryService.copy(
imageSource: .url(assetURL)
).singleOutput()
}
},
UIAction(
title: L10n.Common.Controls.Actions.share,
image: UIImage(systemName: "square.and.arrow.up")!,
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: .off
) { [weak self] _ in
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: share", ((#file as NSString).lastPathComponent), #line, #function)
guard let self = self else { return }
Task {
let applicationActivities: [UIActivity] = [
SafariActivity(sceneCoordinator: self.coordinator)
]
let activityViewController = UIActivityViewController(
activityItems: [assetURL],
applicationActivities: applicationActivities
)
activityViewController.popoverPresentationController?.sourceView = mediaView
self.present(activityViewController, animated: true, completion: nil)
}
}
]
)
}
configuration.indexPath = indexPath
configuration.index = i
return configuration
} // end for in
return nil
}
func aspectTableView(
_ tableView: UITableView,
previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
return aspectTableView(tableView, configuration: configuration)
}
func aspectTableView(
_ tableView: UITableView,
previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
return aspectTableView(tableView, configuration: configuration)
}
private func aspectTableView(
_ tableView: UITableView,
configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return nil }
guard let indexPath = configuration.indexPath, let index = configuration.index else { return nil }
if let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell {
let mediaViews = cell.statusView.mediaGridContainerView.mediaViews
guard index < mediaViews.count else { return nil }
let mediaView = mediaViews[index]
let parameters = UIPreviewParameters()
parameters.backgroundColor = .clear
parameters.visiblePath = UIBezierPath(roundedRect: mediaView.bounds, cornerRadius: MediaView.cornerRadius)
return UITargetedPreview(view: mediaView, parameters: parameters)
} else {
return nil
}
}
func aspectTableView(
_ tableView: UITableView,
willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration,
animator: UIContextMenuInteractionCommitAnimating
) {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)")
guard let configuration = configuration as? TimelineTableViewCellContextMenuConfiguration else { return }
guard let indexPath = configuration.indexPath, let index = configuration.index else { return }
guard let cell = tableView.cellForRow(at: indexPath) as? StatusViewContainerTableViewCell else { return }
let mediaGridContainerView = cell.statusView.mediaGridContainerView
let mediaViews = mediaGridContainerView.mediaViews
guard index < mediaViews.count else { return }
let mediaView = mediaViews[index]
animator.addCompletion {
Task { [weak self] in
guard let self = self else { return }
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
guard let item = await self.item(from: source) else {
assertionFailure()
return
}
guard case let .status(status) = item else {
assertionFailure("only works for status data provider")
return
}
try await DataSourceFacade.coordinateToMediaPreviewScene(
dependency: self,
status: status,
previewContext: DataSourceFacade.AttachmentPreviewContext(
containerView: .mediaGridContainerView(mediaGridContainerView),
mediaView: mediaView,
index: index
)
)
} // end Task
} // end animator.addCompletion { }
}
}