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
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 {
typealias UserID = String
var disposeBag = Set < AnyCancellable > ( )
var observations = Set < NSKeyValueObservation > ( )
private var mastodonUserObserver : AnyCancellable ?
private var currentMastodonUserObserver : AnyCancellable ?
// i n p u t
let context : AppContext
let mastodonUser : CurrentValueSubject < MastodonUser ? , Never >
let currentMastodonUser = CurrentValueSubject < MastodonUser ? , Never > ( nil )
let viewDidAppear = PassthroughSubject < Void , Never > ( )
// o u t p u t
let domain : CurrentValueSubject < String ? , Never >
let userID : CurrentValueSubject < UserID ? , Never >
let bannerImageURL : CurrentValueSubject < URL ? , Never >
let avatarImageURL : CurrentValueSubject < URL ? , Never >
let name : CurrentValueSubject < String ? , Never >
let username : CurrentValueSubject < String ? , Never >
let bioDescription : CurrentValueSubject < String ? , Never >
let url : CurrentValueSubject < String ? , Never >
let statusesCount : CurrentValueSubject < Int ? , Never >
let followingCount : CurrentValueSubject < Int ? , Never >
let followersCount : CurrentValueSubject < Int ? , Never >
2021-06-29 13:27:40 +02:00
let fields : CurrentValueSubject < [ Mastodon . Entity . Field ] , Never >
2021-07-23 13:10:27 +02:00
let emojiMeta : CurrentValueSubject < MastodonContent . Emojis , Never >
2021-04-01 08:39:15 +02:00
2021-06-24 13:20:41 +02:00
// f u l f i l l t h i s b e f o r e e d i t i n g
let accountForEdit = CurrentValueSubject < Mastodon . Entity . Account ? , Never > ( nil )
2021-04-02 12:13:45 +02:00
let protected : CurrentValueSubject < Bool ? , Never >
2021-04-08 10:53:32 +02:00
let suspended : CurrentValueSubject < Bool , Never >
2021-04-02 12:13:45 +02:00
let isEditing = CurrentValueSubject < Bool , Never > ( false )
2021-04-09 11:31:43 +02:00
let isUpdating = CurrentValueSubject < Bool , Never > ( false )
let relationshipActionOptionSet = CurrentValueSubject < RelationshipActionOptionSet , Never > ( . none )
2021-04-02 12:13:45 +02:00
let isFollowedBy = CurrentValueSubject < Bool , Never > ( false )
let isMuting = CurrentValueSubject < Bool , Never > ( false )
let isBlocking = CurrentValueSubject < Bool , Never > ( false )
let isBlockedBy = CurrentValueSubject < Bool , Never > ( false )
let isRelationshipActionButtonHidden = CurrentValueSubject < Bool , Never > ( true )
let isReplyBarButtonItemHidden = CurrentValueSubject < Bool , Never > ( true )
let isMoreMenuBarButtonItemHidden = CurrentValueSubject < Bool , Never > ( true )
2021-04-07 08:24:28 +02:00
let isMeBarButtonItemsHidden = CurrentValueSubject < Bool , Never > ( true )
2021-06-24 09:14:50 +02:00
2021-04-08 10:53:32 +02:00
let needsPagePinToTop = CurrentValueSubject < Bool , Never > ( false )
2021-07-06 11:53:01 +02:00
let needsPagingEnabled = CurrentValueSubject < Bool , Never > ( true )
2021-06-24 10:50:20 +02:00
let needsImageOverlayBlurred = CurrentValueSubject < Bool , Never > ( false )
2021-04-08 10:53:32 +02:00
2021-04-01 08:39:15 +02:00
init ( context : AppContext , optionalMastodonUser mastodonUser : MastodonUser ? ) {
self . context = context
self . mastodonUser = CurrentValueSubject ( mastodonUser )
self . domain = CurrentValueSubject ( context . authenticationService . activeMastodonAuthenticationBox . value ? . domain )
self . userID = CurrentValueSubject ( mastodonUser ? . id )
self . bannerImageURL = CurrentValueSubject ( mastodonUser ? . headerImageURL ( ) )
self . avatarImageURL = CurrentValueSubject ( mastodonUser ? . avatarImageURL ( ) )
self . name = CurrentValueSubject ( mastodonUser ? . displayNameWithFallback )
self . username = CurrentValueSubject ( mastodonUser ? . acctWithDomain )
self . bioDescription = CurrentValueSubject ( mastodonUser ? . note )
self . url = CurrentValueSubject ( mastodonUser ? . url )
self . statusesCount = CurrentValueSubject ( mastodonUser . flatMap { Int ( truncating : $0 . statusesCount ) } )
self . followingCount = CurrentValueSubject ( mastodonUser . flatMap { Int ( truncating : $0 . followingCount ) } )
self . followersCount = CurrentValueSubject ( mastodonUser . flatMap { Int ( truncating : $0 . followersCount ) } )
2021-04-02 12:13:45 +02:00
self . protected = CurrentValueSubject ( mastodonUser ? . locked )
2021-04-08 10:53:32 +02:00
self . suspended = CurrentValueSubject ( mastodonUser ? . suspended ? ? false )
2021-06-29 13:27:40 +02:00
self . fields = CurrentValueSubject ( mastodonUser ? . fields ? ? [ ] )
2021-07-23 13:10:27 +02:00
self . emojiMeta = CurrentValueSubject ( mastodonUser ? . emojiMeta ? ? [ : ] )
2021-04-01 08:39:15 +02:00
super . init ( )
2021-04-02 12:13:45 +02:00
relationshipActionOptionSet
. compactMap { $0 . highPriorityAction ( except : [ ] ) }
. map { $0 = = . none }
. assign ( to : \ . value , on : isRelationshipActionButtonHidden )
. store ( in : & disposeBag )
2021-04-01 08:39:15 +02:00
// b i n d a c t i v e a u t h e n t i c a t i o n
context . authenticationService . activeMastodonAuthentication
. sink { [ weak self ] activeMastodonAuthentication in
guard let self = self else { return }
guard let activeMastodonAuthentication = activeMastodonAuthentication else {
self . domain . value = nil
self . currentMastodonUser . value = nil
return
}
self . domain . value = activeMastodonAuthentication . domain
self . currentMastodonUser . value = activeMastodonAuthentication . user
}
. store ( in : & disposeBag )
2021-04-02 12:13:45 +02:00
// q u e r y r e l a t i o n s h i p
let mastodonUserID = self . mastodonUser . map { $0 ? . id }
let pendingRetryPublisher = CurrentValueSubject < TimeInterval , Never > ( 1 )
Publishers . CombineLatest3 (
mastodonUserID . removeDuplicates ( ) . eraseToAnyPublisher ( ) ,
context . authenticationService . activeMastodonAuthenticationBox . eraseToAnyPublisher ( ) ,
pendingRetryPublisher . eraseToAnyPublisher ( )
)
2021-07-20 10:40:04 +02:00
. compactMap { mastodonUserID , activeMastodonAuthenticationBox , _ -> ( String , MastodonAuthenticationBox ) ? in
2021-04-02 12:13:45 +02:00
guard let mastodonUserID = mastodonUserID , let activeMastodonAuthenticationBox = activeMastodonAuthenticationBox else { return nil }
guard mastodonUserID != activeMastodonAuthenticationBox . userID else { return nil }
return ( mastodonUserID , activeMastodonAuthenticationBox )
}
. setFailureType ( to : Error . self ) // a l l o w f a i l u r e
. flatMap { mastodonUserID , activeMastodonAuthenticationBox -> AnyPublisher < Mastodon . Response . Content < [ Mastodon . Entity . Relationship ] > , Error > in
let domain = activeMastodonAuthenticationBox . domain
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: [Relationship] fetch for user %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , mastodonUserID )
return self . context . apiService . relationship ( domain : domain , accountIDs : [ mastodonUserID ] , authorizationBox : activeMastodonAuthenticationBox )
// . r e t r y ( 3 )
. eraseToAnyPublisher ( )
}
. sink { completion in
switch completion {
case . failure ( let error ) :
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: [Relationship] update fail: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , error . localizedDescription )
case . finished :
break
}
} receiveValue : { response in
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: [Relationship] update success " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function )
// 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
}
}
2021-04-02 12:13:45 +02:00
. store ( in : & disposeBag )
2021-06-24 10:50:20 +02:00
let isBlockingOrBlocked = Publishers . CombineLatest (
2021-06-24 09:14:50 +02:00
isBlocking ,
isBlockedBy
)
2021-06-24 10:50:20 +02:00
. map { $0 || $1 }
. share ( )
isBlockingOrBlocked
. map { ! $0 }
2021-07-06 11:53:01 +02:00
. assign ( to : \ . value , on : needsPagingEnabled )
2021-06-24 10:50:20 +02:00
. store ( in : & disposeBag )
isBlockingOrBlocked
. map { $0 }
. assign ( to : \ . value , on : needsImageOverlayBlurred )
. store ( in : & disposeBag )
2021-06-24 09:14:50 +02:00
2021-04-02 12:13:45 +02:00
setup ( )
2021-04-01 08:39:15 +02:00
}
}
extension ProfileViewModel {
private func setup ( ) {
Publishers . CombineLatest (
mastodonUser . eraseToAnyPublisher ( ) ,
currentMastodonUser . eraseToAnyPublisher ( )
)
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] mastodonUser , currentMastodonUser in
guard let self = self else { return }
2021-04-02 12:13:45 +02:00
// U p d a t e v i e w m o d e l a t t r i b u t e
2021-04-01 08:39:15 +02:00
self . update ( mastodonUser : mastodonUser )
self . update ( mastodonUser : mastodonUser , currentMastodonUser : currentMastodonUser )
2021-04-02 12:13:45 +02:00
// S e t u p o b s e r v e r f o r u s e r
2021-04-01 08:39:15 +02:00
if let mastodonUser = mastodonUser {
// s e t u p o b s e r v e r
self . mastodonUserObserver = ManagedObjectObserver . observe ( object : mastodonUser )
. sink { completion in
switch completion {
case . failure ( let error ) :
assertionFailure ( error . localizedDescription )
case . finished :
assertionFailure ( )
}
} receiveValue : { [ weak self ] change in
guard let self = self else { return }
guard let changeType = change . changeType else { return }
switch changeType {
case . update :
self . update ( mastodonUser : mastodonUser )
self . update ( mastodonUser : mastodonUser , currentMastodonUser : currentMastodonUser )
case . delete :
// TODO:
break
}
}
} else {
self . mastodonUserObserver = nil
}
2021-04-02 12:13:45 +02:00
// S e t u p o b s e r v e r f o r u s e r
2021-04-01 08:39:15 +02:00
if let currentMastodonUser = currentMastodonUser {
// s e t u p o b s e r v e r
self . currentMastodonUserObserver = ManagedObjectObserver . observe ( object : currentMastodonUser )
. sink { completion in
switch completion {
case . failure ( let error ) :
assertionFailure ( error . localizedDescription )
case . finished :
assertionFailure ( )
}
} receiveValue : { [ weak self ] change in
guard let self = self else { return }
guard let changeType = change . changeType else { return }
switch changeType {
case . update :
self . update ( mastodonUser : mastodonUser , currentMastodonUser : currentMastodonUser )
case . delete :
// TODO:
break
}
}
} else {
self . currentMastodonUserObserver = nil
}
}
. store ( in : & disposeBag )
}
private func update ( mastodonUser : MastodonUser ? ) {
self . userID . value = mastodonUser ? . id
self . bannerImageURL . value = mastodonUser ? . headerImageURL ( )
self . avatarImageURL . value = mastodonUser ? . avatarImageURL ( )
self . name . value = mastodonUser ? . displayNameWithFallback
self . username . value = mastodonUser ? . acctWithDomain
self . bioDescription . value = mastodonUser ? . note
self . url . value = mastodonUser ? . url
self . statusesCount . value = mastodonUser . flatMap { Int ( truncating : $0 . statusesCount ) }
self . followingCount . value = mastodonUser . flatMap { Int ( truncating : $0 . followingCount ) }
self . followersCount . value = mastodonUser . flatMap { Int ( truncating : $0 . followersCount ) }
2021-04-02 12:13:45 +02:00
self . protected . value = mastodonUser ? . locked
2021-04-08 10:53:32 +02:00
self . suspended . value = mastodonUser ? . suspended ? ? false
2021-06-29 13:27:40 +02:00
self . fields . value = mastodonUser ? . fields ? ? [ ]
2021-07-23 13:10:27 +02:00
self . emojiMeta . value = mastodonUser ? . emojiMeta ? ? [ : ]
2021-04-01 08:39:15 +02:00
}
private func update ( mastodonUser : MastodonUser ? , currentMastodonUser : MastodonUser ? ) {
2021-04-02 12:13:45 +02:00
guard let mastodonUser = mastodonUser ,
let currentMastodonUser = currentMastodonUser else {
// s e t r e l a t i o n s h i p
self . relationshipActionOptionSet . value = . none
self . isFollowedBy . value = false
self . isMuting . value = false
self . isBlocking . value = false
self . isBlockedBy . value = false
// s e t b a r b u t t o n i t e m s t a t e
self . isReplyBarButtonItemHidden . value = true
self . isMoreMenuBarButtonItemHidden . value = true
2021-04-07 08:24:28 +02:00
self . isMeBarButtonItemsHidden . value = true
2021-04-02 12:13:45 +02:00
return
}
if mastodonUser = = currentMastodonUser {
self . relationshipActionOptionSet . value = [ . edit ]
// s e t b a r b u t t o n i t e m s t a t e
self . isReplyBarButtonItemHidden . value = true
self . isMoreMenuBarButtonItemHidden . value = true
2021-04-07 08:24:28 +02:00
self . isMeBarButtonItemsHidden . value = false
2021-04-02 12:13:45 +02:00
} else {
// s e t w i t h f o l l o w a c t i o n d e f a u l t
var relationshipActionSet = RelationshipActionOptionSet ( [ . follow ] )
2021-04-08 10:53:32 +02:00
if mastodonUser . locked {
relationshipActionSet . insert ( . request )
}
if mastodonUser . suspended {
relationshipActionSet . insert ( . suspended )
}
2021-04-02 12:13:45 +02:00
let isFollowing = mastodonUser . followingBy . flatMap { $0 . contains ( currentMastodonUser ) } ? ? false
if isFollowing {
relationshipActionSet . insert ( . following )
}
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowing: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , mastodonUser . id , isFollowing . description )
let isPending = mastodonUser . followRequestedBy . flatMap { $0 . contains ( currentMastodonUser ) } ? ? false
if isPending {
relationshipActionSet . insert ( . pending )
}
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: [Relationship] update %s isPending: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , mastodonUser . id , isPending . description )
let isFollowedBy = currentMastodonUser . followingBy . flatMap { $0 . contains ( mastodonUser ) } ? ? false
self . isFollowedBy . value = isFollowedBy
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowedBy: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , mastodonUser . id , isFollowedBy . description )
let isMuting = mastodonUser . mutingBy . flatMap { $0 . contains ( currentMastodonUser ) } ? ? false
if isMuting {
relationshipActionSet . insert ( . muting )
}
self . isMuting . value = isMuting
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: [Relationship] update %s isMuting: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , mastodonUser . id , isMuting . description )
let isBlocking = mastodonUser . blockingBy . flatMap { $0 . contains ( currentMastodonUser ) } ? ? false
if isBlocking {
relationshipActionSet . insert ( . blocking )
}
self . isBlocking . value = isBlocking
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlocking: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , mastodonUser . id , isBlocking . description )
let isBlockedBy = currentMastodonUser . blockingBy . flatMap { $0 . contains ( mastodonUser ) } ? ? false
if isBlockedBy {
relationshipActionSet . insert ( . blocked )
}
self . isBlockedBy . value = isBlockedBy
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlockedBy: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , mastodonUser . id , isBlockedBy . description )
self . relationshipActionOptionSet . value = relationshipActionSet
// s e t b a r b u t t o n i t e m s t a t e
self . isReplyBarButtonItemHidden . value = isBlocking || isBlockedBy
self . isMoreMenuBarButtonItemHidden . value = false
2021-04-07 08:24:28 +02:00
self . isMeBarButtonItemsHidden . value = true
2021-04-02 12:13:45 +02:00
}
2021-04-01 08:39:15 +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 > {
guard let currentMastodonUser = currentMastodonUser . value ,
let mastodonAuthentication = currentMastodonUser . mastodonAuthentication else {
return Fail ( error : APIService . APIError . implicit ( . authenticationMissing ) ) . eraseToAnyPublisher ( )
}
let authorization = Mastodon . API . OAuth . Authorization ( accessToken : mastodonAuthentication . userAccessToken )
return context . apiService . accountVerifyCredentials ( domain : currentMastodonUser . domain , authorization : authorization )
// . e r r o
}
}
2021-04-02 12:13:45 +02:00
extension ProfileViewModel {
2021-04-01 08:39:15 +02:00
2021-04-02 12:13:45 +02:00
enum RelationshipAction : Int , CaseIterable {
case none // s e t h i d e f r o m U I
case follow
2021-06-22 14:54:34 +02:00
case request
2021-04-02 12:13:45 +02:00
case pending
case following
case muting
case blocked
2021-04-06 11:12:25 +02:00
case blocking
2021-04-08 10:53:32 +02:00
case suspended
2021-04-02 12:13:45 +02:00
case edit
case editing
2021-04-09 11:31:43 +02:00
case updating
2021-04-02 12:13:45 +02:00
var option : RelationshipActionOptionSet {
return RelationshipActionOptionSet ( rawValue : 1 << rawValue )
}
}
// c o n s t r u c t o p t i o n s e t o n t h e e n u m f o r s a f e i t e r a t o r
struct RelationshipActionOptionSet : OptionSet {
let rawValue : Int
static let none = RelationshipAction . none . option
static let follow = RelationshipAction . follow . option
2021-06-22 14:54:34 +02:00
static let request = RelationshipAction . request . option
2021-04-02 12:13:45 +02:00
static let pending = RelationshipAction . pending . option
static let following = RelationshipAction . following . option
static let muting = RelationshipAction . muting . option
static let blocked = RelationshipAction . blocked . option
2021-04-06 11:12:25 +02:00
static let blocking = RelationshipAction . blocking . option
2021-04-08 10:53:32 +02:00
static let suspended = RelationshipAction . suspended . option
2021-04-02 12:13:45 +02:00
static let edit = RelationshipAction . edit . option
static let editing = RelationshipAction . editing . option
2021-04-09 11:31:43 +02:00
static let updating = RelationshipAction . updating . option
2021-04-02 12:13:45 +02:00
2021-04-09 11:31:43 +02:00
static let editOptions : RelationshipActionOptionSet = [ . edit , . editing , . updating ]
2021-04-02 12:13:45 +02:00
func highPriorityAction ( except : RelationshipActionOptionSet ) -> RelationshipAction ? {
let set = subtracting ( except )
for action in RelationshipAction . allCases . reversed ( ) where set . contains ( action . option ) {
return action
}
return nil
}
var title : String {
guard let highPriorityAction = self . highPriorityAction ( except : [ ] ) else {
assertionFailure ( )
return " "
}
switch highPriorityAction {
case . none : return " "
2021-06-22 14:54:34 +02:00
case . follow : return L10n . Common . Controls . Friendship . follow
case . request : return L10n . Common . Controls . Friendship . request
case . pending : return L10n . Common . Controls . Friendship . pending
case . following : return L10n . Common . Controls . Friendship . following
case . muting : return L10n . Common . Controls . Friendship . muted
case . blocked : return L10n . Common . Controls . Friendship . follow // b l o c k e d b y u s e r
case . blocking : return L10n . Common . Controls . Friendship . blocked
case . suspended : return L10n . Common . Controls . Friendship . follow
case . edit : return L10n . Common . Controls . Friendship . editInfo
2021-04-02 12:13:45 +02:00
case . editing : return L10n . Common . Controls . Actions . done
2021-04-09 11:31:43 +02:00
case . updating : return " "
2021-04-02 12:13:45 +02:00
}
}
var backgroundColor : UIColor {
guard let highPriorityAction = self . highPriorityAction ( except : [ ] ) else {
assertionFailure ( )
2021-06-22 14:52:30 +02:00
return Asset . Colors . brandBlue . color
2021-04-02 12:13:45 +02:00
}
switch highPriorityAction {
2021-06-22 14:52:30 +02:00
case . none : return Asset . Colors . brandBlue . color
case . follow : return Asset . Colors . brandBlue . color
2021-06-22 14:54:34 +02:00
case . request : return Asset . Colors . brandBlue . color
2021-06-22 14:52:30 +02:00
case . pending : return Asset . Colors . brandBlue . color
case . following : return Asset . Colors . brandBlue . color
2021-07-20 13:24:24 +02:00
case . muting : return Asset . Colors . alertYellow . color
2021-06-22 14:52:30 +02:00
case . blocked : return Asset . Colors . brandBlue . color
2021-07-20 13:24:24 +02:00
case . blocking : return Asset . Colors . danger . color
2021-06-22 14:52:30 +02:00
case . suspended : return Asset . Colors . brandBlue . color
case . edit : return Asset . Colors . brandBlue . color
case . editing : return Asset . Colors . brandBlue . color
case . updating : return Asset . Colors . brandBlue . color
2021-04-02 12:13:45 +02:00
}
}
}
2021-04-01 08:39:15 +02:00
}