2021-04-09 11:31:43 +02:00
//
// P r o f i l e H e a d e r 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 - 4 - 9 .
//
2021-05-27 07:56:55 +02:00
import os . log
2021-04-09 11:31:43 +02:00
import UIKit
import Combine
import Kanna
import MastodonSDK
final class ProfileHeaderViewModel {
2021-05-27 07:56:55 +02:00
static let maxProfileFieldCount = 4
2021-04-09 11:31:43 +02:00
var disposeBag = Set < AnyCancellable > ( )
// i n p u t
let context : AppContext
let isEditing = CurrentValueSubject < Bool , Never > ( false )
2021-04-09 13:44:48 +02:00
let viewDidAppear = CurrentValueSubject < Bool , Never > ( false )
2021-04-09 11:31:43 +02:00
let needsSetupBottomShadow = CurrentValueSubject < Bool , Never > ( true )
2021-04-09 13:44:48 +02:00
let isTitleViewContentOffsetSet = CurrentValueSubject < Bool , Never > ( false )
2021-05-27 07:56:55 +02:00
let emojiDict = CurrentValueSubject < MastodonStatusContent . EmojiDict , Never > ( [ : ] )
2021-06-24 13:20:41 +02:00
let accountForEdit = CurrentValueSubject < Mastodon . Entity . Account ? , Never > ( nil )
2021-04-09 13:44:48 +02:00
2021-04-09 11:31:43 +02:00
// o u t p u t
let displayProfileInfo = ProfileInfo ( )
let editProfileInfo = ProfileInfo ( )
2021-04-29 11:13:13 +02:00
let isTitleViewDisplaying = CurrentValueSubject < Bool , Never > ( false )
2021-05-27 07:56:55 +02:00
var fieldDiffableDataSource : UICollectionViewDiffableDataSource < ProfileFieldSection , ProfileFieldItem > !
2021-04-09 11:31:43 +02:00
init ( context : AppContext ) {
self . context = context
2021-06-24 13:20:41 +02:00
Publishers . CombineLatest (
isEditing . removeDuplicates ( ) , // o n l y t r i g g e r w h e n v a l u e t o g g l e
accountForEdit
)
. receive ( on : DispatchQueue . main )
. sink { [ weak self ] isEditing , account in
guard let self = self else { return }
guard isEditing else { return }
// s e t u p e d i t i n g v a l u e w h e n t o g g l e t o e d i t i n g
self . editProfileInfo . name . value = self . displayProfileInfo . name . value // s e t t o n a m e
self . editProfileInfo . avatarImageResource . value = . image ( nil ) // s e t t o e m p t y
self . editProfileInfo . note . value = ProfileHeaderViewModel . normalize ( note : self . displayProfileInfo . note . value )
self . editProfileInfo . fields . value = account ? . source ? . fields ? . compactMap { field in
ProfileFieldItem . FieldValue ( name : field . name , value : field . value )
} ? ? [ ]
}
. store ( in : & disposeBag )
2021-05-27 07:56:55 +02:00
Publishers . CombineLatest4 (
isEditing . removeDuplicates ( ) ,
displayProfileInfo . fields . removeDuplicates ( ) ,
editProfileInfo . fields . removeDuplicates ( ) ,
emojiDict . removeDuplicates ( )
)
. receive ( on : RunLoop . main )
. sink { [ weak self ] isEditing , displayFields , editingFields , emojiDict in
guard let self = self else { return }
guard let diffableDataSource = self . fieldDiffableDataSource else { return }
var snapshot = NSDiffableDataSourceSnapshot < ProfileFieldSection , ProfileFieldItem > ( )
snapshot . appendSections ( [ . main ] )
let oldSnapshot = diffableDataSource . snapshot ( )
let oldFieldAttributeDict : [ UUID : ProfileFieldItem . FieldItemAttribute ] = {
var dict : [ UUID : ProfileFieldItem . FieldItemAttribute ] = [ : ]
for item in oldSnapshot . itemIdentifiers {
switch item {
case . field ( let field , let attribute ) :
dict [ field . id ] = attribute
default :
continue
}
}
return dict
} ( )
let fields : [ ProfileFieldItem . FieldValue ] = isEditing ? editingFields : displayFields
var items = fields . map { field -> ProfileFieldItem in
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: process field item ID: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , field . id . uuidString )
let attribute = oldFieldAttributeDict [ field . id ] ? ? ProfileFieldItem . FieldItemAttribute ( )
attribute . isEditing = isEditing
attribute . emojiDict . value = emojiDict
attribute . isLast = false
return ProfileFieldItem . field ( field : field , attribute : attribute )
}
if isEditing , fields . count < ProfileHeaderViewModel . maxProfileFieldCount {
items . append ( . addEntry ( attribute : ProfileFieldItem . AddEntryItemAttribute ( ) ) )
}
if let last = items . last ? . listSeparatorLineConfigurable {
last . isLast = true
}
snapshot . appendItems ( items , toSection : . main )
diffableDataSource . apply ( snapshot , animatingDifferences : false , completion : nil )
}
. store ( in : & disposeBag )
2021-04-09 11:31:43 +02:00
}
}
extension ProfileHeaderViewModel {
struct ProfileInfo {
let name = CurrentValueSubject < String ? , Never > ( nil )
let avatarImageResource = CurrentValueSubject < ImageResource ? , Never > ( nil )
let note = CurrentValueSubject < String ? , Never > ( nil )
2021-05-27 07:56:55 +02:00
let fields = CurrentValueSubject < [ ProfileFieldItem . FieldValue ] , Never > ( [ ] )
2021-04-09 11:31:43 +02:00
enum ImageResource {
case url ( URL ? )
case image ( UIImage ? )
}
}
}
2021-05-27 07:56:55 +02:00
extension ProfileHeaderViewModel {
func appendFieldItem ( ) {
var fields = editProfileInfo . fields . value
guard fields . count < ProfileHeaderViewModel . maxProfileFieldCount else { return }
fields . append ( ProfileFieldItem . FieldValue ( name : " " , value : " " ) )
editProfileInfo . fields . value = fields
}
func removeFieldItem ( item : ProfileFieldItem ) {
var fields = editProfileInfo . fields . value
guard case let . field ( field , _ ) = item else { return }
guard let removeIndex = fields . firstIndex ( of : field ) else { return }
fields . remove ( at : removeIndex )
editProfileInfo . fields . value = fields
}
}
2021-04-09 11:31:43 +02:00
extension ProfileHeaderViewModel {
static func normalize ( note : String ? ) -> String ? {
guard let note = note ? . trimmingCharacters ( in : . whitespacesAndNewlines ) , ! note . isEmpty else {
return nil
}
let html = try ? HTML ( html : note , encoding : . utf8 )
return html ? . text
}
2021-06-22 13:33:36 +02:00
// c h e c k i f p r o f i l e c h a n g e o r n o t
2021-04-09 11:31:43 +02:00
func isProfileInfoEdited ( ) -> Bool {
guard isEditing . value else { return false }
guard editProfileInfo . name . value = = displayProfileInfo . name . value else { return true }
guard case let . image ( image ) = editProfileInfo . avatarImageResource . value , image = = nil else { return true }
guard editProfileInfo . note . value = = ProfileHeaderViewModel . normalize ( note : displayProfileInfo . note . value ) else { return true }
2021-05-27 08:16:02 +02:00
let isFieldsEqual : Bool = {
2021-06-24 13:20:41 +02:00
let originalFields = self . accountForEdit . value ? . source ? . fields ? . compactMap { field in
ProfileFieldItem . FieldValue ( name : field . name , value : field . value )
} ? ? [ ]
2021-05-27 08:16:02 +02:00
let editFields = editProfileInfo . fields . value
2021-06-24 13:20:41 +02:00
guard editFields . count = = originalFields . count else { return false }
for ( editField , originalField ) in zip ( editFields , originalFields ) {
guard editField . name . value = = originalField . name . value ,
editField . value . value = = originalField . value . value else {
2021-05-27 08:16:02 +02:00
return false
}
}
return true
} ( )
guard isFieldsEqual else { return true }
2021-04-09 11:31:43 +02:00
return false
}
func updateProfileInfo ( ) -> AnyPublisher < Mastodon . Response . Content < Mastodon . Entity . Account > , Error > {
guard let activeMastodonAuthenticationBox = context . authenticationService . activeMastodonAuthenticationBox . value else {
return Fail ( error : APIService . APIError . implicit ( . badRequest ) ) . eraseToAnyPublisher ( )
}
let domain = activeMastodonAuthenticationBox . domain
let authorization = activeMastodonAuthenticationBox . userAuthorization
let image : UIImage ? = {
guard case let . image ( _image ) = editProfileInfo . avatarImageResource . value else { return nil }
guard let image = _image else { return nil }
guard image . size . width <= MastodonRegisterViewController . avatarImageMaxSizeInPixel . width else {
return image . af . imageScaled ( to : MastodonRegisterViewController . avatarImageMaxSizeInPixel )
}
return image
} ( )
2021-05-27 07:56:55 +02:00
let fieldsAttributes = editProfileInfo . fields . value . map { fieldValue in
Mastodon . Entity . Field ( name : fieldValue . name . value , value : fieldValue . value . value )
}
2021-04-09 11:31:43 +02:00
let query = Mastodon . API . Account . UpdateCredentialQuery (
discoverable : nil ,
bot : nil ,
displayName : editProfileInfo . name . value ,
note : editProfileInfo . note . value ,
avatar : image . flatMap { Mastodon . Query . MediaAttachment . png ( $0 . pngData ( ) ) } ,
header : nil ,
locked : nil ,
source : nil ,
2021-05-27 07:56:55 +02:00
fieldsAttributes : fieldsAttributes
2021-04-09 11:31:43 +02:00
)
return context . apiService . accountUpdateCredentials (
domain : domain ,
query : query ,
authorization : authorization
)
}
}