Kurdtvs-Live-Kurdish-TV-Kur.../Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift

425 lines
18 KiB
Swift
Raw Normal View History

2021-02-08 11:29:27 +01:00
//
// StatusProviderFacade.swift
// Mastodon
//
// Created by sxiaojian on 2021/2/8.
//
import os.log
import UIKit
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
import ActiveLabel
2021-04-01 08:39:15 +02:00
enum StatusProviderFacade { }
2021-02-08 11:29:27 +01:00
2021-04-01 08:39:15 +02:00
extension StatusProviderFacade {
static func coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider) {
_coordinateToStatusAuthorProfileScene(
for: target,
provider: provider,
status: provider.status()
)
}
static func coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell) {
_coordinateToStatusAuthorProfileScene(
for: target,
provider: provider,
status: provider.status(for: cell, indexPath: nil)
)
}
private static func _coordinateToStatusAuthorProfileScene(for target: Target, provider: StatusProvider, status: Future<Status?, Never>) {
status
.sink { [weak provider] status in
guard let provider = provider else { return }
let _status: Status? = {
switch target {
case .primary: return status?.reblog ?? status // original status
case .secondary: return status?.replyTo ?? status // reblog or reply to status
}
}()
guard let status = _status else { return }
let mastodonUser = status.author
let profileViewModel = CachedProfileViewModel(context: provider.context, mastodonUser: mastodonUser)
DispatchQueue.main.async {
if provider.navigationController == nil {
let from = provider.presentingViewController ?? provider
provider.dismiss(animated: true) {
provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
}
} else {
provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: provider, transition: .show)
}
}
}
.store(in: &provider.disposeBag)
}
2021-04-13 13:46:42 +02:00
}
extension StatusProviderFacade {
static func coordinateToStatusThreadScene(for target: Target, provider: StatusProvider, indexPath: IndexPath) {
_coordinateToStatusThreadScene(
for: target,
provider: provider,
status: provider.status(for: nil, indexPath: indexPath)
)
}
static func coordinateToStatusThreadScene(for target: Target, provider: StatusProvider, cell: UITableViewCell) {
_coordinateToStatusThreadScene(
for: target,
provider: provider,
status: provider.status(for: cell, indexPath: nil)
)
}
private static func _coordinateToStatusThreadScene(for target: Target, provider: StatusProvider, status: Future<Status?, Never>) {
status
.sink { [weak provider] status in
guard let provider = provider else { return }
let _status: Status? = {
switch target {
case .primary: return status?.reblog ?? status // original status
case .secondary: return status // reblog or status
}
}()
guard let status = _status else { return }
let threadViewModel = CachedThreadViewModel(context: provider.context, status: status)
DispatchQueue.main.async {
if provider.navigationController == nil {
let from = provider.presentingViewController ?? provider
provider.dismiss(animated: true) {
provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
}
} else {
provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: provider, transition: .show)
}
}
}
.store(in: &provider.disposeBag)
}
2021-02-08 11:29:27 +01:00
}
2021-03-09 08:18:43 +01:00
extension StatusProviderFacade {
static func responseToStatusActiveLabelAction(provider: StatusProvider, cell: UITableViewCell, activeLabel: ActiveLabel, didTapEntity entity: ActiveEntity) {
switch entity.type {
case .hashtag(let text, _):
2021-04-07 10:56:31 +02:00
let hashtagTimelienViewModel = HashtagTimelineViewModel(context: provider.context, hashtag: text)
provider.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelienViewModel), from: provider, transition: .show)
case .mention(let text, _):
coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: text)
case .url(_, _, let url, _):
guard let url = URL(string: url) else { return }
provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
default:
break
}
}
private static func coordinateToStatusMentionProfileScene(for target: Target, provider: StatusProvider, cell: UITableViewCell, mention: String) {
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else { return }
let domain = activeMastodonAuthenticationBox.domain
provider.status(for: cell, indexPath: nil)
.sink { [weak provider] status in
guard let provider = provider else { return }
let _status: Status? = {
switch target {
case .primary: return status?.reblog ?? status
case .secondary: return status
}
}()
guard let status = _status else { return }
// cannot continue without meta
guard let mentionMeta = (status.mentions ?? Set()).first(where: { $0.username == mention }) else { return }
let userID = mentionMeta.id
let profileViewModel: ProfileViewModel = {
// check if self
guard userID != activeMastodonAuthenticationBox.userID else {
return MeProfileViewModel(context: provider.context)
}
let request = MastodonUser.sortedFetchRequest
request.fetchLimit = 1
request.predicate = MastodonUser.predicate(domain: domain, id: userID)
let mastodonUser = provider.context.managedObjectContext.safeFetch(request).first
if let mastodonUser = mastodonUser {
return CachedProfileViewModel(context: provider.context, mastodonUser: mastodonUser)
} else {
return RemoteProfileViewModel(context: provider.context, userID: userID)
}
}()
DispatchQueue.main.async {
provider.coordinator.present(scene: .profile(viewModel: profileViewModel), from: provider, transition: .show)
}
}
.store(in: &provider.disposeBag)
}
}
2021-02-08 11:29:27 +01:00
extension StatusProviderFacade {
static func responseToStatusLikeAction(provider: StatusProvider) {
_responseToStatusLikeAction(
provider: provider,
2021-04-01 08:39:15 +02:00
status: provider.status()
2021-02-08 11:29:27 +01:00
)
}
static func responseToStatusLikeAction(provider: StatusProvider, cell: UITableViewCell) {
_responseToStatusLikeAction(
provider: provider,
2021-04-01 08:39:15 +02:00
status: provider.status(for: cell, indexPath: nil)
2021-02-08 11:29:27 +01:00
)
}
2021-04-01 08:39:15 +02:00
private static func _responseToStatusLikeAction(provider: StatusProvider, status: Future<Status?, Never>) {
2021-02-08 11:29:27 +01:00
// prepare authentication
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
assertionFailure()
return
}
// prepare current user infos
guard let _currentMastodonUser = provider.context.authenticationService.activeMastodonAuthentication.value?.user else {
assertionFailure()
return
}
let mastodonUserID = activeMastodonAuthenticationBox.userID
assert(_currentMastodonUser.id == mastodonUserID)
let mastodonUserObjectID = _currentMastodonUser.objectID
guard let context = provider.context else { return }
// haptic feedback generator
let generator = UIImpactFeedbackGenerator(style: .light)
let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
2021-04-01 08:39:15 +02:00
status
.compactMap { status -> (NSManagedObjectID, Mastodon.API.Favorites.FavoriteKind)? in
guard let status = status?.reblog ?? status else { return nil }
2021-02-08 11:29:27 +01:00
let favoriteKind: Mastodon.API.Favorites.FavoriteKind = {
2021-04-01 08:39:15 +02:00
let isLiked = status.favouritedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
2021-02-08 11:29:27 +01:00
return isLiked ? .destroy : .create
}()
2021-04-01 08:39:15 +02:00
return (status.objectID, favoriteKind)
2021-02-08 11:29:27 +01:00
}
2021-04-01 08:39:15 +02:00
.map { statusObjectID, favoriteKind -> AnyPublisher<(Status.ID, Mastodon.API.Favorites.FavoriteKind), Error> in
2021-04-07 08:24:28 +02:00
return context.apiService.favorite(
2021-04-01 08:39:15 +02:00
statusObjectID: statusObjectID,
2021-02-08 11:29:27 +01:00
mastodonUserObjectID: mastodonUserObjectID,
favoriteKind: favoriteKind
)
2021-04-01 08:39:15 +02:00
.map { statusID in (statusID, favoriteKind) }
2021-02-08 11:29:27 +01:00
.eraseToAnyPublisher()
}
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
.switchToLatest()
.receive(on: DispatchQueue.main)
.handleEvents { _ in
generator.prepare()
responseFeedbackGenerator.prepare()
} receiveOutput: { _, favoriteKind in
generator.impactOccurred()
2021-04-01 08:39:15 +02:00
os_log("%{public}s[%{public}ld], %{public}s: [Like] update local status like status to: %s", ((#file as NSString).lastPathComponent), #line, #function, favoriteKind == .create ? "like" : "unlike")
2021-02-08 11:29:27 +01:00
} receiveCompletion: { completion in
switch completion {
case .failure:
2021-02-08 11:29:27 +01:00
// TODO: handle error
break
case .finished:
break
}
}
2021-04-01 08:39:15 +02:00
.map { statusID, favoriteKind in
2021-04-07 08:24:28 +02:00
return context.apiService.favorite(
2021-04-01 08:39:15 +02:00
statusID: statusID,
2021-02-08 11:29:27 +01:00
favoriteKind: favoriteKind,
mastodonAuthenticationBox: activeMastodonAuthenticationBox
)
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.sink { [weak provider] completion in
guard let provider = provider else { return }
if provider.view.window != nil {
responseFeedbackGenerator.impactOccurred()
}
switch completion {
case .failure(let error):
os_log("%{public}s[%{public}ld], %{public}s: [Like] remote like request fail: %{public}s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
case .finished:
os_log("%{public}s[%{public}ld], %{public}s: [Like] remote like request success", ((#file as NSString).lastPathComponent), #line, #function)
}
} receiveValue: { response in
// do nothing
}
.store(in: &provider.disposeBag)
}
}
2021-03-09 08:18:43 +01:00
extension StatusProviderFacade {
2021-03-15 11:22:44 +01:00
static func responseToStatusReblogAction(provider: StatusProvider) {
_responseToStatusReblogAction(
2021-03-09 08:18:43 +01:00
provider: provider,
2021-04-01 08:39:15 +02:00
status: provider.status()
2021-03-09 08:18:43 +01:00
)
}
2021-03-15 11:19:45 +01:00
static func responseToStatusReblogAction(provider: StatusProvider, cell: UITableViewCell) {
2021-03-15 11:22:44 +01:00
_responseToStatusReblogAction(
2021-03-09 08:18:43 +01:00
provider: provider,
2021-04-01 08:39:15 +02:00
status: provider.status(for: cell, indexPath: nil)
2021-03-09 08:18:43 +01:00
)
}
2021-04-01 08:39:15 +02:00
private static func _responseToStatusReblogAction(provider: StatusProvider, status: Future<Status?, Never>) {
2021-03-09 08:18:43 +01:00
// prepare authentication
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
assertionFailure()
return
}
// prepare current user infos
guard let _currentMastodonUser = provider.context.authenticationService.activeMastodonAuthentication.value?.user else {
assertionFailure()
return
}
let mastodonUserID = activeMastodonAuthenticationBox.userID
assert(_currentMastodonUser.id == mastodonUserID)
let mastodonUserObjectID = _currentMastodonUser.objectID
guard let context = provider.context else { return }
// haptic feedback generator
let generator = UIImpactFeedbackGenerator(style: .light)
let responseFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium)
2021-04-01 08:39:15 +02:00
status
.compactMap { status -> (NSManagedObjectID, Mastodon.API.Reblog.ReblogKind)? in
guard let status = status?.reblog ?? status else { return nil }
2021-03-15 11:19:45 +01:00
let reblogKind: Mastodon.API.Reblog.ReblogKind = {
2021-04-01 08:39:15 +02:00
let isReblogged = status.rebloggedBy.flatMap { $0.contains(where: { $0.id == mastodonUserID }) } ?? false
2021-03-15 11:19:45 +01:00
return isReblogged ? .undoReblog : .reblog(query: .init(visibility: nil))
2021-03-09 08:18:43 +01:00
}()
2021-04-01 08:39:15 +02:00
return (status.objectID, reblogKind)
2021-03-09 08:18:43 +01:00
}
2021-04-01 08:39:15 +02:00
.map { statusObjectID, reblogKind -> AnyPublisher<(Status.ID, Mastodon.API.Reblog.ReblogKind), Error> in
2021-03-15 11:19:45 +01:00
return context.apiService.reblog(
2021-04-01 08:39:15 +02:00
statusObjectID: statusObjectID,
2021-03-09 08:18:43 +01:00
mastodonUserObjectID: mastodonUserObjectID,
2021-03-15 11:19:45 +01:00
reblogKind: reblogKind
2021-03-09 08:18:43 +01:00
)
2021-04-01 08:39:15 +02:00
.map { statusID in (statusID, reblogKind) }
2021-03-09 08:18:43 +01:00
.eraseToAnyPublisher()
}
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
.switchToLatest()
.receive(on: DispatchQueue.main)
.handleEvents { _ in
generator.prepare()
responseFeedbackGenerator.prepare()
2021-03-15 11:19:45 +01:00
} receiveOutput: { _, reblogKind in
2021-03-09 08:18:43 +01:00
generator.impactOccurred()
2021-03-15 11:19:45 +01:00
switch reblogKind {
case .reblog:
2021-04-01 08:39:15 +02:00
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local status reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "reblog")
2021-03-15 11:19:45 +01:00
case .undoReblog:
2021-04-01 08:39:15 +02:00
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] update local status reblog status to: %s", ((#file as NSString).lastPathComponent), #line, #function, "unreblog")
2021-03-15 11:19:45 +01:00
}
2021-03-09 08:18:43 +01:00
} receiveCompletion: { completion in
switch completion {
case .failure:
// TODO: handle error
break
case .finished:
break
}
}
2021-04-01 08:39:15 +02:00
.map { statusID, reblogKind in
2021-03-15 11:19:45 +01:00
return context.apiService.reblog(
2021-04-01 08:39:15 +02:00
statusID: statusID,
2021-03-15 11:19:45 +01:00
reblogKind: reblogKind,
2021-03-09 08:18:43 +01:00
mastodonAuthenticationBox: activeMastodonAuthenticationBox
)
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.sink { [weak provider] completion in
guard let provider = provider else { return }
if provider.view.window != nil {
responseFeedbackGenerator.impactOccurred()
}
switch completion {
case .failure(let error):
2021-03-15 11:19:45 +01:00
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] remote reblog request fail: %{public}s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
2021-03-09 08:18:43 +01:00
case .finished:
2021-03-15 11:19:45 +01:00
os_log("%{public}s[%{public}ld], %{public}s: [Reblog] remote reblog request success", ((#file as NSString).lastPathComponent), #line, #function)
2021-03-09 08:18:43 +01:00
}
} receiveValue: { response in
// do nothing
}
.store(in: &provider.disposeBag)
}
}
extension StatusProviderFacade {
static func responseToStatusReplyAction(provider: StatusProvider) {
_responseToStatusReplyAction(
provider: provider,
status: provider.status()
)
}
static func responseToStatusReplyAction(provider: StatusProvider, cell: UITableViewCell) {
_responseToStatusReplyAction(
provider: provider,
status: provider.status(for: cell, indexPath: nil)
)
}
private static func _responseToStatusReplyAction(provider: StatusProvider, status: Future<Status?, Never>) {
status
.sink { [weak provider] status in
guard let provider = provider else { return }
guard let status = status?.reblog ?? status else { return }
let composeViewModel = ComposeViewModel(context: provider.context, composeKind: .reply(repliedToStatusObjectID: status.objectID))
provider.coordinator.present(scene: .compose(viewModel: composeViewModel), from: provider, transition: .modal(animated: true, completion: nil))
}
.store(in: &provider.context.disposeBag)
}
}
2021-02-08 11:29:27 +01:00
extension StatusProviderFacade {
enum Target {
2021-04-13 13:46:42 +02:00
case primary // original status
case secondary // wrapper status or reply (when needs. e.g tap header of status view)
2021-02-08 11:29:27 +01:00
}
}