@ -9,47 +9,34 @@ import SessionMessagingKit
import SessionUtilitiesKit
import SessionUtilitiesKit
import SignalUtilitiesKit
import SignalUtilitiesKit
class SettingsViewModel : SessionTableViewModel , NavigationItemSource , NavigatableStateHolder , EditableStateHolder, ObservableTableSource {
class SettingsViewModel : SessionTableViewModel , NavigationItemSource , NavigatableStateHolder , ObservableTableSource {
public let dependencies : Dependencies
public let dependencies : Dependencies
public let navigatableState : NavigatableState = NavigatableState ( )
public let navigatableState : NavigatableState = NavigatableState ( )
public let editableState : EditableState < TableItem > = EditableState ( )
public let state : TableDataState < Section , TableItem > = TableDataState ( )
public let state : TableDataState < Section , TableItem > = TableDataState ( )
public let observableState : ObservableTableSourceState < Section , TableItem > = ObservableTableSourceState ( )
public let observableState : ObservableTableSourceState < Section , TableItem > = ObservableTableSourceState ( )
private let userSessionId : SessionId
private let userSessionId : SessionId
private var updatedName : String ?
private var onDisplayPictureSelected : ( ( ConfirmationModal . ValueUpdate ) -> Void ) ?
private lazy var imagePickerHandler : ImagePickerHandler = ImagePickerHandler (
private lazy var imagePickerHandler : ImagePickerHandler = ImagePickerHandler (
onTransition : { [ weak self ] in self ? . transitionToScreen ( $0 , transitionType : $1 ) } ,
onTransition : { [ weak self ] in self ? . transitionToScreen ( $0 , transitionType : $1 ) } ,
onImageDataPicked : { [ weak self ] resultImageData in
onImageDataPicked : { [ weak self ] resultImageData in
self ? . updatedProfilePictureSelected (
self ? . onDisplayPictureSelected ? ( . image ( resultImageData ) )
displayPictureUpdate : . currentUserUploadImageData ( resultImageData )
)
}
}
)
)
fileprivate var oldDisplayName : String
private var editedDisplayName : String ?
private var editProfilePictureModal : ConfirmationModal ?
private var editProfilePictureModalInfo : ConfirmationModal . Info ?
// MARK: - I n i t i a l i z a t i o n
// MARK: - I n i t i a l i z a t i o n
init ( using dependencies : Dependencies ) {
init ( using dependencies : Dependencies ) {
self . dependencies = dependencies
self . dependencies = dependencies
self . userSessionId = dependencies [ cache : . general ] . sessionId
self . userSessionId = dependencies [ cache : . general ] . sessionId
self . oldDisplayName = Profile . fetchOrCreateCurrentUser ( using : dependencies ) . name
}
}
// MARK: - C o n f i g
// MARK: - C o n f i g
enum NavState {
case standard
case editing
}
enum NavItem : Equatable {
enum NavItem : Equatable {
case close
case close
case qrCode
case qrCode
case cancel
case done
}
}
public enum Section : SessionTableSection {
public enum Section : SessionTableSection {
@ -96,122 +83,35 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
// MARK: - N a v i g a t i o n I t e m S o u r c e
// MARK: - N a v i g a t i o n I t e m S o u r c e
lazy var navState : AnyPublisher < NavState , Never > = Publishers
lazy var leftNavItems : AnyPublisher < [ SessionNavItem < NavItem > ] , Never > = [
. CombineLatest (
SessionNavItem (
isEditing ,
id : . close ,
textChanged
image : UIImage ( named : " X " ) ?
. handleEvents (
. withRenderingMode ( . alwaysTemplate ) ,
receiveOutput : { [ weak self ] value , _ in
style : . plain ,
self ? . editedDisplayName = value
accessibilityIdentifier : " Close button "
}
) { [ weak self ] in self ? . dismissScreen ( ) }
)
]
. filter { _ in false }
. prepend ( ( nil , . profileName ) )
)
. map { isEditing , _ -> NavState in ( isEditing ? . editing : . standard ) }
. removeDuplicates ( )
. prepend ( . standard ) // I n i t i a l v a l u e
. shareReplay ( 1 )
. eraseToAnyPublisher ( )
lazy var leftNavItems : AnyPublisher < [ SessionNavItem < NavItem > ] , Never > = navState
. map { navState -> [ SessionNavItem < NavItem > ] in
switch navState {
case . standard :
return [
SessionNavItem (
id : . close ,
image : UIImage ( named : " X " ) ?
. withRenderingMode ( . alwaysTemplate ) ,
style : . plain ,
accessibilityIdentifier : " Close button "
) { [ weak self ] in self ? . dismissScreen ( ) }
]
case . editing :
return [
SessionNavItem (
id : . cancel ,
systemItem : . cancel ,
accessibilityIdentifier : " Cancel button "
) { [ weak self ] in
self ? . setIsEditing ( false )
self ? . editedDisplayName = self ? . oldDisplayName
}
]
}
}
. eraseToAnyPublisher ( )
lazy var rightNavItems : AnyPublisher < [ SessionNavItem < NavItem > ] , Never > = navState
lazy var rightNavItems : AnyPublisher < [ SessionNavItem < NavItem > ] , Never > = [
. map { [ weak self , dependencies ] navState -> [ SessionNavItem < NavItem > ] in
SessionNavItem (
switch navState {
id : . qrCode ,
case . standard :
image : UIImage ( named : " QRCode " ) ?
return [
. withRenderingMode ( . alwaysTemplate ) ,
SessionNavItem (
style : . plain ,
id : . qrCode ,
accessibilityIdentifier : " View QR code " ,
image : UIImage ( named : " QRCode " ) ?
action : { [ weak self , dependencies ] in
. withRenderingMode ( . alwaysTemplate ) ,
let viewController : SessionHostingViewController = SessionHostingViewController (
style : . plain ,
rootView : QRCodeScreen ( using : dependencies )
accessibilityIdentifier : " View QR code " ,
)
action : { [ weak self ] in
viewController . setNavBarTitle ( " qrCode " . localized ( ) )
let viewController : SessionHostingViewController = SessionHostingViewController (
self ? . transitionToScreen ( viewController )
rootView : QRCodeScreen ( using : dependencies )
)
viewController . setNavBarTitle ( " qrCode " . localized ( ) )
self ? . transitionToScreen ( viewController )
}
)
]
case . editing :
return [
SessionNavItem (
id : . done ,
systemItem : . done ,
accessibilityIdentifier : " Done "
) { [ weak self ] in
let updatedNickname : String = ( self ? . editedDisplayName ? ? " " )
. trimmingCharacters ( in : . whitespacesAndNewlines )
guard ! updatedNickname . isEmpty else {
self ? . transitionToScreen (
ConfirmationModal (
info : ConfirmationModal . Info (
title : " displayNameErrorDescription " . localized ( ) ,
cancelTitle : " okay " . localized ( ) ,
cancelStyle : . alert_text
)
) ,
transitionType : . present
)
return
}
guard ! Profile . isTooLong ( profileName : updatedNickname ) else {
self ? . transitionToScreen (
ConfirmationModal (
info : ConfirmationModal . Info (
title : " displayNameErrorDescriptionShorter " . localized ( ) ,
cancelTitle : " okay " . localized ( ) ,
cancelStyle : . alert_text
)
) ,
transitionType : . present
)
return
}
self ? . setIsEditing ( false )
self ? . oldDisplayName = updatedNickname
self ? . updateProfile ( displayNameUpdate : . currentUserUpdate ( updatedNickname ) )
}
]
}
}
}
)
. eraseToAnyPublisher ( )
]
// MARK: - C o n t e n t
// MARK: - C o n t e n t
private struct State : Equatable {
private struct State : Equatable {
let profile : Profile
let profile : Profile
let developerModeEnabled : Bool
let developerModeEnabled : Bool
@ -231,6 +131,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
. compactMap { [ weak self ] state -> [ SectionModel ] ? in self ? . content ( state ) }
. compactMap { [ weak self ] state -> [ SectionModel ] ? in self ? . content ( state ) }
private func content ( _ state : State ) -> [ SectionModel ] {
private func content ( _ state : State ) -> [ SectionModel ] {
let editIcon : UIImage ? = UIImage ( systemName : " pencil " )
return [
return [
SectionModel (
SectionModel (
model : . profileInfo ,
model : . profileInfo ,
@ -260,6 +162,12 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
) ,
) ,
SessionCell . Info (
SessionCell . Info (
id : . profileName ,
id : . profileName ,
leadingAccessory : . icon (
editIcon ? . withRenderingMode ( . alwaysTemplate ) ,
size : . mediumAspectFill ,
customTint : . textSecondary ,
shouldFill : true
) ,
title : SessionCell . TextInfo (
title : SessionCell . TextInfo (
state . profile . displayName ( ) ,
state . profile . displayName ( ) ,
font : . titleLarge ,
font : . titleLarge ,
@ -268,14 +176,18 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
) ,
) ,
styling : SessionCell . StyleInfo (
styling : SessionCell . StyleInfo (
alignment : . centerHugging ,
alignment : . centerHugging ,
customPadding : SessionCell . Padding ( top : Values . smallSpacing ) ,
customPadding : SessionCell . Padding (
top : Values . smallSpacing ,
leading : - ( ( IconSize . medium . size + ( Values . smallSpacing * 2 ) ) / 2 ) ,
bottom : Values . mediumSpacing
) ,
backgroundStyle : . noBackground
backgroundStyle : . noBackground
) ,
) ,
accessibility : Accessibility (
accessibility : Accessibility (
identifier : " Username " ,
identifier : " Username " ,
label : state . profile . displayName ( )
label : state . profile . displayName ( )
) ,
) ,
onTap : { [ weak self ] in self ? . setIsEditing( true ) }
onTap : { [ weak self ] in self ? . updateDisplayName( current : state . profile . displayName ( ) ) }
)
)
]
]
) ,
) ,
@ -530,91 +442,123 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
// MARK: - F u n c t i o n s
// MARK: - F u n c t i o n s
private func updateDisplayName ( current : String ) {
// / S e t ` u p d a t e d N a m e ` t o ` c u r r e n t ` s o w e c a n d i s a b l e t h e " s a v e " b u t t o n w h e n t h e r e a r e n o c h a n g e s a n d d o n ' t n e e d t o w o r r y
// / a b o u t r e t r i e v i n g t h e m i n t h e c o n f i r m a t i o n c l o s u r e
self . updatedName = current
self . transitionToScreen (
ConfirmationModal (
info : ConfirmationModal . Info (
title : " displayNameSet " . localized ( ) ,
body : . input (
explanation : nil ,
info : ConfirmationModal . Info . Body . InputInfo (
placeholder : " displayNameEnter " . localized ( ) ,
initialValue : current
) ,
onChange : { [ weak self ] updatedName in self ? . updatedName = updatedName }
) ,
confirmTitle : " save " . localized ( ) ,
confirmEnabled : . afterChange { [ weak self ] _ in
self ? . updatedName ? . trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty = = false &&
self ? . updatedName != current
} ,
cancelStyle : . alert_text ,
dismissOnConfirm : false ,
onConfirm : { [ weak self ] modal in
guard
let finalDisplayName : String = ( self ? . updatedName ? ? " " )
. trimmingCharacters ( in : . whitespacesAndNewlines )
. nullIfEmpty
else { return }
// / C h e c k i f t h e d a t a v i o l a t e s t h e s i z e c o n s t r a i n t s
guard ! Profile . isTooLong ( profileName : finalDisplayName ) else {
self ? . transitionToScreen (
ConfirmationModal (
info : ConfirmationModal . Info (
title : " theError " . localized ( ) ,
body : . text ( " displayNameErrorDescriptionShorter " . localized ( ) ) ,
cancelTitle : " okay " . localized ( ) ,
cancelStyle : . alert_text ,
dismissType : . single
)
) ,
transitionType : . present
)
return
}
// / U p d a t e t h e n i c k n a m e
self ? . updateProfile ( displayNameUpdate : . currentUserUpdate ( finalDisplayName ) ) {
modal . dismiss ( animated : true )
}
}
)
) ,
transitionType : . present
)
}
private func updateProfilePicture ( currentFileName : String ? ) {
private func updateProfilePicture ( currentFileName : String ? ) {
let existingImageData : Data ? = dependencies [ singleton : . storage ] . read { [ userSessionId , dependencies ] db in
let existingImageData : Data ? = dependencies [ singleton : . storage ] . read { [ userSessionId , dependencies ] db in
DisplayPictureManager . displayPicture ( db , id : . user ( userSessionId . hexString ) , using : dependencies )
DisplayPictureManager . displayPicture ( db , id : . user ( userSessionId . hexString ) , using : dependencies )
}
}
let editProfilePictureModalInfo : ConfirmationModal . Info = ConfirmationModal . Info (
self . transitionToScreen (
title : " profileDisplayPictureSet " . localized ( ) ,
ConfirmationModal (
body : . image (
info : ConfirmationModal . Info (
placeholderData : UIImage ( named : " profile_placeholder " ) ? . pngData ( ) ,
title : " profileDisplayPictureSet " . localized ( ) ,
valueData : existingImageData ,
body : . image (
icon : . rightPlus ,
placeholderData : UIImage ( named : " profile_placeholder " ) ? . pngData ( ) ,
style : . circular ,
valueData : existingImageData ,
accessibility : Accessibility (
icon : . rightPlus ,
identifier : " Upload " ,
style : . circular ,
label : " Upload "
accessibility : Accessibility (
) ,
identifier : " Upload " ,
onClick : { [ weak self ] in self ? . showPhotoLibraryForAvatar ( ) }
label : " Upload "
) ,
) ,
confirmTitle : " save " . localized ( ) ,
onClick : { [ weak self ] onDisplayPictureSelected in
confirmAccessibility : Accessibility (
self ? . onDisplayPictureSelected = onDisplayPictureSelected
identifier : " Save button "
self ? . showPhotoLibraryForAvatar ( )
) ,
}
confirmEnabled : . afterChange { info in
) ,
switch info . body {
confirmTitle : " save " . localized ( ) ,
case . image ( _ , let valueData , _ , _ , _ , _ ) : return ( valueData != nil )
confirmAccessibility : Accessibility (
default : return false
identifier : " Save button "
}
) ,
} ,
confirmEnabled : . afterChange { info in
cancelTitle : " remove " . localized ( ) ,
switch info . body {
cancelAccessibility : Accessibility (
case . image ( _ , let valueData , _ , _ , _ , _ ) : return ( valueData != nil )
identifier : " Remove button "
default : return false
) ,
}
cancelEnabled : . bool ( existingImageData != nil ) ,
} ,
hasCloseButton : true ,
cancelTitle : " remove " . localized ( ) ,
dismissOnConfirm : false ,
cancelAccessibility : Accessibility (
onConfirm : { [ weak self ] modal in
identifier : " Remove button "
switch modal . info . body {
) ,
case . image ( _ , . some ( let valueData ) , _ , _ , _ , _ ) :
cancelEnabled : . bool ( existingImageData != nil ) ,
hasCloseButton : true ,
dismissOnConfirm : false ,
onConfirm : { [ weak self ] modal in
switch modal . info . body {
case . image ( _ , . some ( let valueData ) , _ , _ , _ , _ ) :
self ? . updateProfile (
displayPictureUpdate : . currentUserUploadImageData ( valueData ) ,
onComplete : { [ weak modal ] in modal ? . close ( ) }
)
default : modal . close ( )
}
} ,
onCancel : { [ weak self ] modal in
self ? . updateProfile (
self ? . updateProfile (
displayPictureUpdate : . groupUploadImageData ( valueData ) ,
displayPictureUpdate : . currentUserRemove ,
onComplete : { [ weak modal ] in modal ? . close ( ) }
onComplete : { [ weak modal ] in modal ? . close ( ) }
)
)
}
default : modal . close ( )
}
} ,
onCancel : { [ weak self ] modal in
self ? . updateProfile (
displayPictureUpdate : . currentUserRemove ,
onComplete : { [ weak modal ] in modal ? . close ( ) }
)
} ,
afterClosed : { [ weak self ] in
self ? . editProfilePictureModal = nil
self ? . editProfilePictureModalInfo = nil
}
)
let modal : ConfirmationModal = ConfirmationModal ( info : editProfilePictureModalInfo )
self . editProfilePictureModalInfo = editProfilePictureModalInfo
self . editProfilePictureModal = modal
self . transitionToScreen ( modal , transitionType : . present )
}
fileprivate func updatedProfilePictureSelected ( displayPictureUpdate : DisplayPictureManager . Update ) {
guard let info : ConfirmationModal . Info = self . editProfilePictureModalInfo else { return }
self . editProfilePictureModal ? . updateContent (
with : info . with (
body : . image (
placeholderData : UIImage ( named : " profile_placeholder " ) ? . pngData ( ) ,
valueData : {
switch displayPictureUpdate {
case . currentUserUploadImageData ( let imageData ) : return imageData
default : return nil
}
} ( ) ,
icon : . rightPlus ,
style : . circular ,
accessibility : Accessibility (
identifier : " Image picker " ,
label : " Image picker "
) ,
onClick : { [ weak self ] in self ? . showPhotoLibraryForAvatar ( ) }
)
)
)
) ,
transitionType : . present
)
)
}
}
@ -634,7 +578,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
fileprivate func updateProfile (
fileprivate func updateProfile (
displayNameUpdate : Profile . DisplayNameUpdate = . none ,
displayNameUpdate : Profile . DisplayNameUpdate = . none ,
displayPictureUpdate : DisplayPictureManager . Update = . none ,
displayPictureUpdate : DisplayPictureManager . Update = . none ,
onComplete : ( ( ) -> ( ) ) ? = nil
onComplete : @escaping ( ) -> ( )
) {
) {
let viewController = ModalActivityIndicatorViewController ( canCancel : false ) { [ weak self , dependencies ] modalActivityIndicator in
let viewController = ModalActivityIndicatorViewController ( canCancel : false ) { [ weak self , dependencies ] modalActivityIndicator in
Profile . updateLocal (
Profile . updateLocal (
@ -646,7 +590,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
db . afterNextTransactionNested ( using : dependencies ) { _ in
db . afterNextTransactionNested ( using : dependencies ) { _ in
DispatchQueue . main . async {
DispatchQueue . main . async {
modalActivityIndicator . dismiss ( completion : {
modalActivityIndicator . dismiss ( completion : {
onComplete ? ()
onComplete ()
} )
} )
}
}
}
}