Cleaned up the config-based table view controller to be more reusable
Updated the settings screens to have the "rounded group" styling Added a "loading conversations" label to the Message Requests screen before the conversations load Removed the legacy UserCell (replaced with the more reusable 'SessionCell') Renamed a few things to make them more generic and reusable
This commit is contained in:
parent
d9c6f2fc90
commit
40109e0bea
6
Podfile
6
Podfile
|
@ -93,8 +93,10 @@ abstract_target 'GlobalDependencies' do
|
|||
end
|
||||
end
|
||||
|
||||
# No extra dependencies for this
|
||||
target 'SessionUIKit'
|
||||
target 'SessionUIKit' do
|
||||
pod 'GRDB.swift/SQLCipher'
|
||||
pod 'DifferenceKit'
|
||||
end
|
||||
end
|
||||
|
||||
# Actions to perform post-install
|
||||
|
|
|
@ -242,6 +242,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: a646db9086664b8c9e5a0b3d17664a1275dcba9d
|
||||
PODFILE CHECKSUM: 430e3b57d986dc8890415294fc6cf5e4eabfce3e
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
34CF078A203E6B78005C4D61 /* end_call_tone_cept.caf in Resources */ = {isa = PBXBuildFile; fileRef = 34CF0786203E6B78005C4D61 /* end_call_tone_cept.caf */; };
|
||||
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F04F1F7D45A60066283D /* GifPickerCell.swift */; };
|
||||
34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */; };
|
||||
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; };
|
||||
34D99CE4217509C2000AFB39 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99CE3217509C1000AFB39 /* AppEnvironment.swift */; };
|
||||
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; };
|
||||
4503F1BE20470A5B00CEE724 /* classic-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */; };
|
||||
|
@ -243,7 +242,6 @@
|
|||
B8CCF6352396005F0091D419 /* SpaceMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B8CCF6342396005F0091D419 /* SpaceMono-Regular.ttf */; };
|
||||
B8CCF63723961D6D0091D419 /* NewDMVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF63623961D6D0091D419 /* NewDMVC.swift */; };
|
||||
B8CCF63F23975CFB0091D419 /* JoinOpenGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF63E23975CFB0091D419 /* JoinOpenGroupVC.swift */; };
|
||||
B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF6422397711F0091D419 /* SettingsVC.swift */; };
|
||||
B8D07405265C683300F77E07 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B8D07404265C683300F77E07 /* ElegantIcons.ttf */; };
|
||||
B8D07406265C683A00F77E07 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B8D07404265C683300F77E07 /* ElegantIcons.ttf */; };
|
||||
B8D0A25025E3678700C1835E /* LinkDeviceVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D0A24F25E3678700C1835E /* LinkDeviceVC.swift */; };
|
||||
|
@ -301,8 +299,7 @@
|
|||
C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */; };
|
||||
C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */; };
|
||||
C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33100092558FF6D00070591 /* UserCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DDC25217014005D4DA8 /* UserCell.swift */; };
|
||||
C33100282559000A00070591 /* UIView+Rendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33100272559000A00070591 /* UIView+Rendering.swift */; };
|
||||
C33100282559000A00070591 /* UIView+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33100272559000A00070591 /* UIView+Utilities.swift */; };
|
||||
C331FF1F2558F9D300070591 /* SessionUIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C331FF1D2558F9D300070591 /* SessionUIKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C331FF222558F9D300070591 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
|
||||
C331FF232558F9D300070591 /* SessionUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -312,7 +309,7 @@
|
|||
C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */; };
|
||||
C331FFE02558FB0000070591 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B02390C37000BA5194 /* SearchBar.swift */; };
|
||||
C331FFE32558FB0000070591 /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF638239721E20091D419 /* TabBar.swift */; };
|
||||
C331FFE42558FB0000070591 /* OutlineButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B5BCEB2394D869003823C9 /* OutlineButton.swift */; };
|
||||
C331FFE42558FB0000070591 /* SessionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B5BCEB2394D869003823C9 /* SessionButton.swift */; };
|
||||
C331FFE72558FB0000070591 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B423947F2D00BA5194 /* TextField.swift */; };
|
||||
C331FFE82558FB0000070591 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C3CF8824D8EED300E1CCE7 /* TextView.swift */; };
|
||||
C331FFE92558FB0000070591 /* Separator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82B82394911B00BA5194 /* Separator.swift */; };
|
||||
|
@ -388,7 +385,6 @@
|
|||
C38EF2A6255B6D93007E1867 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */; };
|
||||
C38EF2A7255B6D93007E1867 /* ProfilePictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */; };
|
||||
C38EF2B3255B6D9C007E1867 /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */; };
|
||||
C38EF2B4255B6D9C007E1867 /* UIView+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2B2255B6D9C007E1867 /* UIView+Utilities.swift */; };
|
||||
C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */; };
|
||||
C38EF31C255B6DBF007E1867 /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F2255B6DBC007E1867 /* Searcher.swift */; };
|
||||
C38EF324255B6DBF007E1867 /* Bench.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2FA255B6DBD007E1867 /* Bench.swift */; };
|
||||
|
@ -632,9 +628,6 @@
|
|||
FD37EA0128A60473003AE748 /* UIKit+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0028A60473003AE748 /* UIKit+Theme.swift */; };
|
||||
FD37EA0328A9FDCC003AE748 /* HelpViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */; };
|
||||
FD37EA0528AA00C1003AE748 /* NotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0428AA00C1003AE748 /* NotificationSettingsViewModel.swift */; };
|
||||
FD37EA0728AA2CCA003AE748 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0628AA2CCA003AE748 /* SettingsTableViewController.swift */; };
|
||||
FD37EA0928AA2D27003AE748 /* SettingsTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0828AA2D27003AE748 /* SettingsTableViewModel.swift */; };
|
||||
FD37EA0B28AB12E2003AE748 /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0A28AB12E2003AE748 /* SettingsCell.swift */; };
|
||||
FD37EA0D28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */; };
|
||||
FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; };
|
||||
FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */; };
|
||||
|
@ -677,8 +670,6 @@
|
|||
FD6A7A6D2818C61500035AC1 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */; };
|
||||
FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; };
|
||||
FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */; };
|
||||
FD7115EE28C5D79B00B47552 /* SettingsAvatarCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115ED28C5D79B00B47552 /* SettingsAvatarCell.swift */; };
|
||||
FD7115F028C5D7DE00B47552 /* SettingHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EF28C5D7DE00B47552 /* SettingHeaderView.swift */; };
|
||||
FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */; };
|
||||
FD7115F428C71EB200B47552 /* ThreadDisappearingMessagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesViewModel.swift */; };
|
||||
FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115F728C8151C00B47552 /* DisposableBarButtonItem.swift */; };
|
||||
|
@ -699,6 +690,21 @@
|
|||
FD71162A28DA83DF00B47552 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3EE255B6DF6007E1867 /* GradientView.swift */; };
|
||||
FD71162C28E1451400B47552 /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71162B28E1451400B47552 /* Position.swift */; };
|
||||
FD71162E28E168C700B47552 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71162D28E168C700B47552 /* SettingsViewModel.swift */; };
|
||||
FD71163228E2C42A00B47552 /* IconSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71163128E2C42A00B47552 /* IconSize.swift */; };
|
||||
FD71163728E2C50700B47552 /* SessionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */; };
|
||||
FD71163828E2C50700B47552 /* SessionTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */; };
|
||||
FD71163A28E2C53700B47552 /* SessionAvatarCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115ED28C5D79B00B47552 /* SessionAvatarCell.swift */; };
|
||||
FD71163E28E2C82900B47552 /* SessionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0A28AB12E2003AE748 /* SessionCell.swift */; };
|
||||
FD71163F28E2C82C00B47552 /* SessionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */; };
|
||||
FD71164228E2C85A00B47552 /* TransitionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71163328E2C48400B47552 /* TransitionType.swift */; };
|
||||
FD71164428E2CB8A00B47552 /* SessionCell+Accessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164328E2CB8A00B47552 /* SessionCell+Accessory.swift */; };
|
||||
FD71164628E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164528E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift */; };
|
||||
FD71164828E2CE8700B47552 /* SessionCell+AccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164728E2CE8700B47552 /* SessionCell+AccessoryView.swift */; };
|
||||
FD71164A28E3EA5B00B47552 /* DismissType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164928E3EA5B00B47552 /* DismissType.swift */; };
|
||||
FD71164C28E3F5AA00B47552 /* SessionCell+ExtraAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164B28E3F5AA00B47552 /* SessionCell+ExtraAction.swift */; };
|
||||
FD71164E28E3F8CC00B47552 /* SessionCell+Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164D28E3F8CC00B47552 /* SessionCell+Info.swift */; };
|
||||
FD71165028E3F9FA00B47552 /* SessionTableViewModel+NavItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71164F28E3F9FA00B47552 /* SessionTableViewModel+NavItem.swift */; };
|
||||
FD71165228E410BE00B47552 /* SessionTableSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD71165128E410BE00B47552 /* SessionTableSection.swift */; };
|
||||
FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7162DA281B6C440060647B /* TypedTableAlias.swift */; };
|
||||
FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */; };
|
||||
FD716E6628502EE200C96BF4 /* CurrentCallProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */; };
|
||||
|
@ -1069,8 +1075,6 @@
|
|||
34CF0786203E6B78005C4D61 /* end_call_tone_cept.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = end_call_tone_cept.caf; path = Session/Meta/AudioFiles/end_call_tone_cept.caf; sourceTree = SOURCE_ROOT; };
|
||||
34D1F04F1F7D45A60066283D /* GifPickerCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifPickerCell.swift; sourceTree = "<group>"; };
|
||||
34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiphyDownloader.swift; sourceTree = "<group>"; };
|
||||
34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvatarViewHelper.h; sourceTree = "<group>"; };
|
||||
34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AvatarViewHelper.m; sourceTree = "<group>"; };
|
||||
34D99CE3217509C1000AFB39 /* AppEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = "<group>"; };
|
||||
34F308A01ECB469700BB7697 /* OWSBezierPathView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBezierPathView.h; sourceTree = "<group>"; };
|
||||
34F308A11ECB469700BB7697 /* OWSBezierPathView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBezierPathView.m; sourceTree = "<group>"; };
|
||||
|
@ -1299,7 +1303,7 @@
|
|||
B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = "<group>"; };
|
||||
B8B558F026C4BB0600693325 /* CameraManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraManager.swift; sourceTree = "<group>"; };
|
||||
B8B558FE26C4E05E00693325 /* WebRTCSession+MessageHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+MessageHandling.swift"; sourceTree = "<group>"; };
|
||||
B8B5BCEB2394D869003823C9 /* OutlineButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlineButton.swift; sourceTree = "<group>"; };
|
||||
B8B5BCEB2394D869003823C9 /* SessionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionButton.swift; sourceTree = "<group>"; };
|
||||
B8BAC75B2695645400EA1759 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
B8BAC75C2695648500EA1759 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
B8BAC75D2695649000EA1759 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
|
@ -1317,7 +1321,6 @@
|
|||
B8CCF63623961D6D0091D419 /* NewDMVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDMVC.swift; sourceTree = "<group>"; };
|
||||
B8CCF638239721E20091D419 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = "<group>"; };
|
||||
B8CCF63E23975CFB0091D419 /* JoinOpenGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinOpenGroupVC.swift; sourceTree = "<group>"; };
|
||||
B8CCF6422397711F0091D419 /* SettingsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsVC.swift; sourceTree = "<group>"; };
|
||||
B8D07404265C683300F77E07 /* ElegantIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = ElegantIcons.ttf; sourceTree = "<group>"; };
|
||||
B8D0A24F25E3678700C1835E /* LinkDeviceVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkDeviceVC.swift; sourceTree = "<group>"; };
|
||||
B8D0A25825E367AC00C1835E /* Notification+MessageReceiver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Notification+MessageReceiver.swift"; path = "SessionMessagingKit/Sending & Receiving/Notification+MessageReceiver.swift"; sourceTree = SOURCE_ROOT; };
|
||||
|
@ -1346,7 +1349,6 @@
|
|||
C300A5FB2554B0A000555489 /* MessageReceiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiver.swift; sourceTree = "<group>"; };
|
||||
C302093D25DCBF07001F572D /* MentionSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionSelectionView.swift; sourceTree = "<group>"; };
|
||||
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = "<group>"; };
|
||||
C31D1DDC25217014005D4DA8 /* UserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCell.swift; sourceTree = "<group>"; };
|
||||
C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = "<group>"; };
|
||||
C328250E25CA06020062D0A7 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = "<group>"; };
|
||||
C328251E25CA3A900062D0A7 /* QuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1355,7 +1357,7 @@
|
|||
C328254825CA60E60062D0A7 /* ContextMenuVC+Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextMenuVC+Action.swift"; sourceTree = "<group>"; };
|
||||
C328255125CA64470062D0A7 /* ContextMenuVC+ActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContextMenuVC+ActionView.swift"; sourceTree = "<group>"; };
|
||||
C32C5A87256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ClosedGroups.swift"; sourceTree = "<group>"; };
|
||||
C33100272559000A00070591 /* UIView+Rendering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Rendering.swift"; sourceTree = "<group>"; };
|
||||
C33100272559000A00070591 /* UIView+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Utilities.swift"; sourceTree = "<group>"; };
|
||||
C331FF1B2558F9D300070591 /* SessionUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C331FF1D2558F9D300070591 /* SessionUIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionUIKit.h; sourceTree = "<group>"; };
|
||||
C331FF1E2558F9D300070591 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -1472,7 +1474,6 @@
|
|||
C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PlaceholderIcon.swift; path = "SignalUtilitiesKit/Profile Pictures/PlaceholderIcon.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProfilePictureView.swift; path = "SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIViewController+Utilities.swift"; path = "SignalUtilitiesKit/Utilities/UIViewController+Utilities.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2B2255B6D9C007E1867 /* UIView+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+Utilities.swift"; path = "SignalUtilitiesKit/Utilities/UIView+Utilities.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScreenLock.swift; path = "SignalUtilitiesKit/Screen Lock/ScreenLock.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProximityMonitoringManager.swift; path = SessionMessagingKit/Utilities/ProximityMonitoringManager.swift; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2EF255B6DBB007E1867 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Weak.swift; path = SessionUtilitiesKit/General/Weak.swift; sourceTree = SOURCE_ROOT; };
|
||||
|
@ -1712,9 +1713,9 @@
|
|||
FD37EA0028A60473003AE748 /* UIKit+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIKit+Theme.swift"; sourceTree = "<group>"; };
|
||||
FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpViewModel.swift; sourceTree = "<group>"; };
|
||||
FD37EA0428AA00C1003AE748 /* NotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
FD37EA0628AA2CCA003AE748 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = "<group>"; };
|
||||
FD37EA0828AA2D27003AE748 /* SettingsTableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewModel.swift; sourceTree = "<group>"; };
|
||||
FD37EA0A28AB12E2003AE748 /* SettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = "<group>"; };
|
||||
FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewController.swift; sourceTree = "<group>"; };
|
||||
FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableViewModel.swift; sourceTree = "<group>"; };
|
||||
FD37EA0A28AB12E2003AE748 /* SessionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCell.swift; sourceTree = "<group>"; };
|
||||
FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _005_FixDeletedMessageReadState.swift; sourceTree = "<group>"; };
|
||||
FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _006_FixHiddenModAdminSupport.swift; sourceTree = "<group>"; };
|
||||
FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlteration.swift; sourceTree = "<group>"; };
|
||||
|
@ -1756,8 +1757,8 @@
|
|||
FD6A7A6C2818C61500035AC1 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
|
||||
FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
|
||||
FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
FD7115ED28C5D79B00B47552 /* SettingsAvatarCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAvatarCell.swift; sourceTree = "<group>"; };
|
||||
FD7115EF28C5D7DE00B47552 /* SettingHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingHeaderView.swift; sourceTree = "<group>"; };
|
||||
FD7115ED28C5D79B00B47552 /* SessionAvatarCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAvatarCell.swift; sourceTree = "<group>"; };
|
||||
FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHeaderView.swift; sourceTree = "<group>"; };
|
||||
FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _010_AddThreadIdToFTS.swift; sourceTree = "<group>"; };
|
||||
FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDisappearingMessagesViewModel.swift; sourceTree = "<group>"; };
|
||||
FD7115F728C8151C00B47552 /* DisposableBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisposableBarButtonItem.swift; sourceTree = "<group>"; };
|
||||
|
@ -1778,6 +1779,16 @@
|
|||
FD71162128D983ED00B47552 /* QRCodeScanningViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScanningViewController.swift; sourceTree = "<group>"; };
|
||||
FD71162B28E1451400B47552 /* Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = "<group>"; };
|
||||
FD71162D28E168C700B47552 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
FD71163128E2C42A00B47552 /* IconSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSize.swift; sourceTree = "<group>"; };
|
||||
FD71163328E2C48400B47552 /* TransitionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionType.swift; sourceTree = "<group>"; };
|
||||
FD71164328E2CB8A00B47552 /* SessionCell+Accessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+Accessory.swift"; sourceTree = "<group>"; };
|
||||
FD71164528E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionHighlightingBackgroundLabel.swift; sourceTree = "<group>"; };
|
||||
FD71164728E2CE8700B47552 /* SessionCell+AccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+AccessoryView.swift"; sourceTree = "<group>"; };
|
||||
FD71164928E3EA5B00B47552 /* DismissType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissType.swift; sourceTree = "<group>"; };
|
||||
FD71164B28E3F5AA00B47552 /* SessionCell+ExtraAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+ExtraAction.swift"; sourceTree = "<group>"; };
|
||||
FD71164D28E3F8CC00B47552 /* SessionCell+Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCell+Info.swift"; sourceTree = "<group>"; };
|
||||
FD71164F28E3F9FA00B47552 /* SessionTableViewModel+NavItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionTableViewModel+NavItem.swift"; sourceTree = "<group>"; };
|
||||
FD71165128E410BE00B47552 /* SessionTableSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTableSection.swift; sourceTree = "<group>"; };
|
||||
FD7162DA281B6C440060647B /* TypedTableAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableAlias.swift; sourceTree = "<group>"; };
|
||||
FD716E6328502DDD00C96BF4 /* CallManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManagerProtocol.swift; sourceTree = "<group>"; };
|
||||
FD716E6528502EE200C96BF4 /* CurrentCallProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentCallProtocol.swift; sourceTree = "<group>"; };
|
||||
|
@ -2181,8 +2192,6 @@
|
|||
children = (
|
||||
7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */,
|
||||
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */,
|
||||
34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */,
|
||||
34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */,
|
||||
FD848B9728422F1A000E298B /* Date+Utilities.swift */,
|
||||
FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */,
|
||||
45C0DC1A1E68FE9000E04C47 /* UIApplication+OWS.swift */,
|
||||
|
@ -2553,6 +2562,8 @@
|
|||
B8CCF63B239757C10091D419 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD71164128E2C83500B47552 /* Types */,
|
||||
FD71164028E2C83000B47552 /* Views */,
|
||||
4CA46F4B219CCC630038ABDE /* CaptionView.swift */,
|
||||
34F308A01ECB469700BB7697 /* OWSBezierPathView.h */,
|
||||
34F308A11ECB469700BB7697 /* OWSBezierPathView.m */,
|
||||
|
@ -2561,9 +2572,10 @@
|
|||
4542DF53208D40AC007B4E76 /* LoadingViewController.swift */,
|
||||
FD71162128D983ED00B47552 /* QRCodeScanningViewController.swift */,
|
||||
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */,
|
||||
C31D1DDC25217014005D4DA8 /* UserCell.swift */,
|
||||
C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */,
|
||||
FD52090828B59411006098F6 /* ScreenLockUI.swift */,
|
||||
FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */,
|
||||
FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2764,6 +2776,7 @@
|
|||
FD37E9F428A5F0FB003AE748 /* Database */,
|
||||
C331FFCC2558FAF300070591 /* Components */,
|
||||
C331FF5E2558FA0F00070591 /* Style Guide */,
|
||||
FD71163028E2C41900B47552 /* Types */,
|
||||
C331FFAE2558FA7700070591 /* Utilities */,
|
||||
FD37E9F528A5F106003AE748 /* Configuration.swift */,
|
||||
);
|
||||
|
@ -2796,9 +2809,8 @@
|
|||
B8544E3023D16CA500299F14 /* DeviceUtilities.swift */,
|
||||
FD37E9D628A20B5D003AE748 /* UIColor+Utilities.swift */,
|
||||
B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */,
|
||||
C33100272559000A00070591 /* UIView+Rendering.swift */,
|
||||
C33100272559000A00070591 /* UIView+Utilities.swift */,
|
||||
FD71161F28D97ABC00B47552 /* UIImage+Tinting.swift */,
|
||||
FD71162B28E1451400B47552 /* Position.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2806,7 +2818,7 @@
|
|||
C331FFCC2558FAF300070591 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B8B5BCEB2394D869003823C9 /* OutlineButton.swift */,
|
||||
B8B5BCEB2394D869003823C9 /* SessionButton.swift */,
|
||||
FD52090228B4680F006098F6 /* RadioButton.swift */,
|
||||
B8BB82B02390C37000BA5194 /* SearchBar.swift */,
|
||||
B8BB82B82394911B00BA5194 /* Separator.swift */,
|
||||
|
@ -2888,11 +2900,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
FD37E9CD28A1E682003AE748 /* Views */,
|
||||
B8CCF6422397711F0091D419 /* SettingsVC.swift */,
|
||||
B886B4A62398B23E00211ABE /* QRCodeVC.swift */,
|
||||
FD37EA0828AA2D27003AE748 /* SettingsTableViewModel.swift */,
|
||||
FD37EA0628AA2CCA003AE748 /* SettingsTableViewController.swift */,
|
||||
FD71162D28E168C700B47552 /* SettingsViewModel.swift */,
|
||||
B886B4A62398B23E00211ABE /* QRCodeVC.swift */,
|
||||
FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */,
|
||||
FD37EA0428AA00C1003AE748 /* NotificationSettingsViewModel.swift */,
|
||||
FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */,
|
||||
|
@ -3299,7 +3308,6 @@
|
|||
FD71161D28D9772700B47552 /* UIViewController+OWS.swift */,
|
||||
C38EF23C255B6D66007E1867 /* UIColor+OWS.h */,
|
||||
C38EF242255B6D67007E1867 /* UIColor+OWS.m */,
|
||||
C38EF2B2255B6D9C007E1867 /* UIView+Utilities.swift */,
|
||||
C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */,
|
||||
C38EF307255B6DBE007E1867 /* UIGestureRecognizer+OWS.swift */,
|
||||
C38EF239255B6D66007E1867 /* UIFont+OWS.h */,
|
||||
|
@ -3691,9 +3699,6 @@
|
|||
FD37E9D028A1F2EB003AE748 /* ThemeSelectionView.swift */,
|
||||
FD37E9DA28A244E9003AE748 /* ThemePreviewView.swift */,
|
||||
FD37E9DC28A384EB003AE748 /* PrimaryColorSelectionView.swift */,
|
||||
FD7115EF28C5D7DE00B47552 /* SettingHeaderView.swift */,
|
||||
FD7115ED28C5D79B00B47552 /* SettingsAvatarCell.swift */,
|
||||
FD37EA0A28AB12E2003AE748 /* SettingsCell.swift */,
|
||||
FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */,
|
||||
);
|
||||
path = Views;
|
||||
|
@ -3848,6 +3853,41 @@
|
|||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD71163028E2C41900B47552 /* Types */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD71162B28E1451400B47552 /* Position.swift */,
|
||||
FD71163128E2C42A00B47552 /* IconSize.swift */,
|
||||
);
|
||||
path = Types;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD71164028E2C83000B47552 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD7115EF28C5D7DE00B47552 /* SessionHeaderView.swift */,
|
||||
FD7115ED28C5D79B00B47552 /* SessionAvatarCell.swift */,
|
||||
FD37EA0A28AB12E2003AE748 /* SessionCell.swift */,
|
||||
FD71164728E2CE8700B47552 /* SessionCell+AccessoryView.swift */,
|
||||
FD71164528E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD71164128E2C83500B47552 /* Types */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD71164928E3EA5B00B47552 /* DismissType.swift */,
|
||||
FD71163328E2C48400B47552 /* TransitionType.swift */,
|
||||
FD71164D28E3F8CC00B47552 /* SessionCell+Info.swift */,
|
||||
FD71164328E2CB8A00B47552 /* SessionCell+Accessory.swift */,
|
||||
FD71164B28E3F5AA00B47552 /* SessionCell+ExtraAction.swift */,
|
||||
FD71165128E410BE00B47552 /* SessionTableSection.swift */,
|
||||
FD71164F28E3F9FA00B47552 /* SessionTableViewModel+NavItem.swift */,
|
||||
);
|
||||
path = Types;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD716E6F28505E5100C96BF4 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -5125,9 +5165,10 @@
|
|||
FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */,
|
||||
C331FF9A2558FA6B00070591 /* Values.swift in Sources */,
|
||||
FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */,
|
||||
C331FFE42558FB0000070591 /* OutlineButton.swift in Sources */,
|
||||
C331FFE42558FB0000070591 /* SessionButton.swift in Sources */,
|
||||
C331FFE92558FB0000070591 /* Separator.swift in Sources */,
|
||||
C33100282559000A00070591 /* UIView+Rendering.swift in Sources */,
|
||||
FD71163228E2C42A00B47552 /* IconSize.swift in Sources */,
|
||||
C33100282559000A00070591 /* UIView+Utilities.swift in Sources */,
|
||||
FD37E9CA28A1E4BD003AE748 /* Theme+ClassicLight.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -5220,7 +5261,6 @@
|
|||
C38EF2B3255B6D9C007E1867 /* UIViewController+Utilities.swift in Sources */,
|
||||
C38EF3BE255B6DE7007E1867 /* OrderedDictionary.swift in Sources */,
|
||||
C33FDD6E255A582000E217F9 /* NSURLSessionDataTask+StatusCode.m in Sources */,
|
||||
C38EF2B4255B6D9C007E1867 /* UIView+Utilities.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -5560,11 +5600,11 @@
|
|||
B8CCF63723961D6D0091D419 /* NewDMVC.swift in Sources */,
|
||||
FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */,
|
||||
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */,
|
||||
FD71164C28E3F5AA00B47552 /* SessionCell+ExtraAction.swift in Sources */,
|
||||
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */,
|
||||
7BFA8AE32831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift in Sources */,
|
||||
C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */,
|
||||
FD52090528B4915F006098F6 /* PrivacySettingsViewModel.swift in Sources */,
|
||||
FD37EA0928AA2D27003AE748 /* SettingsTableViewModel.swift in Sources */,
|
||||
7BAF54D027ACCEEC003D12F8 /* EmptySearchResultCell.swift in Sources */,
|
||||
B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */,
|
||||
FD37E9D928A230F2003AE748 /* TraitObservingWindow.swift in Sources */,
|
||||
|
@ -5585,6 +5625,7 @@
|
|||
45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */,
|
||||
B83524A525C3BA4B0089A44F /* InfoMessageCell.swift in Sources */,
|
||||
7B9F71D82853100A006DFE7B /* EmojiWithSkinTones.swift in Sources */,
|
||||
FD71164E28E3F8CC00B47552 /* SessionCell+Info.swift in Sources */,
|
||||
B84A89BC25DE328A0040017D /* ProfilePictureVC.swift in Sources */,
|
||||
FDCDB8E02811007F00352A0C /* HomeViewModel.swift in Sources */,
|
||||
7B0EFDF62755CC5400FFAAE7 /* CallMissedTipsModal.swift in Sources */,
|
||||
|
@ -5603,10 +5644,14 @@
|
|||
7B8C44C528B49DDA00FBE25F /* NewConversationVC.swift in Sources */,
|
||||
7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */,
|
||||
B849789625D4A2F500D0D0B3 /* LinkPreviewView.swift in Sources */,
|
||||
FD71164428E2CB8A00B47552 /* SessionCell+Accessory.swift in Sources */,
|
||||
7B1B52DF28580D51006069F2 /* EmojiPickerCollectionView.swift in Sources */,
|
||||
FD71165228E410BE00B47552 /* SessionTableSection.swift in Sources */,
|
||||
C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */,
|
||||
C3548F0624456447009433A8 /* PNModeVC.swift in Sources */,
|
||||
FD71164828E2CE8700B47552 /* SessionCell+AccessoryView.swift in Sources */,
|
||||
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */,
|
||||
FD71163A28E2C53700B47552 /* SessionAvatarCell.swift in Sources */,
|
||||
7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */,
|
||||
B835247925C38D880089A44F /* MessageCell.swift in Sources */,
|
||||
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */,
|
||||
|
@ -5634,6 +5679,7 @@
|
|||
B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */,
|
||||
7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */,
|
||||
7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */,
|
||||
FD71163F28E2C82C00B47552 /* SessionHeaderView.swift in Sources */,
|
||||
B877E24226CA12910007970A /* CallVC.swift in Sources */,
|
||||
7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */,
|
||||
C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */,
|
||||
|
@ -5641,11 +5687,11 @@
|
|||
B88FA7F2260C3EB10049422F /* OpenGroupSuggestionGrid.swift in Sources */,
|
||||
FD71160428C95B5600B47552 /* PhotoCollectionPickerViewModel.swift in Sources */,
|
||||
FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */,
|
||||
FD7115EE28C5D79B00B47552 /* SettingsAvatarCell.swift in Sources */,
|
||||
FD71163E28E2C82900B47552 /* SessionCell.swift in Sources */,
|
||||
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,
|
||||
C328254925CA60E60062D0A7 /* ContextMenuVC+Action.swift in Sources */,
|
||||
FD71164628E2CC1300B47552 /* SessionHighlightingBackgroundLabel.swift in Sources */,
|
||||
4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */,
|
||||
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */,
|
||||
B8B558F126C4BB0600693325 /* CameraManager.swift in Sources */,
|
||||
B8F5F71A25F1B35C003BF8D4 /* MediaPlaceholderView.swift in Sources */,
|
||||
4C21D5D8223AC60F00EF8A77 /* PhotoCapture.swift in Sources */,
|
||||
|
@ -5664,9 +5710,9 @@
|
|||
7B9F71D02852EEE2006DFE7B /* Emoji+Category.swift in Sources */,
|
||||
7BAADFCC27B0EF23007BCF92 /* CallVideoView.swift in Sources */,
|
||||
B8CCF63F23975CFB0091D419 /* JoinOpenGroupVC.swift in Sources */,
|
||||
FD71164228E2C85A00B47552 /* TransitionType.swift in Sources */,
|
||||
FD848B9828422F1A000E298B /* Date+Utilities.swift in Sources */,
|
||||
FD37E9DB28A244E9003AE748 /* ThemePreviewView.swift in Sources */,
|
||||
FD37EA0728AA2CCA003AE748 /* SettingsTableViewController.swift in Sources */,
|
||||
B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */,
|
||||
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
|
||||
7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */,
|
||||
|
@ -5675,8 +5721,8 @@
|
|||
B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */,
|
||||
7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */,
|
||||
FD87DCFC28B755B800AF0F98 /* BlockedContactsViewController.swift in Sources */,
|
||||
B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */,
|
||||
7B13E1E92810F01300BD4F64 /* SessionCallManager+Action.swift in Sources */,
|
||||
FD71163728E2C50700B47552 /* SessionTableViewController.swift in Sources */,
|
||||
C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */,
|
||||
7B1581E827210ECC00848B49 /* RenderView.swift in Sources */,
|
||||
FD87DCFA28B74DB300AF0F98 /* ConversationSettingsViewModel.swift in Sources */,
|
||||
|
@ -5696,7 +5742,6 @@
|
|||
FD09C5E628260FF9000CE219 /* MediaGalleryViewModel.swift in Sources */,
|
||||
7B9F71D32852EEE2006DFE7B /* Emoji.swift in Sources */,
|
||||
C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */,
|
||||
FD7115F028C5D7DE00B47552 /* SettingHeaderView.swift in Sources */,
|
||||
B82B4090239DD75000A248E7 /* RestoreVC.swift in Sources */,
|
||||
3488F9362191CC4000E524CC /* MediaView.swift in Sources */,
|
||||
B8569AC325CB5D2900DBA3DB /* ConversationVC+Interaction.swift in Sources */,
|
||||
|
@ -5708,7 +5753,6 @@
|
|||
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */,
|
||||
B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */,
|
||||
7B0EFDF0275084AA00FFAAE7 /* CallMessageCell.swift in Sources */,
|
||||
FD37EA0B28AB12E2003AE748 /* SettingsCell.swift in Sources */,
|
||||
7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */,
|
||||
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */,
|
||||
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
|
||||
|
@ -5733,13 +5777,15 @@
|
|||
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
|
||||
FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */,
|
||||
C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */,
|
||||
FD71165028E3F9FA00B47552 /* SessionTableViewModel+NavItem.swift in Sources */,
|
||||
FD71163828E2C50700B47552 /* SessionTableViewModel.swift in Sources */,
|
||||
FD71164A28E3EA5B00B47552 /* DismissType.swift in Sources */,
|
||||
C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */,
|
||||
FD37E9CC28A1E578003AE748 /* AppearanceViewController.swift in Sources */,
|
||||
B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */,
|
||||
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */,
|
||||
C328254025CA55880062D0A7 /* ContextMenuVC.swift in Sources */,
|
||||
3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */,
|
||||
C33100092558FF6D00070591 /* UserCell.swift in Sources */,
|
||||
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */,
|
||||
FD71162E28E168C700B47552 /* SettingsViewModel.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
import UIKit
|
||||
import GRDB
|
||||
import DifferenceKit
|
||||
import PromiseKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate {
|
||||
private struct GroupMemberDisplayInfo: FetchableRecord, Decodable {
|
||||
private struct GroupMemberDisplayInfo: FetchableRecord, Equatable, Hashable, Decodable, Differentiable {
|
||||
let profileId: String
|
||||
let role: GroupMember.Role
|
||||
let profile: Profile?
|
||||
|
@ -44,8 +45,8 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var addMembersButton: OutlineButton = {
|
||||
let result: OutlineButton = OutlineButton(style: .regular, size: .medium)
|
||||
private lazy var addMembersButton: SessionButton = {
|
||||
let result: SessionButton = SessionButton(style: .bordered, size: .medium)
|
||||
result.setTitle("Add Members", for: UIControl.State.normal)
|
||||
result.addTarget(self, action: #selector(addMembers), for: UIControl.Event.touchUpInside)
|
||||
result.contentEdgeInsets = UIEdgeInsets(top: 0, leading: Values.mediumSpacing, bottom: 0, trailing: Values.mediumSpacing)
|
||||
|
@ -60,7 +61,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
result.separatorStyle = .none
|
||||
result.themeBackgroundColor = .clear
|
||||
result.isScrollEnabled = false
|
||||
result.register(view: UserCell.self)
|
||||
result.register(view: SessionCell.self)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -200,15 +201,26 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: UserCell = tableView.dequeue(type: UserCell.self, for: indexPath)
|
||||
let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath)
|
||||
let displayInfo: GroupMemberDisplayInfo = membersAndZombies[indexPath.row]
|
||||
cell.update(
|
||||
with: membersAndZombies[indexPath.row].profileId,
|
||||
profile: membersAndZombies[indexPath.row].profile,
|
||||
isZombie: (membersAndZombies[indexPath.row].role == .zombie),
|
||||
accessory: (adminIds.contains(userPublicKey) ?
|
||||
.none :
|
||||
.lock
|
||||
)
|
||||
with: SessionCell.Info(
|
||||
id: displayInfo,
|
||||
leftAccessory: .profile(displayInfo.profileId, displayInfo.profile),
|
||||
title: (
|
||||
displayInfo.profile?.displayName() ??
|
||||
Profile.truncated(id: displayInfo.profileId, threadVariant: .contact)
|
||||
),
|
||||
rightAccessory: (adminIds.contains(userPublicKey) ? nil :
|
||||
.icon(
|
||||
UIImage(named: "ic_lock_outline")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
customTint: .textSecondary
|
||||
)
|
||||
)
|
||||
),
|
||||
style: .edgeToEdge,
|
||||
position: Position.with(indexPath.row, count: membersAndZombies.count)
|
||||
)
|
||||
|
||||
return cell
|
||||
|
|
|
@ -102,10 +102,11 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
bottom: Values.footerGradientHeight(window: UIApplication.shared.keyWindow),
|
||||
trailing: 0
|
||||
)
|
||||
result.register(view: UserCell.self)
|
||||
result.register(view: SessionCell.self)
|
||||
result.touchDelegate = self
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
result.sectionHeaderTopPadding = 0
|
||||
}
|
||||
|
@ -127,8 +128,8 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var createGroupButton: OutlineButton = {
|
||||
let result = OutlineButton(style: .regular, size: .large)
|
||||
private lazy var createGroupButton: SessionButton = {
|
||||
let result = SessionButton(style: .bordered, size: .large)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle("CREATE_GROUP_BUTTON_TITLE".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(createClosedGroup), for: .touchUpInside)
|
||||
|
@ -165,7 +166,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
explanationLabel.numberOfLines = 0
|
||||
|
||||
let createNewPrivateChatButton: OutlineButton = OutlineButton(style: .regular, size: .medium)
|
||||
let createNewPrivateChatButton: SessionButton = SessionButton(style: .bordered, size: .medium)
|
||||
createNewPrivateChatButton.setTitle("vc_create_closed_group_empty_state_button_title".localized(), for: .normal)
|
||||
createNewPrivateChatButton.addTarget(self, action: #selector(createNewDM), for: .touchUpInside)
|
||||
createNewPrivateChatButton.set(.width, to: 196)
|
||||
|
@ -205,13 +206,19 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath)
|
||||
let profile: Profile = data[indexPath.section].elements[indexPath.row]
|
||||
let cell: UserCell = tableView.dequeue(type: UserCell.self, for: indexPath)
|
||||
cell.update(
|
||||
with: profile.id,
|
||||
profile: profile,
|
||||
isZombie: false,
|
||||
accessory: .tick(isSelected: selectedContacts.contains(profile.id))
|
||||
with: SessionCell.Info(
|
||||
id: profile,
|
||||
leftAccessory: .profile(profile.id, profile),
|
||||
title: profile.displayName(),
|
||||
rightAccessory: .radio(isSelected: { [weak self] in
|
||||
self?.selectedContacts.contains(profile.id) == true
|
||||
})
|
||||
),
|
||||
style: .edgeToEdge,
|
||||
position: Position.with(indexPath.row, count: data[indexPath.section].elements.count)
|
||||
)
|
||||
|
||||
return cell
|
||||
|
|
|
@ -29,7 +29,7 @@ extension ConversationVC:
|
|||
}
|
||||
|
||||
@objc func openSettings() {
|
||||
let viewController: SettingsTableViewController = SettingsTableViewController(
|
||||
let viewController: SessionTableViewController = SessionTableViewController(
|
||||
viewModel: ThreadSettingsViewModel(
|
||||
threadId: self.viewModel.threadData.threadId,
|
||||
threadVariant: self.viewModel.threadData.threadVariant,
|
||||
|
@ -73,7 +73,7 @@ extension ConversationVC:
|
|||
) { [weak self] _ in
|
||||
self?.dismiss(animated: true) {
|
||||
let navController: OWSNavigationController = OWSNavigationController(
|
||||
rootViewController: SettingsTableViewController(
|
||||
rootViewController: SessionTableViewController(
|
||||
viewModel: PrivacySettingsViewModel(
|
||||
shouldShowCloseButton: true
|
||||
)
|
||||
|
|
|
@ -232,7 +232,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
}()
|
||||
|
||||
private lazy var messageRequestAcceptButton: UIButton = {
|
||||
let result: OutlineButton = OutlineButton(style: .regular, size: .medium)
|
||||
let result: SessionButton = SessionButton(style: .bordered, size: .medium)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle("TXT_DELETE_ACCEPT".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(acceptMessageRequest), for: .touchUpInside)
|
||||
|
@ -241,7 +241,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
}()
|
||||
|
||||
private lazy var messageRequestDeleteButton: UIButton = {
|
||||
let result: OutlineButton = OutlineButton(style: .destructive, size: .medium)
|
||||
let result: SessionButton = SessionButton(style: .destructive, size: .medium)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle("TXT_DECLINE_TITLE".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(deleteMessageRequest), for: .touchUpInside)
|
||||
|
|
|
@ -8,7 +8,7 @@ import SessionUIKit
|
|||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class ThreadDisappearingMessagesViewModel: SettingsTableViewModel<ThreadDisappearingMessagesViewModel.NavButton, ThreadDisappearingMessagesViewModel.Section, ThreadDisappearingMessagesViewModel.Item> {
|
||||
class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappearingMessagesViewModel.NavButton, ThreadDisappearingMessagesViewModel.Section, ThreadDisappearingMessagesViewModel.Item> {
|
||||
// MARK: - Config
|
||||
|
||||
enum NavButton: Equatable {
|
||||
|
@ -16,7 +16,7 @@ class ThreadDisappearingMessagesViewModel: SettingsTableViewModel<ThreadDisappea
|
|||
case save
|
||||
}
|
||||
|
||||
public enum Section: SettingSection {
|
||||
public enum Section: SessionTableSection {
|
||||
case content
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,7 @@ class ThreadDisappearingMessagesViewModel: SettingsTableViewModel<ThreadDisappea
|
|||
id: .cancel,
|
||||
systemItem: .cancel,
|
||||
accessibilityIdentifier: "Cancel button"
|
||||
)
|
||||
) { [weak self] in self?.dismissScreen() }
|
||||
]).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
@ -66,25 +66,15 @@ class ThreadDisappearingMessagesViewModel: SettingsTableViewModel<ThreadDisappea
|
|||
id: .save,
|
||||
systemItem: .save,
|
||||
accessibilityIdentifier: "Save button"
|
||||
)
|
||||
) { [weak self] in
|
||||
self?.saveChanges()
|
||||
self?.dismissScreen()
|
||||
}
|
||||
]
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
override var closeScreen: AnyPublisher<Bool, Never> {
|
||||
navItemTapped
|
||||
.handleEvents(receiveOutput: { [weak self] navItemId in
|
||||
switch navItemId {
|
||||
case .save: self?.saveChanges()
|
||||
default: break
|
||||
}
|
||||
self?.setIsEditing(true)
|
||||
})
|
||||
.map { _ in false }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
override var title: String { "DISAPPEARING_MESSAGES".localized() }
|
||||
|
@ -107,15 +97,14 @@ class ThreadDisappearingMessagesViewModel: SettingsTableViewModel<ThreadDisappea
|
|||
SectionModel(
|
||||
model: .content,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: Item(title: "DISAPPEARING_MESSAGES_OFF".localized()),
|
||||
title: "DISAPPEARING_MESSAGES_OFF".localized(),
|
||||
action: .listSelection(
|
||||
rightAccessory: .radio(
|
||||
isSelected: { (self?.currentSelection.value == 0) },
|
||||
storedSelection: (self?.config.isEnabled == false),
|
||||
shouldAutoSave: false,
|
||||
selectValue: { self?.currentSelection.send(0) }
|
||||
)
|
||||
storedSelection: (self?.config.isEnabled == false)
|
||||
),
|
||||
onTap: { self?.currentSelection.send(0) }
|
||||
)
|
||||
].appending(
|
||||
contentsOf: DisappearingMessagesConfiguration.validDurationsSeconds
|
||||
|
@ -125,15 +114,14 @@ class ThreadDisappearingMessagesViewModel: SettingsTableViewModel<ThreadDisappea
|
|||
useShortFormat: false
|
||||
)
|
||||
|
||||
return SettingInfo(
|
||||
return SessionCell.Info(
|
||||
id: Item(title: title),
|
||||
title: title,
|
||||
action: .listSelection(
|
||||
rightAccessory: .radio(
|
||||
isSelected: { (self?.currentSelection.value == duration) },
|
||||
storedSelection: (self?.config.durationSeconds == duration),
|
||||
shouldAutoSave: false,
|
||||
selectValue: { self?.currentSelection.send(duration) }
|
||||
)
|
||||
storedSelection: (self?.config.durationSeconds == duration)
|
||||
),
|
||||
onTap: { self?.currentSelection.send(duration) }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -9,7 +9,7 @@ import SessionMessagingKit
|
|||
import SignalUtilitiesKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.NavButton, ThreadSettingsViewModel.Section, ThreadSettingsViewModel.Setting> {
|
||||
class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.NavButton, ThreadSettingsViewModel.Section, ThreadSettingsViewModel.Setting> {
|
||||
// MARK: - Config
|
||||
|
||||
enum NavState {
|
||||
|
@ -23,7 +23,7 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
|
|||
case done
|
||||
}
|
||||
|
||||
public enum Section: SettingSection {
|
||||
public enum Section: SessionTableSection {
|
||||
case conversationInfo
|
||||
case content
|
||||
}
|
||||
|
@ -225,17 +225,18 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
|
|||
SectionModel(
|
||||
model: .conversationInfo,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .threadInfo,
|
||||
title: threadViewModel.displayName,
|
||||
action: .threadInfo(
|
||||
leftAccessory: .threadInfo(
|
||||
threadViewModel: threadViewModel,
|
||||
avatarTapped: { [weak self] in
|
||||
self?.updateProfilePicture(threadViewModel: threadViewModel)
|
||||
},
|
||||
titleTapped: { [weak self] in self?.setIsEditing(true) },
|
||||
titleChanged: { [weak self] text in self?.editedDisplayName = text }
|
||||
)
|
||||
),
|
||||
title: threadViewModel.displayName,
|
||||
shouldHaveBackground: false
|
||||
)
|
||||
]
|
||||
),
|
||||
|
@ -243,74 +244,88 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
|
|||
model: .content,
|
||||
elements: [
|
||||
(threadVariant == .closedGroup ? nil :
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .copyThreadId,
|
||||
icon: UIImage(named: "ic_copy")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "ic_copy")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: (threadVariant == .openGroup ?
|
||||
"COPY_GROUP_URL".localized() :
|
||||
"vc_conversation_settings_copy_session_id_button_title".localized()
|
||||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).copy_thread_id",
|
||||
action: .trigger(showChevron: false) {
|
||||
onTap: {
|
||||
UIPasteboard.general.string = threadId
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .allMedia,
|
||||
icon: UIImage(named: "actionsheet_camera_roll_black")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "actionsheet_camera_roll_black")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: MediaStrings.allMedia,
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).all_media",
|
||||
action: .push(showChevron: false) {
|
||||
return MediaGalleryViewModel.createAllMediaViewController(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
focusedAttachmentId: nil
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
MediaGalleryViewModel.createAllMediaViewController(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
focusedAttachmentId: nil
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .searchConversation,
|
||||
icon: UIImage(named: "conversation_settings_search")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "conversation_settings_search")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "CONVERSATION_SETTINGS_SEARCH".localized(),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).search",
|
||||
action: .trigger(showChevron: false) { [weak self] in
|
||||
onTap: { [weak self] in
|
||||
self?.didTriggerSearch()
|
||||
}
|
||||
),
|
||||
|
||||
(threadVariant != .openGroup ? nil :
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .addToOpenGroup,
|
||||
icon: UIImage(named: "ic_plus_24")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "ic_plus_24")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "vc_conversation_settings_invite_button_title".localized(),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).add_to_open_group",
|
||||
action: .push(showChevron: false) {
|
||||
return UserSelectionVC(
|
||||
with: "vc_conversation_settings_invite_button_title".localized(),
|
||||
excluding: Set()
|
||||
) { [weak self] selectedUsers in
|
||||
self?.addUsersToOpenGoup(selectedUsers: selectedUsers)
|
||||
}
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
UserSelectionVC(
|
||||
with: "vc_conversation_settings_invite_button_title".localized(),
|
||||
excluding: Set()
|
||||
) { [weak self] selectedUsers in
|
||||
self?.addUsersToOpenGoup(selectedUsers: selectedUsers)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
(threadVariant == .openGroup || threadViewModel.threadIsBlocked == true ? nil :
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .disappearingMessages,
|
||||
icon: UIImage(
|
||||
named: (disappearingMessagesConfig.isEnabled ?
|
||||
"ic_timer" :
|
||||
"ic_timer_disabled"
|
||||
)
|
||||
)?.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(
|
||||
named: (disappearingMessagesConfig.isEnabled ?
|
||||
"ic_timer" :
|
||||
"ic_timer_disabled"
|
||||
)
|
||||
)?.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "DISAPPEARING_MESSAGES".localized(),
|
||||
subtitle: (disappearingMessagesConfig.isEnabled ?
|
||||
String(
|
||||
|
@ -320,11 +335,13 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
|
|||
"DISAPPEARING_MESSAGES_SUBTITLE_OFF".localized()
|
||||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).disappearing_messages",
|
||||
action: .push(showChevron: false) {
|
||||
SettingsTableViewController(
|
||||
viewModel: ThreadDisappearingMessagesViewModel(
|
||||
threadId: threadId,
|
||||
config: disappearingMessagesConfig
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
SessionTableViewController(
|
||||
viewModel: ThreadDisappearingMessagesViewModel(
|
||||
threadId: threadId,
|
||||
config: disappearingMessagesConfig
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -332,84 +349,95 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
|
|||
),
|
||||
|
||||
(!currentUserIsClosedGroupMember ? nil :
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .editGroup,
|
||||
icon: UIImage(named: "table_ic_group_edit")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "table_ic_group_edit")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "EDIT_GROUP_ACTION".localized(),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).edit_group",
|
||||
action: .push(showChevron: false) {
|
||||
EditClosedGroupVC(threadId: threadId)
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(EditClosedGroupVC(threadId: threadId))
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
(!currentUserIsClosedGroupMember ? nil :
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .leaveGroup,
|
||||
icon: UIImage(named: "table_ic_group_leave")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "table_ic_group_leave")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "LEAVE_GROUP_ACTION".localized(),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).leave_group",
|
||||
action: .present {
|
||||
ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "CONFIRM_LEAVE_GROUP_TITLE".localized(),
|
||||
explanation: (currentUserIsClosedGroupMember ?
|
||||
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone." :
|
||||
"CONFIRM_LEAVE_GROUP_DESCRIPTION".localized()
|
||||
),
|
||||
confirmTitle: "LEAVE_BUTTON_TITLE".localized(),
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .textPrimary
|
||||
) { _ in
|
||||
Storage.shared.writeAsync { db in
|
||||
try MessageSender.leave(db, groupPublicKey: threadId)
|
||||
}
|
||||
}
|
||||
)
|
||||
confirmationInfo: ConfirmationModal.Info(
|
||||
title: "CONFIRM_LEAVE_GROUP_TITLE".localized(),
|
||||
explanation: (currentUserIsClosedGroupMember ?
|
||||
"Because you are the creator of this group it will be deleted for everyone. This cannot be undone." :
|
||||
"CONFIRM_LEAVE_GROUP_DESCRIPTION".localized()
|
||||
),
|
||||
confirmTitle: "LEAVE_BUTTON_TITLE".localized(),
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .textPrimary
|
||||
),
|
||||
onTap: { [weak self] in
|
||||
Storage.shared.writeAsync { db in
|
||||
try MessageSender.leave(db, groupPublicKey: threadId)
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
(threadViewModel.threadIsNoteToSelf ? nil :
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .notificationSound,
|
||||
icon: UIImage(named: "table_ic_notification_sound")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "table_ic_notification_sound")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "SETTINGS_ITEM_NOTIFICATION_SOUND".localized(),
|
||||
action: .generalEnum(
|
||||
title: notificationSound.displayName,
|
||||
createUpdateScreen: {
|
||||
SettingsTableViewController(
|
||||
rightAccessory: .dropDown(
|
||||
.dynamicString { notificationSound.displayName }
|
||||
),
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
SessionTableViewController(
|
||||
viewModel: NotificationSoundViewModel(threadId: threadId)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
|
||||
(threadVariant == .contact ? nil :
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .notificationMentionsOnly,
|
||||
icon: UIImage(named: "NotifyMentions")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "NotifyMentions")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "vc_conversation_settings_notify_for_mentions_only_title".localized(),
|
||||
subtitle: "vc_conversation_settings_notify_for_mentions_only_explanation".localized(),
|
||||
rightAccessory: .toggle(
|
||||
.boolValue(threadViewModel.threadOnlyNotifyForMentions == true)
|
||||
),
|
||||
isEnabled: (
|
||||
threadViewModel.threadVariant != .closedGroup ||
|
||||
currentUserIsClosedGroupMember
|
||||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).notify_for_mentions_only",
|
||||
action: .customToggle(
|
||||
value: (threadViewModel.threadOnlyNotifyForMentions == true),
|
||||
isEnabled: (
|
||||
threadViewModel.threadVariant != .closedGroup ||
|
||||
currentUserIsClosedGroupMember
|
||||
)
|
||||
) { newValue in
|
||||
onTap: {
|
||||
let newValue: Bool = !(threadViewModel.threadOnlyNotifyForMentions == true)
|
||||
|
||||
Storage.shared.writeAsync { db in
|
||||
try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAll(
|
||||
db,
|
||||
SessionThread.Columns.onlyNotifyForMentions.set(to: newValue)
|
||||
SessionThread.Columns.onlyNotifyForMentions
|
||||
.set(to: newValue)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -417,19 +445,24 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
|
|||
),
|
||||
|
||||
(threadViewModel.threadIsNoteToSelf ? nil :
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .notificationMute,
|
||||
icon: UIImage(named: "Mute")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "Mute")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "CONVERSATION_SETTINGS_MUTE_LABEL".localized(),
|
||||
rightAccessory: .toggle(
|
||||
.boolValue(threadViewModel.threadMutedUntilTimestamp != nil)
|
||||
),
|
||||
isEnabled: (
|
||||
threadViewModel.threadVariant != .closedGroup ||
|
||||
currentUserIsClosedGroupMember
|
||||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).mute",
|
||||
action: .customToggle(
|
||||
value: (threadViewModel.threadMutedUntilTimestamp != nil),
|
||||
isEnabled: (
|
||||
threadViewModel.threadVariant != .closedGroup ||
|
||||
currentUserIsClosedGroupMember
|
||||
)
|
||||
) { newValue in
|
||||
onTap: {
|
||||
let newValue: Bool = !(threadViewModel.threadMutedUntilTimestamp != nil)
|
||||
|
||||
Storage.shared.writeAsync { db in
|
||||
try SessionThread
|
||||
.filter(id: threadId)
|
||||
|
@ -448,50 +481,52 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
|
|||
),
|
||||
|
||||
(threadViewModel.threadIsNoteToSelf || threadVariant != .contact ? nil :
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .blockUser,
|
||||
icon: UIImage(named: "table_ic_block")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "table_ic_block")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "CONVERSATION_SETTINGS_BLOCK_THIS_USER".localized(),
|
||||
rightAccessory: .toggle(
|
||||
.boolValue(threadViewModel.threadIsBlocked == true)
|
||||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).block",
|
||||
action: .customToggle(
|
||||
value: (threadViewModel.threadIsBlocked == true),
|
||||
confirmationInfo: ConfirmationModal.Info(
|
||||
title: {
|
||||
guard threadViewModel.threadIsBlocked == true else {
|
||||
return String(
|
||||
format: "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT".localized(),
|
||||
threadViewModel.displayName
|
||||
)
|
||||
}
|
||||
|
||||
confirmationInfo: ConfirmationModal.Info(
|
||||
title: {
|
||||
guard threadViewModel.threadIsBlocked == true else {
|
||||
return String(
|
||||
format: "BLOCK_LIST_UNBLOCK_TITLE_FORMAT".localized(),
|
||||
format: "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT".localized(),
|
||||
threadViewModel.displayName
|
||||
)
|
||||
}(),
|
||||
explanation: (threadViewModel.threadIsBlocked == true ?
|
||||
nil :
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION".localized()
|
||||
),
|
||||
confirmTitle: (threadViewModel.threadIsBlocked == true ?
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
|
||||
"BLOCK_LIST_BLOCK_BUTTON".localized()
|
||||
),
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .textPrimary
|
||||
) { viewController in
|
||||
let isBlocked: Bool = (threadViewModel.threadIsBlocked == true)
|
||||
}
|
||||
|
||||
self?.updateBlockedState(
|
||||
from: isBlocked,
|
||||
isBlocked: !isBlocked,
|
||||
threadId: threadId,
|
||||
displayName: threadViewModel.displayName,
|
||||
viewController: viewController
|
||||
return String(
|
||||
format: "BLOCK_LIST_UNBLOCK_TITLE_FORMAT".localized(),
|
||||
threadViewModel.displayName
|
||||
)
|
||||
}
|
||||
)
|
||||
}(),
|
||||
explanation: (threadViewModel.threadIsBlocked == true ?
|
||||
nil :
|
||||
"BLOCK_USER_BEHAVIOR_EXPLANATION".localized()
|
||||
),
|
||||
confirmTitle: (threadViewModel.threadIsBlocked == true ?
|
||||
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
|
||||
"BLOCK_LIST_BLOCK_BUTTON".localized()
|
||||
),
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .textPrimary
|
||||
),
|
||||
onTap: {
|
||||
let isBlocked: Bool = (threadViewModel.threadIsBlocked == true)
|
||||
|
||||
self?.updateBlockedState(
|
||||
from: isBlocked,
|
||||
isBlocked: !isBlocked,
|
||||
threadId: threadId,
|
||||
displayName: threadViewModel.displayName
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
].compactMap { $0 }
|
||||
|
@ -579,8 +614,7 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
|
|||
from oldBlockedState: Bool,
|
||||
isBlocked: Bool,
|
||||
threadId: String,
|
||||
displayName: String,
|
||||
viewController: UIViewController
|
||||
displayName: String
|
||||
) {
|
||||
guard oldBlockedState != isBlocked else { return }
|
||||
|
||||
|
@ -591,7 +625,7 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
|
|||
.with(isBlocked: .updateTo(isBlocked))
|
||||
.save(db)
|
||||
},
|
||||
completion: { db, _ in
|
||||
completion: { [weak self] db, _ in
|
||||
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
@ -615,7 +649,8 @@ class ThreadSettingsViewModel: SettingsTableViewModel<ThreadSettingsViewModel.Na
|
|||
cancelStyle: .textPrimary
|
||||
)
|
||||
)
|
||||
viewController.present(modal, animated: true)
|
||||
|
||||
self?.transitionToScreen(modal, transitionType: .present)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -79,8 +79,8 @@ final class ReactionListSheet: BaseVC {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var clearAllButton: OutlineButton = {
|
||||
let result: OutlineButton = OutlineButton(style: .destructiveBorderless, size: .small)
|
||||
private lazy var clearAllButton: SessionButton = {
|
||||
let result: SessionButton = SessionButton(style: .destructiveBorderless, size: .small)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle("MESSAGE_REQUESTS_CLEAR_ALL".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside)
|
||||
|
@ -93,7 +93,7 @@ final class ReactionListSheet: BaseVC {
|
|||
let result: UITableView = UITableView()
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
result.register(view: UserCell.self)
|
||||
result.register(view: SessionCell.self)
|
||||
result.register(view: FooterCell.self)
|
||||
result.separatorStyle = .none
|
||||
result.themeBackgroundColor = .clear
|
||||
|
@ -425,18 +425,31 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource {
|
|||
return footerCell
|
||||
}
|
||||
|
||||
let cell: UserCell = tableView.dequeue(type: UserCell.self, for: indexPath)
|
||||
let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath)
|
||||
let cellViewModel: MessageViewModel.ReactionInfo = self.selectedReactionUserList[indexPath.row]
|
||||
let authorId: String = cellViewModel.reaction.authorId
|
||||
cell.update(
|
||||
with: cellViewModel.reaction.authorId,
|
||||
profile: cellViewModel.profile,
|
||||
isZombie: false,
|
||||
mediumFont: true,
|
||||
accessory: (cellViewModel.reaction.authorId == self.messageViewModel.currentUserPublicKey ?
|
||||
.x :
|
||||
.none
|
||||
with: SessionCell.Info(
|
||||
id: cellViewModel,
|
||||
leftAccessory: .profile(authorId, cellViewModel.profile),
|
||||
title: (
|
||||
cellViewModel.profile?.displayName() ??
|
||||
Profile.truncated(
|
||||
id: authorId,
|
||||
threadVariant: self.messageViewModel.threadVariant
|
||||
)
|
||||
),
|
||||
rightAccessory: (authorId != self.messageViewModel.currentUserPublicKey ? nil :
|
||||
.icon(
|
||||
UIImage(named: "X")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
size: .fit
|
||||
)
|
||||
),
|
||||
isEnabled: (authorId == self.messageViewModel.currentUserPublicKey)
|
||||
),
|
||||
themeBackgroundColor: .backgroundSecondary
|
||||
style: .edgeToEdge,
|
||||
position: Position.with(indexPath.row, count: self.selectedReactionUserList.count)
|
||||
)
|
||||
|
||||
return cell
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import UIKit
|
||||
import GRDB
|
||||
import DifferenceKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
|
@ -62,9 +63,10 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
|
||||
private lazy var loadingConversationsLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = UIFont.systemFont(ofSize: Values.smallFontSize)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.text = "LOADING_CONVERSATIONS".localized()
|
||||
result.themeTextColor = .textPrimary
|
||||
result.themeTextColor = .textSecondary
|
||||
result.textAlignment = .center
|
||||
result.numberOfLines = 0
|
||||
|
||||
|
@ -181,7 +183,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
explanationLabel.numberOfLines = 0
|
||||
|
||||
let createNewPrivateChatButton = OutlineButton(style: .regular, size: .large)
|
||||
let createNewPrivateChatButton = SessionButton(style: .bordered, size: .large)
|
||||
createNewPrivateChatButton.setTitle("vc_home_empty_state_button_title".localized(), for: .normal)
|
||||
createNewPrivateChatButton.addTarget(self, action: #selector(createNewDM), for: .touchUpInside)
|
||||
createNewPrivateChatButton.set(.width, to: Values.iPadButtonWidth)
|
||||
|
@ -460,8 +462,6 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
// Path status indicator
|
||||
let pathStatusView = PathStatusView()
|
||||
pathStatusView.accessibilityLabel = "Current onion routing path indicator"
|
||||
pathStatusView.set(.width, to: PathStatusView.size)
|
||||
pathStatusView.set(.height, to: PathStatusView.size)
|
||||
|
||||
// Container view
|
||||
let profilePictureViewContainer = UIView()
|
||||
|
@ -743,7 +743,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
}
|
||||
|
||||
@objc private func openSettings() {
|
||||
let settingsViewController: SettingsTableViewController = SettingsTableViewController(
|
||||
let settingsViewController: SessionTableViewController = SessionTableViewController(
|
||||
viewModel: SettingsViewModel()
|
||||
)
|
||||
let navigationController = OWSNavigationController(rootViewController: settingsViewController)
|
||||
|
|
|
@ -34,6 +34,18 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private lazy var loadingConversationsLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.text = "LOADING_CONVERSATIONS".localized()
|
||||
result.themeTextColor = .textSecondary
|
||||
result.textAlignment = .center
|
||||
result.numberOfLines = 0
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let result: UITableView = UITableView()
|
||||
|
@ -86,8 +98,8 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var clearAllButton: OutlineButton = {
|
||||
let result: OutlineButton = OutlineButton(style: .destructive, size: .large)
|
||||
private lazy var clearAllButton: SessionButton = {
|
||||
let result: SessionButton = SessionButton(style: .destructive, size: .large)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle("MESSAGE_REQUESTS_CLEAR_ALL".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside)
|
||||
|
@ -108,6 +120,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
|
||||
// Add the UI (MUST be done after the thread freeze so the 'tableView' creation and setting
|
||||
// the dataSource has the correct data)
|
||||
view.addSubview(loadingConversationsLabel)
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(emptyStateLabel)
|
||||
view.addSubview(fadeView)
|
||||
|
@ -161,6 +174,10 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
|
||||
private func setupLayout() {
|
||||
NSLayoutConstraint.activate([
|
||||
loadingConversationsLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.veryLargeSpacing),
|
||||
loadingConversationsLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.massiveSpacing),
|
||||
loadingConversationsLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Values.massiveSpacing),
|
||||
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.smallSpacing),
|
||||
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
|
||||
tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
|
||||
|
@ -208,6 +225,9 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
return
|
||||
}
|
||||
|
||||
// Hide the 'loading conversations' label (now that we have received conversation data)
|
||||
loadingConversationsLabel.isHidden = true
|
||||
|
||||
// Show the empty state if there is no data
|
||||
clearAllButton.isHidden = updatedData.isEmpty
|
||||
emptyStateLabel.isHidden = !updatedData.isEmpty
|
||||
|
|
|
@ -62,7 +62,7 @@ final class NewConversationVC: BaseVC, ThemedNavigation, UITableViewDelegate, UI
|
|||
result.dataSource = self
|
||||
result.separatorStyle = .none
|
||||
result.themeBackgroundColor = .backgroundSecondary
|
||||
result.register(view: UserCell.self)
|
||||
result.register(view: SessionCell.self)
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
result.sectionHeaderTopPadding = 0
|
||||
|
@ -119,13 +119,19 @@ final class NewConversationVC: BaseVC, ThemedNavigation, UITableViewDelegate, UI
|
|||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath)
|
||||
let profile = newConversationViewModel.sectionData[indexPath.section].contacts[indexPath.row]
|
||||
let cell: UserCell = tableView.dequeue(type: UserCell.self, for: indexPath)
|
||||
cell.update(
|
||||
with: profile.id,
|
||||
profile: profile,
|
||||
isZombie: false,
|
||||
accessory: .none
|
||||
with: SessionCell.Info(
|
||||
id: profile,
|
||||
leftAccessory: .profile(profile.id, profile),
|
||||
title: profile.displayName()
|
||||
),
|
||||
style: .edgeToEdge,
|
||||
position: Position.with(
|
||||
indexPath.row,
|
||||
count: newConversationViewModel.sectionData[indexPath.section].contacts.count
|
||||
)
|
||||
)
|
||||
|
||||
return cell
|
||||
|
|
|
@ -349,16 +349,16 @@ private final class EnterPublicKeyVC: UIViewController {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var copyButton: OutlineButton = {
|
||||
let result = OutlineButton(style: .regular, size: .small)
|
||||
private lazy var copyButton: SessionButton = {
|
||||
let result = SessionButton(style: .bordered, size: .small)
|
||||
result.setTitle("copy".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(copyPublicKey), for: .touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var shareButton: OutlineButton = {
|
||||
let result = OutlineButton(style: .regular, size: .small)
|
||||
private lazy var shareButton: SessionButton = {
|
||||
let result = SessionButton(style: .bordered, size: .small)
|
||||
result.setTitle("share".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(sharePublicKey), for: .touchUpInside)
|
||||
|
||||
|
@ -377,8 +377,8 @@ private final class EnterPublicKeyVC: UIViewController {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var nextButton: OutlineButton = {
|
||||
let result = OutlineButton(style: .regular, size: .large)
|
||||
private lazy var nextButton: SessionButton = {
|
||||
let result = SessionButton(style: .bordered, size: .large)
|
||||
result.setTitle("next".localized(), for: .normal)
|
||||
result.isEnabled = false
|
||||
result.addTarget(self, action: #selector(startNewDMIfPossible), for: .touchUpInside)
|
||||
|
|
|
@ -406,7 +406,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
|||
|
||||
var isShowingCollectionPickerController: Bool = false
|
||||
|
||||
lazy var collectionPickerController: SettingsTableViewController = SettingsTableViewController(
|
||||
lazy var collectionPickerController: SessionTableViewController = SessionTableViewController(
|
||||
viewModel: PhotoCollectionPickerViewModel(library: library) { [weak self] collection in
|
||||
guard self?.photoCollection != collection else {
|
||||
self?.hideCollectionPicker()
|
||||
|
|
|
@ -8,10 +8,10 @@ import SessionUIKit
|
|||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class PhotoCollectionPickerViewModel: SettingsTableViewModel<NoNav, PhotoCollectionPickerViewModel.Section, PhotoCollectionPickerViewModel.Item> {
|
||||
class PhotoCollectionPickerViewModel: SessionTableViewModel<NoNav, PhotoCollectionPickerViewModel.Section, PhotoCollectionPickerViewModel.Item> {
|
||||
// MARK: - Config
|
||||
|
||||
public enum Section: SettingSection {
|
||||
public enum Section: SessionTableSection {
|
||||
case content
|
||||
}
|
||||
|
||||
|
@ -48,14 +48,16 @@ class PhotoCollectionPickerViewModel: SettingsTableViewModel<NoNav, PhotoCollect
|
|||
elements: collections.map { collection in
|
||||
let contents: PhotoCollectionContents = collection.contents()
|
||||
let photoMediaSize: PhotoMediaSize = PhotoMediaSize(
|
||||
thumbnailSize: CGSize(width: IconSize.large.size, height: IconSize.large.size)
|
||||
thumbnailSize: CGSize(
|
||||
width: IconSize.veryLarge.size,
|
||||
height: IconSize.veryLarge.size
|
||||
)
|
||||
)
|
||||
let lastAssetItem: PhotoPickerAssetItem? = contents.lastAssetItem(photoMediaSize: photoMediaSize)
|
||||
|
||||
return SettingInfo(
|
||||
return SessionCell.Info(
|
||||
id: Item(id: collection.id),
|
||||
iconSize: .large,
|
||||
iconSetter: { imageView in
|
||||
leftAccessory: .iconAsync(size: .veryLarge, shouldFill: true) { imageView in
|
||||
// Note: We need to capture 'lastAssetItem' otherwise it'll be released and we won't
|
||||
// be able to load the thumbnail
|
||||
lastAssetItem?.asyncThumbnail { [weak imageView] image in
|
||||
|
@ -64,7 +66,7 @@ class PhotoCollectionPickerViewModel: SettingsTableViewModel<NoNav, PhotoCollect
|
|||
},
|
||||
title: collection.localizedTitle(),
|
||||
subtitle: "\(contents.assetCount)",
|
||||
action: .trigger(showChevron: false) { [weak self] in
|
||||
onTap: { [weak self] in
|
||||
self?.onCollectionSelected(collection)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -10,7 +10,7 @@ stream
|
|||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 5.000000 4.957031 cm
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
13.180191 13.115493 m
|
||||
12.402289 13.892069 11.323112 14.042969 9.946011 14.042969 c
|
||||
|
@ -44,7 +44,7 @@ f
|
|||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 5.000000 13.060150 cm
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 8.103119 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
9.412357 -3.042511 m
|
||||
9.788599 -3.042511 10.030975 -2.756910 10.030975 -2.357380 c
|
||||
|
@ -74,13 +74,13 @@ endstream
|
|||
endobj
|
||||
|
||||
3 0 obj
|
||||
2079
|
||||
2078
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/MediaBox [ 0.000000 0.000000 14.098755 14.042969 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
|
@ -105,15 +105,15 @@ xref
|
|||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000002169 00000 n
|
||||
0000002192 00000 n
|
||||
0000002365 00000 n
|
||||
0000002439 00000 n
|
||||
0000002168 00000 n
|
||||
0000002191 00000 n
|
||||
0000002364 00000 n
|
||||
0000002438 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
2498
|
||||
2497
|
||||
%%EOF
|
|
@ -7,7 +7,6 @@
|
|||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
// Separate iOS Frameworks from other imports.
|
||||
#import "AvatarViewHelper.h"
|
||||
#import "AVAudioSession+OWS.h"
|
||||
#import "OWSAudioPlayer.h"
|
||||
#import "OWSBezierPathView.h"
|
||||
|
|
|
@ -55,7 +55,7 @@ final class DisplayNameVC: BaseVC {
|
|||
registerButtonBottomOffsetConstraint = registerButtonBottomOffsetSpacer.set(.height, to: Values.onboardingButtonBottomOffset)
|
||||
|
||||
// Set up register button
|
||||
let registerButton = OutlineButton(style: .filled, size: .large)
|
||||
let registerButton = SessionButton(style: .filled, size: .large)
|
||||
registerButton.setTitle("continue_2".localized(), for: UIControl.State.normal)
|
||||
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
|
|
@ -14,16 +14,16 @@ final class LandingVC: BaseVC {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var registerButton: OutlineButton = {
|
||||
let result = OutlineButton(style: .filled, size: .large)
|
||||
private lazy var registerButton: SessionButton = {
|
||||
let result = SessionButton(style: .filled, size: .large)
|
||||
result.setTitle("vc_landing_register_button_title".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(register), for: .touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var restoreButton: OutlineButton = {
|
||||
let result = OutlineButton(style: .regular, size: .large)
|
||||
private lazy var restoreButton: SessionButton = {
|
||||
let result = SessionButton(style: .bordered, size: .large)
|
||||
result.setTitle("vc_landing_restore_button_title".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(restore), for: .touchUpInside)
|
||||
|
||||
|
|
|
@ -223,7 +223,7 @@ private final class RecoveryPhraseVC: UIViewController {
|
|||
restoreButtonBottomOffsetConstraint = restoreButtonBottomOffsetSpacer.set(.height, to: Values.onboardingButtonBottomOffset)
|
||||
|
||||
// Continue button
|
||||
let continueButton = OutlineButton(style: .filled, size: .large)
|
||||
let continueButton = SessionButton(style: .filled, size: .large)
|
||||
continueButton.setTitle("continue_2".localized(), for: UIControl.State.normal)
|
||||
continueButton.addTarget(self, action: #selector(handleContinueButtonTapped), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ final class PNModeVC: BaseVC, OptionViewDelegate {
|
|||
registerButtonBottomOffsetSpacer.set(.height, to: Values.onboardingButtonBottomOffset)
|
||||
|
||||
// Set up register button
|
||||
let registerButton = OutlineButton(style: .filled, size: .large)
|
||||
let registerButton = SessionButton(style: .filled, size: .large)
|
||||
registerButton.setTitle("continue_2".localized(), for: .normal)
|
||||
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
|
|
@ -24,8 +24,8 @@ final class RegisterVC : BaseVC {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var copyPublicKeyButton: OutlineButton = {
|
||||
let result = OutlineButton(style: .regular, size: .large)
|
||||
private lazy var copyPublicKeyButton: SessionButton = {
|
||||
let result = SessionButton(style: .bordered, size: .large)
|
||||
result.setTitle("copy".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(copyPublicKey), for: .touchUpInside)
|
||||
|
||||
|
@ -85,7 +85,7 @@ final class RegisterVC : BaseVC {
|
|||
let bottomSpacer = UIView.vStretchingSpacer()
|
||||
|
||||
// Set up register button
|
||||
let registerButton = OutlineButton(style: .filled, size: .large)
|
||||
let registerButton = SessionButton(style: .filled, size: .large)
|
||||
registerButton.setTitle("continue_2".localized(), for: .normal)
|
||||
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ final class RestoreVC: BaseVC {
|
|||
restoreButtonBottomOffsetConstraint = restoreButtonBottomOffsetSpacer.set(.height, to: Values.onboardingButtonBottomOffset)
|
||||
|
||||
// Set up restore button
|
||||
let restoreButton = OutlineButton(style: .filled, size: .large)
|
||||
let restoreButton = SessionButton(style: .filled, size: .large)
|
||||
restoreButton.setTitle("continue_2".localized(), for: UIControl.State.normal)
|
||||
restoreButton.addTarget(self, action: #selector(restore), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ final class SeedReminderView: UIView {
|
|||
labelStackView.spacing = 4
|
||||
|
||||
// Set up button
|
||||
let button = OutlineButton(style: .regular, size: .small)
|
||||
let button = SessionButton(style: .bordered, size: .small)
|
||||
button.setTitle("continue_2".localized(), for: UIControl.State.normal)
|
||||
button.set(.width, to: 96)
|
||||
button.addTarget(self, action: #selector(handleContinueButtonTapped), for: UIControl.Event.touchUpInside)
|
||||
|
|
|
@ -55,8 +55,8 @@ final class SeedVC: BaseVC {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var copyButton: OutlineButton = {
|
||||
let result = OutlineButton(style: .regular, size: .large)
|
||||
private lazy var copyButton: SessionButton = {
|
||||
let result = SessionButton(style: .bordered, size: .large)
|
||||
result.setTitle("copy".localized(), for: UIControl.State.normal)
|
||||
result.addTarget(self, action: #selector(copyMnemonic), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
|
|
@ -256,7 +256,7 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O
|
|||
view.themeBackgroundColor = .clear
|
||||
|
||||
// Next button
|
||||
let joinButton = OutlineButton(style: .regular, size: .large)
|
||||
let joinButton = SessionButton(style: .bordered, size: .large)
|
||||
joinButton.setTitle("JOIN_COMMUNITY_BUTTON_TITLE".localized(), for: UIControl.State.normal)
|
||||
joinButton.addTarget(self, action: #selector(joinOpenGroup), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
|
|
@ -4,6 +4,25 @@ import UIKit
|
|||
import SessionUIKit
|
||||
|
||||
final class PathStatusView: UIView {
|
||||
enum Size {
|
||||
case small
|
||||
case large
|
||||
|
||||
var pointSize: CGFloat {
|
||||
switch self {
|
||||
case .small: return 8
|
||||
case .large: return 16
|
||||
}
|
||||
}
|
||||
|
||||
func offset(for interfaceStyle: UIUserInterfaceStyle) -> CGFloat {
|
||||
switch self {
|
||||
case .small: return (interfaceStyle == .light ? 6 : 8)
|
||||
case .large: return (interfaceStyle == .light ? 6 : 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Status {
|
||||
case unknown
|
||||
case connecting
|
||||
|
@ -20,28 +39,44 @@ final class PathStatusView: UIView {
|
|||
}
|
||||
}
|
||||
|
||||
static let size: CGFloat = 8
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
private let size: Size
|
||||
|
||||
init(size: Size = .small) {
|
||||
self.size = size
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
setUpViewHierarchy()
|
||||
registerObservers()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
self.size = .small
|
||||
|
||||
super.init(coder: coder)
|
||||
|
||||
setUpViewHierarchy()
|
||||
registerObservers()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
layer.cornerRadius = (PathStatusView.size / 2)
|
||||
layer.cornerRadius = (self.size.pointSize / 2)
|
||||
layer.masksToBounds = false
|
||||
self.set(.width, to: self.size.pointSize)
|
||||
self.set(.height, to: self.size.pointSize)
|
||||
|
||||
setStatus(to: (!OnionRequestAPI.paths.isEmpty ? .connected : .connecting))
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
private func registerObservers() {
|
||||
let notificationCenter = NotificationCenter.default
|
||||
|
@ -49,10 +84,6 @@ final class PathStatusView: UIView {
|
|||
notificationCenter.addObserver(self, selector: #selector(handlePathsBuiltNotification), name: .pathsBuilt, object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private func setStatus(to status: Status) {
|
||||
themeBackgroundColor = status.themeColor
|
||||
layer.themeShadowColor = status.themeColor
|
||||
|
@ -60,13 +91,13 @@ final class PathStatusView: UIView {
|
|||
layer.shadowPath = UIBezierPath(
|
||||
ovalIn: CGRect(
|
||||
origin: CGPoint.zero,
|
||||
size: CGSize(width: PathStatusView.size, height: PathStatusView.size)
|
||||
size: CGSize(width: self.size.pointSize, height: self.size.pointSize)
|
||||
)
|
||||
).cgPath
|
||||
|
||||
ThemeManager.onThemeChange(observer: self) { [weak self] theme, _ in
|
||||
self?.layer.shadowOpacity = (theme.interfaceStyle == .light ? 0.4 : 1)
|
||||
self?.layer.shadowRadius = (theme.interfaceStyle == .light ? 6 : 8)
|
||||
self?.layer.shadowRadius = (self?.size.offset(for: theme.interfaceStyle) ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,8 +38,8 @@ final class PathVC: BaseVC {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var learnMoreButton: OutlineButton = {
|
||||
let result = OutlineButton(style: .regular, size: .large)
|
||||
private lazy var learnMoreButton: SessionButton = {
|
||||
let result = SessionButton(style: .bordered, size: .large)
|
||||
result.setTitle("vc_path_learn_more_button_title".localized(), for: UIControl.State.normal)
|
||||
result.addTarget(self, action: #selector(learnMore), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
|
|
@ -42,10 +42,10 @@ class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
result.separatorStyle = .none
|
||||
result.themeBackgroundColor = .clear
|
||||
result.showsVerticalScrollIndicator = false
|
||||
result.register(view: BlockedContactCell.self)
|
||||
result.register(view: SessionCell.self)
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
result.layer.cornerRadius = SettingsCell.cornerRadius
|
||||
result.layer.cornerRadius = SessionCell.cornerRadius
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
result.sectionHeaderTopPadding = 0
|
||||
|
@ -82,8 +82,8 @@ class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var unblockButton: OutlineButton = {
|
||||
let result: OutlineButton = OutlineButton(style: .destructive, size: .large)
|
||||
private lazy var unblockButton: SessionButton = {
|
||||
let result: SessionButton = SessionButton(style: .destructive, size: .large)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle("CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(unblockTapped), for: .touchUpInside)
|
||||
|
@ -104,8 +104,6 @@ class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
hasCustomBackButton: false
|
||||
)
|
||||
|
||||
// Add the UI (MUST be done after the thread freeze so the 'tableView' creation and setting
|
||||
// the dataSource has the correct data)
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(emptyStateLabel)
|
||||
view.addSubview(fadeView)
|
||||
|
@ -162,7 +160,10 @@ class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.smallSpacing),
|
||||
tableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.largeSpacing),
|
||||
tableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Values.largeSpacing),
|
||||
tableView.bottomAnchor.constraint(equalTo: unblockButton.topAnchor, constant: -Values.largeSpacing),
|
||||
tableView.bottomAnchor.constraint(
|
||||
equalTo: unblockButton.topAnchor,
|
||||
constant: -Values.largeSpacing
|
||||
),
|
||||
|
||||
emptyStateLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.massiveSpacing),
|
||||
emptyStateLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.mediumSpacing),
|
||||
|
@ -288,11 +289,12 @@ class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
|
||||
switch section.model {
|
||||
case .contacts:
|
||||
let cellViewModel: BlockedContactsViewModel.DataModel = section.elements[indexPath.row]
|
||||
let cell: BlockedContactCell = tableView.dequeue(type: BlockedContactCell.self, for: indexPath)
|
||||
let info: SessionCell.Info<Profile> = section.elements[indexPath.row]
|
||||
let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath)
|
||||
cell.update(
|
||||
with: cellViewModel,
|
||||
isSelected: viewModel.selectedContactIds.contains(cellViewModel.id)
|
||||
with: info,
|
||||
style: .roundedEdgeToEdge,
|
||||
position: Position.with(indexPath.row, count: section.elements.count)
|
||||
)
|
||||
|
||||
return cell
|
||||
|
@ -364,15 +366,60 @@ class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
|
||||
switch section.model {
|
||||
case .contacts:
|
||||
let cellViewModel: BlockedContactsViewModel.DataModel = section.elements[indexPath.row]
|
||||
let info: SessionCell.Info<Profile> = section.elements[indexPath.row]
|
||||
|
||||
self.viewModel.toggleSelection(contactId: cellViewModel.id)
|
||||
self.tableView.reloadRows(at: [indexPath], with: .none)
|
||||
// Do nothing if the item is disabled
|
||||
guard info.isEnabled else { return }
|
||||
|
||||
// Get the view that was tapped (for presenting on iPad)
|
||||
let tappedView: UIView? = tableView.cellForRow(at: indexPath)
|
||||
let maybeOldSelection: (Int, SessionCell.Info<Profile>)? = section.elements
|
||||
.enumerated()
|
||||
.first(where: { index, info in
|
||||
switch (info.leftAccessory, info.rightAccessory) {
|
||||
case (_, .radio(_, let isSelected, _)): return isSelected()
|
||||
case (.radio(_, let isSelected, _), _): return isSelected()
|
||||
default: return false
|
||||
}
|
||||
})
|
||||
|
||||
info.onTap?(tappedView)
|
||||
self.manuallyReload(indexPath: indexPath, section: section, info: info)
|
||||
self.unblockButton.isEnabled = !self.viewModel.selectedContactIds.isEmpty
|
||||
|
||||
// Update the old selection as well
|
||||
if let oldSelection: (index: Int, info: SessionCell.Info<Profile>) = maybeOldSelection {
|
||||
self.manuallyReload(
|
||||
indexPath: IndexPath(
|
||||
row: oldSelection.index,
|
||||
section: indexPath.section
|
||||
),
|
||||
section: section,
|
||||
info: oldSelection.info
|
||||
)
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
private func manuallyReload(
|
||||
indexPath: IndexPath,
|
||||
section: BlockedContactsViewModel.SectionModel,
|
||||
info: SessionCell.Info<Profile>
|
||||
) {
|
||||
// Try update the existing cell to have a nice animation instead of reloading the cell
|
||||
if let existingCell: SessionCell = tableView.cellForRow(at: indexPath) as? SessionCell {
|
||||
existingCell.update(
|
||||
with: info,
|
||||
style: .roundedEdgeToEdge,
|
||||
position: Position.with(indexPath.row, count: section.elements.count)
|
||||
)
|
||||
}
|
||||
else {
|
||||
tableView.reloadRows(at: [indexPath], with: .none)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
|
@ -385,11 +432,11 @@ class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
guard
|
||||
let section: BlockedContactsViewModel.SectionModel = self.viewModel.contactData
|
||||
.first(where: { section in section.model == .contacts }),
|
||||
let viewModel: BlockedContactsViewModel.DataModel = section.elements
|
||||
.first(where: { model in model.id == contactId })
|
||||
let info: SessionCell.Info<Profile> = section.elements
|
||||
.first(where: { info in info.id.id == contactId })
|
||||
else { return contactId }
|
||||
|
||||
return viewModel.profile.displayName()
|
||||
return info.title
|
||||
}
|
||||
let confirmationTitle: String = {
|
||||
guard contactNames.count > 1 else {
|
||||
|
|
|
@ -6,7 +6,7 @@ import DifferenceKit
|
|||
import SignalUtilitiesKit
|
||||
|
||||
public class BlockedContactsViewModel {
|
||||
public typealias SectionModel = ArraySection<Section, DataModel>
|
||||
public typealias SectionModel = ArraySection<Section, SessionCell.Info<Profile>>
|
||||
|
||||
// MARK: - Section
|
||||
|
||||
|
@ -118,6 +118,26 @@ public class BlockedContactsViewModel {
|
|||
.sorted { lhs, rhs -> Bool in
|
||||
lhs.profile.displayName() > rhs.profile.displayName()
|
||||
}
|
||||
.map { model -> SessionCell.Info<Profile> in
|
||||
SessionCell.Info(
|
||||
id: model.profile,
|
||||
leftAccessory: .profile(model.profile.id, model.profile),
|
||||
title: model.profile.displayName(),
|
||||
rightAccessory: .radio(
|
||||
isSelected: { [weak self] in
|
||||
self?.selectedContactIds.contains(model.profile.id) == true
|
||||
}
|
||||
),
|
||||
onTap: { [weak self] in
|
||||
guard self?.selectedContactIds.contains(model.profile.id) == true else {
|
||||
self?.selectedContactIds.insert(model.profile.id)
|
||||
return
|
||||
}
|
||||
|
||||
self?.selectedContactIds.remove(model.profile.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
],
|
||||
(!data.isEmpty && (pageInfo.pageOffset + pageInfo.currentCount) < pageInfo.totalCount ?
|
||||
|
@ -131,15 +151,6 @@ public class BlockedContactsViewModel {
|
|||
self.contactData = updatedData
|
||||
}
|
||||
|
||||
public func toggleSelection(contactId: String) {
|
||||
guard selectedContactIds.contains(contactId) else {
|
||||
selectedContactIds.insert(contactId)
|
||||
return
|
||||
}
|
||||
|
||||
selectedContactIds.remove(contactId)
|
||||
}
|
||||
|
||||
// MARK: - DataModel
|
||||
|
||||
public struct DataModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable {
|
||||
|
|
|
@ -7,10 +7,10 @@ import SessionUIKit
|
|||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class ConversationSettingsViewModel: SettingsTableViewModel<NoNav, ConversationSettingsViewModel.Section, ConversationSettingsViewModel.Section> {
|
||||
class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSettingsViewModel.Section, ConversationSettingsViewModel.Section> {
|
||||
// MARK: - Section
|
||||
|
||||
public enum Section: SettingSection {
|
||||
public enum Section: SessionTableSection {
|
||||
case messageTrimming
|
||||
case audioMessages
|
||||
case blockedContacts
|
||||
|
@ -23,7 +23,7 @@ class ConversationSettingsViewModel: SettingsTableViewModel<NoNav, ConversationS
|
|||
}
|
||||
}
|
||||
|
||||
var style: SettingSectionHeaderStyle {
|
||||
var style: SessionTableSectionStyle {
|
||||
switch self {
|
||||
case .blockedContacts: return .padding
|
||||
default: return .title
|
||||
|
@ -53,36 +53,50 @@ class ConversationSettingsViewModel: SettingsTableViewModel<NoNav, ConversationS
|
|||
SectionModel(
|
||||
model: .messageTrimming,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .messageTrimming,
|
||||
title: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE".localized(),
|
||||
subtitle: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION".localized(),
|
||||
action: .settingBool(key: .trimOpenGroupMessagesOlderThanSixMonths)
|
||||
rightAccessory: .toggle(
|
||||
.settingBool(key: .trimOpenGroupMessagesOlderThanSixMonths)
|
||||
),
|
||||
onTap: {
|
||||
Storage.shared.writeAsync { db in
|
||||
db[.trimOpenGroupMessagesOlderThanSixMonths] = !db[.trimOpenGroupMessagesOlderThanSixMonths]
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .audioMessages,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .audioMessages,
|
||||
title: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE".localized(),
|
||||
subtitle: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION".localized(),
|
||||
action: .settingBool(key: .shouldAutoPlayConsecutiveAudioMessages)
|
||||
rightAccessory: .toggle(
|
||||
.settingBool(key: .shouldAutoPlayConsecutiveAudioMessages)
|
||||
),
|
||||
onTap: {
|
||||
Storage.shared.writeAsync { db in
|
||||
db[.shouldAutoPlayConsecutiveAudioMessages] = !db[.shouldAutoPlayConsecutiveAudioMessages]
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .blockedContacts,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .blockedContacts,
|
||||
title: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE".localized(),
|
||||
action: .push(
|
||||
showChevron: false,
|
||||
tintColor: .danger,
|
||||
shouldHaveBackground: false
|
||||
) { BlockedContactsViewController() }
|
||||
tintColor: .danger,
|
||||
shouldHaveBackground: false,
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(BlockedContactsViewController())
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -7,17 +7,17 @@ import SessionUIKit
|
|||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class HelpViewModel: SettingsTableViewModel<NoNav, HelpViewModel.Section, HelpViewModel.Section> {
|
||||
class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpViewModel.Section> {
|
||||
// MARK: - Section
|
||||
|
||||
public enum Section: SettingSection {
|
||||
public enum Section: SessionTableSection {
|
||||
case report
|
||||
case translate
|
||||
case feedback
|
||||
case faq
|
||||
case support
|
||||
|
||||
var style: SettingSectionHeaderStyle { .padding }
|
||||
var style: SessionTableSectionStyle { .padding }
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
@ -42,78 +42,98 @@ class HelpViewModel: SettingsTableViewModel<NoNav, HelpViewModel.Section, HelpVi
|
|||
SectionModel(
|
||||
model: .report,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .report,
|
||||
title: "HELP_REPORT_BUG_TITLE".localized(),
|
||||
subtitle: "HELP_REPORT_BUG_DESCRIPTION".localized(),
|
||||
action: .rightButtonAction(
|
||||
title: "HELP_REPORT_BUG_ACTION_TITLE".localized(),
|
||||
action: { HelpViewModel.shareLogs(targetView: $0) }
|
||||
)
|
||||
rightAccessory: .highlightingBackgroundLabel(
|
||||
title: "HELP_REPORT_BUG_ACTION_TITLE".localized()
|
||||
),
|
||||
onTap: { HelpViewModel.shareLogs(targetView: $0) }
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .translate,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .translate,
|
||||
title: "HELP_TRANSLATE_TITLE".localized(),
|
||||
action: .trigger(action: {
|
||||
rightAccessory: .icon(
|
||||
UIImage(named: "icon_link")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
size: .fit
|
||||
),
|
||||
onTap: {
|
||||
guard let url: URL = URL(string: "https://crowdin.com/project/session-ios") else {
|
||||
return
|
||||
}
|
||||
|
||||
UIApplication.shared.open(url)
|
||||
})
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .feedback,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .feedback,
|
||||
title: "HELP_FEEDBACK_TITLE".localized(),
|
||||
action: .trigger(action: {
|
||||
rightAccessory: .icon(
|
||||
UIImage(named: "icon_link")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
size: .fit
|
||||
),
|
||||
onTap: {
|
||||
guard let url: URL = URL(string: "https://getsession.org/survey") else {
|
||||
return
|
||||
}
|
||||
|
||||
UIApplication.shared.open(url)
|
||||
})
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .faq,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .faq,
|
||||
title: "HELP_FAQ_TITLE".localized(),
|
||||
action: .trigger(action: {
|
||||
rightAccessory: .icon(
|
||||
UIImage(named: "icon_link")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
size: .fit
|
||||
),
|
||||
onTap: {
|
||||
guard let url: URL = URL(string: "https://getsession.org/faq") else {
|
||||
return
|
||||
}
|
||||
|
||||
UIApplication.shared.open(url)
|
||||
})
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .support,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .support,
|
||||
title: "HELP_SUPPORT_TITLE".localized(),
|
||||
action: .trigger(action: {
|
||||
rightAccessory: .icon(
|
||||
UIImage(named: "icon_link")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
size: .fit
|
||||
),
|
||||
onTap: {
|
||||
guard let url: URL = URL(string: "https://sessionapp.zendesk.com/hc/en-us") else {
|
||||
return
|
||||
}
|
||||
|
||||
UIApplication.shared.open(url)
|
||||
})
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ import SessionUIKit
|
|||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class NotificationContentViewModel: SettingsTableViewModel<NoNav, NotificationSettingsViewModel.Section, Preferences.NotificationPreviewType> {
|
||||
class NotificationContentViewModel: SessionTableViewModel<NoNav, NotificationSettingsViewModel.Section, Preferences.NotificationPreviewType> {
|
||||
private let storage: Storage
|
||||
|
||||
// MARK: - Initialization
|
||||
|
@ -18,7 +18,7 @@ class NotificationContentViewModel: SettingsTableViewModel<NoNav, NotificationSe
|
|||
|
||||
// MARK: - Section
|
||||
|
||||
public enum Section: SettingSection {
|
||||
public enum Section: SessionTableSection {
|
||||
case content
|
||||
}
|
||||
|
||||
|
@ -48,19 +48,20 @@ class NotificationContentViewModel: SettingsTableViewModel<NoNav, NotificationSe
|
|||
model: .content,
|
||||
elements: Preferences.NotificationPreviewType.allCases
|
||||
.map { previewType in
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: previewType,
|
||||
title: previewType.name,
|
||||
action: .listSelection(
|
||||
rightAccessory: .radio(
|
||||
isSelected: { (currentSelection == previewType) },
|
||||
storedSelection: (currentSelection == previewType),
|
||||
shouldAutoSave: true,
|
||||
selectValue: {
|
||||
storage.write { db in
|
||||
db[.preferencesNotificationPreviewType] = previewType
|
||||
}
|
||||
storedSelection: (currentSelection == previewType)
|
||||
),
|
||||
onTap: { [weak self] in
|
||||
storage.writeAsync { db in
|
||||
db[.preferencesNotificationPreviewType] = previewType
|
||||
}
|
||||
)
|
||||
|
||||
self?.dismissScreen()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -7,10 +7,10 @@ import SessionUIKit
|
|||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class NotificationSettingsViewModel: SettingsTableViewModel<NoNav, NotificationSettingsViewModel.Section, NotificationSettingsViewModel.Setting> {
|
||||
class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSettingsViewModel.Section, NotificationSettingsViewModel.Setting> {
|
||||
// MARK: - Config
|
||||
|
||||
public enum Section: SettingSection {
|
||||
public enum Section: SessionTableSection {
|
||||
case strategy
|
||||
case style
|
||||
case content
|
||||
|
@ -23,7 +23,7 @@ class NotificationSettingsViewModel: SettingsTableViewModel<NoNav, NotificationS
|
|||
}
|
||||
}
|
||||
|
||||
var style: SettingSectionHeaderStyle {
|
||||
var style: SessionTableSectionStyle {
|
||||
switch self {
|
||||
case .content: return .padding
|
||||
default: return .title
|
||||
|
@ -60,62 +60,79 @@ class NotificationSettingsViewModel: SettingsTableViewModel<NoNav, NotificationS
|
|||
SectionModel(
|
||||
model: .strategy,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .strategyUseFastMode,
|
||||
title: "NOTIFICATIONS_STRATEGY_FAST_MODE_TITLE".localized(),
|
||||
subtitle: "NOTIFICATIONS_STRATEGY_FAST_MODE_DESCRIPTION".localized(),
|
||||
action: .userDefaultsBool(
|
||||
defaults: UserDefaults.standard,
|
||||
key: "isUsingFullAPNs",
|
||||
onChange: {
|
||||
// Force sync the push tokens on change
|
||||
SyncPushTokensJob.run(uploadOnlyIfStale: false)
|
||||
}
|
||||
rightAccessory: .toggle(
|
||||
.userDefaults(UserDefaults.standard, key: "isUsingFullAPNs")
|
||||
),
|
||||
extraActionTitle: "NOTIFICATIONS_STRATEGY_FAST_MODE_ACTION".localized(),
|
||||
onExtraAction: { UIApplication.shared.openSystemSettings() }
|
||||
extraAction: SessionCell.ExtraAction(
|
||||
title: "NOTIFICATIONS_STRATEGY_FAST_MODE_ACTION".localized(),
|
||||
onTap: { UIApplication.shared.openSystemSettings() }
|
||||
),
|
||||
onTap: {
|
||||
UserDefaults.standard.set(
|
||||
!UserDefaults.standard.bool(forKey: "isUsingFullAPNs"),
|
||||
forKey: "isUsingFullAPNs"
|
||||
)
|
||||
|
||||
// Force sync the push tokens on change
|
||||
SyncPushTokensJob.run(uploadOnlyIfStale: false)
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .style,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .styleSound,
|
||||
title: "NOTIFICATIONS_STYLE_SOUND_TITLE".localized(),
|
||||
action: .settingEnum(
|
||||
db,
|
||||
type: Preferences.Sound.self,
|
||||
key: .defaultNotificationSound,
|
||||
titleGenerator: { $0.defaulting(to: .defaultNotificationSound).displayName },
|
||||
createUpdateScreen: {
|
||||
SettingsTableViewController(viewModel: NotificationSoundViewModel())
|
||||
}
|
||||
)
|
||||
rightAccessory: .dropDown(
|
||||
.dynamicString(
|
||||
type: Preferences.Sound.self,
|
||||
key: .defaultNotificationSound,
|
||||
value: { $0.defaulting(to: .defaultNotificationSound).displayName }
|
||||
)
|
||||
),
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
SessionTableViewController(viewModel: NotificationSoundViewModel())
|
||||
)
|
||||
}
|
||||
),
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .styleSoundWhenAppIsOpen,
|
||||
title: "NOTIFICATIONS_STYLE_SOUND_WHEN_OPEN_TITLE".localized(),
|
||||
action: .settingBool(key: .playNotificationSoundInForeground)
|
||||
rightAccessory: .toggle(.settingBool(key: .playNotificationSoundInForeground)),
|
||||
onTap: {
|
||||
Storage.shared.writeAsync { db in
|
||||
db[.playNotificationSoundInForeground] = !db[.playNotificationSoundInForeground]
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .content,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .content,
|
||||
title: "NOTIFICATIONS_STYLE_CONTENT_TITLE".localized(),
|
||||
subtitle: "NOTIFICATIONS_STYLE_CONTENT_DESCRIPTION".localized(),
|
||||
action: .settingEnum(
|
||||
db,
|
||||
type: Preferences.NotificationPreviewType.self,
|
||||
key: .preferencesNotificationPreviewType,
|
||||
titleGenerator: { $0.defaulting(to: .defaultPreviewType).name },
|
||||
createUpdateScreen: {
|
||||
SettingsTableViewController(viewModel: NotificationContentViewModel())
|
||||
}
|
||||
)
|
||||
rightAccessory: .dropDown(
|
||||
.dynamicString(
|
||||
type: Preferences.NotificationPreviewType.self,
|
||||
key: .preferencesNotificationPreviewType,
|
||||
value: { $0.defaulting(to: .defaultPreviewType).name }
|
||||
)
|
||||
),
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
SessionTableViewController(viewModel: NotificationContentViewModel())
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -8,7 +8,7 @@ import SessionUIKit
|
|||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class NotificationSoundViewModel: SettingsTableViewModel<NotificationSoundViewModel.NavButton, NotificationSettingsViewModel.Section, Preferences.Sound> {
|
||||
class NotificationSoundViewModel: SessionTableViewModel<NotificationSoundViewModel.NavButton, NotificationSettingsViewModel.Section, Preferences.Sound> {
|
||||
// MARK: - Config
|
||||
|
||||
enum NavButton: Equatable {
|
||||
|
@ -16,7 +16,7 @@ class NotificationSoundViewModel: SettingsTableViewModel<NotificationSoundViewMo
|
|||
case save
|
||||
}
|
||||
|
||||
public enum Section: SettingSection {
|
||||
public enum Section: SessionTableSection {
|
||||
case content
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,9 @@ class NotificationSoundViewModel: SettingsTableViewModel<NotificationSoundViewMo
|
|||
id: .cancel,
|
||||
systemItem: .cancel,
|
||||
accessibilityIdentifier: "Cancel button"
|
||||
)
|
||||
) { [weak self] in
|
||||
self?.dismissScreen()
|
||||
}
|
||||
]).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
@ -61,25 +63,15 @@ class NotificationSoundViewModel: SettingsTableViewModel<NotificationSoundViewMo
|
|||
id: .save,
|
||||
systemItem: .save,
|
||||
accessibilityIdentifier: "Save button"
|
||||
)
|
||||
) { [weak self] in
|
||||
self?.saveChanges()
|
||||
self?.dismissScreen()
|
||||
}
|
||||
]
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
override var closeScreen: AnyPublisher<Bool, Never> {
|
||||
navItemTapped
|
||||
.handleEvents(receiveOutput: { [weak self] navItemId in
|
||||
switch navItemId {
|
||||
case .save: self?.saveChanges()
|
||||
default: break
|
||||
}
|
||||
self?.setIsEditing(true)
|
||||
})
|
||||
.map { _ in false }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
override var title: String { "NOTIFICATIONS_STYLE_SOUND_TITLE".localized() }
|
||||
|
@ -121,7 +113,7 @@ class NotificationSoundViewModel: SettingsTableViewModel<NotificationSoundViewMo
|
|||
model: .content,
|
||||
elements: Preferences.Sound.notificationSounds
|
||||
.map { sound in
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: sound,
|
||||
title: {
|
||||
guard sound != .note else {
|
||||
|
@ -133,26 +125,25 @@ class NotificationSoundViewModel: SettingsTableViewModel<NotificationSoundViewMo
|
|||
|
||||
return sound.displayName
|
||||
}(),
|
||||
action: .listSelection(
|
||||
rightAccessory: .radio(
|
||||
isSelected: { (self?.currentSelection.value == sound) },
|
||||
storedSelection: (self?.storedSelection == sound),
|
||||
shouldAutoSave: false,
|
||||
selectValue: {
|
||||
self?.currentSelection.send(sound)
|
||||
|
||||
// Play the sound (to prevent UI lag we dispatch this to the next
|
||||
// run loop
|
||||
DispatchQueue.main.async {
|
||||
self?.audioPlayer?.stop()
|
||||
self?.audioPlayer = Preferences.Sound.audioPlayer(
|
||||
for: sound,
|
||||
behavior: .playback
|
||||
)
|
||||
self?.audioPlayer?.isLooping = false
|
||||
self?.audioPlayer?.play()
|
||||
}
|
||||
storedSelection: (self?.storedSelection == sound)
|
||||
),
|
||||
onTap: {
|
||||
self?.currentSelection.send(sound)
|
||||
|
||||
// Play the sound (to prevent UI lag we dispatch this to the next
|
||||
// run loop
|
||||
DispatchQueue.main.async {
|
||||
self?.audioPlayer?.stop()
|
||||
self?.audioPlayer = Preferences.Sound.audioPlayer(
|
||||
for: sound,
|
||||
behavior: .playback
|
||||
)
|
||||
self?.audioPlayer?.isLooping = false
|
||||
self?.audioPlayer?.play()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -172,7 +163,7 @@ class NotificationSoundViewModel: SettingsTableViewModel<NotificationSoundViewMo
|
|||
|
||||
let threadId: String? = self.threadId
|
||||
|
||||
Storage.shared.write { db in
|
||||
Storage.shared.writeAsync { db in
|
||||
guard let threadId: String = threadId else {
|
||||
db[.defaultNotificationSound] = currentSelection
|
||||
return
|
||||
|
|
|
@ -9,8 +9,8 @@ import SignalUtilitiesKit
|
|||
final class NukeDataModal: Modal {
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(afterClosed: (() -> ())? = nil) {
|
||||
super.init(afterClosed: afterClosed)
|
||||
override init(targetView: UIView? = nil, afterClosed: (() -> ())? = nil) {
|
||||
super.init(targetView: targetView, afterClosed: afterClosed)
|
||||
|
||||
self.modalPresentationStyle = .overFullScreen
|
||||
self.modalTransitionStyle = .crossDissolve
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import GRDB
|
||||
import DifferenceKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class PrivacySettingsViewModel: SettingsTableViewModel<PrivacySettingsViewModel.NavButton, PrivacySettingsViewModel.Section, PrivacySettingsViewModel.Item> {
|
||||
class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.NavButton, PrivacySettingsViewModel.Section, PrivacySettingsViewModel.Item> {
|
||||
private let shouldShowCloseButton: Bool
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(shouldShowCloseButton: Bool = false) {
|
||||
super.init(closeNavItemId: (shouldShowCloseButton ? NavButton.close : nil))
|
||||
self.shouldShowCloseButton = shouldShowCloseButton
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Config
|
||||
|
@ -20,7 +25,7 @@ class PrivacySettingsViewModel: SettingsTableViewModel<PrivacySettingsViewModel.
|
|||
case close
|
||||
}
|
||||
|
||||
public enum Section: SettingSection {
|
||||
public enum Section: SessionTableSection {
|
||||
case screenSecurity
|
||||
case readReceipts
|
||||
case typingIndicators
|
||||
|
@ -37,7 +42,7 @@ class PrivacySettingsViewModel: SettingsTableViewModel<PrivacySettingsViewModel.
|
|||
}
|
||||
}
|
||||
|
||||
var style: SettingSectionHeaderStyle { return .title }
|
||||
var style: SessionTableSectionStyle { return .title }
|
||||
}
|
||||
|
||||
public enum Item: Differentiable {
|
||||
|
@ -49,6 +54,24 @@ class PrivacySettingsViewModel: SettingsTableViewModel<PrivacySettingsViewModel.
|
|||
case calls
|
||||
}
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
override var leftNavItems: AnyPublisher<[NavItem]?, Never> {
|
||||
guard self.shouldShowCloseButton else { return Just([]).eraseToAnyPublisher() }
|
||||
|
||||
return Just([
|
||||
NavItem(
|
||||
id: .close,
|
||||
image: UIImage(named: "X")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
style: .plain,
|
||||
accessibilityIdentifier: "Close Button"
|
||||
) { [weak self] in
|
||||
self?.dismissScreen()
|
||||
}
|
||||
]).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
override var title: String { "PRIVACY_TITLE".localized() }
|
||||
|
@ -71,35 +94,50 @@ class PrivacySettingsViewModel: SettingsTableViewModel<PrivacySettingsViewModel.
|
|||
SectionModel(
|
||||
model: .screenSecurity,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .screenLock,
|
||||
title: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE".localized(),
|
||||
subtitle: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION".localized(),
|
||||
action: .settingBool(key: .isScreenLockEnabled)
|
||||
rightAccessory: .toggle(.settingBool(key: .isScreenLockEnabled)),
|
||||
onTap: {
|
||||
Storage.shared.writeAsync { db in
|
||||
db[.isScreenLockEnabled] = !db[.isScreenLockEnabled]
|
||||
}
|
||||
}
|
||||
),
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .screenshotNotifications,
|
||||
title: "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE".localized(),
|
||||
subtitle: "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION".localized(),
|
||||
action: .settingBool(key: .showScreenshotNotifications)
|
||||
rightAccessory: .toggle(.settingBool(key: .showScreenshotNotifications)),
|
||||
onTap: {
|
||||
Storage.shared.writeAsync { db in
|
||||
db[.showScreenshotNotifications] = !db[.showScreenshotNotifications]
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .readReceipts,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .readReceipts,
|
||||
title: "PRIVACY_READ_RECEIPTS_TITLE".localized(),
|
||||
subtitle: "PRIVACY_READ_RECEIPTS_DESCRIPTION".localized(),
|
||||
action: .settingBool(key: .areReadReceiptsEnabled)
|
||||
rightAccessory: .toggle(.settingBool(key: .areReadReceiptsEnabled)),
|
||||
onTap: {
|
||||
Storage.shared.writeAsync { db in
|
||||
db[.areReadReceiptsEnabled] = !db[.areReadReceiptsEnabled]
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .typingIndicators,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .typingIndicators,
|
||||
title: "PRIVACY_TYPING_INDICATORS_TITLE".localized(),
|
||||
subtitle: "PRIVACY_TYPING_INDICATORS_DESCRIPTION".localized(),
|
||||
|
@ -129,38 +167,52 @@ class PrivacySettingsViewModel: SettingsTableViewModel<PrivacySettingsViewModel.
|
|||
|
||||
return result
|
||||
},
|
||||
action: .settingBool(key: .typingIndicatorsEnabled)
|
||||
rightAccessory: .toggle(.settingBool(key: .typingIndicatorsEnabled)),
|
||||
onTap: {
|
||||
Storage.shared.writeAsync { db in
|
||||
db[.typingIndicatorsEnabled] = !db[.typingIndicatorsEnabled]
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .linkPreviews,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .linkPreviews,
|
||||
title: "PRIVACY_LINK_PREVIEWS_TITLE".localized(),
|
||||
subtitle: "PRIVACY_LINK_PREVIEWS_DESCRIPTION".localized(),
|
||||
action: .settingBool(key: .areLinkPreviewsEnabled)
|
||||
rightAccessory: .toggle(.settingBool(key: .areLinkPreviewsEnabled)),
|
||||
onTap: {
|
||||
Storage.shared.writeAsync { db in
|
||||
db[.areLinkPreviewsEnabled] = !db[.areLinkPreviewsEnabled]
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .calls,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .calls,
|
||||
title: "PRIVACY_CALLS_TITLE".localized(),
|
||||
subtitle: "PRIVACY_CALLS_DESCRIPTION".localized(),
|
||||
action: .settingBool(
|
||||
key: .areCallsEnabled,
|
||||
confirmationInfo: ConfirmationModal.Info(
|
||||
title: "PRIVACY_CALLS_WARNING_TITLE".localized(),
|
||||
explanation: "PRIVACY_CALLS_WARNING_DESCRIPTION".localized(),
|
||||
stateToShow: .whenDisabled,
|
||||
confirmTitle: "continue_2".localized(),
|
||||
confirmStyle: .textPrimary
|
||||
) { _ in Permissions.requestMicrophonePermissionIfNeeded() }
|
||||
)
|
||||
rightAccessory: .toggle(.settingBool(key: .areCallsEnabled)),
|
||||
confirmationInfo: ConfirmationModal.Info(
|
||||
title: "PRIVACY_CALLS_WARNING_TITLE".localized(),
|
||||
explanation: "PRIVACY_CALLS_WARNING_DESCRIPTION".localized(),
|
||||
stateToShow: .whenDisabled,
|
||||
confirmTitle: "continue_2".localized(),
|
||||
confirmStyle: .textPrimary,
|
||||
onConfirm: { _ in Permissions.requestMicrophonePermissionIfNeeded() }
|
||||
),
|
||||
onTap: {
|
||||
Storage.shared.writeAsync { db in
|
||||
db[.areCallsEnabled] = !db[.areCallsEnabled]
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -221,7 +221,7 @@ private final class ViewMyQRCodeVC : UIViewController {
|
|||
explanationLabel.numberOfLines = 0
|
||||
|
||||
// Set up share button
|
||||
let shareButton = OutlineButton(style: .regular, size: .large)
|
||||
let shareButton = SessionButton(style: .bordered, size: .large)
|
||||
shareButton.setTitle("share".localized(), for: .normal)
|
||||
shareButton.addTarget(self, action: #selector(shareQRCode), for: .touchUpInside)
|
||||
|
||||
|
|
|
@ -16,8 +16,8 @@ final class SeedModal: Modal {
|
|||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(afterClosed: (() -> ())? = nil) {
|
||||
super.init(afterClosed: afterClosed)
|
||||
override init(targetView: UIView? = nil, afterClosed: (() -> ())? = nil) {
|
||||
super.init(targetView: targetView, afterClosed: afterClosed)
|
||||
|
||||
self.modalPresentationStyle = .overFullScreen
|
||||
self.modalTransitionStyle = .crossDissolve
|
||||
|
|
|
@ -1,561 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit.UIImage
|
||||
import Combine
|
||||
import GRDB
|
||||
import DifferenceKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class SettingsTableViewModel<NavItemId: Equatable, Section: SettingSection, SettingItem: Hashable & Differentiable> {
|
||||
typealias SectionModel = ArraySection<Section, SettingInfo<SettingItem>>
|
||||
typealias ObservableData = AnyPublisher<[SectionModel], Error>
|
||||
|
||||
var closeNavItemId: NavItemId?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Provide a `closeNavItemId` in order to show a close button
|
||||
init(closeNavItemId: NavItemId? = nil) {
|
||||
self.closeNavItemId = closeNavItemId
|
||||
}
|
||||
|
||||
// MARK: - Input
|
||||
|
||||
let navItemTapped: PassthroughSubject<NavItemId, Never> = PassthroughSubject()
|
||||
private let _isEditing: CurrentValueSubject<Bool, Never> = CurrentValueSubject(false)
|
||||
lazy var isEditing: AnyPublisher<Bool, Never> = _isEditing
|
||||
.removeDuplicates()
|
||||
.shareReplay(1)
|
||||
|
||||
private let _transitionToScreen: PassthroughSubject<(UIViewController, TransitionType), Never> = PassthroughSubject()
|
||||
lazy var transitionToScreen: AnyPublisher<(UIViewController, TransitionType), Never> = _transitionToScreen
|
||||
.shareReplay(0)
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
open var leftNavItems: AnyPublisher<[NavItem]?, Never> {
|
||||
guard let closeNavItemId: NavItemId = self.closeNavItemId else {
|
||||
return Just(nil).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return Just([
|
||||
NavItem(
|
||||
id: closeNavItemId,
|
||||
image: UIImage(named: "X")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
style: .plain,
|
||||
accessibilityIdentifier: "Close Button"
|
||||
)
|
||||
]).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
open var rightNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() }
|
||||
|
||||
open var closeScreen: AnyPublisher<Bool, Never> {
|
||||
navItemTapped
|
||||
.filter { [weak self] itemId in itemId == self?.closeNavItemId }
|
||||
.map { _ in true }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
open var title: String { preconditionFailure("abstract class - override in subclass") }
|
||||
open var settingsData: [SectionModel] { preconditionFailure("abstract class - override in subclass") }
|
||||
open var observableSettingsData: ObservableData {
|
||||
preconditionFailure("abstract class - override in subclass")
|
||||
}
|
||||
|
||||
func updateSettings(_ updatedSettings: [SectionModel]) {
|
||||
preconditionFailure("abstract class - override in subclass")
|
||||
}
|
||||
|
||||
func setIsEditing(_ isEditing: Bool) {
|
||||
_isEditing.send(isEditing)
|
||||
}
|
||||
|
||||
func transitionToScreen(_ viewController: UIViewController, transitionType: TransitionType = .push) {
|
||||
_transitionToScreen.send((viewController, transitionType))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NavItem
|
||||
|
||||
public enum NoNav: Equatable {}
|
||||
|
||||
extension SettingsTableViewModel {
|
||||
public struct NavItem {
|
||||
let id: NavItemId
|
||||
let image: UIImage?
|
||||
let style: UIBarButtonItem.Style
|
||||
let systemItem: UIBarButtonItem.SystemItem?
|
||||
let accessibilityIdentifier: String
|
||||
let action: (() -> Void)?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
id: NavItemId,
|
||||
systemItem: UIBarButtonItem.SystemItem?,
|
||||
accessibilityIdentifier: String,
|
||||
action: (() -> Void)? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.image = nil
|
||||
self.style = .plain
|
||||
self.systemItem = systemItem
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public init(
|
||||
id: NavItemId,
|
||||
image: UIImage?,
|
||||
style: UIBarButtonItem.Style,
|
||||
accessibilityIdentifier: String,
|
||||
action: (() -> Void)? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.image = image
|
||||
self.style = style
|
||||
self.systemItem = nil
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.action = action
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
public func createBarButtonItem() -> DisposableBarButtonItem {
|
||||
guard let systemItem: UIBarButtonItem.SystemItem = systemItem else {
|
||||
return DisposableBarButtonItem(
|
||||
image: image,
|
||||
style: style,
|
||||
target: nil,
|
||||
action: nil,
|
||||
accessibilityIdentifier: accessibilityIdentifier
|
||||
)
|
||||
}
|
||||
|
||||
return DisposableBarButtonItem(
|
||||
barButtonSystemItem: systemItem,
|
||||
target: nil,
|
||||
action: nil,
|
||||
accessibilityIdentifier: accessibilityIdentifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SettingSectionHeaderStyle
|
||||
|
||||
public enum SettingSectionHeaderStyle: Differentiable {
|
||||
case none
|
||||
case title
|
||||
case padding
|
||||
}
|
||||
|
||||
// MARK: - SettingSection
|
||||
|
||||
protocol SettingSection: Differentiable {
|
||||
var title: String? { get }
|
||||
var style: SettingSectionHeaderStyle { get }
|
||||
}
|
||||
|
||||
extension SettingSection {
|
||||
var title: String? { nil }
|
||||
var style: SettingSectionHeaderStyle { .none }
|
||||
}
|
||||
|
||||
// MARK: - IconSize
|
||||
|
||||
public enum IconSize: Differentiable {
|
||||
case small
|
||||
case medium
|
||||
case large
|
||||
|
||||
var size: CGFloat {
|
||||
switch self {
|
||||
case .small: return 24
|
||||
case .medium: return 32
|
||||
case .large: return 80
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TransitionType
|
||||
|
||||
public enum TransitionType {
|
||||
case push
|
||||
case present
|
||||
}
|
||||
|
||||
// MARK: - SettingInfo
|
||||
|
||||
struct SettingInfo<ID: Hashable & Differentiable>: Equatable, Hashable, Differentiable {
|
||||
let id: ID
|
||||
let icon: UIImage?
|
||||
let iconSize: IconSize
|
||||
let iconSetter: ((UIImageView) -> Void)?
|
||||
let title: String
|
||||
let subtitle: String?
|
||||
let alignment: NSTextAlignment
|
||||
let accessibilityIdentifier: String?
|
||||
let action: SettingsAction
|
||||
let subtitleExtraViewGenerator: (() -> UIView)?
|
||||
let extraActionTitle: String?
|
||||
let onExtraAction: (() -> Void)?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
id: ID,
|
||||
icon: UIImage? = nil,
|
||||
iconSize: IconSize = .small,
|
||||
iconSetter: ((UIImageView) -> Void)? = nil,
|
||||
title: String,
|
||||
subtitle: String? = nil,
|
||||
alignment: NSTextAlignment = .left,
|
||||
accessibilityIdentifier: String? = nil,
|
||||
subtitleExtraViewGenerator: (() -> UIView)? = nil,
|
||||
action: SettingsAction,
|
||||
extraActionTitle: String? = nil,
|
||||
onExtraAction: (() -> Void)? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.icon = icon
|
||||
self.iconSize = iconSize
|
||||
self.iconSetter = iconSetter
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.alignment = alignment
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.subtitleExtraViewGenerator = subtitleExtraViewGenerator
|
||||
self.action = action
|
||||
self.extraActionTitle = extraActionTitle
|
||||
self.onExtraAction = onExtraAction
|
||||
}
|
||||
|
||||
// MARK: - Conformance
|
||||
|
||||
var differenceIdentifier: ID { id }
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
id.hash(into: &hasher)
|
||||
icon.hash(into: &hasher)
|
||||
iconSize.hash(into: &hasher)
|
||||
title.hash(into: &hasher)
|
||||
subtitle.hash(into: &hasher)
|
||||
alignment.hash(into: &hasher)
|
||||
accessibilityIdentifier.hash(into: &hasher)
|
||||
action.hash(into: &hasher)
|
||||
extraActionTitle.hash(into: &hasher)
|
||||
}
|
||||
|
||||
static func == (lhs: SettingInfo<ID>, rhs: SettingInfo<ID>) -> Bool {
|
||||
return (
|
||||
lhs.id == rhs.id &&
|
||||
lhs.icon == rhs.icon &&
|
||||
lhs.iconSize == rhs.iconSize &&
|
||||
lhs.title == rhs.title &&
|
||||
lhs.subtitle == rhs.subtitle &&
|
||||
lhs.alignment == rhs.alignment &&
|
||||
lhs.accessibilityIdentifier == rhs.accessibilityIdentifier &&
|
||||
lhs.action == rhs.action &&
|
||||
lhs.extraActionTitle == rhs.extraActionTitle
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Mutation
|
||||
|
||||
func with(action: SettingsAction) -> SettingInfo {
|
||||
return SettingInfo(
|
||||
id: self.id,
|
||||
icon: self.icon,
|
||||
title: self.title,
|
||||
subtitle: self.subtitle,
|
||||
alignment: self.alignment,
|
||||
accessibilityIdentifier: self.accessibilityIdentifier,
|
||||
subtitleExtraViewGenerator: self.subtitleExtraViewGenerator,
|
||||
action: action,
|
||||
extraActionTitle: self.extraActionTitle,
|
||||
onExtraAction: self.onExtraAction
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SettingsAction
|
||||
|
||||
public enum SettingsAction: Hashable, Equatable {
|
||||
case threadInfo(
|
||||
threadViewModel: SessionThreadViewModel,
|
||||
style: ThreadInfoStyle = ThreadInfoStyle(),
|
||||
avatarTapped: (() -> Void)? = nil,
|
||||
titleTapped: (() -> Void)? = nil,
|
||||
titleChanged: ((String) -> Void)? = nil
|
||||
)
|
||||
case userDefaultsBool(
|
||||
defaults: UserDefaults,
|
||||
key: String,
|
||||
isEnabled: Bool = true,
|
||||
onChange: (() -> Void)?
|
||||
)
|
||||
case settingBool(
|
||||
key: Setting.BoolKey,
|
||||
confirmationInfo: ConfirmationModal.Info?,
|
||||
isEnabled: Bool = true
|
||||
)
|
||||
case customToggle(
|
||||
value: Bool,
|
||||
isEnabled: Bool = true,
|
||||
confirmationInfo: ConfirmationModal.Info? = nil,
|
||||
onChange: ((Bool) -> Void)? = nil
|
||||
)
|
||||
case settingEnum(
|
||||
key: String,
|
||||
title: String?,
|
||||
createUpdateScreen: () -> UIViewController
|
||||
)
|
||||
case generalEnum(
|
||||
title: String?,
|
||||
createUpdateScreen: () -> UIViewController
|
||||
)
|
||||
|
||||
case trigger(
|
||||
showChevron: Bool = true,
|
||||
action: () -> Void
|
||||
)
|
||||
case push(
|
||||
showChevron: Bool = true,
|
||||
tintColor: ThemeValue = .textPrimary,
|
||||
shouldHaveBackground: Bool = true,
|
||||
createDestination: () -> UIViewController
|
||||
)
|
||||
case present(
|
||||
tintColor: ThemeValue = .textPrimary,
|
||||
createDestination: () -> UIViewController
|
||||
)
|
||||
case listSelection(
|
||||
isSelected: () -> Bool,
|
||||
storedSelection: Bool,
|
||||
shouldAutoSave: Bool,
|
||||
selectValue: () -> Void
|
||||
)
|
||||
case rightButtonAction(
|
||||
title: String,
|
||||
action: (UIView) -> ()
|
||||
)
|
||||
|
||||
private var actionName: String {
|
||||
switch self {
|
||||
case .threadInfo: return "threadInfo"
|
||||
case .userDefaultsBool: return "userDefaultsBool"
|
||||
case .settingBool: return "settingBool"
|
||||
case .customToggle: return "customToggle"
|
||||
case .settingEnum: return "settingEnum"
|
||||
case .generalEnum: return "generalEnum"
|
||||
|
||||
case .trigger: return "trigger"
|
||||
case .push: return "push"
|
||||
case .present: return "present"
|
||||
case .listSelection: return "listSelection"
|
||||
case .rightButtonAction: return "rightButtonAction"
|
||||
}
|
||||
}
|
||||
|
||||
var shouldHaveBackground: Bool {
|
||||
switch self {
|
||||
case .threadInfo: return false
|
||||
case .push(_, _, let shouldHaveBackground, _): return shouldHaveBackground
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
public static func settingEnum<ET: EnumIntSetting>(
|
||||
_ db: Database,
|
||||
type: ET.Type,
|
||||
key: Setting.EnumKey,
|
||||
titleGenerator: @escaping ((ET?) -> String?),
|
||||
createUpdateScreen: @escaping () -> UIViewController
|
||||
) -> SettingsAction {
|
||||
return SettingsAction.settingEnum(
|
||||
key: key.rawValue,
|
||||
title: titleGenerator(db[key]),
|
||||
createUpdateScreen: createUpdateScreen
|
||||
)
|
||||
}
|
||||
|
||||
public static func settingEnum<ET: EnumStringSetting>(
|
||||
_ db: Database,
|
||||
type: ET.Type,
|
||||
key: Setting.EnumKey,
|
||||
titleGenerator: @escaping ((ET?) -> String?),
|
||||
createUpdateScreen: @escaping () -> UIViewController
|
||||
) -> SettingsAction {
|
||||
return SettingsAction.settingEnum(
|
||||
key: key.rawValue,
|
||||
title: titleGenerator(db[key]),
|
||||
createUpdateScreen: createUpdateScreen
|
||||
)
|
||||
}
|
||||
|
||||
public static func settingBool(key: Setting.BoolKey) -> SettingsAction {
|
||||
return .settingBool(key: key, confirmationInfo: nil)
|
||||
}
|
||||
|
||||
// MARK: - Conformance
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
actionName.hash(into: &hasher)
|
||||
|
||||
switch self {
|
||||
case .threadInfo(let threadViewModel, let style, _, _, _):
|
||||
threadViewModel.hash(into: &hasher)
|
||||
style.hash(into: &hasher)
|
||||
|
||||
case .userDefaultsBool(_, let key, let isEnabled, _):
|
||||
key.hash(into: &hasher)
|
||||
isEnabled.hash(into: &hasher)
|
||||
|
||||
case .settingBool(let key, let confirmationInfo, let isEnabled):
|
||||
key.hash(into: &hasher)
|
||||
confirmationInfo.hash(into: &hasher)
|
||||
isEnabled.hash(into: &hasher)
|
||||
|
||||
case .customToggle(let value, let isEnabled, let confirmationInfo, _):
|
||||
value.hash(into: &hasher)
|
||||
isEnabled.hash(into: &hasher)
|
||||
confirmationInfo.hash(into: &hasher)
|
||||
|
||||
case .settingEnum(let key, let title, _):
|
||||
key.hash(into: &hasher)
|
||||
title.hash(into: &hasher)
|
||||
|
||||
case .generalEnum(let title, _):
|
||||
title.hash(into: &hasher)
|
||||
|
||||
case .trigger(let showChevron, _):
|
||||
showChevron.hash(into: &hasher)
|
||||
|
||||
case .push(let showChevron, let tintColor, let shouldHaveBackground, _):
|
||||
showChevron.hash(into: &hasher)
|
||||
tintColor.hash(into: &hasher)
|
||||
shouldHaveBackground.hash(into: &hasher)
|
||||
|
||||
case .present(let tintColor, _):
|
||||
tintColor.hash(into: &hasher)
|
||||
|
||||
case .listSelection(let isSelected, let storedSelection, let shouldAutoSave, _):
|
||||
isSelected().hash(into: &hasher)
|
||||
storedSelection.hash(into: &hasher)
|
||||
shouldAutoSave.hash(into: &hasher)
|
||||
|
||||
case .rightButtonAction(let title, _):
|
||||
title.hash(into: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
public static func == (lhs: SettingsAction, rhs: SettingsAction) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.threadInfo(let lhsThreadViewModel, let lhsStyle, _, _, _), .threadInfo(let rhsThreadViewModel, let rhsStyle, _, _, _)):
|
||||
return (
|
||||
lhsThreadViewModel == rhsThreadViewModel &&
|
||||
lhsStyle == rhsStyle
|
||||
)
|
||||
|
||||
case (.userDefaultsBool(_, let lhsKey, let lhsIsEnabled, _), .userDefaultsBool(_, let rhsKey, let rhsIsEnabled, _)):
|
||||
return (
|
||||
lhsKey == rhsKey &&
|
||||
lhsIsEnabled == rhsIsEnabled
|
||||
)
|
||||
|
||||
case (.settingBool(let lhsKey, let lhsConfirmationInfo, let lhsIsEnabled), .settingBool(let rhsKey, let rhsConfirmationInfo, let rhsIsEnabled)):
|
||||
return (
|
||||
lhsKey == rhsKey &&
|
||||
lhsConfirmationInfo == rhsConfirmationInfo &&
|
||||
lhsIsEnabled == rhsIsEnabled
|
||||
)
|
||||
|
||||
case (.customToggle(let lhsValue, let lhsIsEnabled, let lhsConfirmationInfo, _), .customToggle(let rhsValue, let rhsIsEnabled, let rhsConfirmationInfo, _)):
|
||||
return (
|
||||
lhsValue == rhsValue &&
|
||||
lhsIsEnabled == rhsIsEnabled &&
|
||||
lhsConfirmationInfo == rhsConfirmationInfo
|
||||
)
|
||||
|
||||
case (.settingEnum(let lhsKey, let lhsTitle, _), .settingEnum(let rhsKey, let rhsTitle, _)):
|
||||
return (
|
||||
lhsKey == rhsKey &&
|
||||
lhsTitle == rhsTitle
|
||||
)
|
||||
|
||||
case (.generalEnum(let lhsTitle, _), .generalEnum(let rhsTitle, _)):
|
||||
return (lhsTitle == rhsTitle)
|
||||
|
||||
case (.trigger(let lhsShowChevron, _), .trigger(let rhsShowChevron, _)):
|
||||
return (lhsShowChevron == rhsShowChevron)
|
||||
|
||||
case (.push(let lhsShowChevron, let lhsTintColor, let lhsHasBackground, _), .push(let rhsShowChevron, let rhsTintColor, let rhsHasBackground, _)):
|
||||
return (
|
||||
lhsShowChevron == rhsShowChevron &&
|
||||
lhsTintColor == rhsTintColor &&
|
||||
lhsHasBackground == rhsHasBackground
|
||||
)
|
||||
|
||||
case (.present(let lhsTintColor, _), .present(let rhsTintColor, _)):
|
||||
return (lhsTintColor == rhsTintColor)
|
||||
|
||||
case (.listSelection(let lhsIsSelected, let lhsStoredSelection, let lhsShouldAutoSave, _), .listSelection(let rhsIsSelected, let rhsStoredSelection, let rhsShouldAutoSave, _)):
|
||||
return (
|
||||
lhsIsSelected() == rhsIsSelected() &&
|
||||
lhsStoredSelection == rhsStoredSelection &&
|
||||
lhsShouldAutoSave == rhsShouldAutoSave
|
||||
)
|
||||
|
||||
case (.rightButtonAction(let lhsTitle, _), .rightButtonAction(let rhsTitle, _)):
|
||||
return (lhsTitle == rhsTitle)
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ThreadInfoStyle
|
||||
|
||||
public struct ThreadInfoStyle: Hashable, Equatable {
|
||||
public enum Style: Hashable, Equatable {
|
||||
case small
|
||||
case monoSmall
|
||||
case monoLarge
|
||||
}
|
||||
|
||||
public struct Action: Hashable, Equatable {
|
||||
let title: String
|
||||
let run: (OutlineButton?) -> ()
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
title.hash(into: &hasher)
|
||||
}
|
||||
|
||||
public static func == (lhs: Action, rhs: Action) -> Bool {
|
||||
return (lhs.title == rhs.title)
|
||||
}
|
||||
}
|
||||
|
||||
public let separatorTitle: String?
|
||||
public let descriptionStyle: Style
|
||||
public let descriptionActions: [Action]
|
||||
|
||||
public init(
|
||||
separatorTitle: String? = nil,
|
||||
descriptionStyle: Style = .monoSmall,
|
||||
descriptionActions: [Action] = []
|
||||
) {
|
||||
self.separatorTitle = separatorTitle
|
||||
self.descriptionStyle = descriptionStyle
|
||||
self.descriptionActions = descriptionActions
|
||||
}
|
||||
}
|
|
@ -1,561 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
final class SettingsVC: BaseVC, AvatarViewHelperDelegate {
|
||||
private var displayNameToBeUploaded: String?
|
||||
private var isEditingDisplayName = false { didSet { handleIsEditingDisplayNameChanged() } }
|
||||
|
||||
// MARK: - Components
|
||||
|
||||
private lazy var profilePictureView: ProfilePictureView = {
|
||||
let result = ProfilePictureView()
|
||||
let size = Values.largeProfilePictureSize
|
||||
result.size = size
|
||||
result.set(.width, to: size)
|
||||
result.set(.height, to: size)
|
||||
result.accessibilityLabel = "Edit profile picture button"
|
||||
result.isAccessibilityElement = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var profilePictureUtilities: AvatarViewHelper = {
|
||||
let result = AvatarViewHelper()
|
||||
result.delegate = self
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var displayNameTextField: TextField = {
|
||||
let result = TextField(
|
||||
placeholder: "vc_settings_display_name_text_field_hint".localized(),
|
||||
usesDefaultHeight: false
|
||||
)
|
||||
result.textAlignment = .center
|
||||
result.accessibilityLabel = "Edit display name text field"
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var publicKeyLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.font = Fonts.spaceMono(ofSize: isIPhone5OrSmaller ? Values.mediumFontSize : Values.largeFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.numberOfLines = 0
|
||||
result.textAlignment = .center
|
||||
result.lineBreakMode = .byCharWrapping
|
||||
result.text = getUserHexEncodedPublicKey()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var copyButton: OutlineButton = {
|
||||
let result = OutlineButton(style: .regular, size: .medium)
|
||||
result.setTitle("copy".localized(), for: UIControl.State.normal)
|
||||
result.addTarget(self, action: #selector(copyPublicKey), for: UIControl.Event.touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var shareButton: OutlineButton = {
|
||||
let result = OutlineButton(style: .regular, size: .medium)
|
||||
result.setTitle("share".localized(), for: UIControl.State.normal)
|
||||
result.addTarget(self, action: #selector(sharePublicKey), for: UIControl.Event.touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var settingButtonsStackView: UIStackView = {
|
||||
let result = UIStackView()
|
||||
result.axis = .vertical
|
||||
result.alignment = .fill
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var logoImageView: UIImageView = {
|
||||
let result = UIImageView(
|
||||
image: UIImage(named: "OxenLightMode")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
)
|
||||
result.themeTintColor = .textPrimary
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.set(.height, to: 24)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var versionLabel: UILabel = {
|
||||
let version: String = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)
|
||||
.defaulting(to: "0.0.0")
|
||||
let buildNumber: String = (Bundle.main.infoDictionary?["CFBundleVersion"] as? String)
|
||||
.defaulting(to: "0")
|
||||
|
||||
let result = UILabel()
|
||||
result.font = .systemFont(ofSize: Values.verySmallFontSize)
|
||||
result.text = "Version \(version) (\(buildNumber))"
|
||||
result.themeTextColor = .textPrimary
|
||||
result.numberOfLines = 0
|
||||
result.textAlignment = .center
|
||||
result.lineBreakMode = .byCharWrapping
|
||||
result.alpha = Values.mediumOpacity
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
private static let buttonHeight = isIPhone5OrSmaller ? CGFloat(52) : CGFloat(75)
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setNavBarTitle("vc_settings_title".localized())
|
||||
|
||||
// Navigation bar buttons
|
||||
updateNavigationBarButtons()
|
||||
|
||||
// Profile picture view
|
||||
let profile: Profile = Profile.fetchOrCreateCurrentUser()
|
||||
let profilePictureTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditProfilePictureUI))
|
||||
profilePictureView.addGestureRecognizer(profilePictureTapGestureRecognizer)
|
||||
profilePictureView
|
||||
.update(
|
||||
publicKey: profile.id,
|
||||
profile: profile,
|
||||
threadVariant: .contact
|
||||
)
|
||||
// Display name label
|
||||
displayNameLabel.text = profile.name
|
||||
|
||||
// Display name container
|
||||
let displayNameContainer = UIView()
|
||||
displayNameContainer.accessibilityLabel = "Edit display name text field"
|
||||
displayNameContainer.isAccessibilityElement = true
|
||||
displayNameContainer.addSubview(displayNameLabel)
|
||||
displayNameLabel.pin(to: displayNameContainer)
|
||||
displayNameContainer.addSubview(displayNameTextField)
|
||||
displayNameTextField.pin(to: displayNameContainer)
|
||||
displayNameContainer.set(.height, to: 40)
|
||||
displayNameTextField.alpha = 0
|
||||
let displayNameContainerTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showEditDisplayNameUI))
|
||||
displayNameContainer.addGestureRecognizer(displayNameContainerTapGestureRecognizer)
|
||||
|
||||
// Header view
|
||||
let headerStackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameContainer ])
|
||||
headerStackView.axis = .vertical
|
||||
headerStackView.spacing = Values.smallSpacing
|
||||
headerStackView.alignment = .center
|
||||
|
||||
// Separator
|
||||
let separator = Separator(title: "your_session_id".localized())
|
||||
|
||||
// Button container
|
||||
let buttonContainer = UIStackView(arrangedSubviews: [ copyButton, shareButton ])
|
||||
buttonContainer.axis = .horizontal
|
||||
buttonContainer.spacing = UIDevice.current.isIPad ? Values.iPadButtonSpacing : Values.mediumSpacing
|
||||
buttonContainer.distribution = .fillEqually
|
||||
|
||||
if (UIDevice.current.isIPad) {
|
||||
buttonContainer.layoutMargins = UIEdgeInsets(top: 0, left: Values.iPadButtonContainerMargin, bottom: 0, right: Values.iPadButtonContainerMargin)
|
||||
buttonContainer.isLayoutMarginsRelativeArrangement = true
|
||||
}
|
||||
// User session id container
|
||||
let userPublicKeyContainer = UIView(wrapping: publicKeyLabel, withInsets: .zero, shouldAdaptForIPadWithWidth: Values.iPadUserSessionIdContainerWidth)
|
||||
|
||||
// Top stack view
|
||||
let topStackView = UIStackView(arrangedSubviews: [ headerStackView, separator, userPublicKeyContainer, buttonContainer ])
|
||||
topStackView.axis = .vertical
|
||||
topStackView.spacing = Values.largeSpacing
|
||||
topStackView.alignment = .fill
|
||||
topStackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.largeSpacing, bottom: 0, right: Values.largeSpacing)
|
||||
topStackView.isLayoutMarginsRelativeArrangement = true
|
||||
|
||||
// Setting buttons stack view
|
||||
getSettingButtons().forEach { settingButtonOrSeparator in
|
||||
settingButtonsStackView.addArrangedSubview(settingButtonOrSeparator)
|
||||
}
|
||||
|
||||
// Oxen logo
|
||||
let logoContainer = UIView()
|
||||
logoContainer.addSubview(logoImageView)
|
||||
logoImageView.pin(.top, to: .top, of: logoContainer)
|
||||
logoContainer.pin(.bottom, to: .bottom, of: logoImageView)
|
||||
logoImageView.centerXAnchor.constraint(equalTo: logoContainer.centerXAnchor, constant: -2).isActive = true
|
||||
|
||||
// Main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ topStackView, settingButtonsStackView, logoContainer, versionLabel ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.largeSpacing
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: Values.mediumSpacing, left: 0, bottom: Values.mediumSpacing, right: 0)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width)
|
||||
|
||||
// Scroll view
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.addSubview(stackView)
|
||||
stackView.pin(to: scrollView)
|
||||
view.addSubview(scrollView)
|
||||
scrollView.pin(to: view)
|
||||
}
|
||||
|
||||
private func getSettingButtons() -> [UIView] {
|
||||
func getSettingButton(
|
||||
title: String,
|
||||
color: ThemeValue = .textPrimary,
|
||||
action selector: Selector
|
||||
) -> UIButton {
|
||||
let result: UIButton = UIButton()
|
||||
result.setTitle(title, for: UIControl.State.normal)
|
||||
result.setThemeTitleColor(color, for: UIControl.State.normal)
|
||||
result.titleLabel?.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.titleLabel?.textAlignment = .center
|
||||
result.setThemeBackgroundColor(.settings_tabBackground, for: .normal)
|
||||
result.setThemeBackgroundColor(.settings_tabHighlight, for: .highlighted)
|
||||
result.addTarget(self, action: selector, for: UIControl.Event.touchUpInside)
|
||||
result.set(.height, to: SettingsVC.buttonHeight)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
let pathButton = getSettingButton(title: "vc_path_title".localized(), action: #selector(showPath))
|
||||
let pathStatusView = PathStatusView()
|
||||
pathStatusView.set(.width, to: PathStatusView.size)
|
||||
pathStatusView.set(.height, to: PathStatusView.size)
|
||||
|
||||
pathButton.addSubview(pathStatusView)
|
||||
pathStatusView.pin(.leading, to: .trailing, of: pathButton.titleLabel!, withInset: Values.smallSpacing)
|
||||
pathStatusView.autoVCenterInSuperview()
|
||||
|
||||
return [
|
||||
UIView.separator(),
|
||||
pathButton,
|
||||
UIView.separator(),
|
||||
getSettingButton(title: "vc_settings_privacy_button_title".localized(), action: #selector(showPrivacySettings)),
|
||||
UIView.separator(),
|
||||
getSettingButton(title: "vc_settings_notifications_button_title".localized(), action: #selector(showNotificationSettings)),
|
||||
UIView.separator(),
|
||||
getSettingButton(title: "CONVERSATION_SETTINGS_TITLE".localized(), action: #selector(showConversationSettings)),
|
||||
UIView.separator(),
|
||||
getSettingButton(title: "MESSAGE_REQUESTS_TITLE".localized(), action: #selector(showMessageRequests)),
|
||||
UIView.separator(),
|
||||
getSettingButton(title: "APPEARANCE_TITLE".localized(), action: #selector(showAppearanceSettings)),
|
||||
UIView.separator(),
|
||||
getSettingButton(title: "vc_settings_invite_a_friend_button_title".localized(), action: #selector(sendInvitation)),
|
||||
UIView.separator(),
|
||||
getSettingButton(title: "vc_settings_recovery_phrase_button_title".localized(), action: #selector(showSeed)),
|
||||
UIView.separator(),
|
||||
getSettingButton(title: "HELP_TITLE".localized(), action: #selector(showHelp)),
|
||||
UIView.separator(),
|
||||
getSettingButton(title: "vc_settings_clear_all_data_button_title".localized(), color: .danger, action: #selector(clearAllData)),
|
||||
UIView.separator()
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - General
|
||||
|
||||
@objc private func enableCopyButton() {
|
||||
copyButton.isUserInteractionEnabled = true
|
||||
|
||||
UIView.transition(
|
||||
with: copyButton,
|
||||
duration: 0.25,
|
||||
options: .transitionCrossDissolve,
|
||||
animations: {
|
||||
self.copyButton.setTitle("copy".localized(), for: .normal)
|
||||
},
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
|
||||
func avatarActionSheetTitle() -> String? { return "Update Profile Picture" }
|
||||
func fromViewController() -> UIViewController { return self }
|
||||
func hasClearAvatarAction() -> Bool { return false }
|
||||
func clearAvatarActionLabel() -> String { return "Clear" }
|
||||
|
||||
// MARK: - Updating
|
||||
|
||||
private func handleIsEditingDisplayNameChanged() {
|
||||
updateNavigationBarButtons()
|
||||
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.displayNameLabel.alpha = self.isEditingDisplayName ? 0 : 1
|
||||
self.displayNameTextField.alpha = self.isEditingDisplayName ? 1 : 0
|
||||
}
|
||||
|
||||
if isEditingDisplayName {
|
||||
displayNameTextField.becomeFirstResponder()
|
||||
}
|
||||
else {
|
||||
displayNameTextField.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNavigationBarButtons() {
|
||||
if isEditingDisplayName {
|
||||
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(handleCancelDisplayNameEditingButtonTapped))
|
||||
cancelButton.themeTintColor = .textPrimary
|
||||
cancelButton.accessibilityLabel = "Cancel button"
|
||||
cancelButton.isAccessibilityElement = true
|
||||
navigationItem.leftBarButtonItem = cancelButton
|
||||
|
||||
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleSaveDisplayNameButtonTapped))
|
||||
doneButton.themeTintColor = .textPrimary
|
||||
doneButton.accessibilityLabel = "Done button"
|
||||
doneButton.isAccessibilityElement = true
|
||||
navigationItem.rightBarButtonItem = doneButton
|
||||
}
|
||||
else {
|
||||
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
|
||||
closeButton.themeTintColor = .textPrimary
|
||||
closeButton.accessibilityLabel = "Close button"
|
||||
closeButton.isAccessibilityElement = true
|
||||
navigationItem.leftBarButtonItem = closeButton
|
||||
|
||||
let qrCodeButton = UIButton()
|
||||
qrCodeButton.setImage(
|
||||
UIImage(named: "QRCode")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
for: .normal
|
||||
)
|
||||
qrCodeButton.themeTintColor = .textPrimary
|
||||
qrCodeButton.addTarget(self, action: #selector(showQRCode), for: UIControl.Event.touchUpInside)
|
||||
qrCodeButton.accessibilityLabel = "Show QR code button"
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [ qrCodeButton ])
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = Values.mediumSpacing
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: stackView)
|
||||
}
|
||||
}
|
||||
|
||||
func avatarDidChange(_ image: UIImage?, filePath: String?) {
|
||||
updateProfile(
|
||||
profilePicture: image,
|
||||
profilePictureFilePath: filePath,
|
||||
isUpdatingDisplayName: false,
|
||||
isUpdatingProfilePicture: true
|
||||
)
|
||||
}
|
||||
|
||||
func clearAvatar() {
|
||||
updateProfile(
|
||||
profilePicture: nil,
|
||||
profilePictureFilePath: nil,
|
||||
isUpdatingDisplayName: false,
|
||||
isUpdatingProfilePicture: true
|
||||
)
|
||||
}
|
||||
|
||||
private func updateProfile(
|
||||
profilePicture: UIImage?,
|
||||
profilePictureFilePath: String?,
|
||||
isUpdatingDisplayName: Bool,
|
||||
isUpdatingProfilePicture: Bool
|
||||
) {
|
||||
let userDefaults = UserDefaults.standard
|
||||
let name: String? = (displayNameToBeUploaded ?? Profile.fetchOrCreateCurrentUser().name)
|
||||
let imageFilePath: String? = (profilePictureFilePath ?? ProfileManager.profileAvatarFilepath(id: getUserHexEncodedPublicKey()))
|
||||
|
||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self, displayNameToBeUploaded] modalActivityIndicator in
|
||||
ProfileManager.updateLocal(
|
||||
queue: DispatchQueue.global(qos: .default),
|
||||
profileName: (name ?? ""),
|
||||
image: profilePicture,
|
||||
imageFilePath: imageFilePath,
|
||||
success: { db, updatedProfile in
|
||||
if displayNameToBeUploaded != nil {
|
||||
userDefaults[.lastDisplayNameUpdate] = Date()
|
||||
}
|
||||
|
||||
if isUpdatingProfilePicture {
|
||||
userDefaults[.lastProfilePictureUpdate] = Date()
|
||||
}
|
||||
|
||||
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||
|
||||
// Wait for the database transaction to complete before updating the UI
|
||||
db.afterNextTransactionCommit { _ in
|
||||
DispatchQueue.main.async {
|
||||
modalActivityIndicator.dismiss {
|
||||
self?.profilePictureView.update(
|
||||
publicKey: updatedProfile.id,
|
||||
profile: updatedProfile,
|
||||
threadVariant: .contact
|
||||
)
|
||||
self?.displayNameLabel.text = name
|
||||
self?.displayNameToBeUploaded = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
failure: { error in
|
||||
DispatchQueue.main.async {
|
||||
modalActivityIndicator.dismiss {
|
||||
let isMaxFileSizeExceeded = (error == .avatarUploadMaxFileSizeExceeded)
|
||||
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile"
|
||||
let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : "Please check your internet connection and try again"
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: nil))
|
||||
self?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
@objc private func close() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func showQRCode() {
|
||||
let qrCodeVC = QRCodeVC()
|
||||
navigationController!.pushViewController(qrCodeVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func handleCancelDisplayNameEditingButtonTapped() {
|
||||
isEditingDisplayName = false
|
||||
}
|
||||
|
||||
@objc private func handleSaveDisplayNameButtonTapped() {
|
||||
func showError(title: String, message: String = "") {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil))
|
||||
presentAlert(alert)
|
||||
}
|
||||
let displayName = displayNameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
guard !displayName.isEmpty else {
|
||||
return showError(title: NSLocalizedString("vc_settings_display_name_missing_error", comment: ""))
|
||||
}
|
||||
guard !ProfileManager.isToLong(profileName: displayName) else {
|
||||
return showError(title: NSLocalizedString("vc_settings_display_name_too_long_error", comment: ""))
|
||||
}
|
||||
isEditingDisplayName = false
|
||||
displayNameToBeUploaded = displayName
|
||||
updateProfile(
|
||||
profilePicture: nil,
|
||||
profilePictureFilePath: nil,
|
||||
isUpdatingDisplayName: true,
|
||||
isUpdatingProfilePicture: false
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func showEditProfilePictureUI() {
|
||||
profilePictureUtilities.showChangeAvatarUI()
|
||||
}
|
||||
|
||||
@objc private func showEditDisplayNameUI() {
|
||||
isEditingDisplayName = true
|
||||
}
|
||||
|
||||
@objc private func copyPublicKey() {
|
||||
UIPasteboard.general.string = getUserHexEncodedPublicKey()
|
||||
copyButton.isUserInteractionEnabled = false
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle(NSLocalizedString("copied", comment: ""), for: UIControl.State.normal)
|
||||
}, completion: nil)
|
||||
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
|
||||
}
|
||||
|
||||
@objc private func sharePublicKey() {
|
||||
let shareVC = UIActivityViewController(activityItems: [ getUserHexEncodedPublicKey() ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = self.view
|
||||
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func showPath() {
|
||||
let pathVC = PathVC()
|
||||
self.navigationController?.pushViewController(pathVC, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showPrivacySettings() {
|
||||
let settingsViewController: SettingsTableViewController = SettingsTableViewController(
|
||||
viewModel: PrivacySettingsViewModel()
|
||||
)
|
||||
self.navigationController?.pushViewController(settingsViewController, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showNotificationSettings() {
|
||||
let settingsViewController: SettingsTableViewController = SettingsTableViewController(
|
||||
viewModel: NotificationSettingsViewModel()
|
||||
)
|
||||
self.navigationController?.pushViewController(settingsViewController, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showMessageRequests() {
|
||||
let viewController: MessageRequestsViewController = MessageRequestsViewController()
|
||||
self.navigationController?.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showConversationSettings() {
|
||||
let settingsViewController: SettingsTableViewController = SettingsTableViewController(
|
||||
viewModel: ConversationSettingsViewModel()
|
||||
)
|
||||
self.navigationController?.pushViewController(settingsViewController, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showAppearanceSettings() {
|
||||
let appearanceViewController: AppearanceViewController = AppearanceViewController()
|
||||
self.navigationController?.pushViewController(appearanceViewController, animated: true)
|
||||
}
|
||||
|
||||
@objc private func showSeed() {
|
||||
let seedModal = SeedModal()
|
||||
seedModal.modalPresentationStyle = .overFullScreen
|
||||
seedModal.modalTransitionStyle = .crossDissolve
|
||||
present(seedModal, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func showHelp() {
|
||||
let settingsViewController: SettingsTableViewController = SettingsTableViewController(
|
||||
viewModel: HelpViewModel()
|
||||
)
|
||||
self.navigationController?.pushViewController(settingsViewController, animated: true)
|
||||
}
|
||||
|
||||
@objc private func clearAllData() {
|
||||
let nukeDataModal = NukeDataModal()
|
||||
nukeDataModal.modalPresentationStyle = .overFullScreen
|
||||
nukeDataModal.modalTransitionStyle = .crossDissolve
|
||||
present(nukeDataModal, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func sendInvitation() {
|
||||
let invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is \(getUserHexEncodedPublicKey()) !"
|
||||
let shareVC = UIActivityViewController(activityItems: [ invitation ], applicationActivities: nil)
|
||||
if UIDevice.current.isIPad {
|
||||
shareVC.excludedActivityTypes = []
|
||||
shareVC.popoverPresentationController?.permittedArrowDirections = []
|
||||
shareVC.popoverPresentationController?.sourceView = self.view
|
||||
shareVC.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
navigationController!.present(shareVC, animated: true, completion: nil)
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ import SessionMessagingKit
|
|||
import SessionUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
class SettingsViewModel: SettingsTableViewModel<SettingsViewModel.NavButton, SettingsViewModel.Section, SettingsViewModel.Item> {
|
||||
class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, SettingsViewModel.Section, SettingsViewModel.Item> {
|
||||
// MARK: - Config
|
||||
|
||||
enum NavState {
|
||||
|
@ -24,7 +24,7 @@ class SettingsViewModel: SettingsTableViewModel<SettingsViewModel.NavButton, Set
|
|||
case done
|
||||
}
|
||||
|
||||
public enum Section: SettingSection {
|
||||
public enum Section: SessionTableSection {
|
||||
case profileInfo
|
||||
case menus
|
||||
case footer
|
||||
|
@ -53,11 +53,11 @@ class SettingsViewModel: SettingsTableViewModel<SettingsViewModel.NavButton, Set
|
|||
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
override init() {
|
||||
self.userSessionId = getUserHexEncodedPublicKey()
|
||||
self.oldDisplayName = Profile.fetchOrCreateCurrentUser().name
|
||||
|
||||
super.init(closeNavItemId: .close)
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - Navigation
|
||||
|
@ -140,7 +140,7 @@ class SettingsViewModel: SettingsTableViewModel<SettingsViewModel.NavButton, Set
|
|||
.withRenderingMode(.alwaysTemplate),
|
||||
style: .plain,
|
||||
accessibilityIdentifier: "Close button"
|
||||
)
|
||||
) { [weak self] in self?.dismissScreen() }
|
||||
]
|
||||
|
||||
case .editing:
|
||||
|
@ -212,26 +212,25 @@ class SettingsViewModel: SettingsTableViewModel<SettingsViewModel.NavButton, Set
|
|||
SectionModel(
|
||||
model: .profileInfo,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .profileInfo,
|
||||
title: profile.displayName(),
|
||||
action: .threadInfo(
|
||||
leftAccessory: .threadInfo(
|
||||
threadViewModel: SessionThreadViewModel(
|
||||
threadId: profile.id,
|
||||
threadIsNoteToSelf: true,
|
||||
contactProfile: profile
|
||||
),
|
||||
style: ThreadInfoStyle(
|
||||
style: SessionCell.Accessory.ThreadInfoStyle(
|
||||
separatorTitle: "your_session_id".localized(),
|
||||
descriptionStyle: .monoLarge,
|
||||
descriptionActions: [
|
||||
ThreadInfoStyle.Action(
|
||||
SessionCell.Accessory.ThreadInfoStyle.Action(
|
||||
title: "copy".localized(),
|
||||
run: { [weak self] button in
|
||||
self?.copySessionId(profile.id, button: button)
|
||||
}
|
||||
),
|
||||
ThreadInfoStyle.Action(
|
||||
SessionCell.Accessory.ThreadInfoStyle.Action(
|
||||
title: "share".localized(),
|
||||
run: { [weak self] _ in
|
||||
self?.shareSessionId(profile.id)
|
||||
|
@ -242,90 +241,147 @@ class SettingsViewModel: SettingsTableViewModel<SettingsViewModel.NavButton, Set
|
|||
avatarTapped: { [weak self] in self?.updateProfilePicture() },
|
||||
titleTapped: { [weak self] in self?.setIsEditing(true) },
|
||||
titleChanged: { [weak self] text in self?.editedDisplayName = text }
|
||||
)
|
||||
),
|
||||
title: profile.displayName(),
|
||||
shouldHaveBackground: false
|
||||
)
|
||||
]
|
||||
),
|
||||
SectionModel(
|
||||
model: .menus,
|
||||
elements: [
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .path,
|
||||
leftAccessory: .customView {
|
||||
// Need to ensure this view is the same size as the icons so
|
||||
// wrap it in a larger view
|
||||
let result: UIView = UIView()
|
||||
let pathView: PathStatusView = PathStatusView(size: .large)
|
||||
result.addSubview(pathView)
|
||||
|
||||
result.set(.width, to: IconSize.small.size)
|
||||
result.set(.height, to: IconSize.small.size)
|
||||
pathView.center(in: result)
|
||||
|
||||
return result
|
||||
},
|
||||
title: "vc_path_title".localized(),
|
||||
onTap: { [weak self] in self?.transitionToScreen(PathVC()) }
|
||||
),
|
||||
SessionCell.Info(
|
||||
id: .privacy,
|
||||
icon: UIImage(named: "icon_privacy")?.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "icon_privacy")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "vc_settings_privacy_button_title".localized(),
|
||||
action: .push(showChevron: false) {
|
||||
SettingsTableViewController(viewModel: PrivacySettingsViewModel())
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
SessionTableViewController(viewModel: PrivacySettingsViewModel())
|
||||
)
|
||||
}
|
||||
),
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .notifications,
|
||||
icon: UIImage(named: "icon_speaker")?.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "icon_speaker")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "vc_settings_notifications_button_title".localized(),
|
||||
action: .push(showChevron: false) {
|
||||
SettingsTableViewController(viewModel: NotificationSettingsViewModel())
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
SessionTableViewController(viewModel: NotificationSettingsViewModel())
|
||||
)
|
||||
}
|
||||
),
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .conversations,
|
||||
icon: UIImage(named: "icon_msg")?.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "icon_msg")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "CONVERSATION_SETTINGS_TITLE".localized(),
|
||||
action: .push(showChevron: false) {
|
||||
SettingsTableViewController(viewModel: ConversationSettingsViewModel())
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
SessionTableViewController(viewModel: ConversationSettingsViewModel())
|
||||
)
|
||||
}
|
||||
),
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .messageRequests,
|
||||
icon: UIImage(named: "icon_msg_req")?.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "icon_msg_req")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "MESSAGE_REQUESTS_TITLE".localized(),
|
||||
action: .push(showChevron: false) {
|
||||
MessageRequestsViewController()
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(MessageRequestsViewController())
|
||||
}
|
||||
),
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .appearance,
|
||||
icon: UIImage(named: "icon_apperance")?.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "icon_apperance")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "APPEARANCE_TITLE".localized(),
|
||||
action: .push(showChevron: false) {
|
||||
AppearanceViewController()
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(AppearanceViewController())
|
||||
}
|
||||
),
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .inviteAFriend,
|
||||
icon: UIImage(named: "icon_invite")?.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "icon_invite")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "vc_settings_invite_a_friend_button_title".localized(),
|
||||
action: .present {
|
||||
onTap: { [weak self] in
|
||||
let invitation: String = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is \(profile.id) !"
|
||||
|
||||
return UIActivityViewController(
|
||||
activityItems: [ invitation ],
|
||||
applicationActivities: nil
|
||||
self?.transitionToScreen(
|
||||
UIActivityViewController(
|
||||
activityItems: [ invitation ],
|
||||
applicationActivities: nil
|
||||
),
|
||||
transitionType: .present
|
||||
)
|
||||
}
|
||||
),
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .recoveryPhrase,
|
||||
icon: UIImage(named: "icon_recovery")?.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "icon_recovery")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "vc_settings_recovery_phrase_button_title".localized(),
|
||||
action: .present {
|
||||
SeedModal()
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(SeedModal(), transitionType: .present)
|
||||
}
|
||||
),
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .help,
|
||||
icon: UIImage(named: "icon_help")?.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "icon_help")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "HELP_TITLE".localized(),
|
||||
action: .push(showChevron: false) {
|
||||
SettingsTableViewController(
|
||||
viewModel: HelpViewModel()
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
SessionTableViewController(viewModel: HelpViewModel())
|
||||
)
|
||||
}
|
||||
),
|
||||
SettingInfo(
|
||||
SessionCell.Info(
|
||||
id: .clearData,
|
||||
icon: UIImage(named: "icon_bin")?.withRenderingMode(.alwaysTemplate),
|
||||
leftAccessory: .icon(
|
||||
UIImage(named: "icon_bin")?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "vc_settings_clear_all_data_button_title".localized(),
|
||||
action: .present(tintColor: .danger) {
|
||||
NukeDataModal()
|
||||
tintColor: .danger,
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(NukeDataModal(), transitionType: .present)
|
||||
}
|
||||
)
|
||||
]
|
||||
|
@ -437,10 +493,10 @@ class SettingsViewModel: SettingsTableViewModel<SettingsViewModel.NavButton, Set
|
|||
self.transitionToScreen(viewController, transitionType: .present)
|
||||
}
|
||||
|
||||
private func copySessionId(_ sessionId: String, button: OutlineButton?) {
|
||||
private func copySessionId(_ sessionId: String, button: SessionButton?) {
|
||||
UIPasteboard.general.string = sessionId
|
||||
|
||||
guard let button: OutlineButton = button else { return }
|
||||
guard let button: SessionButton = button else { return }
|
||||
|
||||
// Ensure we are on the main thread just in case
|
||||
DispatchQueue.main.async {
|
||||
|
|
|
@ -8,14 +8,14 @@ import SessionUIKit
|
|||
import SessionUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
protocol SettingsViewModelAccessible {
|
||||
protocol SessionViewModelAccessible {
|
||||
var viewModelType: AnyObject.Type { get }
|
||||
}
|
||||
|
||||
class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection, SettingItem: Hashable & Differentiable>: BaseVC, UITableViewDataSource, UITableViewDelegate, SettingsViewModelAccessible {
|
||||
typealias SectionModel = SettingsTableViewModel<NavItemId, Section, SettingItem>.SectionModel
|
||||
class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSection, SettingItem: Hashable & Differentiable>: BaseVC, UITableViewDataSource, UITableViewDelegate, SessionViewModelAccessible {
|
||||
typealias SectionModel = SessionTableViewModel<NavItemId, Section, SettingItem>.SectionModel
|
||||
|
||||
private let viewModel: SettingsTableViewModel<NavItemId, Section, SettingItem>
|
||||
private let viewModel: SessionTableViewModel<NavItemId, Section, SettingItem>
|
||||
private var hasLoadedInitialSettingsData: Bool = false
|
||||
private var dataStreamJustFailed: Bool = false
|
||||
private var dataChangeCancellable: AnyCancellable?
|
||||
|
@ -32,9 +32,9 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
|
|||
result.themeBackgroundColor = .clear
|
||||
result.showsVerticalScrollIndicator = false
|
||||
result.showsHorizontalScrollIndicator = false
|
||||
result.register(view: SettingsAvatarCell.self)
|
||||
result.register(view: SettingsCell.self)
|
||||
result.registerHeaderFooterView(view: SettingHeaderView.self)
|
||||
result.register(view: SessionAvatarCell.self)
|
||||
result.register(view: SessionCell.self)
|
||||
result.registerHeaderFooterView(view: SessionHeaderView.self)
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
|
||||
|
@ -47,7 +47,7 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
|
|||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(viewModel: SettingsTableViewModel<NavItemId, Section, SettingItem>) {
|
||||
init(viewModel: SessionTableViewModel<NavItemId, Section, SettingItem>) {
|
||||
self.viewModel = viewModel
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
@ -192,10 +192,10 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
|
|||
|
||||
self?.tableView.visibleCells.forEach { cell in
|
||||
switch cell {
|
||||
case let settingsCell as SettingsCell:
|
||||
settingsCell.update(isEditing: isEditing, animated: true)
|
||||
case let cell as SessionCell:
|
||||
cell.update(isEditing: isEditing, animated: true)
|
||||
|
||||
case let avatarCell as SettingsAvatarCell:
|
||||
case let avatarCell as SessionAvatarCell:
|
||||
avatarCell.update(isEditing: isEditing, animated: true)
|
||||
|
||||
default: break
|
||||
|
@ -269,15 +269,26 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
|
|||
}
|
||||
.store(in: &disposables)
|
||||
|
||||
viewModel.closeScreen
|
||||
viewModel.dismissScreen
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] shouldDismiss in
|
||||
guard shouldDismiss else {
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
return
|
||||
.sink { [weak self] dismissType in
|
||||
switch dismissType {
|
||||
case .auto:
|
||||
guard
|
||||
let viewController: UIViewController = self,
|
||||
(self?.navigationController?.viewControllers
|
||||
.firstIndex(of: viewController))
|
||||
.defaulting(to: 0) > 0
|
||||
else {
|
||||
self?.dismiss(animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
|
||||
case .dismiss: self?.dismiss(animated: true)
|
||||
case .pop: self?.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
self?.navigationController?.dismiss(animated: true)
|
||||
}
|
||||
.store(in: &disposables)
|
||||
}
|
||||
|
@ -294,11 +305,11 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
|
|||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let section: SectionModel = viewModel.settingsData[indexPath.section]
|
||||
let settingInfo: SettingInfo<SettingItem> = section.elements[indexPath.row]
|
||||
let info: SessionCell.Info<SettingItem> = section.elements[indexPath.row]
|
||||
|
||||
switch settingInfo.action {
|
||||
switch info.leftAccessory {
|
||||
case .threadInfo(let threadViewModel, let style, let avatarTapped, let titleTapped, let titleChanged):
|
||||
let cell: SettingsAvatarCell = tableView.dequeue(type: SettingsAvatarCell.self, for: indexPath)
|
||||
let cell: SessionAvatarCell = tableView.dequeue(type: SessionAvatarCell.self, for: indexPath)
|
||||
cell.update(
|
||||
threadViewModel: threadViewModel,
|
||||
style: style,
|
||||
|
@ -323,28 +334,11 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
|
|||
return cell
|
||||
|
||||
default:
|
||||
let cell: SettingsCell = tableView.dequeue(type: SettingsCell.self, for: indexPath)
|
||||
let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath)
|
||||
cell.update(
|
||||
icon: settingInfo.icon,
|
||||
iconSize: settingInfo.iconSize,
|
||||
iconSetter: settingInfo.iconSetter,
|
||||
title: settingInfo.title,
|
||||
subtitle: settingInfo.subtitle,
|
||||
alignment: settingInfo.alignment,
|
||||
accessibilityIdentifier: settingInfo.accessibilityIdentifier,
|
||||
subtitleExtraViewGenerator: settingInfo.subtitleExtraViewGenerator,
|
||||
action: settingInfo.action,
|
||||
extraActionTitle: settingInfo.extraActionTitle,
|
||||
onExtraAction: settingInfo.onExtraAction,
|
||||
position: {
|
||||
guard section.elements.count > 1 else { return .individual }
|
||||
|
||||
switch indexPath.row {
|
||||
case 0: return .top
|
||||
case (section.elements.count - 1): return .bottom
|
||||
default: return .middle
|
||||
}
|
||||
}()
|
||||
with: info,
|
||||
style: .rounded,
|
||||
position: Position.with(indexPath.row, count: section.elements.count)
|
||||
)
|
||||
cell.update(isEditing: self.isEditing, animated: false)
|
||||
|
||||
|
@ -360,10 +354,10 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
|
|||
return UIView()
|
||||
|
||||
case .padding, .title:
|
||||
let result: SettingHeaderView = tableView.dequeueHeaderFooterView(type: SettingHeaderView.self)
|
||||
let result: SessionHeaderView = tableView.dequeueHeaderFooterView(type: SessionHeaderView.self)
|
||||
result.update(
|
||||
title: section.model.title,
|
||||
hasSeparator: (section.elements.first?.action.shouldHaveBackground != false)
|
||||
hasSeparator: (section.elements.first?.shouldHaveBackground != false)
|
||||
)
|
||||
|
||||
return result
|
||||
|
@ -393,173 +387,86 @@ class SettingsTableViewController<NavItemId: Equatable, Section: SettingSection,
|
|||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
|
||||
let section: SectionModel = self.viewModel.settingsData[indexPath.section]
|
||||
let settingInfo: SettingInfo<SettingItem> = section.elements[indexPath.row]
|
||||
|
||||
switch settingInfo.action {
|
||||
case .threadInfo: break
|
||||
let info: SessionCell.Info<SettingItem> = section.elements[indexPath.row]
|
||||
|
||||
// Do nothing if the item is disabled
|
||||
guard info.isEnabled else { return }
|
||||
|
||||
// Get the view that was tapped (for presenting on iPad)
|
||||
let tappedView: UIView? = {
|
||||
guard let cell: SessionCell = tableView.cellForRow(at: indexPath) as? SessionCell else {
|
||||
return nil
|
||||
}
|
||||
|
||||
case .trigger(_, let action):
|
||||
action()
|
||||
|
||||
case .rightButtonAction(_, let action):
|
||||
guard let cell: SettingsCell = tableView.cellForRow(at: indexPath) as? SettingsCell else {
|
||||
return
|
||||
}
|
||||
|
||||
action(cell.rightActionButtonContainerView)
|
||||
|
||||
case .userDefaultsBool(let defaults, let key, let isEnabled, let onChange):
|
||||
guard isEnabled else { return }
|
||||
|
||||
defaults.set(!defaults.bool(forKey: key), forKey: key)
|
||||
manuallyReload(indexPath: indexPath, section: section, settingInfo: settingInfo)
|
||||
onChange?()
|
||||
|
||||
case .settingBool(let key, let confirmationInfo, let isEnabled):
|
||||
guard isEnabled else { return }
|
||||
guard
|
||||
let confirmationInfo: ConfirmationModal.Info = confirmationInfo,
|
||||
confirmationInfo.stateToShow.shouldShow(for: Storage.shared[key])
|
||||
else {
|
||||
Storage.shared.write { db in db[key] = !db[key] }
|
||||
manuallyReload(indexPath: indexPath, section: section, settingInfo: settingInfo)
|
||||
return
|
||||
}
|
||||
|
||||
// Show a confirmation modal before continuing
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
info: confirmationInfo
|
||||
.with(onConfirm: { [weak self] _ in
|
||||
Storage.shared.write { db in db[key] = !db[key] }
|
||||
self?.manuallyReload(indexPath: indexPath, section: section, settingInfo: settingInfo)
|
||||
self?.dismiss(animated: true)
|
||||
})
|
||||
)
|
||||
present(confirmationModal, animated: true, completion: nil)
|
||||
|
||||
case .customToggle(let value, let isEnabled, let confirmationInfo, let onChange):
|
||||
guard isEnabled else { return }
|
||||
|
||||
let updatedValue: Bool = !value
|
||||
let performChange: () -> () = { [weak self] in
|
||||
self?.manuallyReload(
|
||||
indexPath: indexPath,
|
||||
section: section,
|
||||
settingInfo: settingInfo
|
||||
.with(
|
||||
action: .customToggle(
|
||||
value: updatedValue,
|
||||
isEnabled: isEnabled,
|
||||
onChange: onChange
|
||||
)
|
||||
)
|
||||
)
|
||||
onChange?(updatedValue)
|
||||
switch (info.leftAccessory, info.rightAccessory) {
|
||||
case (_, .highlightingBackgroundLabel(_)):
|
||||
return (!cell.rightAccessoryView.isHidden ? cell.rightAccessoryView : cell)
|
||||
|
||||
// In this case we need to restart the database observation to force a re-query as
|
||||
// the change here might not actually trigger a database update so the content wouldn't
|
||||
// be updated
|
||||
self?.stopObservingChanges()
|
||||
self?.startObservingChanges()
|
||||
}
|
||||
case (.highlightingBackgroundLabel(_), _):
|
||||
return (!cell.leftAccessoryView.isHidden ? cell.leftAccessoryView : cell)
|
||||
|
||||
guard
|
||||
let confirmationInfo: ConfirmationModal.Info = confirmationInfo,
|
||||
confirmationInfo.stateToShow.shouldShow(for: value)
|
||||
else {
|
||||
performChange()
|
||||
return
|
||||
default:
|
||||
return cell
|
||||
}
|
||||
}()
|
||||
let maybeOldSelection: (Int, SessionCell.Info<SettingItem>)? = section.elements
|
||||
.enumerated()
|
||||
.first(where: { index, info in
|
||||
switch (info.leftAccessory, info.rightAccessory) {
|
||||
case (_, .radio(_, let isSelected, _)): return isSelected()
|
||||
case (.radio(_, let isSelected, _), _): return isSelected()
|
||||
default: return false
|
||||
}
|
||||
|
||||
// Show a confirmation modal before continuing
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
info: confirmationInfo
|
||||
.with(onConfirm: { [weak self] _ in
|
||||
performChange()
|
||||
|
||||
self?.dismiss(animated: true) {
|
||||
guard let strongSelf: UIViewController = self else { return }
|
||||
|
||||
confirmationInfo.onConfirm?(strongSelf)
|
||||
}
|
||||
})
|
||||
)
|
||||
present(confirmationModal, animated: true, completion: nil)
|
||||
})
|
||||
|
||||
let performAction: () -> Void = { [weak self, weak tappedView] in
|
||||
info.onTap?(tappedView)
|
||||
self?.manuallyReload(indexPath: indexPath, section: section, info: info)
|
||||
|
||||
case .push(_, _, _, let createDestination), .settingEnum(_, _, let createDestination), .generalEnum(_, let createDestination):
|
||||
let viewController: UIViewController = createDestination()
|
||||
navigationController?.pushViewController(viewController, animated: true)
|
||||
|
||||
case .present(_, let createDestination):
|
||||
let viewController: UIViewController = createDestination()
|
||||
|
||||
if UIDevice.current.isIPad {
|
||||
viewController.popoverPresentationController?.permittedArrowDirections = []
|
||||
viewController.popoverPresentationController?.sourceView = self.view
|
||||
viewController.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
}
|
||||
|
||||
navigationController?.present(viewController, animated: true)
|
||||
|
||||
case .listSelection(_, _, let shouldAutoSave, let selectValue):
|
||||
let maybeOldSelection: (Int, SettingInfo<SettingItem>)? = section.elements
|
||||
.enumerated()
|
||||
.first(where: { index, info in
|
||||
switch info.action {
|
||||
case .listSelection(let isSelected, _, _, _): return isSelected()
|
||||
default: return false
|
||||
}
|
||||
})
|
||||
|
||||
selectValue()
|
||||
manuallyReload(indexPath: indexPath, section: section, settingInfo: settingInfo)
|
||||
|
||||
// Update the old selection as well
|
||||
if let oldSelection: (index: Int, info: SettingInfo<SettingItem>) = maybeOldSelection {
|
||||
manuallyReload(
|
||||
indexPath: IndexPath(
|
||||
row: oldSelection.index,
|
||||
section: indexPath.section
|
||||
),
|
||||
section: section,
|
||||
settingInfo: oldSelection.info
|
||||
)
|
||||
}
|
||||
|
||||
guard shouldAutoSave else { return }
|
||||
|
||||
navigationController?.popViewController(animated: true)
|
||||
// Update the old selection as well
|
||||
if let oldSelection: (index: Int, info: SessionCell.Info<SettingItem>) = maybeOldSelection {
|
||||
self?.manuallyReload(
|
||||
indexPath: IndexPath(
|
||||
row: oldSelection.index,
|
||||
section: indexPath.section
|
||||
),
|
||||
section: section,
|
||||
info: oldSelection.info
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
guard
|
||||
let confirmationInfo: ConfirmationModal.Info = info.confirmationInfo,
|
||||
confirmationInfo.stateToShow.shouldShow(for: info.currentBoolValue)
|
||||
else {
|
||||
performAction()
|
||||
return
|
||||
}
|
||||
|
||||
// Show a confirmation modal before continuing
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
targetView: tappedView,
|
||||
info: confirmationInfo
|
||||
.with(onConfirm: { [weak self] _ in
|
||||
performAction()
|
||||
self?.dismiss(animated: true)
|
||||
})
|
||||
)
|
||||
present(confirmationModal, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func manuallyReload(
|
||||
indexPath: IndexPath,
|
||||
section: SectionModel,
|
||||
settingInfo: SettingInfo<SettingItem>
|
||||
info: SessionCell.Info<SettingItem>
|
||||
) {
|
||||
// Try update the existing cell to have a nice animation instead of reloading the cell
|
||||
if let existingCell: SettingsCell = tableView.cellForRow(at: indexPath) as? SettingsCell {
|
||||
if let existingCell: SessionCell = tableView.cellForRow(at: indexPath) as? SessionCell {
|
||||
existingCell.update(
|
||||
icon: settingInfo.icon,
|
||||
iconSize: settingInfo.iconSize,
|
||||
iconSetter: settingInfo.iconSetter,
|
||||
title: settingInfo.title,
|
||||
subtitle: settingInfo.subtitle,
|
||||
alignment: settingInfo.alignment,
|
||||
accessibilityIdentifier: settingInfo.accessibilityIdentifier,
|
||||
subtitleExtraViewGenerator: settingInfo.subtitleExtraViewGenerator,
|
||||
action: settingInfo.action,
|
||||
extraActionTitle: settingInfo.extraActionTitle,
|
||||
onExtraAction: settingInfo.onExtraAction,
|
||||
position: {
|
||||
guard section.elements.count > 1 else { return .individual }
|
||||
|
||||
switch indexPath.row {
|
||||
case 0: return .top
|
||||
case (section.elements.count - 1): return .bottom
|
||||
default: return .middle
|
||||
}
|
||||
}()
|
||||
with: info,
|
||||
style: .rounded,
|
||||
position: Position.with(indexPath.row, count: section.elements.count)
|
||||
)
|
||||
}
|
||||
else {
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit.UIImage
|
||||
import Combine
|
||||
import GRDB
|
||||
import DifferenceKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class SessionTableViewModel<NavItemId: Equatable, Section: SessionTableSection, SettingItem: Hashable & Differentiable> {
|
||||
typealias SectionModel = ArraySection<Section, SessionCell.Info<SettingItem>>
|
||||
typealias ObservableData = AnyPublisher<[SectionModel], Error>
|
||||
|
||||
// MARK: - Input
|
||||
|
||||
let navItemTapped: PassthroughSubject<NavItemId, Never> = PassthroughSubject()
|
||||
private let _isEditing: CurrentValueSubject<Bool, Never> = CurrentValueSubject(false)
|
||||
lazy var isEditing: AnyPublisher<Bool, Never> = _isEditing
|
||||
.removeDuplicates()
|
||||
.shareReplay(1)
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
open var leftNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() }
|
||||
open var rightNavItems: AnyPublisher<[NavItem]?, Never> { Just(nil).eraseToAnyPublisher() }
|
||||
|
||||
private let _transitionToScreen: PassthroughSubject<(UIViewController, TransitionType), Never> = PassthroughSubject()
|
||||
lazy var transitionToScreen: AnyPublisher<(UIViewController, TransitionType), Never> = _transitionToScreen
|
||||
.shareReplay(0)
|
||||
private let _dismissScreen: PassthroughSubject<DismissType, Never> = PassthroughSubject()
|
||||
lazy var dismissScreen: AnyPublisher<DismissType, Never> = _dismissScreen
|
||||
.shareReplay(0)
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
open var title: String { preconditionFailure("abstract class - override in subclass") }
|
||||
open var settingsData: [SectionModel] { preconditionFailure("abstract class - override in subclass") }
|
||||
open var observableSettingsData: ObservableData {
|
||||
preconditionFailure("abstract class - override in subclass")
|
||||
}
|
||||
|
||||
func updateSettings(_ updatedSettings: [SectionModel]) {
|
||||
preconditionFailure("abstract class - override in subclass")
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
func setIsEditing(_ isEditing: Bool) {
|
||||
_isEditing.send(isEditing)
|
||||
}
|
||||
|
||||
func dismissScreen(type: DismissType = .auto) {
|
||||
_dismissScreen.send(type)
|
||||
}
|
||||
|
||||
func transitionToScreen(_ viewController: UIViewController, transitionType: TransitionType = .push) {
|
||||
_transitionToScreen.send((viewController, transitionType))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum DismissType {
|
||||
/// If this screen is within a navigation controller and isn't the first screen, it will trigger a `popViewController` otherwise
|
||||
/// this will trigger a `dismiss`
|
||||
case auto
|
||||
|
||||
/// This will only trigger a `popViewController` call (if the screen was presented it'll do nothing)
|
||||
case pop
|
||||
|
||||
/// This will only trigger a `dismiss` call (if the screen was pushed to a presented navigation controller it'll dismiss
|
||||
/// the navigation controller, otherwise this will do nothing)
|
||||
case dismiss
|
||||
}
|
|
@ -0,0 +1,360 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import GRDB
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
extension SessionCell {
|
||||
public enum Accessory: Hashable, Equatable {
|
||||
case icon(
|
||||
UIImage?,
|
||||
size: IconSize,
|
||||
customTint: ThemeValue?,
|
||||
shouldFill: Bool
|
||||
)
|
||||
case iconAsync(
|
||||
size: IconSize,
|
||||
customTint: ThemeValue?,
|
||||
shouldFill: Bool,
|
||||
setter: (UIImageView) -> Void
|
||||
)
|
||||
case toggle(DataSource)
|
||||
case dropDown(DataSource)
|
||||
case radio(
|
||||
size: RadioSize,
|
||||
isSelected: () -> Bool,
|
||||
storedSelection: Bool
|
||||
)
|
||||
|
||||
case highlightingBackgroundLabel(title: String)
|
||||
case profile(String, Profile?)
|
||||
case customView(viewGenerator: () -> UIView)
|
||||
case threadInfo(
|
||||
threadViewModel: SessionThreadViewModel,
|
||||
style: ThreadInfoStyle = ThreadInfoStyle(),
|
||||
avatarTapped: (() -> Void)? = nil,
|
||||
titleTapped: (() -> Void)? = nil,
|
||||
titleChanged: ((String) -> Void)? = nil
|
||||
)
|
||||
|
||||
// MARK: - Convenience Vatiables
|
||||
|
||||
var shouldFitToEdge: Bool {
|
||||
switch self {
|
||||
case .icon(_, _, _, let shouldFill), .iconAsync(_, _, let shouldFill, _): return shouldFill
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var currentBoolValue: Bool {
|
||||
switch self {
|
||||
case .toggle(let dataSource), .dropDown(let dataSource): return dataSource.currentBoolValue
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Conformance
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .icon(let image, let size, let customTint, let shouldFill):
|
||||
image.hash(into: &hasher)
|
||||
size.hash(into: &hasher)
|
||||
customTint.hash(into: &hasher)
|
||||
shouldFill.hash(into: &hasher)
|
||||
|
||||
case .iconAsync(let size, let customTint, let shouldFill, _):
|
||||
size.hash(into: &hasher)
|
||||
customTint.hash(into: &hasher)
|
||||
shouldFill.hash(into: &hasher)
|
||||
|
||||
case .toggle(let dataSource):
|
||||
dataSource.hash(into: &hasher)
|
||||
|
||||
case .dropDown(let dataSource):
|
||||
dataSource.hash(into: &hasher)
|
||||
|
||||
case .radio(let size, let isSelected, let storedSelection):
|
||||
size.hash(into: &hasher)
|
||||
isSelected().hash(into: &hasher)
|
||||
storedSelection.hash(into: &hasher)
|
||||
|
||||
case .highlightingBackgroundLabel(let title):
|
||||
title.hash(into: &hasher)
|
||||
|
||||
case .profile(let profileId, let profile):
|
||||
profileId.hash(into: &hasher)
|
||||
profile.hash(into: &hasher)
|
||||
|
||||
case .customView: break
|
||||
|
||||
case .threadInfo(let threadViewModel, let style, _, _, _):
|
||||
threadViewModel.hash(into: &hasher)
|
||||
style.hash(into: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
public static func == (lhs: Accessory, rhs: Accessory) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.icon(let lhsImage, let lhsSize, let lhsCustomTint, let lhsShouldFill), .icon(let rhsImage, let rhsSize, let rhsCustomTint, let rhsShouldFill)):
|
||||
return (
|
||||
lhsImage == rhsImage &&
|
||||
lhsSize == rhsSize &&
|
||||
lhsCustomTint == rhsCustomTint &&
|
||||
lhsShouldFill == rhsShouldFill
|
||||
)
|
||||
|
||||
case (.iconAsync(let lhsSize, let lhsCustomTint, let lhsShouldFill, _), .iconAsync(let rhsSize, let rhsCustomTint, let rhsShouldFill, _)):
|
||||
return (
|
||||
lhsSize == rhsSize &&
|
||||
lhsCustomTint == rhsCustomTint &&
|
||||
lhsShouldFill == rhsShouldFill
|
||||
)
|
||||
|
||||
case (.toggle(let lhsDataSource), .toggle(let rhsDataSource)):
|
||||
return (lhsDataSource == rhsDataSource)
|
||||
|
||||
case (.dropDown(let lhsDataSource), .dropDown(let rhsDataSource)):
|
||||
return (lhsDataSource == rhsDataSource)
|
||||
|
||||
case (.radio(let lhsSize, let lhsIsSelected, let lhsStoredSelection), .radio(let rhsSize, let rhsIsSelected, let rhsStoredSelection)):
|
||||
return (
|
||||
lhsSize == rhsSize &&
|
||||
lhsIsSelected() == rhsIsSelected() &&
|
||||
lhsStoredSelection == rhsStoredSelection
|
||||
)
|
||||
|
||||
case (.highlightingBackgroundLabel(let lhsTitle), .highlightingBackgroundLabel(let rhsTitle)):
|
||||
return (lhsTitle == rhsTitle)
|
||||
|
||||
case (.profile(let lhsProfileId, let lhsProfile), .profile(let rhsProfileId, let rhsProfile)):
|
||||
return (
|
||||
lhsProfileId == rhsProfileId &&
|
||||
lhsProfile == rhsProfile
|
||||
)
|
||||
|
||||
case (.customView, .customView): return false
|
||||
|
||||
case (.threadInfo(let lhsThreadViewModel, let lhsStyle, _, _, _), .threadInfo(let rhsThreadViewModel, let rhsStyle, _, _, _)):
|
||||
return (
|
||||
lhsThreadViewModel == rhsThreadViewModel &&
|
||||
lhsStyle == rhsStyle
|
||||
)
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Types
|
||||
|
||||
/// These are here because XCode doesn't realy like default values within enums so auto-complete and syntax
|
||||
/// highlighting don't work properly
|
||||
extension SessionCell.Accessory {
|
||||
// MARK: - .icon Variants
|
||||
|
||||
public static func icon(_ image: UIImage?) -> SessionCell.Accessory {
|
||||
return .icon(image, size: .small, customTint: nil, shouldFill: false)
|
||||
}
|
||||
|
||||
public static func icon(_ image: UIImage?, customTint: ThemeValue) -> SessionCell.Accessory {
|
||||
return .icon(image, size: .small, customTint: customTint, shouldFill: false)
|
||||
}
|
||||
|
||||
public static func icon(_ image: UIImage?, size: IconSize) -> SessionCell.Accessory {
|
||||
return .icon(image, size: size, customTint: nil, shouldFill: false)
|
||||
}
|
||||
|
||||
public static func icon(_ image: UIImage?, size: IconSize, customTint: ThemeValue) -> SessionCell.Accessory {
|
||||
return .icon(image, size: size, customTint: customTint, shouldFill: false)
|
||||
}
|
||||
|
||||
public static func icon(_ image: UIImage?, shouldFill: Bool) -> SessionCell.Accessory {
|
||||
return .icon(image, size: .small, customTint: nil, shouldFill: shouldFill)
|
||||
}
|
||||
|
||||
// MARK: - .iconAsync Variants
|
||||
|
||||
public static func iconAsync(_ setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory {
|
||||
return .iconAsync(size: .small, customTint: nil, shouldFill: false, setter: setter)
|
||||
}
|
||||
|
||||
public static func iconAsync(customTint: ThemeValue, _ setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory {
|
||||
return .iconAsync(size: .small, customTint: customTint, shouldFill: false, setter: setter)
|
||||
}
|
||||
|
||||
public static func iconAsync(size: IconSize, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory {
|
||||
return .iconAsync(size: size, customTint: nil, shouldFill: false, setter: setter)
|
||||
}
|
||||
|
||||
public static func iconAsync(shouldFill: Bool, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory {
|
||||
return .iconAsync(size: .small, customTint: nil, shouldFill: shouldFill, setter: setter)
|
||||
}
|
||||
|
||||
public static func iconAsync(size: IconSize, customTint: ThemeValue, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory {
|
||||
return .iconAsync(size: size, customTint: customTint, shouldFill: false, setter: setter)
|
||||
}
|
||||
|
||||
public static func iconAsync(size: IconSize, shouldFill: Bool, setter: @escaping (UIImageView) -> Void) -> SessionCell.Accessory {
|
||||
return .iconAsync(size: size, customTint: nil, shouldFill: shouldFill, setter: setter)
|
||||
}
|
||||
|
||||
// MARK: - .radio Variants
|
||||
|
||||
public static func radio(isSelected: @escaping () -> Bool) -> SessionCell.Accessory {
|
||||
return .radio(size: .medium, isSelected: isSelected, storedSelection: false)
|
||||
}
|
||||
|
||||
public static func radio(isSelected: @escaping () -> Bool, storedSelection: Bool) -> SessionCell.Accessory {
|
||||
return .radio(size: .medium, isSelected: isSelected, storedSelection: storedSelection)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SessionCell.Accessory.DataSource
|
||||
|
||||
extension SessionCell.Accessory {
|
||||
public enum DataSource: Hashable, Equatable {
|
||||
case boolValue(Bool)
|
||||
case dynamicString(() -> String?)
|
||||
case userDefaults(UserDefaults, key: String)
|
||||
case settingBool(key: Setting.BoolKey)
|
||||
|
||||
// MARK: - Convenience Types
|
||||
|
||||
public static func dynamicString<ET: EnumIntSetting>(
|
||||
type: ET.Type,
|
||||
key: Setting.EnumKey,
|
||||
value: @escaping ((ET?) -> String?)
|
||||
) -> DataSource {
|
||||
return .dynamicString {
|
||||
let currentValue: ET? = Storage.shared[key]
|
||||
|
||||
return value(currentValue)
|
||||
}
|
||||
}
|
||||
|
||||
public static func dynamicString<ET: EnumStringSetting>(
|
||||
type: ET.Type,
|
||||
key: Setting.EnumKey,
|
||||
value: @escaping ((ET?) -> String?)
|
||||
) -> DataSource {
|
||||
return .dynamicString {
|
||||
let currentValue: ET? = Storage.shared[key]
|
||||
|
||||
return value(currentValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
public var currentBoolValue: Bool {
|
||||
switch self {
|
||||
case .boolValue(let value): return value
|
||||
case .dynamicString: return false
|
||||
case .userDefaults(let defaults, let key): return defaults.bool(forKey: key)
|
||||
case .settingBool(let key): return Storage.shared[key]
|
||||
}
|
||||
}
|
||||
|
||||
public var currentStringValue: String? {
|
||||
switch self {
|
||||
case .dynamicString(let value): return value()
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Conformance
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .boolValue(let value): value.hash(into: &hasher)
|
||||
case .dynamicString(let generator): generator().hash(into: &hasher)
|
||||
case .userDefaults(_, let key): key.hash(into: &hasher)
|
||||
case .settingBool(let key): key.hash(into: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
public static func == (lhs: DataSource, rhs: DataSource) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.boolValue(let lhsValue), .boolValue(let rhsValue)):
|
||||
return (lhsValue == rhsValue)
|
||||
|
||||
case (.dynamicString(let lhsGenerator), .dynamicString(let rhsGenerator)):
|
||||
return (lhsGenerator() == rhsGenerator())
|
||||
|
||||
case (.userDefaults(_, let lhsKey), .userDefaults(_, let rhsKey)):
|
||||
return (lhsKey == rhsKey)
|
||||
|
||||
case (.settingBool(let lhsKey), .settingBool(let rhsKey)):
|
||||
return (lhsKey == rhsKey)
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SessionCell.Accessory.RadioSize
|
||||
|
||||
extension SessionCell.Accessory {
|
||||
public enum RadioSize {
|
||||
case small
|
||||
case medium
|
||||
|
||||
var borderSize: CGFloat {
|
||||
switch self {
|
||||
case .small: return 20
|
||||
case .medium: return 26
|
||||
}
|
||||
}
|
||||
|
||||
var selectionSize: CGFloat {
|
||||
switch self {
|
||||
case .small: return 15
|
||||
case .medium: return 20
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SessionCell.Accessory.ThreadInfoStyle
|
||||
|
||||
extension SessionCell.Accessory {
|
||||
public struct ThreadInfoStyle: Hashable, Equatable {
|
||||
public enum Style: Hashable, Equatable {
|
||||
case small
|
||||
case monoSmall
|
||||
case monoLarge
|
||||
}
|
||||
|
||||
public struct Action: Hashable, Equatable {
|
||||
let title: String
|
||||
let run: (SessionButton?) -> ()
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
title.hash(into: &hasher)
|
||||
}
|
||||
|
||||
public static func == (lhs: Action, rhs: Action) -> Bool {
|
||||
return (lhs.title == rhs.title)
|
||||
}
|
||||
}
|
||||
|
||||
public let separatorTitle: String?
|
||||
public let descriptionStyle: Style
|
||||
public let descriptionActions: [Action]
|
||||
|
||||
public init(
|
||||
separatorTitle: String? = nil,
|
||||
descriptionStyle: Style = .monoSmall,
|
||||
descriptionActions: [Action] = []
|
||||
) {
|
||||
self.separatorTitle = separatorTitle
|
||||
self.descriptionStyle = descriptionStyle
|
||||
self.descriptionActions = descriptionActions
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension SessionCell {
|
||||
struct ExtraAction: Hashable, Equatable {
|
||||
let title: String
|
||||
let onTap: (() -> Void)
|
||||
|
||||
// MARK: - Conformance
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
title.hash(into: &hasher)
|
||||
}
|
||||
|
||||
static func == (lhs: SessionCell.ExtraAction, rhs: SessionCell.ExtraAction) -> Bool {
|
||||
return (lhs.title == rhs.title)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import DifferenceKit
|
||||
import SessionUIKit
|
||||
|
||||
extension SessionCell {
|
||||
public struct Info<ID: Hashable & Differentiable>: Equatable, Hashable, Differentiable {
|
||||
let id: ID
|
||||
let leftAccessory: SessionCell.Accessory?
|
||||
let title: String
|
||||
let subtitle: String?
|
||||
let subtitleExtraViewGenerator: (() -> UIView)?
|
||||
let tintColor: ThemeValue
|
||||
let rightAccessory: SessionCell.Accessory?
|
||||
let extraAction: SessionCell.ExtraAction?
|
||||
let isEnabled: Bool
|
||||
let shouldHaveBackground: Bool
|
||||
let accessibilityIdentifier: String?
|
||||
let confirmationInfo: ConfirmationModal.Info?
|
||||
let onTap: ((UIView?) -> Void)?
|
||||
|
||||
var currentBoolValue: Bool {
|
||||
return (
|
||||
(leftAccessory?.currentBoolValue ?? false) ||
|
||||
(rightAccessory?.currentBoolValue ?? false)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
id: ID,
|
||||
leftAccessory: SessionCell.Accessory? = nil,
|
||||
title: String,
|
||||
subtitle: String? = nil,
|
||||
subtitleExtraViewGenerator: (() -> UIView)? = nil,
|
||||
tintColor: ThemeValue = .textPrimary,
|
||||
rightAccessory: SessionCell.Accessory? = nil,
|
||||
extraAction: SessionCell.ExtraAction? = nil,
|
||||
isEnabled: Bool = true,
|
||||
shouldHaveBackground: Bool = true,
|
||||
accessibilityIdentifier: String? = nil,
|
||||
confirmationInfo: ConfirmationModal.Info? = nil,
|
||||
onTap: ((UIView?) -> Void)?
|
||||
) {
|
||||
self.id = id
|
||||
self.leftAccessory = leftAccessory
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.subtitleExtraViewGenerator = subtitleExtraViewGenerator
|
||||
self.tintColor = tintColor
|
||||
self.rightAccessory = rightAccessory
|
||||
self.extraAction = extraAction
|
||||
self.isEnabled = isEnabled
|
||||
self.shouldHaveBackground = shouldHaveBackground
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.confirmationInfo = confirmationInfo
|
||||
self.onTap = onTap
|
||||
}
|
||||
|
||||
init(
|
||||
id: ID,
|
||||
leftAccessory: SessionCell.Accessory? = nil,
|
||||
title: String,
|
||||
subtitle: String? = nil,
|
||||
subtitleExtraViewGenerator: (() -> UIView)? = nil,
|
||||
tintColor: ThemeValue = .textPrimary,
|
||||
rightAccessory: SessionCell.Accessory? = nil,
|
||||
extraAction: SessionCell.ExtraAction? = nil,
|
||||
isEnabled: Bool = true,
|
||||
shouldHaveBackground: Bool = true,
|
||||
accessibilityIdentifier: String? = nil,
|
||||
confirmationInfo: ConfirmationModal.Info? = nil,
|
||||
onTap: (() -> Void)? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.leftAccessory = leftAccessory
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.subtitleExtraViewGenerator = subtitleExtraViewGenerator
|
||||
self.tintColor = tintColor
|
||||
self.rightAccessory = rightAccessory
|
||||
self.extraAction = extraAction
|
||||
self.isEnabled = isEnabled
|
||||
self.shouldHaveBackground = shouldHaveBackground
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.confirmationInfo = confirmationInfo
|
||||
self.onTap = (onTap != nil ? { _ in onTap?() } : nil)
|
||||
}
|
||||
|
||||
// MARK: - Conformance
|
||||
|
||||
public var differenceIdentifier: ID { id }
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
id.hash(into: &hasher)
|
||||
leftAccessory.hash(into: &hasher)
|
||||
title.hash(into: &hasher)
|
||||
subtitle.hash(into: &hasher)
|
||||
tintColor.hash(into: &hasher)
|
||||
rightAccessory.hash(into: &hasher)
|
||||
extraAction.hash(into: &hasher)
|
||||
isEnabled.hash(into: &hasher)
|
||||
shouldHaveBackground.hash(into: &hasher)
|
||||
accessibilityIdentifier.hash(into: &hasher)
|
||||
confirmationInfo.hash(into: &hasher)
|
||||
}
|
||||
|
||||
public static func == (lhs: Info<ID>, rhs: Info<ID>) -> Bool {
|
||||
return (
|
||||
lhs.id == rhs.id &&
|
||||
lhs.leftAccessory == rhs.leftAccessory &&
|
||||
lhs.title == rhs.title &&
|
||||
lhs.subtitle == rhs.subtitle &&
|
||||
lhs.tintColor == rhs.tintColor &&
|
||||
lhs.rightAccessory == rhs.rightAccessory &&
|
||||
lhs.extraAction == rhs.extraAction &&
|
||||
lhs.isEnabled == rhs.isEnabled &&
|
||||
lhs.shouldHaveBackground == rhs.shouldHaveBackground &&
|
||||
lhs.accessibilityIdentifier == rhs.accessibilityIdentifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import DifferenceKit
|
||||
|
||||
protocol SessionTableSection: Differentiable {
|
||||
var title: String? { get }
|
||||
var style: SessionTableSectionStyle { get }
|
||||
}
|
||||
|
||||
extension SessionTableSection {
|
||||
var title: String? { nil }
|
||||
var style: SessionTableSectionStyle { .none }
|
||||
}
|
||||
|
||||
public enum SessionTableSectionStyle: Differentiable {
|
||||
case none
|
||||
case title
|
||||
case padding
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public enum NoNav: Equatable {}
|
||||
|
||||
extension SessionTableViewModel {
|
||||
public struct NavItem {
|
||||
let id: NavItemId
|
||||
let image: UIImage?
|
||||
let style: UIBarButtonItem.Style
|
||||
let systemItem: UIBarButtonItem.SystemItem?
|
||||
let accessibilityIdentifier: String
|
||||
let action: (() -> Void)?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
id: NavItemId,
|
||||
systemItem: UIBarButtonItem.SystemItem?,
|
||||
accessibilityIdentifier: String,
|
||||
action: (() -> Void)? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.image = nil
|
||||
self.style = .plain
|
||||
self.systemItem = systemItem
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public init(
|
||||
id: NavItemId,
|
||||
image: UIImage?,
|
||||
style: UIBarButtonItem.Style,
|
||||
accessibilityIdentifier: String,
|
||||
action: (() -> Void)? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.image = image
|
||||
self.style = style
|
||||
self.systemItem = nil
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.action = action
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
public func createBarButtonItem() -> DisposableBarButtonItem {
|
||||
guard let systemItem: UIBarButtonItem.SystemItem = systemItem else {
|
||||
return DisposableBarButtonItem(
|
||||
image: image,
|
||||
style: style,
|
||||
target: nil,
|
||||
action: nil,
|
||||
accessibilityIdentifier: accessibilityIdentifier
|
||||
)
|
||||
}
|
||||
|
||||
return DisposableBarButtonItem(
|
||||
barButtonSystemItem: systemItem,
|
||||
target: nil,
|
||||
action: nil,
|
||||
accessibilityIdentifier: accessibilityIdentifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum TransitionType {
|
||||
case push
|
||||
case present
|
||||
}
|
|
@ -1,192 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
final class UserCell: UITableViewCell {
|
||||
// MARK: - Accessory
|
||||
|
||||
enum Accessory {
|
||||
case none
|
||||
case lock
|
||||
case tick(isSelected: Bool)
|
||||
case x
|
||||
}
|
||||
|
||||
// MARK: - Components
|
||||
|
||||
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView()
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let spacer: UIView = {
|
||||
let result: UIView = UIView.hStretchingSpacer()
|
||||
result.widthAnchor
|
||||
.constraint(greaterThanOrEqualToConstant: Values.mediumSpacing)
|
||||
.isActive = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let selectionView: RadioButton = {
|
||||
let result: RadioButton = RadioButton(size: .medium)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isUserInteractionEnabled = false
|
||||
result.font = .systemFont(ofSize: Values.mediumFontSize, weight: .bold)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var accessoryImageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView()
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.set(.width, to: 24)
|
||||
result.set(.height, to: 24)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var separator: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.themeBackgroundColor = .borderSeparator
|
||||
result.set(.height, to: Values.separatorThickness)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
// Highlight color
|
||||
let selectedBackgroundView = UIView()
|
||||
selectedBackgroundView.themeBackgroundColor = .clear // Disabled for now
|
||||
self.selectedBackgroundView = selectedBackgroundView
|
||||
|
||||
// Profile picture image view
|
||||
let profilePictureViewSize = Values.smallProfilePictureSize
|
||||
profilePictureView.set(.width, to: profilePictureViewSize)
|
||||
profilePictureView.set(.height, to: profilePictureViewSize)
|
||||
profilePictureView.size = profilePictureViewSize
|
||||
|
||||
// Main stack view
|
||||
let stackView = UIStackView(
|
||||
arrangedSubviews: [
|
||||
profilePictureView,
|
||||
UIView.hSpacer(Values.mediumSpacing),
|
||||
displayNameLabel,
|
||||
spacer,
|
||||
accessoryImageView,
|
||||
selectionView
|
||||
]
|
||||
)
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
stackView.layoutMargins = UIEdgeInsets(uniform: Values.mediumSpacing)
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(to: contentView)
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width)
|
||||
|
||||
// Set up the separator
|
||||
contentView.addSubview(separator)
|
||||
separator.pin(
|
||||
[
|
||||
UIView.HorizontalEdge.leading,
|
||||
UIView.VerticalEdge.bottom,
|
||||
UIView.HorizontalEdge.trailing
|
||||
],
|
||||
to: contentView
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Updating
|
||||
|
||||
func update(
|
||||
with publicKey: String,
|
||||
profile: Profile?,
|
||||
isZombie: Bool,
|
||||
mediumFont: Bool = false,
|
||||
accessory: Accessory,
|
||||
themeBackgroundColor: ThemeValue = .conversationButton_background
|
||||
) {
|
||||
self.themeBackgroundColor = themeBackgroundColor
|
||||
|
||||
profilePictureView.update(
|
||||
publicKey: publicKey,
|
||||
profile: profile,
|
||||
threadVariant: .contact
|
||||
)
|
||||
|
||||
displayNameLabel.font = (mediumFont ?
|
||||
.systemFont(ofSize: Values.mediumFontSize) :
|
||||
.boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
)
|
||||
|
||||
displayNameLabel.text = (getUserHexEncodedPublicKey() == publicKey ?
|
||||
"MEDIA_GALLERY_SENDER_NAME_YOU".localized() :
|
||||
Profile.displayName(
|
||||
for: .contact,
|
||||
id: publicKey,
|
||||
name: profile?.name,
|
||||
nickname: profile?.nickname
|
||||
)
|
||||
)
|
||||
|
||||
switch accessory {
|
||||
case .none:
|
||||
selectionView.isHidden = true
|
||||
accessoryImageView.isHidden = true
|
||||
displayNameLabel.isHidden = false
|
||||
spacer.isHidden = false
|
||||
|
||||
case .lock:
|
||||
selectionView.isHidden = true
|
||||
accessoryImageView.isHidden = false
|
||||
accessoryImageView.image = #imageLiteral(resourceName: "ic_lock_outline").withRenderingMode(.alwaysTemplate)
|
||||
accessoryImageView.themeTintColor = .textPrimary
|
||||
accessoryImageView.alpha = Values.mediumOpacity
|
||||
displayNameLabel.isHidden = false
|
||||
spacer.isHidden = false
|
||||
|
||||
case .tick(let isSelected):
|
||||
selectionView.isHidden = false
|
||||
selectionView.text = displayNameLabel.text
|
||||
selectionView.update(isSelected: isSelected)
|
||||
accessoryImageView.isHidden = true
|
||||
displayNameLabel.isHidden = true
|
||||
spacer.isHidden = true
|
||||
|
||||
case .x:
|
||||
selectionView.isHidden = true
|
||||
accessoryImageView.isHidden = false
|
||||
accessoryImageView.image = #imageLiteral(resourceName: "X").withRenderingMode(.alwaysTemplate)
|
||||
accessoryImageView.contentMode = .center
|
||||
accessoryImageView.themeTintColor = .textPrimary
|
||||
accessoryImageView.alpha = 1
|
||||
displayNameLabel.isHidden = false
|
||||
spacer.isHidden = false
|
||||
}
|
||||
|
||||
let alpha: CGFloat = (isZombie ? 0.5 : 1)
|
||||
[ profilePictureView, displayNameLabel, accessoryImageView, selectionView ]
|
||||
.forEach { $0.alpha = alpha }
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ final class UserSelectionVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
result.themeBackgroundColor = .clear
|
||||
result.showsVerticalScrollIndicator = false
|
||||
result.alwaysBounceVertical = false
|
||||
result.register(view: UserCell.self)
|
||||
result.register(view: SessionCell.self)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -60,12 +60,19 @@ final class UserSelectionVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: UserCell = tableView.dequeue(type: UserCell.self, for: indexPath)
|
||||
let cell: SessionCell = tableView.dequeue(type: SessionCell.self, for: indexPath)
|
||||
let profile: Profile = users[indexPath.row]
|
||||
cell.update(
|
||||
with: users[indexPath.row].id,
|
||||
profile: users[indexPath.row],
|
||||
isZombie: false,
|
||||
accessory: .tick(isSelected: selectedUsers.contains(users[indexPath.row].id))
|
||||
with: SessionCell.Info(
|
||||
id: profile,
|
||||
leftAccessory: .profile(profile.id, profile),
|
||||
title: profile.displayName(),
|
||||
rightAccessory: .radio(isSelected: { [weak self] in
|
||||
self?.selectedUsers.contains(profile.id) == true
|
||||
})
|
||||
),
|
||||
style: .edgeToEdge,
|
||||
position: Position.with(indexPath.row, count: users.count)
|
||||
)
|
||||
|
||||
return cell
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
import UIKit
|
||||
import Combine
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
class SettingsAvatarCell: UITableViewCell {
|
||||
class SessionAvatarCell: UITableViewCell {
|
||||
var disposables: Set<AnyCancellable> = Set()
|
||||
private var originalInputValue: String?
|
||||
|
||||
|
@ -194,7 +195,7 @@ class SettingsAvatarCell: UITableViewCell {
|
|||
|
||||
func update(
|
||||
threadViewModel: SessionThreadViewModel,
|
||||
style: ThreadInfoStyle,
|
||||
style: SessionCell.Accessory.ThreadInfoStyle,
|
||||
viewController: UIViewController
|
||||
) {
|
||||
profilePictureView.update(
|
||||
|
@ -242,7 +243,7 @@ class SettingsAvatarCell: UITableViewCell {
|
|||
descriptionSeparator.isHidden = (style.separatorTitle == nil)
|
||||
|
||||
style.descriptionActions.forEach { action in
|
||||
let result: OutlineButton = OutlineButton(style: .regular, size: .medium)
|
||||
let result: SessionButton = SessionButton(style: .bordered, size: .medium)
|
||||
result.setTitle(action.title, for: UIControl.State.normal)
|
||||
result.tapPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
@ -282,7 +283,7 @@ class SettingsAvatarCell: UITableViewCell {
|
|||
|
||||
// MARK: - Compose
|
||||
|
||||
extension CombineCompatible where Self: SettingsAvatarCell {
|
||||
extension CombineCompatible where Self: SessionAvatarCell {
|
||||
var textPublisher: AnyPublisher<String, Never> {
|
||||
return self.displayNameTextField.publisher(for: .editingChanged)
|
||||
.map { textField -> String in (textField.text ?? "") }
|
|
@ -0,0 +1,366 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
extension SessionCell {
|
||||
public class AccessoryView: UIView {
|
||||
// MARK: - UI
|
||||
|
||||
private lazy var imageViewConstraints: [NSLayoutConstraint] = [
|
||||
imageView.pin(.top, to: .top, of: self),
|
||||
imageView.pin(.leading, to: .leading, of: self),
|
||||
imageView.pin(.trailing, to: .trailing, of: self),
|
||||
imageView.pin(.bottom, to: .bottom, of: self)
|
||||
]
|
||||
private lazy var imageViewWidthConstraint: NSLayoutConstraint = imageView.set(.width, to: 0)
|
||||
private lazy var imageViewHeightConstraint: NSLayoutConstraint = imageView.set(.height, to: 0)
|
||||
private lazy var toggleSwitchConstraints: [NSLayoutConstraint] = [
|
||||
toggleSwitch.pin(.top, to: .top, of: self),
|
||||
toggleSwitch.pin(.leading, to: .leading, of: self),
|
||||
toggleSwitch.pin(.trailing, to: .trailing, of: self),
|
||||
toggleSwitch.pin(.bottom, to: .bottom, of: self)
|
||||
]
|
||||
private lazy var dropDownStackViewConstraints: [NSLayoutConstraint] = [
|
||||
dropDownStackView.pin(.top, to: .top, of: self),
|
||||
dropDownStackView.pin(.leading, to: .leading, of: self),
|
||||
dropDownStackView.pin(.trailing, to: .trailing, of: self),
|
||||
dropDownStackView.pin(.bottom, to: .bottom, of: self)
|
||||
]
|
||||
private lazy var radioViewWidthConstraint: NSLayoutConstraint = radioView.set(.width, to: 0)
|
||||
private lazy var radioViewHeightConstraint: NSLayoutConstraint = radioView.set(.height, to: 0)
|
||||
private lazy var radioBorderViewWidthConstraint: NSLayoutConstraint = radioBorderView.set(.width, to: 0)
|
||||
private lazy var radioBorderViewHeightConstraint: NSLayoutConstraint = radioBorderView.set(.height, to: 0)
|
||||
private lazy var radioBorderViewConstraints: [NSLayoutConstraint] = [
|
||||
radioBorderView.pin(.top, to: .top, of: self),
|
||||
radioBorderView.pin(.leading, to: .leading, of: self),
|
||||
radioBorderView.pin(.trailing, to: .trailing, of: self),
|
||||
radioBorderView.pin(.bottom, to: .bottom, of: self)
|
||||
]
|
||||
private lazy var highlightingBackgroundLabelConstraints: [NSLayoutConstraint] = [
|
||||
highlightingBackgroundLabel.pin(.top, to: .top, of: self),
|
||||
highlightingBackgroundLabel.pin(.leading, to: .leading, of: self),
|
||||
highlightingBackgroundLabel.pin(.trailing, to: .trailing, of: self),
|
||||
highlightingBackgroundLabel.pin(.bottom, to: .bottom, of: self)
|
||||
]
|
||||
private lazy var profilePictureViewConstraints: [NSLayoutConstraint] = [
|
||||
profilePictureView.pin(.top, to: .top, of: self),
|
||||
profilePictureView.pin(.leading, to: .leading, of: self),
|
||||
profilePictureView.pin(.trailing, to: .trailing, of: self),
|
||||
profilePictureView.pin(.bottom, to: .bottom, of: self)
|
||||
]
|
||||
|
||||
private let imageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.themeTintColor = .textPrimary
|
||||
result.layer.minificationFilter = .trilinear
|
||||
result.layer.magnificationFilter = .trilinear
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let toggleSwitch: UISwitch = {
|
||||
let result: UISwitch = UISwitch()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isUserInteractionEnabled = false // Triggered by didSelectCell instead
|
||||
result.themeOnTintColor = .primary
|
||||
result.isHidden = true
|
||||
result.setContentHuggingHigh()
|
||||
result.setCompressionResistanceHigh()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let dropDownStackView: UIStackView = {
|
||||
let result: UIStackView = UIStackView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.axis = .horizontal
|
||||
result.distribution = .fill
|
||||
result.alignment = .center
|
||||
result.spacing = Values.verySmallSpacing
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let dropDownImageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView(image: UIImage(systemName: "arrowtriangle.down.fill"))
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.themeTintColor = .textPrimary
|
||||
result.set(.width, to: 10)
|
||||
result.set(.height, to: 10)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let dropDownLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize, weight: .medium)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.setContentHuggingHigh()
|
||||
result.setCompressionResistanceHigh()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let radioBorderView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isUserInteractionEnabled = false
|
||||
result.layer.borderWidth = 1
|
||||
result.themeBorderColor = .radioButton_unselectedBorder
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let radioView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isUserInteractionEnabled = false
|
||||
result.themeBackgroundColor = .radioButton_unselectedBackground
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
public lazy var highlightingBackgroundLabel: SessionHighlightingBackgroundLabel = {
|
||||
let result: SessionHighlightingBackgroundLabel = SessionHighlightingBackgroundLabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var profilePictureView: ProfilePictureView = {
|
||||
let result: ProfilePictureView = ProfilePictureView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.size = Values.smallProfilePictureSize
|
||||
result.isHidden = true
|
||||
result.set(.width, to: Values.smallProfilePictureSize)
|
||||
result.set(.height, to: Values.smallProfilePictureSize)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private var customView: UIView?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
setupViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
|
||||
setupViewHierarchy()
|
||||
}
|
||||
|
||||
private func setupViewHierarchy() {
|
||||
addSubview(imageView)
|
||||
addSubview(toggleSwitch)
|
||||
addSubview(dropDownStackView)
|
||||
addSubview(radioBorderView)
|
||||
addSubview(highlightingBackgroundLabel)
|
||||
addSubview(profilePictureView)
|
||||
|
||||
dropDownStackView.addArrangedSubview(dropDownImageView)
|
||||
dropDownStackView.addArrangedSubview(dropDownLabel)
|
||||
|
||||
radioBorderView.addSubview(radioView)
|
||||
radioView.center(in: radioBorderView)
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
func prepareForReuse() {
|
||||
self.isHidden = true
|
||||
|
||||
imageView.image = nil
|
||||
imageView.themeTintColor = .textPrimary
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
dropDownImageView.themeTintColor = .textPrimary
|
||||
dropDownLabel.text = ""
|
||||
dropDownLabel.themeTextColor = .textPrimary
|
||||
radioBorderView.themeBorderColor = .radioButton_unselectedBorder
|
||||
radioView.themeBackgroundColor = .radioButton_unselectedBackground
|
||||
highlightingBackgroundLabel.text = ""
|
||||
highlightingBackgroundLabel.themeTextColor = .textPrimary
|
||||
customView?.removeFromSuperview()
|
||||
|
||||
imageView.isHidden = true
|
||||
toggleSwitch.isHidden = true
|
||||
dropDownStackView.isHidden = true
|
||||
radioBorderView.isHidden = true
|
||||
radioView.alpha = 1
|
||||
radioView.isHidden = true
|
||||
highlightingBackgroundLabel.isHidden = true
|
||||
profilePictureView.isHidden = true
|
||||
|
||||
imageViewWidthConstraint.isActive = false
|
||||
imageViewHeightConstraint.isActive = false
|
||||
imageViewConstraints.forEach { $0.isActive = false }
|
||||
toggleSwitchConstraints.forEach { $0.isActive = false }
|
||||
dropDownStackViewConstraints.forEach { $0.isActive = false }
|
||||
radioViewWidthConstraint.isActive = false
|
||||
radioViewHeightConstraint.isActive = false
|
||||
radioBorderViewWidthConstraint.isActive = false
|
||||
radioBorderViewHeightConstraint.isActive = false
|
||||
radioBorderViewConstraints.forEach { $0.isActive = false }
|
||||
highlightingBackgroundLabelConstraints.forEach { $0.isActive = false }
|
||||
profilePictureViewConstraints.forEach { $0.isActive = false }
|
||||
}
|
||||
|
||||
public func update(
|
||||
with accessory: Accessory?,
|
||||
tintColor: ThemeValue,
|
||||
isEnabled: Bool
|
||||
) {
|
||||
guard let accessory: Accessory = accessory else { return }
|
||||
|
||||
// If we have an accessory value then this shouldn't be hidden
|
||||
self.isHidden = false
|
||||
|
||||
switch accessory {
|
||||
case .icon(let image, let iconSize, let customTint, let shouldFill):
|
||||
imageView.image = image
|
||||
imageView.themeTintColor = (customTint ?? tintColor)
|
||||
imageView.contentMode = (shouldFill ? .scaleAspectFill : .scaleAspectFit)
|
||||
imageView.isHidden = false
|
||||
|
||||
switch iconSize {
|
||||
case .fit:
|
||||
imageView.sizeToFit()
|
||||
imageViewWidthConstraint.constant = imageView.bounds.width
|
||||
imageViewHeightConstraint.constant = imageView.bounds.height
|
||||
|
||||
default:
|
||||
imageViewWidthConstraint.constant = iconSize.size
|
||||
imageViewHeightConstraint.constant = iconSize.size
|
||||
}
|
||||
|
||||
imageViewWidthConstraint.isActive = true
|
||||
imageViewHeightConstraint.isActive = true
|
||||
imageViewConstraints.forEach { $0.isActive = true }
|
||||
|
||||
case .iconAsync(let iconSize, let customTint, let shouldFill, let setter):
|
||||
setter(imageView)
|
||||
imageView.themeTintColor = (customTint ?? tintColor)
|
||||
imageView.contentMode = (shouldFill ? .scaleAspectFill : .scaleAspectFit)
|
||||
imageView.isHidden = false
|
||||
|
||||
switch iconSize {
|
||||
case .fit:
|
||||
imageView.sizeToFit()
|
||||
imageViewWidthConstraint.constant = imageView.bounds.width
|
||||
imageViewHeightConstraint.constant = imageView.bounds.height
|
||||
|
||||
default:
|
||||
imageViewWidthConstraint.constant = iconSize.size
|
||||
imageViewHeightConstraint.constant = iconSize.size
|
||||
}
|
||||
|
||||
imageViewWidthConstraint.isActive = true
|
||||
imageViewHeightConstraint.isActive = true
|
||||
imageViewConstraints.forEach { $0.isActive = true }
|
||||
|
||||
case .toggle(let dataSource):
|
||||
toggleSwitch.isHidden = false
|
||||
toggleSwitch.isEnabled = isEnabled
|
||||
toggleSwitchConstraints.forEach { $0.isActive = true }
|
||||
|
||||
let newValue: Bool = dataSource.currentBoolValue
|
||||
|
||||
if newValue != toggleSwitch.isOn {
|
||||
toggleSwitch.setOn(newValue, animated: true)
|
||||
}
|
||||
|
||||
case .dropDown(let dataSource):
|
||||
dropDownLabel.text = dataSource.currentStringValue
|
||||
dropDownStackView.isHidden = false
|
||||
dropDownStackViewConstraints.forEach { $0.isActive = true }
|
||||
|
||||
case .radio(let size, let isSelectedRetriever, let storedSelection):
|
||||
let isSelected: Bool = isSelectedRetriever()
|
||||
let wasOldSelection: Bool = (!isSelected && storedSelection)
|
||||
|
||||
radioBorderView.isHidden = false
|
||||
radioBorderView.themeBorderColor = (isSelected ?
|
||||
.radioButton_selectedBorder :
|
||||
.radioButton_unselectedBorder
|
||||
)
|
||||
radioBorderView.layer.cornerRadius = (size.borderSize / 2)
|
||||
|
||||
radioView.alpha = (wasOldSelection ? 0.3 : 1)
|
||||
radioView.isHidden = (!isSelected && !storedSelection)
|
||||
radioView.themeBackgroundColor = (isSelected || wasOldSelection ?
|
||||
.radioButton_selectedBackground :
|
||||
.radioButton_unselectedBackground
|
||||
)
|
||||
radioView.layer.cornerRadius = (size.selectionSize / 2)
|
||||
|
||||
radioViewWidthConstraint.constant = size.selectionSize
|
||||
radioViewHeightConstraint.constant = size.selectionSize
|
||||
radioBorderViewWidthConstraint.constant = size.borderSize
|
||||
radioBorderViewHeightConstraint.constant = size.borderSize
|
||||
|
||||
radioViewWidthConstraint.isActive = true
|
||||
radioViewHeightConstraint.isActive = true
|
||||
radioBorderViewWidthConstraint.isActive = true
|
||||
radioBorderViewHeightConstraint.isActive = true
|
||||
radioBorderViewConstraints.forEach { $0.isActive = true }
|
||||
|
||||
case .highlightingBackgroundLabel(let title):
|
||||
highlightingBackgroundLabel.text = title
|
||||
highlightingBackgroundLabel.themeTextColor = tintColor
|
||||
highlightingBackgroundLabel.isHidden = false
|
||||
highlightingBackgroundLabelConstraints.forEach { $0.isActive = true }
|
||||
|
||||
case .profile(let profileId, let profile):
|
||||
profilePictureView.update(
|
||||
publicKey: profileId,
|
||||
profile: profile,
|
||||
threadVariant: .contact
|
||||
)
|
||||
profilePictureView.isHidden = false
|
||||
profilePictureViewConstraints.forEach { $0.isActive = true }
|
||||
|
||||
case .customView(let viewGenerator):
|
||||
let generatedView: UIView = viewGenerator()
|
||||
addSubview(generatedView)
|
||||
|
||||
generatedView.pin(.top, to: .top, of: self)
|
||||
generatedView.pin(.leading, to: .leading, of: self)
|
||||
generatedView.pin(.trailing, to: .trailing, of: self)
|
||||
generatedView.pin(.bottom, to: .bottom, of: self)
|
||||
|
||||
self.customView?.removeFromSuperview() // Just in case
|
||||
self.customView = generatedView
|
||||
|
||||
case .threadInfo: break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||
highlightingBackgroundLabel.setHighlighted(highlighted, animated: animated)
|
||||
}
|
||||
|
||||
func setSelected(_ selected: Bool, animated: Bool) {
|
||||
highlightingBackgroundLabel.setSelected(selected, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -2,13 +2,16 @@
|
|||
|
||||
import UIKit
|
||||
import GRDB
|
||||
import DifferenceKit
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
class SettingsCell: UITableViewCell {
|
||||
public class SessionCell: UITableViewCell {
|
||||
public static let cornerRadius: CGFloat = 17
|
||||
|
||||
enum Style {
|
||||
public enum Style {
|
||||
case rounded
|
||||
case roundedEdgeToEdge
|
||||
case edgeToEdge
|
||||
}
|
||||
|
||||
|
@ -16,7 +19,7 @@ class SettingsCell: UITableViewCell {
|
|||
private var instanceView: UIView = UIView()
|
||||
private var position: Position?
|
||||
private var subtitleExtraView: UIView?
|
||||
private var onExtraAction: (() -> Void)?
|
||||
private var onExtraActionTap: (() -> Void)?
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
|
@ -26,7 +29,8 @@ class SettingsCell: UITableViewCell {
|
|||
private var topSeparatorRightConstraint: NSLayoutConstraint = NSLayoutConstraint()
|
||||
private var botSeparatorLeftConstraint: NSLayoutConstraint = NSLayoutConstraint()
|
||||
private var botSeparatorRightConstraint: NSLayoutConstraint = NSLayoutConstraint()
|
||||
private lazy var stackViewImageHeightConstraint: NSLayoutConstraint = contentStackView.heightAnchor.constraint(equalTo: iconImageView.heightAnchor)
|
||||
private lazy var leftAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: leftAccessoryView)
|
||||
private lazy var rightAccessoryFillConstraint: NSLayoutConstraint = contentStackView.set(.height, to: .height, of: rightAccessoryView)// .heightAnchor.constraint(equalTo: iconImageView.heightAnchor)
|
||||
|
||||
private let cellBackgroundView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
|
@ -66,13 +70,8 @@ class SettingsCell: UITableViewCell {
|
|||
return result
|
||||
}()
|
||||
|
||||
private let iconImageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.themeTintColor = .textPrimary
|
||||
result.layer.minificationFilter = .trilinear
|
||||
result.layer.magnificationFilter = .trilinear
|
||||
public let leftAccessoryView: AccessoryView = {
|
||||
let result: AccessoryView = AccessoryView()
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
|
@ -142,90 +141,9 @@ class SettingsCell: UITableViewCell {
|
|||
return result
|
||||
}()
|
||||
|
||||
private let pushChevronImageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView(image: UIImage(systemName: "chevron.right"))
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.themeTintColor = .textPrimary
|
||||
public let rightAccessoryView: AccessoryView = {
|
||||
let result: AccessoryView = AccessoryView()
|
||||
result.isHidden = true
|
||||
result.setContentHuggingHigh()
|
||||
result.setCompressionResistanceHigh()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let toggleSwitch: UISwitch = {
|
||||
let result: UISwitch = UISwitch()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isUserInteractionEnabled = false // Triggered by didSelectCell instead
|
||||
result.themeOnTintColor = .primary
|
||||
result.isHidden = true
|
||||
result.setContentHuggingHigh()
|
||||
result.setCompressionResistanceHigh()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let dropDownStackView: UIStackView = {
|
||||
let result: UIStackView = UIStackView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.axis = .horizontal
|
||||
result.distribution = .fill
|
||||
result.alignment = .center
|
||||
result.spacing = Values.verySmallSpacing
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let dropDownImageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView(image: UIImage(systemName: "arrowtriangle.down.fill"))
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.themeTintColor = .textPrimary
|
||||
result.set(.width, to: 10)
|
||||
result.set(.height, to: 10)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let dropDownLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize, weight: .medium)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.setContentHuggingHigh()
|
||||
result.setCompressionResistanceHigh()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private let tickImageView: UIImageView = {
|
||||
let result: UIImageView = UIImageView(image: UIImage(systemName: "checkmark"))
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.themeTintColor = .primary
|
||||
result.isHidden = true
|
||||
result.setContentHuggingHigh()
|
||||
result.setCompressionResistanceHigh()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
public lazy var rightActionButtonContainerView: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.themeBackgroundColor = .solidButton_background
|
||||
result.layer.cornerRadius = 5
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var rightActionButtonLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.setContentHuggingHigh()
|
||||
result.setCompressionResistanceHigh()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -262,24 +180,15 @@ class SettingsCell: UITableViewCell {
|
|||
cellBackgroundView.addSubview(contentStackView)
|
||||
cellBackgroundView.addSubview(botSeparator)
|
||||
|
||||
contentStackView.addArrangedSubview(iconImageView)
|
||||
contentStackView.addArrangedSubview(leftAccessoryView)
|
||||
contentStackView.addArrangedSubview(titleStackView)
|
||||
contentStackView.addArrangedSubview(pushChevronImageView)
|
||||
contentStackView.addArrangedSubview(toggleSwitch)
|
||||
contentStackView.addArrangedSubview(tickImageView)
|
||||
contentStackView.addArrangedSubview(dropDownStackView)
|
||||
contentStackView.addArrangedSubview(rightActionButtonContainerView)
|
||||
contentStackView.addArrangedSubview(rightAccessoryView)
|
||||
|
||||
titleStackView.addArrangedSubview(titleLabel)
|
||||
titleStackView.addArrangedSubview(subtitleLabel)
|
||||
titleStackView.addArrangedSubview(extraActionTopSpacingView)
|
||||
titleStackView.addArrangedSubview(extraActionButton)
|
||||
|
||||
dropDownStackView.addArrangedSubview(dropDownImageView)
|
||||
dropDownStackView.addArrangedSubview(dropDownLabel)
|
||||
|
||||
rightActionButtonContainerView.addSubview(rightActionButtonLabel)
|
||||
|
||||
setupLayout()
|
||||
}
|
||||
|
||||
|
@ -294,17 +203,15 @@ class SettingsCell: UITableViewCell {
|
|||
topSeparator.pin(.top, to: .top, of: cellBackgroundView)
|
||||
topSeparatorLeftConstraint = topSeparator.pin(.left, to: .left, of: cellBackgroundView)
|
||||
topSeparatorRightConstraint = topSeparator.pin(.right, to: .right, of: cellBackgroundView)
|
||||
contentStackView.pin(to: cellBackgroundView)
|
||||
|
||||
rightActionButtonContainerView.center(.vertical, in: contentStackView)
|
||||
rightActionButtonLabel.pin(to: rightActionButtonContainerView, withInset: Values.smallSpacing)
|
||||
contentStackView.pin(to: cellBackgroundView)
|
||||
|
||||
botSeparatorLeftConstraint = botSeparator.pin(.left, to: .left, of: cellBackgroundView)
|
||||
botSeparatorRightConstraint = botSeparator.pin(.right, to: .right, of: cellBackgroundView)
|
||||
botSeparator.pin(.bottom, to: .bottom, of: cellBackgroundView)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
// Need to force the contentStackView to layout if needed as it might not have updated it's
|
||||
|
@ -363,109 +270,125 @@ class SettingsCell: UITableViewCell {
|
|||
|
||||
// MARK: - Content
|
||||
|
||||
override func prepareForReuse() {
|
||||
public override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
self.instanceView = UIView()
|
||||
self.position = nil
|
||||
self.onExtraAction = nil
|
||||
self.onExtraActionTap = nil
|
||||
self.accessibilityIdentifier = nil
|
||||
|
||||
stackViewImageHeightConstraint.isActive = false
|
||||
iconImageView.removeConstraints(iconImageView.constraints)
|
||||
iconImageView.image = nil
|
||||
iconImageView.themeTintColor = .textPrimary
|
||||
leftAccessoryView.prepareForReuse()
|
||||
leftAccessoryFillConstraint.isActive = false
|
||||
titleLabel.text = ""
|
||||
titleLabel.themeTextColor = .textPrimary
|
||||
subtitleLabel.text = ""
|
||||
dropDownLabel.text = ""
|
||||
subtitleLabel.themeTextColor = .textPrimary
|
||||
rightAccessoryView.prepareForReuse()
|
||||
rightAccessoryFillConstraint.isActive = false
|
||||
|
||||
topSeparator.isHidden = true
|
||||
iconImageView.isHidden = true
|
||||
subtitleLabel.isHidden = true
|
||||
extraActionTopSpacingView.isHidden = true
|
||||
extraActionButton.setTitle("", for: .normal)
|
||||
extraActionButton.isHidden = true
|
||||
pushChevronImageView.isHidden = true
|
||||
toggleSwitch.isHidden = true
|
||||
dropDownStackView.isHidden = true
|
||||
tickImageView.isHidden = true
|
||||
tickImageView.alpha = 1
|
||||
rightActionButtonContainerView.isHidden = true
|
||||
botSeparator.isHidden = true
|
||||
|
||||
subtitleExtraView?.removeFromSuperview()
|
||||
subtitleExtraView = nil
|
||||
}
|
||||
|
||||
public func update(
|
||||
style: Style = .rounded,
|
||||
icon: UIImage?,
|
||||
iconSize: IconSize,
|
||||
iconSetter: ((UIImageView) -> Void)?,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
alignment: NSTextAlignment,
|
||||
accessibilityIdentifier: String?,
|
||||
subtitleExtraViewGenerator: (() -> UIView)?,
|
||||
action: SettingsAction,
|
||||
extraActionTitle: String?,
|
||||
onExtraAction: (() -> Void)?,
|
||||
public func update<ID: Hashable & Differentiable>(
|
||||
with info: Info<ID>,
|
||||
style: Style,
|
||||
position: Position
|
||||
) {
|
||||
self.instanceView = UIView()
|
||||
self.position = position
|
||||
self.subtitleExtraView = subtitleExtraViewGenerator?()
|
||||
self.onExtraAction = onExtraAction
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.subtitleExtraView = info.subtitleExtraViewGenerator?()
|
||||
self.onExtraActionTap = info.extraAction?.onTap
|
||||
self.accessibilityIdentifier = info.accessibilityIdentifier
|
||||
|
||||
stackViewImageHeightConstraint.isActive = {
|
||||
switch iconSize {
|
||||
case .small, .medium: return false
|
||||
case .large: return true // Edge-to-edge in this case
|
||||
}
|
||||
}()
|
||||
let leftFitToEdge: Bool = (info.leftAccessory?.shouldFitToEdge == true)
|
||||
let rightFitToEdge: Bool = (!leftFitToEdge && info.rightAccessory?.shouldFitToEdge == true)
|
||||
leftAccessoryFillConstraint.isActive = leftFitToEdge
|
||||
leftAccessoryView.update(
|
||||
with: info.leftAccessory,
|
||||
tintColor: info.tintColor,
|
||||
isEnabled: info.isEnabled
|
||||
)
|
||||
rightAccessoryView.update(
|
||||
with: info.rightAccessory,
|
||||
tintColor: info.tintColor,
|
||||
isEnabled: info.isEnabled
|
||||
)
|
||||
rightAccessoryFillConstraint.isActive = rightFitToEdge
|
||||
contentStackView.layoutMargins = UIEdgeInsets(
|
||||
top: Values.mediumSpacing,
|
||||
leading: {
|
||||
switch iconSize {
|
||||
case .small, .medium: return Values.largeSpacing
|
||||
case .large: return 0 // Edge-to-edge in this case
|
||||
}
|
||||
}(),
|
||||
bottom: Values.mediumSpacing,
|
||||
trailing: Values.largeSpacing
|
||||
top: (leftFitToEdge || rightFitToEdge ? 0 : Values.mediumSpacing),
|
||||
left: (leftFitToEdge ? 0 : Values.largeSpacing),
|
||||
bottom: (leftFitToEdge || rightFitToEdge ? 0 : Values.mediumSpacing),
|
||||
right: (rightFitToEdge ? 0 : Values.largeSpacing)
|
||||
)
|
||||
|
||||
// Left content
|
||||
iconImageView.set(.width, to: iconSize.size)
|
||||
iconImageView.set(.height, to: iconSize.size)
|
||||
iconImageView.image = icon
|
||||
iconImageView.isHidden = (icon == nil && iconSetter == nil)
|
||||
titleLabel.text = title
|
||||
titleLabel.textAlignment = alignment
|
||||
subtitleLabel.text = subtitle
|
||||
subtitleLabel.isHidden = (subtitle == nil)
|
||||
extraActionTopSpacingView.isHidden = (extraActionTitle == nil)
|
||||
extraActionButton.setTitle(extraActionTitle, for: .normal)
|
||||
extraActionButton.isHidden = (extraActionTitle == nil)
|
||||
|
||||
// Call the iconSetter closure if provided to set the icon
|
||||
iconSetter?(iconImageView)
|
||||
titleLabel.text = info.title
|
||||
titleLabel.themeTextColor = info.tintColor
|
||||
subtitleLabel.text = info.subtitle
|
||||
subtitleLabel.themeTextColor = info.tintColor
|
||||
subtitleLabel.isHidden = (info.subtitle == nil)
|
||||
extraActionTopSpacingView.isHidden = (info.extraAction == nil)
|
||||
extraActionButton.setTitle(info.extraAction?.title, for: .normal)
|
||||
extraActionButton.isHidden = (info.extraAction == nil)
|
||||
|
||||
// Styling and positioning
|
||||
cellBackgroundView.themeBackgroundColor = (action.shouldHaveBackground ?
|
||||
let defaultEdgePadding: CGFloat
|
||||
cellBackgroundView.themeBackgroundColor = (info.shouldHaveBackground ?
|
||||
.settings_tabBackground :
|
||||
nil
|
||||
)
|
||||
cellSelectedBackgroundView.isHidden = !action.shouldHaveBackground
|
||||
backgroundLeftConstraint.constant = (style == .rounded ? Values.largeSpacing : 0)
|
||||
backgroundRightConstraint.constant = (style == .rounded ? -Values.largeSpacing : 0)
|
||||
topSeparatorLeftConstraint.constant = (style == .rounded ? Values.mediumSpacing : 0)
|
||||
topSeparatorRightConstraint.constant = (style == .rounded ? -Values.mediumSpacing : 0)
|
||||
botSeparatorLeftConstraint.constant = (style == .rounded ? Values.mediumSpacing : 0)
|
||||
botSeparatorRightConstraint.constant = (style == .rounded ? -Values.mediumSpacing : 0)
|
||||
cellBackgroundView.layer.cornerRadius = (style == .rounded ? SettingsCell.cornerRadius : 0)
|
||||
cellSelectedBackgroundView.isHidden = (!info.isEnabled || !info.shouldHaveBackground)
|
||||
|
||||
switch style {
|
||||
case .rounded:
|
||||
defaultEdgePadding = Values.mediumSpacing
|
||||
backgroundLeftConstraint.constant = Values.largeSpacing
|
||||
backgroundRightConstraint.constant = -Values.largeSpacing
|
||||
cellBackgroundView.layer.cornerRadius = SessionCell.cornerRadius
|
||||
|
||||
case .edgeToEdge:
|
||||
defaultEdgePadding = 0
|
||||
backgroundLeftConstraint.constant = 0
|
||||
backgroundRightConstraint.constant = 0
|
||||
cellBackgroundView.layer.cornerRadius = 0
|
||||
|
||||
case .roundedEdgeToEdge:
|
||||
defaultEdgePadding = Values.mediumSpacing
|
||||
backgroundLeftConstraint.constant = 0
|
||||
backgroundRightConstraint.constant = 0
|
||||
cellBackgroundView.layer.cornerRadius = SessionCell.cornerRadius
|
||||
}
|
||||
|
||||
let fittedEdgePadding: CGFloat = {
|
||||
func targetSize(accessory: Accessory?) -> CGFloat {
|
||||
switch accessory {
|
||||
case .icon(_, let iconSize, _, _), .iconAsync(let iconSize, _, _, _):
|
||||
return iconSize.size
|
||||
|
||||
default: return defaultEdgePadding
|
||||
}
|
||||
}
|
||||
|
||||
guard leftFitToEdge else {
|
||||
guard rightFitToEdge else { return defaultEdgePadding }
|
||||
|
||||
return targetSize(accessory: info.rightAccessory)
|
||||
}
|
||||
|
||||
return targetSize(accessory: info.leftAccessory)
|
||||
}()
|
||||
topSeparatorLeftConstraint.constant = (leftFitToEdge ? fittedEdgePadding : defaultEdgePadding)
|
||||
topSeparatorRightConstraint.constant = (rightFitToEdge ? -fittedEdgePadding : -defaultEdgePadding)
|
||||
botSeparatorLeftConstraint.constant = (leftFitToEdge ? fittedEdgePadding : defaultEdgePadding)
|
||||
botSeparatorRightConstraint.constant = (rightFitToEdge ? -fittedEdgePadding : -defaultEdgePadding)
|
||||
|
||||
switch position {
|
||||
case .top:
|
||||
|
@ -491,79 +414,13 @@ class SettingsCell: UITableViewCell {
|
|||
topSeparator.isHidden = true
|
||||
botSeparator.isHidden = true
|
||||
}
|
||||
|
||||
// Action Behaviours
|
||||
switch action {
|
||||
case .threadInfo: break
|
||||
|
||||
case .userDefaultsBool(let defaults, let key, let isEnabled, _):
|
||||
toggleSwitch.isHidden = false
|
||||
toggleSwitch.isEnabled = isEnabled
|
||||
|
||||
// Remove the selection view if the setting is disabled
|
||||
cellSelectedBackgroundView.isHidden = !isEnabled
|
||||
|
||||
let newValue: Bool = defaults.bool(forKey: key)
|
||||
|
||||
if newValue != toggleSwitch.isOn {
|
||||
toggleSwitch.setOn(newValue, animated: true)
|
||||
}
|
||||
|
||||
case .settingBool(let key, _, let isEnabled):
|
||||
toggleSwitch.isHidden = false
|
||||
toggleSwitch.isEnabled = isEnabled
|
||||
|
||||
// Remove the selection view if the setting is disabled
|
||||
cellSelectedBackgroundView.isHidden = !isEnabled
|
||||
|
||||
let newValue: Bool = Storage.shared[key]
|
||||
|
||||
if newValue != toggleSwitch.isOn {
|
||||
toggleSwitch.setOn(newValue, animated: true)
|
||||
}
|
||||
|
||||
case .customToggle(let value, let isEnabled, _, _):
|
||||
toggleSwitch.isHidden = false
|
||||
toggleSwitch.isEnabled = isEnabled
|
||||
|
||||
// Remove the selection view if the setting is disabled
|
||||
cellSelectedBackgroundView.isHidden = !isEnabled
|
||||
|
||||
if value != toggleSwitch.isOn {
|
||||
toggleSwitch.setOn(value, animated: true)
|
||||
}
|
||||
|
||||
case .settingEnum(_, let value, _), .generalEnum(let value, _):
|
||||
dropDownStackView.isHidden = false
|
||||
dropDownLabel.text = value
|
||||
|
||||
case .listSelection(let isSelected, let storedSelection, _, _):
|
||||
tickImageView.isHidden = (!isSelected() && !storedSelection)
|
||||
tickImageView.alpha = (!isSelected() && storedSelection ? 0.3 : 1)
|
||||
|
||||
case .trigger(let showChevron, _):
|
||||
pushChevronImageView.isHidden = !showChevron
|
||||
|
||||
case .push(let showChevron, let tintColor, _, _):
|
||||
titleLabel.themeTextColor = tintColor
|
||||
iconImageView.themeTintColor = tintColor
|
||||
pushChevronImageView.isHidden = !showChevron
|
||||
|
||||
case .present(let tintColor, _):
|
||||
titleLabel.themeTextColor = tintColor
|
||||
iconImageView.themeTintColor = tintColor
|
||||
|
||||
case .rightButtonAction(let title, _):
|
||||
rightActionButtonContainerView.isHidden = false
|
||||
rightActionButtonLabel.text = title
|
||||
}
|
||||
}
|
||||
|
||||
public func update(isEditing: Bool, animated: Bool) {}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||
public override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||
super.setHighlighted(highlighted, animated: animated)
|
||||
|
||||
// If the 'cellSelectedBackgroundView' is hidden then there is no background so we
|
||||
|
@ -573,34 +430,18 @@ class SettingsCell: UITableViewCell {
|
|||
}
|
||||
|
||||
cellSelectedBackgroundView.alpha = (highlighted ? 1 : 0)
|
||||
rightActionButtonContainerView.themeBackgroundColor = (highlighted ?
|
||||
.solidButton_highlight :
|
||||
.solidButton_background
|
||||
)
|
||||
leftAccessoryView.setHighlighted(highlighted, animated: animated)
|
||||
rightAccessoryView.setHighlighted(highlighted, animated: animated)
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
public override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
|
||||
// Note: When initially triggering a selection we will be coming from the highlighted
|
||||
// state but will have already set highlighted to false at this stage, as a result we
|
||||
// need to swap back into the "highlighted" state so we can properly unhighlight within
|
||||
// the "deselect" animation
|
||||
guard !selected else {
|
||||
rightActionButtonContainerView.themeBackgroundColor = .solidButton_highlight
|
||||
return
|
||||
}
|
||||
guard animated else {
|
||||
rightActionButtonContainerView.themeBackgroundColor = .solidButton_background
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.4) { [weak self] in
|
||||
self?.rightActionButtonContainerView.themeBackgroundColor = .solidButton_background
|
||||
}
|
||||
leftAccessoryView.setSelected(selected, animated: animated)
|
||||
rightAccessoryView.setSelected(selected, animated: animated)
|
||||
}
|
||||
|
||||
@objc private func extraActionTapped() {
|
||||
onExtraAction?()
|
||||
onExtraActionTap?()
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
class SettingHeaderView: UITableViewHeaderFooterView {
|
||||
class SessionHeaderView: UITableViewHeaderFooterView {
|
||||
private lazy var emptyHeightConstraint: NSLayoutConstraint = self.heightAnchor
|
||||
.constraint(equalToConstant: (Values.verySmallSpacing * 2))
|
||||
private lazy var filledHeightConstraint: NSLayoutConstraint = self.heightAnchor
|
||||
|
@ -64,7 +64,7 @@ class SettingHeaderView: UITableViewHeaderFooterView {
|
|||
// MARK: - Content
|
||||
|
||||
public func update(
|
||||
style: SettingsCell.Style = .rounded,
|
||||
style: SessionCell.Style = .rounded,
|
||||
title: String?,
|
||||
hasSeparator: Bool
|
||||
) {
|
||||
|
@ -75,7 +75,7 @@ class SettingHeaderView: UITableViewHeaderFooterView {
|
|||
// Align to the start of the text in the cell
|
||||
return (Values.largeSpacing + Values.mediumSpacing)
|
||||
|
||||
case .edgeToEdge: return Values.largeSpacing
|
||||
case .edgeToEdge, .roundedEdgeToEdge: return Values.largeSpacing
|
||||
}
|
||||
}()
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public class SessionHighlightingBackgroundLabel: UIView {
|
||||
var text: String? {
|
||||
get { label.text }
|
||||
set { label.text = newValue }
|
||||
}
|
||||
|
||||
var themeTextColor: ThemeValue? {
|
||||
get { label.themeTextColor }
|
||||
set { label.themeTextColor = newValue }
|
||||
}
|
||||
|
||||
// MARK: - Components
|
||||
|
||||
private let label: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.setContentHuggingHigh()
|
||||
result.setCompressionResistanceHigh()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.themeBackgroundColor = .solidButton_background
|
||||
self.layer.cornerRadius = 5
|
||||
|
||||
self.setupViewHierarchy()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
private func setupViewHierarchy() {
|
||||
addSubview(label)
|
||||
|
||||
label.pin(to: self, withInset: Values.smallSpacing)
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||
self.themeBackgroundColor = (highlighted ?
|
||||
.solidButton_highlight :
|
||||
.solidButton_background
|
||||
)
|
||||
}
|
||||
|
||||
func setSelected(_ selected: Bool, animated: Bool) {
|
||||
// Note: When initially triggering a selection we will be coming from the highlighted
|
||||
// state but will have already set highlighted to false at this stage, as a result we
|
||||
// need to swap back into the "highlighted" state so we can properly unhighlight within
|
||||
// the "deselect" animation
|
||||
guard !selected else {
|
||||
self.themeBackgroundColor = .solidButton_highlight
|
||||
return
|
||||
}
|
||||
guard animated else {
|
||||
self.themeBackgroundColor = .solidButton_background
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.4) { [weak self] in
|
||||
self?.themeBackgroundColor = .solidButton_background
|
||||
}
|
||||
}
|
||||
}
|
|
@ -177,7 +177,7 @@ public class ConfirmationModal: Modal {
|
|||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
init(info: Info) {
|
||||
init(targetView: UIView? = nil, info: Info) {
|
||||
self.internalOnConfirm = { viewController in
|
||||
if info.dismissOnConfirm {
|
||||
viewController.dismiss(animated: true)
|
||||
|
@ -186,7 +186,7 @@ public class ConfirmationModal: Modal {
|
|||
info.onConfirm?(viewController)
|
||||
}
|
||||
|
||||
super.init(afterClosed: info.afterClosed)
|
||||
super.init(targetView: targetView, afterClosed: info.afterClosed)
|
||||
|
||||
self.modalPresentationStyle = .overFullScreen
|
||||
self.modalTransitionStyle = .crossDissolve
|
||||
|
|
|
@ -54,7 +54,7 @@ public class Modal: BaseVC, UIGestureRecognizerDelegate {
|
|||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
public init(afterClosed: (() -> ())? = nil) {
|
||||
public init(targetView: UIView? = nil, afterClosed: (() -> ())? = nil) {
|
||||
self.afterClosed = afterClosed
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
@ -62,8 +62,8 @@ public class Modal: BaseVC, UIGestureRecognizerDelegate {
|
|||
// Ensure the modal doesn't crash on iPad when being presented
|
||||
if UIDevice.current.isIPad {
|
||||
self.popoverPresentationController?.permittedArrowDirections = []
|
||||
self.popoverPresentationController?.sourceView = self.view
|
||||
self.popoverPresentationController?.sourceRect = self.view.bounds
|
||||
self.popoverPresentationController?.sourceView = (targetView ?? self.view)
|
||||
self.popoverPresentationController?.sourceRect = (targetView ?? self.view).bounds
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class AvatarViewHelper;
|
||||
@class OWSContactsManager;
|
||||
|
||||
@protocol AvatarViewHelperDelegate <NSObject>
|
||||
|
||||
- (nullable NSString *)avatarActionSheetTitle;
|
||||
|
||||
- (void)avatarDidChange:(nullable UIImage *)image filePath:(nullable NSString *)filePath;
|
||||
|
||||
- (UIViewController *)fromViewController;
|
||||
|
||||
- (BOOL)hasClearAvatarAction;
|
||||
|
||||
@optional
|
||||
|
||||
- (NSString *)clearAvatarActionLabel;
|
||||
|
||||
- (void)clearAvatar;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
typedef void (^AvatarViewSuccessBlock)(void);
|
||||
|
||||
@interface AvatarViewHelper : NSObject
|
||||
|
||||
@property (nonatomic, weak) id<AvatarViewHelperDelegate> delegate;
|
||||
|
||||
- (void)showChangeAvatarUI;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,133 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AvatarViewHelper.h"
|
||||
#import "OWSNavigationController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <MobileCoreServices/UTCoreTypes.h>
|
||||
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AvatarViewHelper () <UIImagePickerControllerDelegate, UINavigationControllerDelegate>
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation AvatarViewHelper
|
||||
|
||||
#pragma mark - Avatar Avatar
|
||||
|
||||
- (void)showChangeAvatarUI
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(self.delegate);
|
||||
|
||||
UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:self.delegate.avatarActionSheetTitle
|
||||
message:nil
|
||||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
[actionSheet addAction:[OWSAlerts cancelAction]];
|
||||
|
||||
UIAlertAction *choosePictureAction = [UIAlertAction
|
||||
actionWithTitle:NSLocalizedString(@"MEDIA_FROM_LIBRARY_BUTTON", @"media picker option to choose from library")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self chooseFromLibrary];
|
||||
}];
|
||||
[actionSheet addAction:choosePictureAction];
|
||||
|
||||
if (self.delegate.hasClearAvatarAction) {
|
||||
UIAlertAction *clearAction = [UIAlertAction actionWithTitle:self.delegate.clearAvatarActionLabel
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self.delegate clearAvatar];
|
||||
}];
|
||||
|
||||
[actionSheet addAction:clearAction];
|
||||
}
|
||||
|
||||
[self.delegate.fromViewController presentAlert:actionSheet];
|
||||
}
|
||||
|
||||
- (void)chooseFromLibrary
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(self.delegate);
|
||||
|
||||
[self.delegate.fromViewController ows_askForMediaLibraryPermissions:^(BOOL granted) {
|
||||
if (!granted) {
|
||||
OWSLogWarn(@"Media Library permission denied.");
|
||||
return;
|
||||
}
|
||||
|
||||
UIImagePickerController *picker = [OWSImagePickerController new];
|
||||
picker.delegate = self;
|
||||
picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
|
||||
picker.mediaTypes = @[ (__bridge NSString *)kUTTypeImage ];
|
||||
|
||||
[self.delegate.fromViewController presentViewController:picker animated:YES completion:nil];
|
||||
}];
|
||||
}
|
||||
|
||||
/*
|
||||
* Dismissing UIImagePickerController
|
||||
*/
|
||||
|
||||
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(self.delegate);
|
||||
|
||||
[self.delegate.fromViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
/*
|
||||
* Fetch data from UIImagePickerController
|
||||
*/
|
||||
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(self.delegate);
|
||||
|
||||
NSURL* imageURL = [info objectForKey:UIImagePickerControllerImageURL];
|
||||
UIImage *rawAvatar = [info objectForKey:UIImagePickerControllerOriginalImage];
|
||||
|
||||
[self.delegate.fromViewController
|
||||
dismissViewControllerAnimated:YES
|
||||
completion:^{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
// Check if the user selected an animated image (if so then don't crop, just
|
||||
// set the avatar directly
|
||||
NSString *type;
|
||||
if ([imageURL getResourceValue:&type forKey:NSURLTypeIdentifierKey error:nil]) {
|
||||
if ([[MIMETypeUtil supportedAnimatedImageUTITypes] containsObject:type]) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.delegate avatarDidChange:nil filePath: imageURL.path];
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (rawAvatar) {
|
||||
CropScaleImageViewController *vc = [[CropScaleImageViewController alloc]
|
||||
initWithSrcImage:rawAvatar
|
||||
successCompletion:^(UIImage *_Nonnull dstImage) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.delegate avatarDidChange:dstImage filePath:nil];
|
||||
});
|
||||
}];
|
||||
[self.delegate.fromViewController presentViewController:vc
|
||||
animated:YES
|
||||
completion:nil];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -319,12 +319,12 @@ public extension Profile {
|
|||
}
|
||||
|
||||
/// A standardised mechanism for truncating a user id for a given thread
|
||||
static func truncated(id: String, threadVariant: SessionThread.Variant = .contact) -> String {
|
||||
static func truncated(id: String, threadVariant: SessionThread.Variant) -> String {
|
||||
return truncated(id: id, truncating: .middle)
|
||||
}
|
||||
|
||||
/// A standardised mechanism for truncating a user id
|
||||
static func truncated(id: String, truncating: Truncation = .middle) -> String {
|
||||
static func truncated(id: String, truncating: Truncation) -> String {
|
||||
guard id.count > 8 else { return id }
|
||||
|
||||
switch truncating {
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
public final class OutlineButton: UIButton {
|
||||
public final class SessionButton: UIButton {
|
||||
public enum Style {
|
||||
case regular
|
||||
case bordered
|
||||
case borderless
|
||||
case destructive
|
||||
case destructiveBorderless
|
||||
|
@ -25,7 +25,7 @@ public final class OutlineButton: UIButton {
|
|||
setThemeTitleColor(
|
||||
{
|
||||
switch style {
|
||||
case .regular, .borderless, .destructive,
|
||||
case .bordered, .borderless, .destructive,
|
||||
.destructiveBorderless:
|
||||
return .disabled
|
||||
|
||||
|
@ -37,7 +37,7 @@ public final class OutlineButton: UIButton {
|
|||
setThemeBackgroundColor(
|
||||
{
|
||||
switch style {
|
||||
case .regular, .borderless, .destructive,
|
||||
case .bordered, .borderless, .destructive,
|
||||
.destructiveBorderless:
|
||||
return .clear
|
||||
|
||||
|
@ -50,7 +50,7 @@ public final class OutlineButton: UIButton {
|
|||
|
||||
themeBorderColor = {
|
||||
switch style {
|
||||
case .regular, .destructive: return .disabled
|
||||
case .bordered, .destructive: return .disabled
|
||||
case .filled, .borderless, .destructiveBorderless: return nil
|
||||
}
|
||||
}()
|
||||
|
@ -114,9 +114,9 @@ public final class OutlineButton: UIButton {
|
|||
setThemeTitleColor(
|
||||
{
|
||||
switch style {
|
||||
case .regular, .borderless: return .outlineButton_text
|
||||
case .destructive, .destructiveBorderless: return .outlineButton_destructiveText
|
||||
case .filled: return .outlineButton_filledText
|
||||
case .bordered, .borderless: return .sessionButton_text
|
||||
case .destructive, .destructiveBorderless: return .sessionButton_destructiveText
|
||||
case .filled: return .sessionButton_filledText
|
||||
}
|
||||
}(),
|
||||
for: .normal
|
||||
|
@ -125,9 +125,9 @@ public final class OutlineButton: UIButton {
|
|||
setThemeBackgroundColor(
|
||||
{
|
||||
switch style {
|
||||
case .regular, .borderless: return .outlineButton_background
|
||||
case .destructive, .destructiveBorderless: return .outlineButton_destructiveBackground
|
||||
case .filled: return .outlineButton_filledBackground
|
||||
case .bordered, .borderless: return .sessionButton_background
|
||||
case .destructive, .destructiveBorderless: return .sessionButton_destructiveBackground
|
||||
case .filled: return .sessionButton_filledBackground
|
||||
}
|
||||
}(),
|
||||
for: .normal
|
||||
|
@ -135,9 +135,9 @@ public final class OutlineButton: UIButton {
|
|||
setThemeBackgroundColor(
|
||||
{
|
||||
switch style {
|
||||
case .regular, .borderless: return .outlineButton_highlight
|
||||
case .destructive, .destructiveBorderless: return .outlineButton_destructiveHighlight
|
||||
case .filled: return .outlineButton_filledHighlight
|
||||
case .bordered, .borderless: return .sessionButton_highlight
|
||||
case .destructive, .destructiveBorderless: return .sessionButton_destructiveHighlight
|
||||
case .filled: return .sessionButton_filledHighlight
|
||||
}
|
||||
}(),
|
||||
for: .highlighted
|
||||
|
@ -151,8 +151,8 @@ public final class OutlineButton: UIButton {
|
|||
}()
|
||||
themeBorderColor = {
|
||||
switch style {
|
||||
case .regular: return .outlineButton_border
|
||||
case .destructive: return .outlineButton_destructiveBorder
|
||||
case .bordered: return .sessionButton_border
|
||||
case .destructive: return .sessionButton_destructiveBorder
|
||||
case .filled, .borderless, .destructiveBorderless: return nil
|
||||
}
|
||||
}()
|
|
@ -47,18 +47,18 @@ internal enum Theme_ClassicDark: ThemeColors {
|
|||
.radioButton_selectedBorder: .classicDark6,
|
||||
.radioButton_unselectedBorder: .classicDark6,
|
||||
|
||||
// OutlineButton
|
||||
.outlineButton_text: .primary,
|
||||
.outlineButton_background: .clear,
|
||||
.outlineButton_highlight: .classicDark6.withAlphaComponent(0.3),
|
||||
.outlineButton_border: .primary,
|
||||
.outlineButton_filledText: .classicDark6,
|
||||
.outlineButton_filledBackground: .classicDark1,
|
||||
.outlineButton_filledHighlight: .classicDark3,
|
||||
.outlineButton_destructiveText: .dangerDark,
|
||||
.outlineButton_destructiveBackground: .clear,
|
||||
.outlineButton_destructiveHighlight: .dangerDark.withAlphaComponent(0.3),
|
||||
.outlineButton_destructiveBorder: .dangerDark,
|
||||
// SessionButton
|
||||
.sessionButton_text: .primary,
|
||||
.sessionButton_background: .clear,
|
||||
.sessionButton_highlight: .classicDark6.withAlphaComponent(0.3),
|
||||
.sessionButton_border: .primary,
|
||||
.sessionButton_filledText: .classicDark6,
|
||||
.sessionButton_filledBackground: .classicDark1,
|
||||
.sessionButton_filledHighlight: .classicDark3,
|
||||
.sessionButton_destructiveText: .dangerDark,
|
||||
.sessionButton_destructiveBackground: .clear,
|
||||
.sessionButton_destructiveHighlight: .dangerDark.withAlphaComponent(0.3),
|
||||
.sessionButton_destructiveBorder: .dangerDark,
|
||||
|
||||
// SolidButton
|
||||
.solidButton_background: .classicDark3,
|
||||
|
|
|
@ -48,17 +48,17 @@ internal enum Theme_ClassicLight: ThemeColors {
|
|||
.radioButton_unselectedBorder: .classicLight0,
|
||||
|
||||
// OutlineButton
|
||||
.outlineButton_text: .classicLight0,
|
||||
.outlineButton_background: .clear,
|
||||
.outlineButton_highlight: .classicLight0.withAlphaComponent(0.1),
|
||||
.outlineButton_border: .classicLight0,
|
||||
.outlineButton_filledText: .classicLight6,
|
||||
.outlineButton_filledBackground: .classicLight0,
|
||||
.outlineButton_filledHighlight: .classicLight1,
|
||||
.outlineButton_destructiveText: .dangerLight,
|
||||
.outlineButton_destructiveBackground: .clear,
|
||||
.outlineButton_destructiveHighlight: .dangerLight.withAlphaComponent(0.3),
|
||||
.outlineButton_destructiveBorder: .dangerLight,
|
||||
.sessionButton_text: .classicLight0,
|
||||
.sessionButton_background: .clear,
|
||||
.sessionButton_highlight: .classicLight0.withAlphaComponent(0.1),
|
||||
.sessionButton_border: .classicLight0,
|
||||
.sessionButton_filledText: .classicLight6,
|
||||
.sessionButton_filledBackground: .classicLight0,
|
||||
.sessionButton_filledHighlight: .classicLight1,
|
||||
.sessionButton_destructiveText: .dangerLight,
|
||||
.sessionButton_destructiveBackground: .clear,
|
||||
.sessionButton_destructiveHighlight: .dangerLight.withAlphaComponent(0.3),
|
||||
.sessionButton_destructiveBorder: .dangerLight,
|
||||
|
||||
// SolidButton
|
||||
.solidButton_background: .classicLight3,
|
||||
|
|
|
@ -47,18 +47,18 @@ internal enum Theme_OceanDark: ThemeColors {
|
|||
.radioButton_selectedBorder: .oceanDark7,
|
||||
.radioButton_unselectedBorder: .oceanDark7,
|
||||
|
||||
// OutlineButton
|
||||
.outlineButton_text: .primary,
|
||||
.outlineButton_background: .clear,
|
||||
.outlineButton_highlight: .oceanDark7.withAlphaComponent(0.3),
|
||||
.outlineButton_border: .primary,
|
||||
.outlineButton_filledText: .oceanDark7,
|
||||
.outlineButton_filledBackground: .oceanDark1,
|
||||
.outlineButton_filledHighlight: .oceanDark3,
|
||||
.outlineButton_destructiveText: .dangerDark,
|
||||
.outlineButton_destructiveBackground: .clear,
|
||||
.outlineButton_destructiveHighlight: .dangerDark.withAlphaComponent(0.3),
|
||||
.outlineButton_destructiveBorder: .dangerDark,
|
||||
// SessionButton
|
||||
.sessionButton_text: .primary,
|
||||
.sessionButton_background: .clear,
|
||||
.sessionButton_highlight: .oceanDark7.withAlphaComponent(0.3),
|
||||
.sessionButton_border: .primary,
|
||||
.sessionButton_filledText: .oceanDark7,
|
||||
.sessionButton_filledBackground: .oceanDark1,
|
||||
.sessionButton_filledHighlight: .oceanDark3,
|
||||
.sessionButton_destructiveText: .dangerDark,
|
||||
.sessionButton_destructiveBackground: .clear,
|
||||
.sessionButton_destructiveHighlight: .dangerDark.withAlphaComponent(0.3),
|
||||
.sessionButton_destructiveBorder: .dangerDark,
|
||||
|
||||
// SolidButton
|
||||
.solidButton_background: .oceanDark2,
|
||||
|
|
|
@ -47,18 +47,18 @@ internal enum Theme_OceanLight: ThemeColors {
|
|||
.radioButton_selectedBorder: .oceanLight1,
|
||||
.radioButton_unselectedBorder: .oceanLight3,
|
||||
|
||||
// OutlineButton
|
||||
.outlineButton_text: .oceanLight1,
|
||||
.outlineButton_background: .clear,
|
||||
.outlineButton_highlight: .oceanLight1.withAlphaComponent(0.1),
|
||||
.outlineButton_border: .oceanLight1,
|
||||
.outlineButton_filledText: .oceanLight7,
|
||||
.outlineButton_filledBackground: .oceanLight1,
|
||||
.outlineButton_filledHighlight: .oceanLight2,
|
||||
.outlineButton_destructiveText: .dangerLight,
|
||||
.outlineButton_destructiveBackground: .clear,
|
||||
.outlineButton_destructiveHighlight: .dangerLight.withAlphaComponent(0.3),
|
||||
.outlineButton_destructiveBorder: .dangerLight,
|
||||
// SessionButton
|
||||
.sessionButton_text: .oceanLight1,
|
||||
.sessionButton_background: .clear,
|
||||
.sessionButton_highlight: .oceanLight1.withAlphaComponent(0.1),
|
||||
.sessionButton_border: .oceanLight1,
|
||||
.sessionButton_filledText: .oceanLight7,
|
||||
.sessionButton_filledBackground: .oceanLight1,
|
||||
.sessionButton_filledHighlight: .oceanLight2,
|
||||
.sessionButton_destructiveText: .dangerLight,
|
||||
.sessionButton_destructiveBackground: .clear,
|
||||
.sessionButton_destructiveHighlight: .dangerLight.withAlphaComponent(0.3),
|
||||
.sessionButton_destructiveBorder: .dangerLight,
|
||||
|
||||
// SolidButton
|
||||
.solidButton_background: .oceanLight5,
|
||||
|
|
|
@ -125,18 +125,18 @@ public indirect enum ThemeValue: Hashable {
|
|||
case radioButton_selectedBorder
|
||||
case radioButton_unselectedBorder
|
||||
|
||||
// OutlineButton
|
||||
case outlineButton_text
|
||||
case outlineButton_background
|
||||
case outlineButton_highlight
|
||||
case outlineButton_border
|
||||
case outlineButton_filledText
|
||||
case outlineButton_filledBackground
|
||||
case outlineButton_filledHighlight
|
||||
case outlineButton_destructiveText
|
||||
case outlineButton_destructiveBackground
|
||||
case outlineButton_destructiveHighlight
|
||||
case outlineButton_destructiveBorder
|
||||
// SessionButton
|
||||
case sessionButton_text
|
||||
case sessionButton_background
|
||||
case sessionButton_highlight
|
||||
case sessionButton_border
|
||||
case sessionButton_filledText
|
||||
case sessionButton_filledBackground
|
||||
case sessionButton_filledHighlight
|
||||
case sessionButton_destructiveText
|
||||
case sessionButton_destructiveBackground
|
||||
case sessionButton_destructiveHighlight
|
||||
case sessionButton_destructiveBorder
|
||||
|
||||
// SolidButton
|
||||
case solidButton_background
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import DifferenceKit
|
||||
|
||||
public enum IconSize: Differentiable {
|
||||
case small
|
||||
case medium
|
||||
case veryLarge
|
||||
|
||||
case fit
|
||||
|
||||
public var size: CGFloat {
|
||||
switch self {
|
||||
case .small: return 24
|
||||
case .medium: return 32
|
||||
case .veryLarge: return 80
|
||||
case .fit: return 0
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
||||
public enum Position: Int, Decodable, Equatable, Hashable, DatabaseValueConvertible {
|
||||
case top
|
||||
case middle
|
||||
case bottom
|
||||
|
||||
case individual
|
||||
|
||||
public static func with(_ index: Int, count: Int) -> Position {
|
||||
guard count > 1 else { return .individual }
|
||||
|
||||
switch index {
|
||||
case 0: return .top
|
||||
case (count - 1): return .bottom
|
||||
default: return .middle
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
|
||||
public enum Position: Int, Decodable, Equatable, Hashable, DatabaseValueConvertible {
|
||||
case top
|
||||
case middle
|
||||
case bottom
|
||||
|
||||
case individual
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
public extension UIView {
|
||||
|
||||
func toImage(isOpaque: Bool, scale: CGFloat) -> UIImage? {
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = scale
|
||||
format.opaque = isOpaque
|
||||
let renderer = UIGraphicsImageRenderer(bounds: self.bounds, format: format)
|
||||
return renderer.image { context in
|
||||
self.layer.render(in: context.cgContext)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
public extension UIView {
|
||||
func toImage(isOpaque: Bool, scale: CGFloat) -> UIImage? {
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = scale
|
||||
format.opaque = isOpaque
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(bounds: self.bounds, format: format)
|
||||
|
||||
return renderer.image { context in
|
||||
self.layer.render(in: context.cgContext)
|
||||
}
|
||||
}
|
||||
|
||||
class func spacer(withWidth width: CGFloat) -> UIView {
|
||||
let view = UIView()
|
||||
view.autoSetDimension(.width, toSize: width)
|
||||
return view
|
||||
}
|
||||
|
||||
class func spacer(withHeight height: CGFloat) -> UIView {
|
||||
let view = UIView()
|
||||
view.autoSetDimension(.height, toSize: height)
|
||||
return view
|
||||
}
|
||||
|
||||
class func hStretchingSpacer() -> UIView {
|
||||
let view = UIView()
|
||||
view.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
view.setContentCompressionResistancePriority(UILayoutPriority(0), for: .horizontal)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
class func vStretchingSpacer() -> UIView {
|
||||
let view = UIView()
|
||||
view.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
view.setContentCompressionResistancePriority(UILayoutPriority(0), for: .vertical)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
static func hSpacer(_ width: CGFloat) -> UIView {
|
||||
let result: UIView = UIView()
|
||||
result.set(.width, to: width)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func vSpacer(_ height: CGFloat) -> UIView {
|
||||
let result: UIView = UIView()
|
||||
result.set(.height, to: height)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func vhSpacer(_ width: CGFloat, _ height: CGFloat) -> UIView {
|
||||
let result: UIView = UIView()
|
||||
result.set(.width, to: width)
|
||||
result.set(.height, to: height)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func separator() -> UIView {
|
||||
let result: UIView = UIView()
|
||||
result.set(.height, to: Values.separatorThickness)
|
||||
result.themeBackgroundColor = .borderSeparator
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -659,8 +659,8 @@ public class ImageEditorCanvasView: UIView {
|
|||
}
|
||||
|
||||
CATransaction.commit()
|
||||
|
||||
let image = view.renderAsImage(opaque: !hasAlpha, scale: dstScale)
|
||||
|
||||
let image = view.toImage(isOpaque: !hasAlpha, scale: dstScale)
|
||||
return image
|
||||
}
|
||||
|
||||
|
|
|
@ -379,7 +379,7 @@ public class ImageEditorPaletteView: UIView {
|
|||
gradientLayer.startPoint = CGPoint.zero
|
||||
gradientLayer.endPoint = CGPoint(x: 0, y: gradientSize.height)
|
||||
gradientLayer.endPoint = CGPoint(x: 0, y: 1.0)
|
||||
return gradientView.renderAsImage(opaque: true, scale: UIScreen.main.scale)
|
||||
return gradientView.toImage(isOpaque: true, scale: UIScreen.main.scale)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,8 +33,8 @@ open class ScreenLockViewController: UIViewController {
|
|||
return result
|
||||
}()
|
||||
|
||||
public lazy var unlockButton: OutlineButton = {
|
||||
let result: OutlineButton = OutlineButton(style: .regular, size: .medium)
|
||||
public lazy var unlockButton: SessionButton = {
|
||||
let result: SessionButton = SessionButton(style: .bordered, size: .medium)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle("Unlock Session", for: .normal)
|
||||
result.addTarget(self, action: #selector(showUnlockUI), for: .touchUpInside)
|
||||
|
|
|
@ -143,7 +143,7 @@ public class ModalActivityIndicatorViewController: OWSViewController {
|
|||
}
|
||||
|
||||
if canCancel {
|
||||
let cancelButton: OutlineButton = OutlineButton(style: .destructive, size: .large)
|
||||
let cancelButton: SessionButton = SessionButton(style: .destructive, size: .large)
|
||||
cancelButton.setTitle(CommonStrings.cancelButton, for: .normal)
|
||||
cancelButton.addTarget(self, action: #selector(cancelPressed), for: .touchUpInside)
|
||||
self.view.addSubview(cancelButton)
|
||||
|
|
|
@ -49,47 +49,6 @@ public extension UINavigationController {
|
|||
|
||||
@objc
|
||||
public extension UIView {
|
||||
func renderAsImage() -> UIImage? {
|
||||
return renderAsImage(opaque: false, scale: UIScreen.main.scale)
|
||||
}
|
||||
|
||||
func renderAsImage(opaque: Bool, scale: CGFloat) -> UIImage? {
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = scale
|
||||
format.opaque = opaque
|
||||
let renderer = UIGraphicsImageRenderer(bounds: self.bounds,
|
||||
format: format)
|
||||
return renderer.image { (context) in
|
||||
self.layer.render(in: context.cgContext)
|
||||
}
|
||||
}
|
||||
|
||||
class func spacer(withWidth width: CGFloat) -> UIView {
|
||||
let view = UIView()
|
||||
view.autoSetDimension(.width, toSize: width)
|
||||
return view
|
||||
}
|
||||
|
||||
class func spacer(withHeight height: CGFloat) -> UIView {
|
||||
let view = UIView()
|
||||
view.autoSetDimension(.height, toSize: height)
|
||||
return view
|
||||
}
|
||||
|
||||
class func hStretchingSpacer() -> UIView {
|
||||
let view = UIView()
|
||||
view.setContentHuggingHorizontalLow()
|
||||
view.setCompressionResistanceHorizontalLow()
|
||||
return view
|
||||
}
|
||||
|
||||
class func vStretchingSpacer() -> UIView {
|
||||
let view = UIView()
|
||||
view.setContentHuggingVerticalLow()
|
||||
view.setCompressionResistanceVerticalLow()
|
||||
return view
|
||||
}
|
||||
|
||||
func applyScaleAspectFitLayout(subview: UIView, aspectRatio: CGFloat) -> [NSLayoutConstraint] {
|
||||
guard subviews.contains(subview) else {
|
||||
owsFailDebug("Not a subview.")
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import SessionUIKit
|
||||
|
||||
public extension UIView {
|
||||
static func hSpacer(_ width: CGFloat) -> UIView {
|
||||
let result: UIView = UIView()
|
||||
result.set(.width, to: width)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func vSpacer(_ height: CGFloat) -> UIView {
|
||||
let result: UIView = UIView()
|
||||
result.set(.height, to: height)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func vhSpacer(_ width: CGFloat, _ height: CGFloat) -> UIView {
|
||||
let result: UIView = UIView()
|
||||
result.set(.width, to: width)
|
||||
result.set(.height, to: height)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func separator() -> UIView {
|
||||
let result: UIView = UIView()
|
||||
result.set(.height, to: Values.separatorThickness)
|
||||
result.themeBackgroundColor = .borderSeparator
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue