2021-02-08 11:29:27 +01:00
//
// S t a t u s P r o v i d e r F a c a d e . s w i f t
// M a s t o d o n
//
// C r e a t e d b y s x i a o j i a n o n 2 0 2 1 / 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 // o r i g i n a l s t a t u s
case . secondary : return status ? . replyTo ? ? status // r e b l o g o r r e p l y t o s t a t u s
}
} ( )
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 // o r i g i n a l s t a t u s
case . secondary : return status // r e b l o g o r s t a t u s
}
} ( )
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
2021-04-02 13:33:29 +02:00
extension StatusProviderFacade {
static func responseToStatusActiveLabelAction ( provider : StatusProvider , cell : UITableViewCell , activeLabel : ActiveLabel , didTapEntity entity : ActiveEntity ) {
switch entity . type {
2021-04-07 10:55:07 +02:00
case . hashtag ( let text , _ ) :
2021-04-07 10:56:31 +02:00
let hashtagTimelienViewModel = HashtagTimelineViewModel ( context : provider . context , hashtag : text )
2021-04-07 10:55:07 +02:00
provider . coordinator . present ( scene : . hashtagTimeline ( viewModel : hashtagTimelienViewModel ) , from : provider , transition : . show )
2021-04-06 10:43:08 +02:00
case . mention ( let text , _ ) :
2021-04-02 13:33:29 +02:00
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 }
// c a n n o t c o n t i n u e w i t h o u t m e t a
guard let mentionMeta = ( status . mentions ? ? Set ( ) ) . first ( where : { $0 . username = = mention } ) else { return }
let userID = mentionMeta . id
let profileViewModel : ProfileViewModel = {
// c h e c k i f s e l f
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
// p r e p a r e a u t h e n t i c a t i o n
guard let activeMastodonAuthenticationBox = provider . context . authenticationService . activeMastodonAuthenticationBox . value else {
assertionFailure ( )
return
}
// p r e p a r e c u r r e n t u s e r i n f o s
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 }
// h a p t i c f e e d b a c k g e n e r a t o r
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 {
2021-02-24 11:40:47 +01:00
case . failure :
2021-02-08 11:29:27 +01:00
// TODO: h a n d l e e r r o r
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
// d o n o t h i n g
}
. 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
// p r e p a r e a u t h e n t i c a t i o n
guard let activeMastodonAuthenticationBox = provider . context . authenticationService . activeMastodonAuthenticationBox . value else {
assertionFailure ( )
return
}
// p r e p a r e c u r r e n t u s e r i n f o s
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 }
// h a p t i c f e e d b a c k g e n e r a t o r
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: h a n d l e e r r o r
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
// d o n o t h i n g
}
. store ( in : & provider . disposeBag )
}
}
2021-04-14 09:59:29 +02:00
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 // o r i g i n a l s t a t u s
case secondary // w r a p p e r s t a t u s o r r e p l y ( w h e n n e e d s . e . g t a p h e a d e r o f s t a t u s v i e w )
2021-02-08 11:29:27 +01:00
}
}