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:
Morgan Pretty 2022-09-28 17:30:31 +10:00
parent d9c6f2fc90
commit 40109e0bea
81 changed files with 2481 additions and 2703 deletions

View File

@ -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

View File

@ -242,6 +242,6 @@ SPEC CHECKSUMS:
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: a646db9086664b8c9e5a0b3d17664a1275dcba9d
PODFILE CHECKSUM: 430e3b57d986dc8890415294fc6cf5e4eabfce3e
COCOAPODS: 1.11.3

View File

@ -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 */,
);

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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)

View File

@ -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) }
)
}
)

View File

@ -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)
}
}
)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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)
}
)

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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())
}
)
]
)

View File

@ -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)
})
}
)
]
)

View File

@ -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()
}
)
}
)

View File

@ -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())
)
}
)
]
)

View File

@ -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

View File

@ -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

View File

@ -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]
}
}
)
]
)

View File

@ -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)

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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))
}
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
)
}
}
}

View File

@ -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
}

View File

@ -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
)
}
}
}

View File

@ -0,0 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public enum TransitionType {
case push
case present
}

View File

@ -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 }
}
}

View File

@ -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

View File

@ -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 ?? "") }

View File

@ -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)
}
}
}

View File

@ -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?()
}
}

View File

@ -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
}
}()

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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
}
}()

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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.")

View File

@ -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
}
}