2021-04-01 08:39:15 +02:00
//
// P r o f i l e V i e w M o d e l . s w i f t
// M a s t o d o n
//
// C r e a t e d b y M a i n a s u K C i r n o o n 2 0 2 1 - 3 - 2 9 .
//
import os . log
import UIKit
import Combine
import CoreDataStack
import MastodonSDK
2021-07-23 13:10:27 +02:00
import MastodonMeta
2022-01-27 14:23:39 +01:00
import MastodonAsset
2022-10-08 07:43:06 +02:00
import MastodonCore
2022-01-27 14:23:39 +01:00
import MastodonLocalization
2022-04-14 15:15:21 +02:00
import MastodonUI
2021-04-01 08:39:15 +02:00
// p l e a s e o v e r r i d e t h i s b a s e c l a s s
class ProfileViewModel : NSObject {
2022-04-14 15:15:21 +02:00
2022-01-27 14:23:39 +01:00
let logger = Logger ( subsystem : " ProfileViewModel " , category : " ViewModel " )
2021-04-01 08:39:15 +02:00
typealias UserID = String
var disposeBag = Set < AnyCancellable > ( )
var observations = Set < NSKeyValueObservation > ( )
private var mastodonUserObserver : AnyCancellable ?
private var currentMastodonUserObserver : AnyCancellable ?
2022-05-13 11:23:35 +02:00
let postsUserTimelineViewModel : UserTimelineViewModel
let repliesUserTimelineViewModel : UserTimelineViewModel
let mediaUserTimelineViewModel : UserTimelineViewModel
let profileAboutViewModel : ProfileAboutViewModel
2021-04-01 08:39:15 +02:00
// i n p u t
let context : AppContext
2022-10-09 14:07:57 +02:00
let authContext : AuthContext
2022-02-10 12:30:41 +01:00
@ Published var me : MastodonUser ?
@ Published var user : MastodonUser ?
2022-05-13 11:23:35 +02:00
2021-04-01 08:39:15 +02:00
let viewDidAppear = PassthroughSubject < Void , Never > ( )
2022-05-13 11:23:35 +02:00
@ Published var isEditing = false
@ Published var isUpdating = false
2022-05-26 17:19:47 +02:00
@ Published var accountForEdit : Mastodon . Entity . Account ?
2021-04-01 08:39:15 +02:00
// o u t p u t
2022-05-26 17:19:47 +02:00
let relationshipViewModel = RelationshipViewModel ( )
2022-05-13 11:23:35 +02:00
@ Published var userIdentifier : UserIdentifier ? = nil
2022-05-26 17:19:47 +02:00
@ Published var isRelationshipActionButtonHidden : Bool = true
@ Published var isReplyBarButtonItemHidden : Bool = true
@ Published var isMoreMenuBarButtonItemHidden : Bool = true
@ Published var isMeBarButtonItemsHidden : Bool = true
@ Published var isPagingEnabled = true
// @ P u b l i s h e d v a r p r o t e c t e d : B o o l ? = n i l
// l e t n e e d s P a g e P i n T o T o p = C u r r e n t V a l u e S u b j e c t < B o o l , N e v e r > ( f a l s e )
2021-04-08 10:53:32 +02:00
2022-10-09 14:07:57 +02:00
init ( context : AppContext , authContext : AuthContext , optionalMastodonUser mastodonUser : MastodonUser ? ) {
2021-04-01 08:39:15 +02:00
self . context = context
2022-10-09 14:07:57 +02:00
self . authContext = authContext
2022-02-10 12:30:41 +01:00
self . user = mastodonUser
2022-05-13 11:23:35 +02:00
self . postsUserTimelineViewModel = UserTimelineViewModel (
context : context ,
2022-10-09 14:07:57 +02:00
authContext : authContext ,
2022-05-26 17:19:47 +02:00
title : L10n . Scene . Profile . SegmentedControl . posts ,
2022-05-13 11:23:35 +02:00
queryFilter : . init ( excludeReplies : true )
)
self . repliesUserTimelineViewModel = UserTimelineViewModel (
context : context ,
2022-10-09 14:07:57 +02:00
authContext : authContext ,
2022-05-26 17:19:47 +02:00
title : L10n . Scene . Profile . SegmentedControl . postsAndReplies ,
2022-07-01 07:53:53 +02:00
queryFilter : . init ( excludeReplies : false )
2022-05-13 11:23:35 +02:00
)
self . mediaUserTimelineViewModel = UserTimelineViewModel (
context : context ,
2022-10-09 14:07:57 +02:00
authContext : authContext ,
2022-05-26 17:19:47 +02:00
title : L10n . Scene . Profile . SegmentedControl . media ,
2022-05-13 11:23:35 +02:00
queryFilter : . init ( onlyMedia : true )
)
self . profileAboutViewModel = ProfileAboutViewModel ( context : context )
2021-04-01 08:39:15 +02:00
super . init ( )
2021-04-02 12:13:45 +02:00
2022-05-13 11:23:35 +02:00
// b i n d m e
2022-10-09 14:07:57 +02:00
self . me = authContext . mastodonAuthenticationBox . authenticationRecord . object ( in : context . managedObjectContext ) ? . user
2022-05-26 17:19:47 +02:00
$ me
. assign ( to : \ . me , on : relationshipViewModel )
. store ( in : & disposeBag )
2022-05-13 11:23:35 +02:00
// b i n d u s e r
$ user
. map { user -> UserIdentifier ? in
guard let user = user else { return nil }
return MastodonUserIdentifier ( domain : user . domain , userID : user . id )
}
. assign ( to : & $ userIdentifier )
2022-05-26 17:19:47 +02:00
$ user
. assign ( to : \ . user , on : relationshipViewModel )
. store ( in : & disposeBag )
2021-04-01 08:39:15 +02:00
2022-05-26 17:19:47 +02:00
// b i n d u s e r I d e n t i f i e r
2022-05-13 11:23:35 +02:00
$ userIdentifier . assign ( to : & postsUserTimelineViewModel . $ userIdentifier )
$ userIdentifier . assign ( to : & repliesUserTimelineViewModel . $ userIdentifier )
$ userIdentifier . assign ( to : & mediaUserTimelineViewModel . $ userIdentifier )
2022-01-27 14:23:39 +01:00
2022-05-26 17:19:47 +02:00
// b i n d b a r b u t t o n i t e m s
relationshipViewModel . $ optionSet
. sink { [ weak self ] optionSet in
guard let self = self else { return }
guard let optionSet = optionSet , ! optionSet . contains ( . none ) else {
self . isReplyBarButtonItemHidden = true
self . isMoreMenuBarButtonItemHidden = true
self . isMeBarButtonItemsHidden = true
return
}
let isMyself = optionSet . contains ( . isMyself )
self . isReplyBarButtonItemHidden = isMyself
self . isMoreMenuBarButtonItemHidden = isMyself
self . isMeBarButtonItemsHidden = ! isMyself
}
. store ( in : & disposeBag )
2021-06-24 09:14:50 +02:00
2022-05-26 17:19:47 +02:00
// q u e r y r e l a t i o n s h i p
let userRecord = $ user . map { user -> ManagedObjectRecord < MastodonUser > ? in
user . flatMap { ManagedObjectRecord < MastodonUser > ( objectID : $0 . objectID ) }
}
let pendingRetryPublisher = CurrentValueSubject < TimeInterval , Never > ( 1 )
2021-04-01 08:39:15 +02:00
2022-05-26 17:19:47 +02:00
// o b s e r v e f r i e n d s h i p
2022-10-09 14:07:57 +02:00
Publishers . CombineLatest (
2022-05-26 17:19:47 +02:00
userRecord ,
pendingRetryPublisher
2021-04-01 08:39:15 +02:00
)
2022-10-09 14:07:57 +02:00
. sink { [ weak self ] userRecord , _ in
2021-04-01 08:39:15 +02:00
guard let self = self else { return }
2022-10-09 14:07:57 +02:00
guard let userRecord = userRecord else { return }
2022-05-26 17:19:47 +02:00
Task {
do {
let response = try await self . updateRelationship (
record : userRecord ,
2022-10-09 14:07:57 +02:00
authenticationBox : self . authContext . mastodonAuthenticationBox
2022-05-26 17:19:47 +02:00
)
// t h e r e a r e s e c o n d s d e l a y a f t e r r e q u e s t f o l l o w b e f o r e r e q u e s t e d - > f o l l o w i n g . Q u e r y a g a i n w h e n n e e d s
guard let relationship = response . value . first else { return }
if relationship . requested = = true {
let delay = pendingRetryPublisher . value
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + delay ) { [ weak self ] in
guard let _ = self else { return }
pendingRetryPublisher . value = min ( 2 * delay , 60 )
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: [Relationship] fetch again due to pending " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
2021-04-01 08:39:15 +02:00
}
}
2022-05-26 17:19:47 +02:00
} catch {
self . logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : [Relationship] update user relationship failure: \( error . localizedDescription ) " )
}
} // e n d T a s k
2021-04-01 08:39:15 +02:00
}
. store ( in : & disposeBag )
2022-06-14 07:44:32 +02:00
2022-05-26 17:19:47 +02:00
let isBlockingOrBlocked = Publishers . CombineLatest (
relationshipViewModel . $ isBlocking ,
relationshipViewModel . $ isBlockingBy
)
. map { $0 || $1 }
. share ( )
Publishers . CombineLatest (
isBlockingOrBlocked ,
$ isEditing
)
. map { ! $0 && ! $1 }
. assign ( to : & $ isPagingEnabled )
2021-04-01 08:39:15 +02:00
}
2022-05-26 17:19:47 +02:00
2021-04-02 12:13:45 +02:00
}
2021-06-24 13:20:41 +02:00
extension ProfileViewModel {
// f e t c h p r o f i l e i n f o b e f o r e e d i t
func fetchEditProfileInfo ( ) -> AnyPublisher < Mastodon . Response . Content < Mastodon . Entity . Account > , Error > {
2022-02-10 12:30:41 +01:00
guard let me = me ,
let mastodonAuthentication = me . mastodonAuthentication
else {
2021-06-24 13:20:41 +02:00
return Fail ( error : APIService . APIError . implicit ( . authenticationMissing ) ) . eraseToAnyPublisher ( )
}
let authorization = Mastodon . API . OAuth . Authorization ( accessToken : mastodonAuthentication . userAccessToken )
2022-02-10 12:30:41 +01:00
return context . apiService . accountVerifyCredentials ( domain : me . domain , authorization : authorization )
2022-01-27 14:23:39 +01:00
}
private func updateRelationship (
record : ManagedObjectRecord < MastodonUser > ,
authenticationBox : MastodonAuthenticationBox
) async throws -> Mastodon . Response . Content < [ Mastodon . Entity . Relationship ] > {
logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : [Relationship] update user relationship... " )
let response = try await context . apiService . relationship (
records : [ record ] ,
authenticationBox : authenticationBox
)
logger . log ( level : . debug , " \( ( #file as NSString ) . lastPathComponent , privacy : . public ) [ \( #line , privacy : . public ) ], \( #function , privacy : . public ) : [Relationship] did update MastodonUser relationship " )
return response
2021-06-24 13:20:41 +02:00
}
}
2022-01-27 14:23:39 +01:00
extension ProfileViewModel {
func updateProfileInfo (
headerProfileInfo : ProfileHeaderViewModel . ProfileInfo ,
aboutProfileInfo : ProfileAboutViewModel . ProfileInfo
) async throws -> Mastodon . Response . Content < Mastodon . Entity . Account > {
2022-10-09 14:07:57 +02:00
let authenticationBox = authContext . mastodonAuthenticationBox
2022-01-27 14:23:39 +01:00
let domain = authenticationBox . domain
let authorization = authenticationBox . userAuthorization
2022-11-17 04:49:49 +01:00
// TODO: c o n s t r a i n s i z e ?
2022-11-17 04:52:44 +01:00
let _header : UIImage ? = {
guard let image = headerProfileInfo . header else { return nil }
guard image . size . width <= ProfileHeaderViewModel . bannerImageMaxSizeInPixel . width else {
return image . af . imageScaled ( to : ProfileHeaderViewModel . bannerImageMaxSizeInPixel )
}
return image
} ( )
2022-11-17 04:49:49 +01:00
let _avatar : UIImage ? = {
2022-05-26 17:19:47 +02:00
guard let image = headerProfileInfo . avatar else { return nil }
2022-01-27 14:23:39 +01:00
guard image . size . width <= ProfileHeaderViewModel . avatarImageMaxSizeInPixel . width else {
return image . af . imageScaled ( to : ProfileHeaderViewModel . avatarImageMaxSizeInPixel )
}
return image
} ( )
let fieldsAttributes = aboutProfileInfo . fields . map { field in
Mastodon . Entity . Field ( name : field . name . value , value : field . value . value )
}
let query = Mastodon . API . Account . UpdateCredentialQuery (
discoverable : nil ,
bot : nil ,
displayName : headerProfileInfo . name ,
note : headerProfileInfo . note ,
2022-11-17 04:49:49 +01:00
avatar : _avatar . flatMap { Mastodon . Query . MediaAttachment . png ( $0 . pngData ( ) ) } ,
header : _header . flatMap { Mastodon . Query . MediaAttachment . png ( $0 . pngData ( ) ) } ,
2022-01-27 14:23:39 +01:00
locked : nil ,
source : nil ,
fieldsAttributes : fieldsAttributes
)
return try await context . apiService . accountUpdateCredentials (
domain : domain ,
query : query ,
authorization : authorization
)
}
}