Merge remote-tracking branch 'origin/feature/tweak-profile-modal-ui' into feature/updated-user-config-handling

# Conflicts:
#	Session.xcodeproj/project.pbxproj
#	Session/Calls/Call Management/SessionCall.swift
#	Session/Conversations/ConversationVC.swift
#	Session/Conversations/Input View/MentionSelectionView.swift
#	Session/Conversations/Message Cells/VisibleMessageCell.swift
#	Session/Settings/SettingsViewModel.swift
#	Session/Shared/Views/SessionAvatarCell.swift
#	Session/Shared/Views/SessionCell+AccessoryView.swift
#	SessionUIKit/Components/ConfirmationModal.swift
#	SessionUIKit/Components/PlaceholderIcon.swift
#	SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift
This commit is contained in:
Morgan Pretty 2023-05-26 17:54:45 +10:00
commit 5d88db7a8a
55 changed files with 953 additions and 749 deletions

View File

@ -373,9 +373,6 @@
C38EF24C255B6D67007E1867 /* NSAttributedString+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF23F255B6D67007E1867 /* NSAttributedString+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; C38EF24C255B6D67007E1867 /* NSAttributedString+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF23F255B6D67007E1867 /* NSAttributedString+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF240255B6D67007E1867 /* UIView+OWS.swift */; }; C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF240255B6D67007E1867 /* UIView+OWS.swift */; };
C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF241255B6D67007E1867 /* Collection+OWS.swift */; }; C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF241255B6D67007E1867 /* Collection+OWS.swift */; };
C38EF2A5255B6D93007E1867 /* Identicon+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A2255B6D93007E1867 /* Identicon+ObjC.swift */; };
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 */; }; C38EF2B3255B6D9C007E1867 /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */; };
C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */; }; C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */; };
C38EF31C255B6DBF007E1867 /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F2255B6DBC007E1867 /* Searcher.swift */; }; C38EF31C255B6DBF007E1867 /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F2255B6DBC007E1867 /* Searcher.swift */; };
@ -520,6 +517,9 @@
FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5EB282B8F17000CE219 /* AttachmentError.swift */; }; FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5EB282B8F17000CE219 /* AttachmentError.swift */; };
FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0B77AF29B69A65009169BA /* TopBannerController.swift */; }; FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0B77AF29B69A65009169BA /* TopBannerController.swift */; };
FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */; }; FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */; };
FD16AB5B2A1DD7CA0083D849 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */; };
FD16AB5F2A1DD98F0083D849 /* ProfilePictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */; };
FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */; };
FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */; }; FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */; };
FD17D79C27F40B2E00122BE0 /* SMKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacy.swift */; }; FD17D79C27F40B2E00122BE0 /* SMKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacy.swift */; };
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; }; FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; };
@ -1524,9 +1524,8 @@
C38EF240255B6D67007E1867 /* UIView+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+OWS.swift"; path = "SignalUtilitiesKit/Utilities/UIView+OWS.swift"; sourceTree = SOURCE_ROOT; }; C38EF240255B6D67007E1867 /* UIView+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+OWS.swift"; path = "SignalUtilitiesKit/Utilities/UIView+OWS.swift"; sourceTree = SOURCE_ROOT; };
C38EF241255B6D67007E1867 /* Collection+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Collection+OWS.swift"; path = "SignalUtilitiesKit/Utilities/Collection+OWS.swift"; sourceTree = SOURCE_ROOT; }; C38EF241255B6D67007E1867 /* Collection+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Collection+OWS.swift"; path = "SignalUtilitiesKit/Utilities/Collection+OWS.swift"; sourceTree = SOURCE_ROOT; };
C38EF281255B6D84007E1867 /* OWSAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSAudioSession.swift; path = SessionMessagingKit/Utilities/OWSAudioSession.swift; sourceTree = SOURCE_ROOT; }; C38EF281255B6D84007E1867 /* OWSAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSAudioSession.swift; path = SessionMessagingKit/Utilities/OWSAudioSession.swift; sourceTree = SOURCE_ROOT; };
C38EF2A2255B6D93007E1867 /* Identicon+ObjC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Identicon+ObjC.swift"; path = "SignalUtilitiesKit/Profile Pictures/Identicon+ObjC.swift"; sourceTree = SOURCE_ROOT; }; C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PlaceholderIcon.swift; path = SessionUIKit/Components/PlaceholderIcon.swift; sourceTree = SOURCE_ROOT; };
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 = SessionUIKit/Components/ProfilePictureView.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; }; C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIViewController+Utilities.swift"; path = "SignalUtilitiesKit/Utilities/UIViewController+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; }; 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; }; C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProximityMonitoringManager.swift; path = SessionMessagingKit/Utilities/ProximityMonitoringManager.swift; sourceTree = SOURCE_ROOT; };
@ -1692,6 +1691,7 @@
FD09C5EB282B8F17000CE219 /* AttachmentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentError.swift; sourceTree = "<group>"; }; FD09C5EB282B8F17000CE219 /* AttachmentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentError.swift; sourceTree = "<group>"; };
FD0B77AF29B69A65009169BA /* TopBannerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBannerController.swift; sourceTree = "<group>"; }; FD0B77AF29B69A65009169BA /* TopBannerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBannerController.swift; sourceTree = "<group>"; };
FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUtilitiesSpec.swift; sourceTree = "<group>"; }; FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUtilitiesSpec.swift; sourceTree = "<group>"; };
FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfilePictureView+Convenience.swift"; sourceTree = "<group>"; };
FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = "<group>"; }; FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = "<group>"; };
FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = "<group>"; }; FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
FD17D79B27F40B2E00122BE0 /* SMKLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKLegacy.swift; sourceTree = "<group>"; }; FD17D79B27F40B2E00122BE0 /* SMKLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKLegacy.swift; sourceTree = "<group>"; };
@ -2939,6 +2939,8 @@
C38EF3EE255B6DF6007E1867 /* GradientView.swift */, C38EF3EE255B6DF6007E1867 /* GradientView.swift */,
B86BD08323399ACF000F5AE3 /* Modal.swift */, B86BD08323399ACF000F5AE3 /* Modal.swift */,
FD52090628B49738006098F6 /* ConfirmationModal.swift */, FD52090628B49738006098F6 /* ConfirmationModal.swift */,
C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */,
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */,
FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */, FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */,
FD0B77AF29B69A65009169BA /* TopBannerController.swift */, FD0B77AF29B69A65009169BA /* TopBannerController.swift */,
); );
@ -2950,7 +2952,7 @@
children = ( children = (
C33FD9B7255A54A300E217F9 /* Meta */, C33FD9B7255A54A300E217F9 /* Meta */,
C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */, C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */,
C36096EF25AD2268008B62B2 /* Profile Pictures */, FD16AB5D2A1DD8E70083D849 /* Profile Pictures */,
C36096EE25AD21BC008B62B2 /* Screen Lock */, C36096EE25AD21BC008B62B2 /* Screen Lock */,
C3851CD225624B060061EEB0 /* Shared Views */, C3851CD225624B060061EEB0 /* Shared Views */,
C360970125AD22D3008B62B2 /* Shared View Controllers */, C360970125AD22D3008B62B2 /* Shared View Controllers */,
@ -3128,16 +3130,6 @@
path = "Screen Lock"; path = "Screen Lock";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C36096EF25AD2268008B62B2 /* Profile Pictures */ = {
isa = PBXGroup;
children = (
C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */,
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */,
C38EF2A2255B6D93007E1867 /* Identicon+ObjC.swift */,
);
path = "Profile Pictures";
sourceTree = "<group>";
};
C360970125AD22D3008B62B2 /* Shared View Controllers */ = { C360970125AD22D3008B62B2 /* Shared View Controllers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -3253,6 +3245,7 @@
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */, C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */,
FDC4386827B4E6B700C60D73 /* String+Utlities.swift */, FDC4386827B4E6B700C60D73 /* String+Utlities.swift */,
FD772899284AF1BD0018502F /* Sodium+Utilities.swift */, FD772899284AF1BD0018502F /* Sodium+Utilities.swift */,
FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */,
C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */, C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */,
C3ECBF7A257056B700EA7FCE /* Threading.swift */, C3ECBF7A257056B700EA7FCE /* Threading.swift */,
FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */, FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */,
@ -3634,6 +3627,13 @@
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
FD16AB5D2A1DD8E70083D849 /* Profile Pictures */ = {
isa = PBXGroup;
children = (
);
path = "Profile Pictures";
sourceTree = "<group>";
};
FD17D79427F3E03300122BE0 /* Migrations */ = { FD17D79427F3E03300122BE0 /* Migrations */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -5431,6 +5431,7 @@
FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */, FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */,
FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */, FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */,
C331FFE02558FB0000070591 /* SearchBar.swift in Sources */, C331FFE02558FB0000070591 /* SearchBar.swift in Sources */,
FD16AB5B2A1DD7CA0083D849 /* PlaceholderIcon.swift in Sources */,
FD71162C28E1451400B47552 /* Position.swift in Sources */, FD71162C28E1451400B47552 /* Position.swift in Sources */,
FD52090328B4680F006098F6 /* RadioButton.swift in Sources */, FD52090328B4680F006098F6 /* RadioButton.swift in Sources */,
C331FFE82558FB0000070591 /* TextView.swift in Sources */, C331FFE82558FB0000070591 /* TextView.swift in Sources */,
@ -5441,6 +5442,7 @@
FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */, FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */,
C331FF9A2558FA6B00070591 /* Values.swift in Sources */, C331FF9A2558FA6B00070591 /* Values.swift in Sources */,
FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */, FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */,
FD16AB5F2A1DD98F0083D849 /* ProfilePictureView.swift in Sources */,
C331FFE42558FB0000070591 /* SessionButton.swift in Sources */, C331FFE42558FB0000070591 /* SessionButton.swift in Sources */,
C331FFE92558FB0000070591 /* Separator.swift in Sources */, C331FFE92558FB0000070591 /* Separator.swift in Sources */,
FD71163228E2C42A00B47552 /* IconSize.swift in Sources */, FD71163228E2C42A00B47552 /* IconSize.swift in Sources */,
@ -5460,7 +5462,6 @@
C38EF247255B6D67007E1867 /* NSAttributedString+OWS.m in Sources */, C38EF247255B6D67007E1867 /* NSAttributedString+OWS.m in Sources */,
C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */, C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */,
C38EF3C5255B6DE7007E1867 /* OWSViewController+ImageEditor.swift in Sources */, C38EF3C5255B6DE7007E1867 /* OWSViewController+ImageEditor.swift in Sources */,
C38EF2A5255B6D93007E1867 /* Identicon+ObjC.swift in Sources */,
C38EF385255B6DD2007E1867 /* AttachmentTextToolbar.swift in Sources */, C38EF385255B6DD2007E1867 /* AttachmentTextToolbar.swift in Sources */,
C33FDD23255A582000E217F9 /* FeatureFlags.swift in Sources */, C33FDD23255A582000E217F9 /* FeatureFlags.swift in Sources */,
FD71161E28D9772700B47552 /* UIViewController+OWS.swift in Sources */, FD71161E28D9772700B47552 /* UIViewController+OWS.swift in Sources */,
@ -5480,12 +5481,10 @@
C3F0A530255C80BC007BE2A3 /* NoopNotificationsManager.swift in Sources */, C3F0A530255C80BC007BE2A3 /* NoopNotificationsManager.swift in Sources */,
C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */, C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */,
C38EF388255B6DD2007E1867 /* AttachmentApprovalViewController.swift in Sources */, C38EF388255B6DD2007E1867 /* AttachmentApprovalViewController.swift in Sources */,
C38EF2A7255B6D93007E1867 /* ProfilePictureView.swift in Sources */,
C38EF22C255B6D5D007E1867 /* OWSVideoPlayer.swift in Sources */, C38EF22C255B6D5D007E1867 /* OWSVideoPlayer.swift in Sources */,
C33FDC29255A581F00E217F9 /* ReachabilityManager.swift in Sources */, C33FDC29255A581F00E217F9 /* ReachabilityManager.swift in Sources */,
C38EF407255B6DF7007E1867 /* Toast.swift in Sources */, C38EF407255B6DF7007E1867 /* Toast.swift in Sources */,
C38EF38C255B6DD2007E1867 /* ApprovalRailCellView.swift in Sources */, C38EF38C255B6DD2007E1867 /* ApprovalRailCellView.swift in Sources */,
C38EF2A6255B6D93007E1867 /* PlaceholderIcon.swift in Sources */,
C33FDD92255A582000E217F9 /* SignalIOS.pb.swift in Sources */, C33FDD92255A582000E217F9 /* SignalIOS.pb.swift in Sources */,
C33FDC45255A581F00E217F9 /* AppVersion.m in Sources */, C33FDC45255A581F00E217F9 /* AppVersion.m in Sources */,
C38EF3C7255B6DE7007E1867 /* ImageEditorCanvasView.swift in Sources */, C38EF3C7255B6DE7007E1867 /* ImageEditorCanvasView.swift in Sources */,
@ -5859,6 +5858,7 @@
FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */, FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */,
FDA1E83B29A5F2D500C5C3BD /* SessionUtil+Shared.swift in Sources */, FDA1E83B29A5F2D500C5C3BD /* SessionUtil+Shared.swift in Sources */,
C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */, C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */,
FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */,
FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */, FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */,
B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */,
C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */, C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */,

View File

@ -6,6 +6,7 @@ import Combine
import CallKit import CallKit
import GRDB import GRDB
import WebRTC import WebRTC
import SessionUIKit
import SignalUtilitiesKit import SignalUtilitiesKit
import SessionMessagingKit import SessionMessagingKit
@ -157,7 +158,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
self.contactName = Profile.displayName(db, id: sessionId, threadVariant: .contact) self.contactName = Profile.displayName(db, id: sessionId, threadVariant: .contact)
self.profilePicture = avatarData self.profilePicture = avatarData
.map { UIImage(data: $0) } .map { UIImage(data: $0) }
.defaulting(to: Identicon.generatePlaceholderIcon(seed: sessionId, text: self.contactName, size: 300)) .defaulting(to: PlaceholderIcon.generate(seed: sessionId, text: self.contactName, size: 300))
self.animatedProfilePicture = avatarData self.animatedProfilePicture = avatarData
.map { data in .map { data in
switch data.guessedImageFormat { switch data.guessedImageFormat {

View File

@ -206,9 +206,9 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
return return
} }
guard CurrentAppContext().isMainAppAndActive else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
guard CurrentAppContext().isMainAppAndActive else { return }
guard let presentingVC = CurrentAppContext().frontmostViewController() else { guard let presentingVC = CurrentAppContext().frontmostViewController() else {
preconditionFailure() // FIXME: Handle more gracefully preconditionFailure() // FIXME: Handle more gracefully
} }

View File

@ -4,6 +4,7 @@ import UIKit
import WebRTC import WebRTC
import SessionUIKit import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SignalUtilitiesKit
final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate { final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
private static let swipeToOperateThreshold: CGFloat = 60 private static let swipeToOperateThreshold: CGFloat = 60
@ -20,14 +21,7 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
return result return result
}() }()
private lazy var profilePictureView: ProfilePictureView = { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView(size: .list)
let result = ProfilePictureView()
let size: CGFloat = 60
result.size = size
result.set(.width, to: size)
result.set(.height, to: size)
return result
}()
private lazy var displayNameLabel: UILabel = { private lazy var displayNameLabel: UILabel = {
let result = UILabel() let result = UILabel()

View File

@ -1243,8 +1243,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
switch threadData.threadVariant { switch threadData.threadVariant {
case .contact: case .contact:
let profilePictureView = ProfilePictureView() let profilePictureView = ProfilePictureView(size: .navigation)
profilePictureView.size = Values.verySmallProfilePictureSize
profilePictureView.update( profilePictureView.update(
publicKey: threadData.threadId, // Contact thread uses the contactId publicKey: threadData.threadId, // Contact thread uses the contactId
threadVariant: threadData.threadVariant, threadVariant: threadData.threadVariant,
@ -1252,9 +1251,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
profile: threadData.profile, profile: threadData.profile,
additionalProfile: nil additionalProfile: nil
) )
profilePictureView.customWidth = (44 - 16) // Width of the standard back button
profilePictureView.set(.width, to: (44 - 16)) // Width of the standard back button
profilePictureView.set(.height, to: Values.verySmallProfilePictureSize)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings)) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
profilePictureView.addGestureRecognizer(tapGestureRecognizer) profilePictureView.addGestureRecognizer(tapGestureRecognizer)

View File

@ -5,6 +5,7 @@ import Combine
import SessionUIKit import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SignalUtilitiesKit
final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, MentionSelectionViewDelegate { final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, MentionSelectionViewDelegate {
// MARK: - Variables // MARK: - Variables
@ -499,7 +500,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
func showMentionsUI(for candidates: [MentionInfo]) { func showMentionsUI(for candidates: [MentionInfo]) {
mentionsView.candidates = candidates mentionsView.candidates = candidates
let mentionCellHeight = (Values.smallProfilePictureSize + 2 * Values.smallSpacing) let mentionCellHeight = (ProfilePictureView.Size.message.viewSize + 2 * Values.smallSpacing)
mentionsViewHeightConstraint.constant = CGFloat(min(3, candidates.count)) * mentionCellHeight mentionsViewHeightConstraint.constant = CGFloat(min(3, candidates.count)) * mentionCellHeight
layoutIfNeeded() layoutIfNeeded()

View File

@ -116,9 +116,7 @@ private extension MentionSelectionView {
final class Cell: UITableViewCell { final class Cell: UITableViewCell {
// MARK: - UI // MARK: - UI
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView() private lazy var profilePictureView: ProfilePictureView = ProfilePictureView(size: .message)
private lazy var moderatorIconImageView: UIImageView = UIImageView(image: #imageLiteral(resourceName: "Crown"))
private lazy var displayNameLabel: UILabel = { private lazy var displayNameLabel: UILabel = {
let result: UILabel = UILabel() let result: UILabel = UILabel()
@ -160,18 +158,12 @@ private extension MentionSelectionView {
selectedBackgroundView.themeBackgroundColor = .highlighted(.settings_tabBackground) selectedBackgroundView.themeBackgroundColor = .highlighted(.settings_tabBackground)
self.selectedBackgroundView = selectedBackgroundView 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 // Main stack view
let mainStackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel ]) let mainStackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel ])
mainStackView.axis = .horizontal mainStackView.axis = .horizontal
mainStackView.alignment = .center mainStackView.alignment = .center
mainStackView.spacing = Values.mediumSpacing mainStackView.spacing = Values.mediumSpacing
mainStackView.set(.height, to: profilePictureViewSize) mainStackView.set(.height, to: ProfilePictureView.Size.message.viewSize)
contentView.addSubview(mainStackView) contentView.addSubview(mainStackView)
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing) mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing)
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.smallSpacing) mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.smallSpacing)
@ -179,13 +171,6 @@ private extension MentionSelectionView {
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: Values.smallSpacing) contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: Values.smallSpacing)
mainStackView.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing) mainStackView.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing)
// Moderator icon image view
moderatorIconImageView.set(.width, to: 20)
moderatorIconImageView.set(.height, to: 20)
contentView.addSubview(moderatorIconImageView)
moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureView, withInset: 1)
moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: 4.5)
// Separator // Separator
addSubview(separator) addSubview(separator)
separator.pin(.leading, to: .leading, of: self) separator.pin(.leading, to: .leading, of: self)
@ -204,12 +189,11 @@ private extension MentionSelectionView {
displayNameLabel.text = profile.displayName(for: threadVariant) displayNameLabel.text = profile.displayName(for: threadVariant)
profilePictureView.update( profilePictureView.update(
publicKey: profile.id, publicKey: profile.id,
threadVariant: .contact, threadVariant: .contact, // Always show the display picture in 'contact' mode
customImageData: nil, customImageData: nil,
profile: profile, profile: profile,
additionalProfile: nil profileIcon: (isUserModeratorOrAdmin ? .crown : .none)
) )
moderatorIconImageView.isHidden = !isUserModeratorOrAdmin
separator.isHidden = isLast separator.isHidden = isLast
} }
} }

View File

@ -22,7 +22,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
private lazy var authorLabelTopConstraint = authorLabel.pin(.top, to: .top, of: self) private lazy var authorLabelTopConstraint = authorLabel.pin(.top, to: .top, of: self)
private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0) private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0)
private lazy var profilePictureViewLeadingConstraint = profilePictureView.pin(.leading, to: .leading, of: self, withInset: VisibleMessageCell.groupThreadHSpacing) private lazy var profilePictureViewLeadingConstraint = profilePictureView.pin(.leading, to: .leading, of: self, withInset: VisibleMessageCell.groupThreadHSpacing)
private lazy var profilePictureViewWidthConstraint = profilePictureView.set(.width, to: Values.verySmallProfilePictureSize)
private lazy var contentViewLeadingConstraint1 = snContentView.pin(.leading, to: .trailing, of: profilePictureView, withInset: VisibleMessageCell.groupThreadHSpacing) private lazy var contentViewLeadingConstraint1 = snContentView.pin(.leading, to: .trailing, of: profilePictureView, withInset: VisibleMessageCell.groupThreadHSpacing)
private lazy var contentViewLeadingConstraint2 = snContentView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: VisibleMessageCell.gutterSize) private lazy var contentViewLeadingConstraint2 = snContentView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: VisibleMessageCell.gutterSize)
private lazy var contentViewTopConstraint = snContentView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing) private lazy var contentViewTopConstraint = snContentView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing)
@ -51,22 +50,13 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
private lazy var viewsToMoveForReply: [UIView] = [ private lazy var viewsToMoveForReply: [UIView] = [
snContentView, snContentView,
profilePictureView, profilePictureView,
moderatorIconImageView,
replyButton, replyButton,
timerView, timerView,
messageStatusImageView, messageStatusImageView,
reactionContainerView reactionContainerView
] ]
private lazy var profilePictureView: ProfilePictureView = { private lazy var profilePictureView: ProfilePictureView = ProfilePictureView(size: .message)
let result: ProfilePictureView = ProfilePictureView()
result.set(.height, to: Values.verySmallProfilePictureSize)
result.size = Values.verySmallProfilePictureSize
return result
}()
private lazy var moderatorIconImageView = UIImageView(image: #imageLiteral(resourceName: "Crown"))
lazy var bubbleBackgroundView: UIView = { lazy var bubbleBackgroundView: UIView = {
let result = UIView() let result = UIView()
@ -176,7 +166,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
private static let messageStatusImageViewSize: CGFloat = 12 private static let messageStatusImageViewSize: CGFloat = 12
private static let authorLabelBottomSpacing: CGFloat = 4 private static let authorLabelBottomSpacing: CGFloat = 4
private static let groupThreadHSpacing: CGFloat = 12 private static let groupThreadHSpacing: CGFloat = 12
private static let profilePictureSize = Values.verySmallProfilePictureSize
private static let authorLabelInset: CGFloat = 12 private static let authorLabelInset: CGFloat = 12
private static let replyButtonSize: CGFloat = 24 private static let replyButtonSize: CGFloat = 24
private static let maxBubbleTranslationX: CGFloat = 40 private static let maxBubbleTranslationX: CGFloat = 40
@ -186,7 +175,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
static let contactThreadHSpacing = Values.mediumSpacing static let contactThreadHSpacing = Values.mediumSpacing
static var gutterSize: CGFloat = { static var gutterSize: CGFloat = {
var result = groupThreadHSpacing + profilePictureSize + groupThreadHSpacing var result = groupThreadHSpacing + ProfilePictureView.Size.message.viewSize + groupThreadHSpacing
if UIDevice.current.isIPad { if UIDevice.current.isIPad {
result += 168 result += 168
@ -195,7 +184,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
return result return result
}() }()
static var leftGutterSize: CGFloat { groupThreadHSpacing + profilePictureSize + groupThreadHSpacing } static var leftGutterSize: CGFloat { groupThreadHSpacing + ProfilePictureView.Size.message.viewSize + groupThreadHSpacing }
// MARK: Direction & Position // MARK: Direction & Position
@ -214,21 +203,13 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
// Profile picture view // Profile picture view
addSubview(profilePictureView) addSubview(profilePictureView)
profilePictureViewLeadingConstraint.isActive = true profilePictureViewLeadingConstraint.isActive = true
profilePictureViewWidthConstraint.isActive = true
// Moderator icon image view
moderatorIconImageView.set(.width, to: 20)
moderatorIconImageView.set(.height, to: 20)
addSubview(moderatorIconImageView)
moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureView, withInset: 1)
moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: 4.5)
// Content view // Content view
addSubview(snContentView) addSubview(snContentView)
contentViewLeadingConstraint1.isActive = true contentViewLeadingConstraint1.isActive = true
contentViewTopConstraint.isActive = true contentViewTopConstraint.isActive = true
contentViewTrailingConstraint1.isActive = true contentViewTrailingConstraint1.isActive = true
snContentView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: -1) snContentView.pin(.bottom, to: .bottom, of: profilePictureView)
// Bubble background view // Bubble background view
bubbleBackgroundView.addSubview(bubbleView) bubbleBackgroundView.addSubview(bubbleView)
@ -319,16 +300,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
// Profile picture view (should always be handled as a standard 'contact' profile picture) // Profile picture view (should always be handled as a standard 'contact' profile picture)
profilePictureViewLeadingConstraint.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : 0) profilePictureViewLeadingConstraint.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : 0)
profilePictureViewWidthConstraint.constant = (isGroupThread ? VisibleMessageCell.profilePictureSize : 0)
profilePictureView.isHidden = (!cellViewModel.shouldShowProfile || cellViewModel.profile == nil) profilePictureView.isHidden = (!cellViewModel.shouldShowProfile || cellViewModel.profile == nil)
profilePictureView.update( profilePictureView.update(
publicKey: cellViewModel.authorId, publicKey: cellViewModel.authorId,
threadVariant: .contact, // Should always be '.contact' threadVariant: .contact, // Always show the display picture in 'contact' mode
customImageData: nil, customImageData: nil,
profile: cellViewModel.profile, profile: cellViewModel.profile,
additionalProfile: nil profileIcon: (cellViewModel.isSenderOpenGroupModerator ? .crown : .none)
) )
moderatorIconImageView.isHidden = (!cellViewModel.isSenderOpenGroupModerator || !cellViewModel.shouldShowProfile)
// Bubble view // Bubble view
contentViewLeadingConstraint1.isActive = ( contentViewLeadingConstraint1.isActive = (

View File

@ -244,12 +244,13 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
id: .avatar, id: .avatar,
accessory: .profile( accessory: .profile(
id: threadViewModel.id, id: threadViewModel.id,
size: .extraLarge, size: .hero,
threadVariant: threadVariant, threadVariant: threadVariant,
customImageData: threadViewModel.openGroupProfilePictureData, customImageData: threadViewModel.openGroupProfilePictureData,
profile: threadViewModel.profile, profile: threadViewModel.profile,
profileIcon: .none,
additionalProfile: threadViewModel.additionalProfile, additionalProfile: threadViewModel.additionalProfile,
cornerIcon: nil, additionalProfileIcon: .none,
accessibility: nil accessibility: nil
), ),
styling: SessionCell.StyleInfo( styling: SessionCell.StyleInfo(

View File

@ -484,12 +484,10 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
private func updateNavBarButtons() { private func updateNavBarButtons() {
// Profile picture view // Profile picture view
let profilePictureSize = Values.verySmallProfilePictureSize let profilePictureView = ProfilePictureView(size: .navigation)
let profilePictureView = ProfilePictureView()
profilePictureView.accessibilityIdentifier = "User settings" profilePictureView.accessibilityIdentifier = "User settings"
profilePictureView.accessibilityLabel = "User settings" profilePictureView.accessibilityLabel = "User settings"
profilePictureView.isAccessibilityElement = true profilePictureView.isAccessibilityElement = true
profilePictureView.size = profilePictureSize
profilePictureView.update( profilePictureView.update(
publicKey: getUserHexEncodedPublicKey(), publicKey: getUserHexEncodedPublicKey(),
threadVariant: .contact, threadVariant: .contact,
@ -497,8 +495,6 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
profile: Profile.fetchOrCreateCurrentUser(), profile: Profile.fetchOrCreateCurrentUser(),
additionalProfile: nil additionalProfile: nil
) )
profilePictureView.set(.width, to: profilePictureSize)
profilePictureView.set(.height, to: profilePictureSize)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings)) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
profilePictureView.addGestureRecognizer(tapGestureRecognizer) profilePictureView.addGestureRecognizer(tapGestureRecognizer)

View File

@ -2,6 +2,7 @@
import UIKit import UIKit
import SessionUIKit import SessionUIKit
import SignalUtilitiesKit
class MessageRequestsCell: UITableViewCell { class MessageRequestsCell: UITableViewCell {
static let reuseIdentifier = "MessageRequestsCell" static let reuseIdentifier = "MessageRequestsCell"
@ -29,7 +30,7 @@ class MessageRequestsCell: UITableViewCell {
result.translatesAutoresizingMaskIntoConstraints = false result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true result.clipsToBounds = true
result.themeBackgroundColor = .conversationButton_unreadBubbleBackground result.themeBackgroundColor = .conversationButton_unreadBubbleBackground
result.layer.cornerRadius = (Values.mediumProfilePictureSize / 2) result.layer.cornerRadius = (ProfilePictureView.Size.list.viewSize / 2)
return result return result
}() }()
@ -100,8 +101,8 @@ class MessageRequestsCell: UITableViewCell {
constant: (Values.accentLineThickness + Values.mediumSpacing) constant: (Values.accentLineThickness + Values.mediumSpacing)
), ),
iconContainerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), iconContainerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
iconContainerView.widthAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize), iconContainerView.widthAnchor.constraint(equalToConstant: ProfilePictureView.Size.list.viewSize),
iconContainerView.heightAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize), iconContainerView.heightAnchor.constraint(equalToConstant: ProfilePictureView.Size.list.viewSize),
iconImageView.centerXAnchor.constraint(equalTo: iconContainerView.centerXAnchor), iconImageView.centerXAnchor.constraint(equalTo: iconContainerView.centerXAnchor),
iconImageView.centerYAnchor.constraint(equalTo: iconContainerView.centerYAnchor), iconImageView.centerYAnchor.constraint(equalTo: iconContainerView.centerYAnchor),

View File

@ -31,7 +31,7 @@ import SignalCoreKit
let srcImage: UIImage let srcImage: UIImage
let successCompletion: ((UIImage) -> Void) let successCompletion: ((Data) -> Void)
var imageView: UIView! var imageView: UIView!
@ -78,7 +78,7 @@ import SignalCoreKit
notImplemented() notImplemented()
} }
@objc required init(srcImage: UIImage, successCompletion : @escaping (UIImage) -> Void) { @objc required init(srcImage: UIImage, successCompletion : @escaping (Data) -> Void) {
// normalized() can be slightly expensive but in practice this is fine. // normalized() can be slightly expensive but in practice this is fine.
self.srcImage = srcImage.normalized() self.srcImage = srcImage.normalized()
self.successCompletion = successCompletion self.successCompletion = successCompletion
@ -486,10 +486,9 @@ import SignalCoreKit
@objc func donePressed(sender: UIButton) { @objc func donePressed(sender: UIButton) {
let successCompletion = self.successCompletion let successCompletion = self.successCompletion
dismiss(animated: true, completion: { dismiss(animated: true, completion: {
guard let dstImage = self.generateDstImage() else { guard let dstImageData: Data = self.generateDstImageData() else { return }
return
} successCompletion(dstImageData)
successCompletion(dstImage)
}) })
} }
@ -516,4 +515,8 @@ import SignalCoreKit
UIGraphicsEndImageContext() UIGraphicsEndImageContext()
return scaledImage return scaledImage
} }
func generateDstImageData() -> Data? {
return generateDstImage().map { $0.pngData() }
}
} }

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -627,7 +627,7 @@
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self"; "hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?"; "hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"update_profile_modal_title" = "Set Display Picture"; "update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload"; "update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove"; "update_profile_modal_remove" = "Remove";
"update_profile_modal_remove_error_title" = "Unable to remove avatar image"; "update_profile_modal_remove_error_title" = "Unable to remove avatar image";
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded"; "update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";

View File

@ -4,19 +4,16 @@ import Foundation
class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigationControllerDelegate { class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigationControllerDelegate {
private let onTransition: (UIViewController, TransitionType) -> Void private let onTransition: (UIViewController, TransitionType) -> Void
private let onImagePicked: (UIImage) -> Void private let onImageDataPicked: (Data) -> Void
private let onImageFilePicked: (String) -> Void
// MARK: - Initialization // MARK: - Initialization
init( init(
onTransition: @escaping (UIViewController, TransitionType) -> Void, onTransition: @escaping (UIViewController, TransitionType) -> Void,
onImagePicked: @escaping (UIImage) -> Void, onImageDataPicked: @escaping (Data) -> Void
onImageFilePicked: @escaping (String) -> Void
) { ) {
self.onTransition = onTransition self.onTransition = onTransition
self.onImagePicked = onImagePicked self.onImageDataPicked = onImageDataPicked
self.onImageFilePicked = onImageFilePicked
} }
// MARK: - UIImagePickerControllerDelegate // MARK: - UIImagePickerControllerDelegate
@ -45,15 +42,17 @@ class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigati
else { else {
let viewController: CropScaleImageViewController = CropScaleImageViewController( let viewController: CropScaleImageViewController = CropScaleImageViewController(
srcImage: rawAvatar, srcImage: rawAvatar,
successCompletion: { resultImage in successCompletion: { resultImageData in
self?.onImagePicked(resultImage) self?.onImageDataPicked(resultImageData)
} }
) )
self?.onTransition(viewController, .present) self?.onTransition(viewController, .present)
return return
} }
self?.onImageFilePicked(imageUrl.path) guard let imageData: Data = try? Data(contentsOf: URL(fileURLWithPath: imageUrl.path)) else { return }
self?.onImageDataPicked(imageData)
} }
} }
} }

View File

@ -71,20 +71,12 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
private let userSessionId: String private let userSessionId: String
private lazy var imagePickerHandler: ImagePickerHandler = ImagePickerHandler( private lazy var imagePickerHandler: ImagePickerHandler = ImagePickerHandler(
onTransition: { [weak self] in self?.transitionToScreen($0, transitionType: $1) }, onTransition: { [weak self] in self?.transitionToScreen($0, transitionType: $1) },
onImagePicked: { [weak self] resultImage in onImageDataPicked: { [weak self] resultImageData in
guard let oldDisplayName: String = self?.oldDisplayName else { return } guard let oldDisplayName: String = self?.oldDisplayName else { return }
self?.updatedProfilePictureSelected( self?.updatedProfilePictureSelected(
name: oldDisplayName, name: oldDisplayName,
avatarUpdate: .uploadImage(resultImage) avatarUpdate: .uploadImageData(resultImageData)
)
},
onImageFilePicked: { [weak self] resultImagePath in
guard let oldDisplayName: String = self?.oldDisplayName else { return }
self?.updatedProfilePictureSelected(
name: oldDisplayName,
avatarUpdate: .uploadFilePath(resultImagePath)
) )
} }
) )
@ -249,7 +241,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
id: .avatar, id: .avatar,
accessory: .profile( accessory: .profile(
id: profile.id, id: profile.id,
size: .extraLarge, size: .hero,
profile: profile profile: profile
), ),
styling: SessionCell.StyleInfo( styling: SessionCell.StyleInfo(
@ -486,21 +478,14 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
private func updateProfilePicture(currentFileName: String?) { private func updateProfilePicture(currentFileName: String?) {
let existingDisplayName: String = self.oldDisplayName let existingDisplayName: String = self.oldDisplayName
let existingImageData: Data? = ProfileManager
let existingImageData: (image: UIImage?, animatedImage: YYImage?)? = currentFileName .profileAvatar(id: self.userSessionId)
.map { ProfileManager.loadProfileData(with: $0) }
.map { imageData in
switch imageData.guessedImageFormat {
case .gif, .webp: return (nil, YYImage(data: imageData))
default: return (UIImage(data: imageData), nil)
}
}
let editProfilePictureModalInfo: ConfirmationModal.Info = ConfirmationModal.Info( let editProfilePictureModalInfo: ConfirmationModal.Info = ConfirmationModal.Info(
title: "update_profile_modal_title".localized(), title: "update_profile_modal_title".localized(),
body: .image( body: .image(
placeholder: UIImage(named: "profile_placeholder"), placeholderData: UIImage(named: "profile_placeholder")?.pngData(),
value: existingImageData?.image, valueData: existingImageData,
animatedValue: existingImageData?.animatedImage, icon: .rightPlus,
style: .circular, style: .circular,
accessibility: Accessibility( accessibility: Accessibility(
identifier: "Image picker", identifier: "Image picker",
@ -508,7 +493,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
), ),
onClick: { [weak self] in self?.showPhotoLibraryForAvatar() } onClick: { [weak self] in self?.showPhotoLibraryForAvatar() }
), ),
confirmTitle: "update_profile_modal_upload".localized(), confirmTitle: "update_profile_modal_save".localized(),
confirmEnabled: false, confirmEnabled: false,
cancelTitle: "update_profile_modal_remove".localized(), cancelTitle: "update_profile_modal_remove".localized(),
cancelEnabled: (existingImageData != nil), cancelEnabled: (existingImageData != nil),
@ -533,26 +518,21 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
self.editProfilePictureModal = modal self.editProfilePictureModal = modal
self.transitionToScreen(modal, transitionType: .present) self.transitionToScreen(modal, transitionType: .present)
} }
fileprivate func updatedProfilePictureSelected(name: String, avatarUpdate: ProfileManager.AvatarUpdate) { fileprivate func updatedProfilePictureSelected(name: String, avatarUpdate: ProfileManager.AvatarUpdate) {
guard let info: ConfirmationModal.Info = self.editProfilePictureModalInfo else { return } guard let info: ConfirmationModal.Info = self.editProfilePictureModalInfo else { return }
self.editProfilePictureModal?.updateContent( self.editProfilePictureModal?.updateContent(
with: info.with( with: info.with(
body: .image( body: .image(
placeholder: UIImage(named: "profile_placeholder"), placeholderData: UIImage(named: "profile_placeholder")?.pngData(),
value: { valueData: {
switch avatarUpdate { switch avatarUpdate {
case .uploadImage(let image): return image case .uploadImageData(let imageData): return imageData
default: return nil
}
}(),
animatedValue: {
switch avatarUpdate {
case .uploadFilePath(let filePath): return YYImage(contentsOfFile: filePath)
default: return nil default: return nil
} }
}(), }(),
icon: .rightPlus,
style: .circular, style: .circular,
accessibility: Accessibility( accessibility: Accessibility(
identifier: "Image picker", identifier: "Image picker",

View File

@ -8,7 +8,7 @@ import SignalUtilitiesKit
class BlockedContactCell: UITableViewCell { class BlockedContactCell: UITableViewCell {
// MARK: - Components // MARK: - Components
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView() private lazy var profilePictureView: ProfilePictureView = ProfilePictureView(size: .list)
private let selectionView: RadioButton = { private let selectionView: RadioButton = {
let result: RadioButton = RadioButton(size: .medium) let result: RadioButton = RadioButton(size: .medium)
@ -61,9 +61,6 @@ class BlockedContactCell: UITableViewCell {
.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -Values.mediumSpacing) .constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -Values.mediumSpacing)
.isActive = true .isActive = true
profilePictureView.pin(.left, to: .left, of: contentView, withInset: Values.veryLargeSpacing) profilePictureView.pin(.left, to: .left, of: contentView, withInset: Values.veryLargeSpacing)
profilePictureView.set(.width, to: Values.mediumProfilePictureSize)
profilePictureView.set(.height, to: Values.mediumProfilePictureSize)
profilePictureView.size = Values.mediumProfilePictureSize
selectionView.center(.vertical, in: contentView) selectionView.center(.vertical, in: contentView)
selectionView.topAnchor selectionView.topAnchor

View File

@ -19,7 +19,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC
private let accentLineView: UIView = UIView() private let accentLineView: UIView = UIView()
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView() private lazy var profilePictureView: ProfilePictureView = ProfilePictureView(size: .list)
private lazy var displayNameLabel: UILabel = { private lazy var displayNameLabel: UILabel = {
let result: UILabel = UILabel() let result: UILabel = UILabel()
@ -204,12 +204,6 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC
accentLineView.set(.width, to: Values.accentLineThickness) accentLineView.set(.width, to: Values.accentLineThickness)
accentLineView.set(.height, to: cellHeight) accentLineView.set(.height, to: cellHeight)
// Profile picture view
let profilePictureViewSize = Values.mediumProfilePictureSize
profilePictureView.set(.width, to: profilePictureViewSize)
profilePictureView.set(.height, to: profilePictureViewSize)
profilePictureView.size = profilePictureViewSize
// Unread count view // Unread count view
unreadCountView.addSubview(unreadCountLabel) unreadCountView.addSubview(unreadCountLabel)
unreadCountLabel.setCompressionResistanceHigh() unreadCountLabel.setCompressionResistanceHigh()
@ -721,7 +715,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC
// This method determines if the content is probably too long and returns the truncated or untruncated // This method determines if the content is probably too long and returns the truncated or untruncated
// content accordingly // content accordingly
func truncatingIfNeeded(approxWidth: CGFloat, content: NSAttributedString) -> NSAttributedString { func truncatingIfNeeded(approxWidth: CGFloat, content: NSAttributedString) -> NSAttributedString {
let approxFullWidth: CGFloat = (approxWidth + profilePictureView.size + (Values.mediumSpacing * 3)) let approxFullWidth: CGFloat = (approxWidth + profilePictureView.size.viewSize + (Values.mediumSpacing * 3))
guard ((bounds.width - approxFullWidth) < 0) else { return content } guard ((bounds.width - approxFullWidth) < 0) else { return content }

View File

@ -43,12 +43,13 @@ extension SessionCell {
) )
case profile( case profile(
id: String, id: String,
size: IconSize, size: ProfilePictureView.Size,
threadVariant: SessionThread.Variant, threadVariant: SessionThread.Variant,
customImageData: Data?, customImageData: Data?,
profile: Profile?, profile: Profile?,
profileIcon: ProfilePictureView.ProfileIcon,
additionalProfile: Profile?, additionalProfile: Profile?,
cornerIcon: UIImage?, additionalProfileIcon: ProfilePictureView.ProfileIcon,
accessibility: Accessibility? accessibility: Accessibility?
) )
@ -127,8 +128,9 @@ extension SessionCell {
let threadVariant, let threadVariant,
let customImageData, let customImageData,
let profile, let profile,
let profileIcon,
let additionalProfile, let additionalProfile,
let cornerIcon, let additionalProfileIcon,
let accessibility let accessibility
): ):
profileId.hash(into: &hasher) profileId.hash(into: &hasher)
@ -136,8 +138,9 @@ extension SessionCell {
threadVariant.hash(into: &hasher) threadVariant.hash(into: &hasher)
customImageData.hash(into: &hasher) customImageData.hash(into: &hasher)
profile.hash(into: &hasher) profile.hash(into: &hasher)
profileIcon.hash(into: &hasher)
additionalProfile.hash(into: &hasher) additionalProfile.hash(into: &hasher)
cornerIcon.hash(into: &hasher) additionalProfileIcon.hash(into: &hasher)
accessibility.hash(into: &hasher) accessibility.hash(into: &hasher)
case .search(let placeholder, let accessibility, _): case .search(let placeholder, let accessibility, _):
@ -204,20 +207,22 @@ extension SessionCell {
let lhsProfileId, let lhsProfileId,
let lhsSize, let lhsSize,
let lhsThreadVariant, let lhsThreadVariant,
let lhsProfile,
let lhsAdditionalProfile,
let lhsCustomImageData, let lhsCustomImageData,
let lhsCornerIcon, let lhsProfile,
let lhsProfileIcon,
let lhsAdditionalProfile,
let lhsAdditionalProfileIcon,
let lhsAccessibility let lhsAccessibility
), ),
.profile( .profile(
let rhsProfileId, let rhsProfileId,
let rhsSize, let rhsSize,
let rhsThreadVariant, let rhsThreadVariant,
let rhsProfile,
let rhsAdditionalProfile,
let rhsCustomImageData, let rhsCustomImageData,
let rhsCornerIcon, let rhsProfile,
let rhsProfileIcon,
let rhsAdditionalProfile,
let rhsAdditionalProfileIcon,
let rhsAccessibility let rhsAccessibility
) )
): ):
@ -225,10 +230,11 @@ extension SessionCell {
lhsProfileId == rhsProfileId && lhsProfileId == rhsProfileId &&
lhsSize == rhsSize && lhsSize == rhsSize &&
lhsThreadVariant == rhsThreadVariant && lhsThreadVariant == rhsThreadVariant &&
lhsProfile == rhsProfile &&
lhsAdditionalProfile == rhsAdditionalProfile &&
lhsCustomImageData == rhsCustomImageData && lhsCustomImageData == rhsCustomImageData &&
lhsCornerIcon == rhsCornerIcon && lhsProfile == rhsProfile &&
lhsProfileIcon == rhsProfileIcon &&
lhsAdditionalProfile == rhsAdditionalProfile &&
lhsAdditionalProfileIcon == rhsAdditionalProfileIcon &&
lhsAccessibility == rhsAccessibility lhsAccessibility == rhsAccessibility
) )
@ -346,25 +352,27 @@ extension SessionCell.Accessory {
public static func profile(id: String, profile: Profile?) -> SessionCell.Accessory { public static func profile(id: String, profile: Profile?) -> SessionCell.Accessory {
return .profile( return .profile(
id: id, id: id,
size: .veryLarge, size: .list,
threadVariant: .contact, threadVariant: .contact,
customImageData: nil, customImageData: nil,
profile: profile, profile: profile,
profileIcon: .none,
additionalProfile: nil, additionalProfile: nil,
cornerIcon: nil, additionalProfileIcon: .none,
accessibility: nil accessibility: nil
) )
} }
public static func profile(id: String, size: IconSize, profile: Profile?) -> SessionCell.Accessory { public static func profile(id: String, size: ProfilePictureView.Size, profile: Profile?) -> SessionCell.Accessory {
return .profile( return .profile(
id: id, id: id,
size: size, size: size,
threadVariant: .contact, threadVariant: .contact,
customImageData: nil, customImageData: nil,
profile: profile, profile: profile,
profileIcon: .none,
additionalProfile: nil, additionalProfile: nil,
cornerIcon: nil, additionalProfileIcon: .none,
accessibility: nil accessibility: nil
) )
} }

View File

@ -61,8 +61,6 @@ extension SessionCell {
profilePictureView.pin(.top, to: .top, of: self), profilePictureView.pin(.top, to: .top, of: self),
profilePictureView.pin(.bottom, to: .bottom, of: self) profilePictureView.pin(.bottom, to: .bottom, of: self)
] ]
private lazy var profilePictureViewWidthConstraint: NSLayoutConstraint = profilePictureView.set(.width, to: 0)
private lazy var profilePictureViewHeightConstraint: NSLayoutConstraint = profilePictureView.set(.height, to: 0)
private lazy var searchBarConstraints: [NSLayoutConstraint] = [ private lazy var searchBarConstraints: [NSLayoutConstraint] = [
searchBar.pin(.top, to: .top, of: self), searchBar.pin(.top, to: .top, of: self),
searchBar.pin(.leading, to: .leading, of: self, withInset: -8), // Removing default inset searchBar.pin(.leading, to: .leading, of: self, withInset: -8), // Removing default inset
@ -164,32 +162,13 @@ extension SessionCell {
}() }()
private lazy var profilePictureView: ProfilePictureView = { private lazy var profilePictureView: ProfilePictureView = {
let result: ProfilePictureView = ProfilePictureView() let result: ProfilePictureView = ProfilePictureView(size: .list)
result.translatesAutoresizingMaskIntoConstraints = false result.translatesAutoresizingMaskIntoConstraints = false
result.isHidden = true result.isHidden = true
return result return result
}() }()
private lazy var profileIconContainerView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.themeBackgroundColor = .primary
result.isHidden = true
result.set(.width, to: 26)
result.set(.height, to: 26)
result.layer.cornerRadius = (26 / 2)
return result
}()
private lazy var profileIconImageView: UIImageView = {
let result: UIImageView = UIImageView()
result.translatesAutoresizingMaskIntoConstraints = false
return result
}()
private lazy var searchBar: UISearchBar = { private lazy var searchBar: UISearchBar = {
let result: ContactsSearchBar = ContactsSearchBar() let result: ContactsSearchBar = ContactsSearchBar()
result.themeTintColor = .textPrimary result.themeTintColor = .textPrimary
@ -233,7 +212,6 @@ extension SessionCell {
addSubview(radioBorderView) addSubview(radioBorderView)
addSubview(highlightingBackgroundLabel) addSubview(highlightingBackgroundLabel)
addSubview(profilePictureView) addSubview(profilePictureView)
addSubview(profileIconContainerView)
addSubview(button) addSubview(button)
addSubview(searchBar) addSubview(searchBar)
@ -242,12 +220,6 @@ extension SessionCell {
radioBorderView.addSubview(radioView) radioBorderView.addSubview(radioView)
radioView.center(in: radioBorderView) radioView.center(in: radioBorderView)
profileIconContainerView.addSubview(profileIconImageView)
profileIconContainerView.pin(.bottom, to: .bottom, of: profilePictureView)
profileIconContainerView.pin(.trailing, to: .trailing, of: profilePictureView)
profileIconImageView.pin(to: profileIconContainerView, withInset: Values.verySmallSpacing)
} }
// MARK: - Content // MARK: - Content
@ -277,7 +249,6 @@ extension SessionCell {
radioView.isHidden = true radioView.isHidden = true
highlightingBackgroundLabel.isHidden = true highlightingBackgroundLabel.isHidden = true
profilePictureView.isHidden = true profilePictureView.isHidden = true
profileIconContainerView.isHidden = true
button.isHidden = true button.isHidden = true
searchBar.isHidden = true searchBar.isHidden = true
@ -300,8 +271,6 @@ extension SessionCell {
highlightingBackgroundLabelConstraints.forEach { $0.isActive = false } highlightingBackgroundLabelConstraints.forEach { $0.isActive = false }
profilePictureViewLeadingConstraint.isActive = false profilePictureViewLeadingConstraint.isActive = false
profilePictureViewTrailingConstraint.isActive = false profilePictureViewTrailingConstraint.isActive = false
profilePictureViewWidthConstraint.isActive = false
profilePictureViewHeightConstraint.isActive = false
profilePictureViewConstraints.forEach { $0.isActive = false } profilePictureViewConstraints.forEach { $0.isActive = false }
searchBarConstraints.forEach { $0.isActive = false } searchBarConstraints.forEach { $0.isActive = false }
buttonConstraints.forEach { $0.isActive = false } buttonConstraints.forEach { $0.isActive = false }
@ -465,46 +434,34 @@ extension SessionCell {
let threadVariant, let threadVariant,
let customImageData, let customImageData,
let profile, let profile,
let profileIcon,
let additionalProfile, let additionalProfile,
let cornerIcon, let additionalProfileIcon,
let accessibility let accessibility
): ):
// Note: We MUST set the 'size' property before triggering the 'update' // Note: We MUST set the 'size' property before triggering the 'update'
// function or the profile picture won't layout correctly // function or the profile picture won't layout correctly
switch profileSize {
case .fit:
profilePictureView.size = IconSize.large.size
profilePictureViewWidthConstraint.constant = IconSize.large.size
profilePictureViewHeightConstraint.constant = IconSize.large.size
default:
profilePictureView.size = profileSize.size
profilePictureViewWidthConstraint.constant = profileSize.size
profilePictureViewHeightConstraint.constant = profileSize.size
}
profilePictureView.accessibilityIdentifier = accessibility?.identifier profilePictureView.accessibilityIdentifier = accessibility?.identifier
profilePictureView.accessibilityLabel = accessibility?.label profilePictureView.accessibilityLabel = accessibility?.label
profilePictureView.isAccessibilityElement = (accessibility != nil) profilePictureView.isAccessibilityElement = (accessibility != nil)
profilePictureView.size = profileSize
profilePictureView.update( profilePictureView.update(
publicKey: profileId, publicKey: profileId,
threadVariant: threadVariant, threadVariant: threadVariant,
customImageData: customImageData, customImageData: customImageData,
profile: profile, profile: profile,
additionalProfile: additionalProfile profileIcon: profileIcon,
additionalProfile: additionalProfile,
additionalProfileIcon: additionalProfileIcon
) )
profilePictureView.isHidden = false profilePictureView.isHidden = false
profileIconContainerView.isHidden = (cornerIcon == nil)
profileIconImageView.image = cornerIcon
fixedWidthConstraint.constant = profilePictureViewWidthConstraint.constant fixedWidthConstraint.constant = profileSize.viewSize
fixedWidthConstraint.isActive = true fixedWidthConstraint.isActive = true
profilePictureViewLeadingConstraint.constant = (profilePictureView.size > AccessoryView.minWidth ? 0 : Values.smallSpacing) profilePictureViewLeadingConstraint.constant = (profileSize.viewSize > AccessoryView.minWidth ? 0 : Values.smallSpacing)
profilePictureViewTrailingConstraint.constant = (profilePictureView.size > AccessoryView.minWidth ? 0 : -Values.smallSpacing) profilePictureViewTrailingConstraint.constant = (profileSize.viewSize > AccessoryView.minWidth ? 0 : -Values.smallSpacing)
profilePictureViewLeadingConstraint.isActive = true profilePictureViewLeadingConstraint.isActive = true
profilePictureViewTrailingConstraint.isActive = true profilePictureViewTrailingConstraint.isActive = true
profilePictureViewWidthConstraint.isActive = true
profilePictureViewHeightConstraint.isActive = true
profilePictureViewConstraints.forEach { $0.isActive = true } profilePictureViewConstraints.forEach { $0.isActive = true }
case .search(let placeholder, let accessibility, let searchTermChanged): case .search(let placeholder, let accessibility, let searchTermChanged):

View File

@ -43,13 +43,13 @@ public enum UpdateProfilePictureJob: JobExecutor {
// Note: The user defaults flag is updated in ProfileManager // Note: The user defaults flag is updated in ProfileManager
let profile: Profile = Profile.fetchOrCreateCurrentUser() let profile: Profile = Profile.fetchOrCreateCurrentUser()
let profileFilePath: String? = profile.profilePictureFileName let profilePictureData: Data? = profile.profilePictureFileName
.map { ProfileManager.profileAvatarFilepath(filename: $0) } .map { ProfileManager.loadProfileData(with: $0) }
ProfileManager.updateLocal( ProfileManager.updateLocal(
queue: queue, queue: queue,
profileName: profile.name, profileName: profile.name,
avatarUpdate: (profileFilePath.map { .uploadFilePath($0) } ?? .none), avatarUpdate: (profilePictureData.map { .uploadImageData($0) } ?? .none),
success: { db in success: { db in
// Need to call the 'success' closure asynchronously on the queue to prevent a reentrancy // Need to call the 'success' closure asynchronously on the queue to prevent a reentrancy
// issue as it will write to the database and this closure is already called within // issue as it will write to the database and this closure is already called within

View File

@ -11,23 +11,8 @@ public struct ProfileManager {
public enum AvatarUpdate { public enum AvatarUpdate {
case none case none
case remove case remove
case uploadImage(UIImage) case uploadImageData(Data)
case uploadFilePath(String)
case updateTo(url: String, key: Data, fileName: String?) case updateTo(url: String, key: Data, fileName: String?)
var image: UIImage? {
switch self {
case .uploadImage(let image): return image
default: return nil
}
}
var filePath: String? {
switch self {
case .uploadFilePath(let filePath): return filePath
default: return nil
}
}
} }
// The max bytes for a user's profile name, encoded in UTF8. // The max bytes for a user's profile name, encoded in UTF8.
@ -339,11 +324,10 @@ public struct ProfileManager {
try success?(db) try success?(db)
} }
case .uploadFilePath, .uploadImage: case .uploadImageData(let data):
prepareAndUploadAvatarImage( prepareAndUploadAvatarImage(
queue: queue, queue: queue,
image: avatarUpdate.image, imageData: data,
imageFilePath: avatarUpdate.filePath,
success: { downloadUrl, fileName, newProfileKey in success: { downloadUrl, fileName, newProfileKey in
Storage.shared.writeAsync { db in Storage.shared.writeAsync { db in
try ProfileManager.updateProfileIfNeeded( try ProfileManager.updateProfileIfNeeded(
@ -365,8 +349,7 @@ public struct ProfileManager {
private static func prepareAndUploadAvatarImage( private static func prepareAndUploadAvatarImage(
queue: DispatchQueue, queue: DispatchQueue,
image: UIImage?, imageData: Data,
imageFilePath: String?,
success: @escaping ((downloadUrl: String, fileName: String, profileKey: Data)) -> (), success: @escaping ((downloadUrl: String, fileName: String, profileKey: Data)) -> (),
failure: ((ProfileManagerError) -> ())? = nil failure: ((ProfileManagerError) -> ())? = nil
) { ) {
@ -374,31 +357,34 @@ public struct ProfileManager {
// If the profile avatar was updated or removed then encrypt with a new profile key // If the profile avatar was updated or removed then encrypt with a new profile key
// to ensure that other users know that our profile picture was updated // to ensure that other users know that our profile picture was updated
let newProfileKey: Data let newProfileKey: Data
let avatarImageData: Data? let avatarImageData: Data
let fileExtension: String
do { do {
newProfileKey = try Randomness.generateRandomBytes(numberBytes: ProfileManager.avatarAES256KeyByteLength) let guessedFormat: ImageFormat = imageData.guessedImageFormat
avatarImageData = try { avatarImageData = try {
guard var image: UIImage = image else { switch guessedFormat {
guard let imageFilePath: String = imageFilePath else { case .gif, .webp:
throw ProfileManagerError.invalidCall // Animated images can't be resized so if the data is too large we should error
} guard imageData.count <= maxAvatarBytes else {
// Our avatar dimensions are so small that it's incredibly unlikely we wouldn't
let data: Data = try Data(contentsOf: URL(fileURLWithPath: imageFilePath)) // be able to fit our profile photo (eg. generating pure noise at our resolution
// compresses to ~200k)
guard data.count <= maxAvatarBytes else { SNLog("Animated profile avatar was too large.")
// Our avatar dimensions are so small that it's incredibly unlikely we wouldn't SNLog("Updating service with profile failed.")
// be able to fit our profile photo (eg. generating pure noise at our resolution throw ProfileManagerError.avatarUploadMaxFileSizeExceeded
// compresses to ~200k) }
SNLog("Animated profile avatar was too large.")
SNLog("Updating service with profile failed.") return imageData
throw ProfileManagerError.avatarUploadMaxFileSizeExceeded
} default: break
return data
} }
// Process the image to ensure it meets our standards for size and compress it to
// standardise the formwat and remove any metadata
guard var image: UIImage = UIImage(data: imageData) else { throw ProfileManagerError.invalidCall }
if image.size.width != maxAvatarDiameter || image.size.height != maxAvatarDiameter { if image.size.width != maxAvatarDiameter || image.size.height != maxAvatarDiameter {
// To help ensure the user is being shown the same cropping of their avatar as // To help ensure the user is being shown the same cropping of their avatar as
// everyone else will see, we want to be sure that the image was resized before this point. // everyone else will see, we want to be sure that the image was resized before this point.
@ -422,19 +408,19 @@ public struct ProfileManager {
return data return data
}() }()
newProfileKey = try Randomness.generateRandomBytes(numberBytes: ProfileManager.avatarAES256KeyByteLength)
fileExtension = {
switch guessedFormat {
case .gif: return "gif"
case .webp: return "webp"
default: return "jpg"
}
}()
} }
catch { // TODO: Test that this actually works
if let profileManagerError: ProfileManagerError = error as? ProfileManagerError { catch let error as ProfileManagerError { return (failure?(error) ?? {}()) }
failure?(profileManagerError) catch { return (failure?(ProfileManagerError.invalidCall) ?? {}()) }
}
return
}
// If we have no image then we should succeed (database changes happen in the callback)
guard let data: Data = avatarImageData else {
failure?(ProfileManagerError.invalidCall)
return
}
// If we have a new avatar image, we must first: // If we have a new avatar image, we must first:
// //
@ -444,16 +430,11 @@ public struct ProfileManager {
// * Send asset service info to Signal Service // * Send asset service info to Signal Service
OWSLogger.verbose("Updating local profile on service with new avatar.") OWSLogger.verbose("Updating local profile on service with new avatar.")
let fileName: String = UUID().uuidString let fileName: String = UUID().uuidString.appendingFileExtension(fileExtension)
.appendingFileExtension(
imageFilePath
.map { URL(fileURLWithPath: $0).pathExtension }
.defaulting(to: "jpg")
)
let filePath: String = ProfileManager.profileAvatarFilepath(filename: fileName) let filePath: String = ProfileManager.profileAvatarFilepath(filename: fileName)
// Write the avatar to disk // Write the avatar to disk
do { try data.write(to: URL(fileURLWithPath: filePath), options: [.atomic]) } do { try avatarImageData.write(to: URL(fileURLWithPath: filePath), options: [.atomic]) }
catch { catch {
SNLog("Updating service with profile failed.") SNLog("Updating service with profile failed.")
failure?(.avatarWriteFailed) failure?(.avatarWriteFailed)
@ -461,7 +442,7 @@ public struct ProfileManager {
} }
// Encrypt the avatar for upload // Encrypt the avatar for upload
guard let encryptedAvatarData: Data = encryptData(data: data, key: newProfileKey) else { guard let encryptedAvatarData: Data = encryptData(data: avatarImageData, key: newProfileKey) else {
SNLog("Updating service with profile failed.") SNLog("Updating service with profile failed.")
failure?(.avatarEncryptionFailed) failure?(.avatarEncryptionFailed)
return return
@ -489,7 +470,7 @@ public struct ProfileManager {
let downloadUrl: String = "\(FileServerAPI.server)/file/\(fileUploadResponse.id)" let downloadUrl: String = "\(FileServerAPI.server)/file/\(fileUploadResponse.id)"
// Update the cached avatar image value // Update the cached avatar image value
profileAvatarCache.mutate { $0[fileName] = data } profileAvatarCache.mutate { $0[fileName] = avatarImageData }
UserDefaults.standard[.lastProfilePictureUpload] = Date() UserDefaults.standard[.lastProfilePictureUpload] = Date()
SNLog("Successfully uploaded avatar image.") SNLog("Successfully uploaded avatar image.")
@ -545,7 +526,7 @@ public struct ProfileManager {
if shouldUpdateAvatar { if shouldUpdateAvatar {
switch avatarUpdate { switch avatarUpdate {
case .none: break case .none: break
case .uploadImage, .uploadFilePath: preconditionFailure("Invalid options for this function") case .uploadImageData: preconditionFailure("Invalid options for this function")
case .remove: case .remove:
if isCurrentUser { if isCurrentUser {

View File

@ -0,0 +1,115 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUIKit
public extension ProfilePictureView {
func update(
publicKey: String,
threadVariant: SessionThread.Variant,
customImageData: Data?,
profile: Profile?,
profileIcon: ProfileIcon = .none,
additionalProfile: Profile? = nil,
additionalProfileIcon: ProfileIcon = .none
) {
// If we are given 'customImageData' then only use that
guard customImageData == nil else { return update(Info(imageData: customImageData)) }
// Otherwise there are conversation-type-specific behaviours
switch threadVariant {
case .community:
let placeholderImage: UIImage = {
switch self.size {
case .navigation, .message: return #imageLiteral(resourceName: "SessionWhite16")
case .list: return #imageLiteral(resourceName: "SessionWhite24")
case .hero: return #imageLiteral(resourceName: "SessionWhite40")
}
}()
update(
Info(
imageData: placeholderImage.pngData(),
inset: UIEdgeInsets(
top: 12,
left: 12,
bottom: 12,
right: 12
),
icon: profileIcon,
forcedBackgroundColor: .theme(.classicDark, color: .borderSeparator)
)
)
case .legacyGroup, .group:
guard !publicKey.isEmpty else { return }
update(
Info(
imageData: (
profile.map { ProfileManager.profileAvatar(profile: $0) } ??
PlaceholderIcon.generate(
seed: publicKey,
text: (profile?.displayName(for: threadVariant))
.defaulting(to: publicKey),
size: (additionalProfile != nil ?
self.size.multiImageSize :
self.size.viewSize
)
).pngData()
),
icon: profileIcon
),
additionalInfo: additionalProfile
.map { otherProfile in
Info(
imageData: (
ProfileManager.profileAvatar(profile: otherProfile) ??
PlaceholderIcon.generate(
seed: otherProfile.id,
text: otherProfile.displayName(for: threadVariant),
size: self.size.multiImageSize
).pngData()
),
icon: additionalProfileIcon
)
}
.defaulting(
to: Info(
imageData: UIImage(systemName: "person.fill")?.pngData(),
renderingMode: .alwaysTemplate,
themeTintColor: .white,
inset: UIEdgeInsets(
top: 3,
left: 0,
bottom: -5,
right: 0
),
icon: additionalProfileIcon
)
)
)
case .contact:
guard !publicKey.isEmpty else { return }
update(
Info(
imageData: (
profile.map { ProfileManager.profileAvatar(profile: $0) } ??
PlaceholderIcon.generate(
seed: publicKey,
text: (profile?.displayName(for: threadVariant))
.defaulting(to: publicKey),
size: (additionalProfile != nil ?
self.size.multiImageSize :
self.size.viewSize
)
).pngData()
),
icon: profileIcon
)
)
}
}
}

View File

@ -39,7 +39,7 @@ final class SimplifiedConversationCell: UITableViewCell {
}() }()
private lazy var profilePictureView: ProfilePictureView = { private lazy var profilePictureView: ProfilePictureView = {
let view: ProfilePictureView = ProfilePictureView() let view: ProfilePictureView = ProfilePictureView(size: .list)
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
return view return view
@ -79,10 +79,6 @@ final class SimplifiedConversationCell: UITableViewCell {
accentLineView.set(.width, to: Values.accentLineThickness) accentLineView.set(.width, to: Values.accentLineThickness)
accentLineView.set(.height, to: 68) accentLineView.set(.height, to: 68)
profilePictureView.set(.width, to: Values.mediumProfilePictureSize)
profilePictureView.set(.height, to: Values.mediumProfilePictureSize)
profilePictureView.size = Values.mediumProfilePictureSize
stackView.pin(to: self) stackView.pin(to: self)
} }

View File

@ -1,12 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit import UIKit
import YYImage
import SessionUtilitiesKit import SessionUtilitiesKit
// FIXME: Refactor as part of the Groups Rebuild // FIXME: Refactor as part of the Groups Rebuild
public class ConfirmationModal: Modal { public class ConfirmationModal: Modal {
private static let imageSize: CGFloat = 80
private static let closeSize: CGFloat = 24 private static let closeSize: CGFloat = 24
private var internalOnConfirm: ((ConfirmationModal) -> ())? = nil private var internalOnConfirm: ((ConfirmationModal) -> ())? = nil
@ -51,27 +49,7 @@ public class ConfirmationModal: Modal {
return result return result
}() }()
private lazy var imageView: UIImageView = { private lazy var profileView: ProfilePictureView = ProfilePictureView(size: .hero)
let result: UIImageView = UIImageView()
result.clipsToBounds = true
result.contentMode = .scaleAspectFill
result.set(.width, to: ConfirmationModal.imageSize)
result.set(.height, to: ConfirmationModal.imageSize)
result.isHidden = true
return result
}()
private lazy var animatedImageView: YYAnimatedImageView = {
let result: YYAnimatedImageView = YYAnimatedImageView()
result.clipsToBounds = true
result.contentMode = .scaleAspectFill
result.set(.width, to: ConfirmationModal.imageSize)
result.set(.height, to: ConfirmationModal.imageSize)
result.isHidden = true
return result
}()
private lazy var confirmButton: UIButton = { private lazy var confirmButton: UIButton = {
let result: UIButton = Modal.createButton( let result: UIButton = Modal.createButton(
@ -157,15 +135,10 @@ public class ConfirmationModal: Modal {
contentView.addSubview(mainStackView) contentView.addSubview(mainStackView)
contentView.addSubview(closeButton) contentView.addSubview(closeButton)
imageViewContainer.addSubview(imageView) imageViewContainer.addSubview(profileView)
imageView.center(.horizontal, in: imageViewContainer) profileView.center(.horizontal, in: imageViewContainer)
imageView.pin(.top, to: .top, of: imageViewContainer, withInset: 15) profileView.pin(.top, to: .top, of: imageViewContainer)//, withInset: 15)
imageView.pin(.bottom, to: .bottom, of: imageViewContainer, withInset: -15) profileView.pin(.bottom, to: .bottom, of: imageViewContainer)//, withInset: -15)
imageViewContainer.addSubview(animatedImageView)
animatedImageView.center(.horizontal, in: imageViewContainer)
animatedImageView.pin(.top, to: .top, of: imageViewContainer, withInset: 15)
animatedImageView.pin(.bottom, to: .bottom, of: imageViewContainer, withInset: -15)
mainStackView.pin(to: contentView) mainStackView.pin(to: contentView)
closeButton.pin(.top, to: .top, of: contentView, withInset: 8) closeButton.pin(.top, to: .top, of: contentView, withInset: 8)
@ -206,32 +179,20 @@ public class ConfirmationModal: Modal {
explanationLabel.attributedText = attributedText explanationLabel.attributedText = attributedText
explanationLabel.isHidden = false explanationLabel.isHidden = false
case .image(let placeholder, let value, let animatedValue, let style, let accessibility, let onClick): case .image(let placeholder, let value, let icon, let style, let accessibility, let onClick):
imageViewContainer.isAccessibilityElement = (accessibility != nil) imageViewContainer.isAccessibilityElement = (accessibility != nil)
imageViewContainer.accessibilityIdentifier = accessibility?.identifier imageViewContainer.accessibilityIdentifier = accessibility?.identifier
imageViewContainer.accessibilityLabel = accessibility?.label imageViewContainer.accessibilityLabel = accessibility?.label
mainStackView.spacing = 0 mainStackView.spacing = 0
imageViewContainer.isHidden = false imageViewContainer.isHidden = false
profileView.clipsToBounds = (style == .circular)
profileView.update(
ProfilePictureView.Info(
imageData: (value ?? placeholder),
icon: icon
)
)
internalOnBodyTap = onClick internalOnBodyTap = onClick
if let animatedValue: YYImage = animatedValue {
imageView.isHidden = true
animatedImageView.image = animatedValue
animatedImageView.isHidden = false
animatedImageView.layer.cornerRadius = (style == .circular ?
(ConfirmationModal.imageSize / 2) :
0
)
}
else {
animatedImageView.isHidden = true
imageView.image = (value ?? placeholder)
imageView.isHidden = false
imageView.layer.cornerRadius = (style == .circular ?
(ConfirmationModal.imageSize / 2) :
0
)
}
} }
confirmButton.accessibilityLabel = info.confirmAccessibility?.label confirmButton.accessibilityLabel = info.confirmAccessibility?.label
@ -443,9 +404,9 @@ public extension ConfirmationModal.Info {
// case input(placeholder: String, value: String?) // case input(placeholder: String, value: String?)
// case radio(explanation: NSAttributedString?, options: [(title: String, selected: Bool)]) // case radio(explanation: NSAttributedString?, options: [(title: String, selected: Bool)])
case image( case image(
placeholder: UIImage?, placeholderData: Data?,
value: UIImage?, valueData: Data?,
animatedValue: YYImage?, icon: ProfilePictureView.ProfileIcon = .none,
style: ImageStyle, style: ImageStyle,
accessibility: Accessibility?, accessibility: Accessibility?,
onClick: (() -> ()) onClick: (() -> ())
@ -471,11 +432,11 @@ public extension ConfirmationModal.Info {
// lhsOptions.map { "\($0.0)-\($0.1)" } == rhsValue.map { "\($0.0)-\($0.1)" } // lhsOptions.map { "\($0.0)-\($0.1)" } == rhsValue.map { "\($0.0)-\($0.1)" }
// ) // )
case (.image(let lhsPlaceholder, let lhsValue, let lhsAnimatedValue, let lhsStyle, let lhsAccessibility, _), .image(let rhsPlaceholder, let rhsValue, let rhsAnimatedValue, let rhsStyle, let rhsAccessibility, _)): case (.image(let lhsPlaceholder, let lhsValue, let lhsIcon, let lhsStyle, let lhsAccessibility, _), .image(let rhsPlaceholder, let rhsValue, let rhsIcon, let rhsStyle, let rhsAccessibility, _)):
return ( return (
lhsPlaceholder == rhsPlaceholder && lhsPlaceholder == rhsPlaceholder &&
lhsValue == rhsValue && lhsValue == rhsValue &&
lhsAnimatedValue == rhsAnimatedValue && lhsIcon == rhsIcon &&
lhsStyle == rhsStyle && lhsStyle == rhsStyle &&
lhsAccessibility == rhsAccessibility lhsAccessibility == rhsAccessibility
) )
@ -490,10 +451,10 @@ public extension ConfirmationModal.Info {
case .text(let text): text.hash(into: &hasher) case .text(let text): text.hash(into: &hasher)
case .attributedText(let text): text.hash(into: &hasher) case .attributedText(let text): text.hash(into: &hasher)
case .image(let placeholder, let value, let animatedValue, let style, let accessibility, _): case .image(let placeholder, let value, let icon, let style, let accessibility, _):
placeholder.hash(into: &hasher) placeholder.hash(into: &hasher)
value.hash(into: &hasher) value.hash(into: &hasher)
animatedValue.hash(into: &hasher) icon.hash(into: &hasher)
style.hash(into: &hasher) style.hash(into: &hasher)
accessibility.hash(into: &hasher) accessibility.hash(into: &hasher)
} }

View File

@ -2,16 +2,23 @@
import UIKit import UIKit
import CryptoKit import CryptoKit
import SessionUIKit
import SignalCoreKit
import SessionUtilitiesKit import SessionUtilitiesKit
public class PlaceholderIcon { public class PlaceholderIcon {
private static let placeholderCache: Atomic<NSCache<NSString, UIImage>> = {
let result = NSCache<NSString, UIImage>()
result.countLimit = 50
return Atomic(result)
}()
private let seed: Int private let seed: Int
// Colour palette // Colour palette
private var colors: [UIColor] = Theme.PrimaryColor.allCases.map { $0.color } private var colors: [UIColor] = Theme.PrimaryColor.allCases.map { $0.color }
// MARK: - Initialization
init(seed: Int, colors: [UIColor]? = nil) { init(seed: Int, colors: [UIColor]? = nil) {
self.seed = seed self.seed = seed
if let colors = colors { self.colors = colors } if let colors = colors { self.colors = colors }
@ -26,7 +33,7 @@ public class PlaceholderIcon {
} }
guard let number = Int(hash.substring(to: 12), radix: 16) else { guard let number = Int(hash.substring(to: 12), radix: 16) else {
owsFailDebug("Failed to generate number from seed string: \(seed).") SNLog("Failed to generate number from seed string: \(seed).")
self.init(seed: 0, colors: colors) self.init(seed: 0, colors: colors)
return return
} }
@ -34,7 +41,53 @@ public class PlaceholderIcon {
self.init(seed: number, colors: colors) self.init(seed: number, colors: colors)
} }
public func generateLayer(with diameter: CGFloat, text: String) -> CALayer { // MARK: - Convenience
public static func generate(seed: String, text: String, size: CGFloat) -> UIImage {
let icon = PlaceholderIcon(seed: seed)
var content: String = (text.hasSuffix("\(String(seed.suffix(4))))") ?
(text.split(separator: "(")
.first
.map { String($0) })
.defaulting(to: text) :
text
)
if content.count > 2 && SessionId.Prefix(from: content) != nil {
content.removeFirst(2)
}
let initials: String = content
.split(separator: " ")
.compactMap { word in word.first.map { String($0) } }
.joined()
let cacheKey: String = "\(content)-\(Int(floor(size)))"
if let cachedIcon: UIImage = placeholderCache.wrappedValue.object(forKey: cacheKey as NSString) {
return cachedIcon
}
let layer = icon.generateLayer(
with: size,
text: (initials.count >= 2 ?
initials.substring(to: 2).uppercased() :
content.substring(to: 2).uppercased()
)
)
let rect = CGRect(origin: CGPoint.zero, size: layer.frame.size)
let renderer = UIGraphicsImageRenderer(size: rect.size)
let result = renderer.image { layer.render(in: $0.cgContext) }
placeholderCache.mutate { $0.setObject(result, forKey: cacheKey as NSString) }
return result
}
// MARK: - Internal
private func generateLayer(with diameter: CGFloat, text: String) -> CALayer {
let color: UIColor = self.colors[seed % self.colors.count] let color: UIColor = self.colors[seed % self.colors.count]
let base: CALayer = getTextLayer(with: diameter, color: color, text: text) let base: CALayer = getTextLayer(with: diameter, color: color, text: text)
base.masksToBounds = true base.masksToBounds = true

View File

@ -0,0 +1,546 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import YYImage
public final class ProfilePictureView: UIView {
public struct Info {
let imageData: Data?
let renderingMode: UIImage.RenderingMode
let themeTintColor: ThemeValue?
let inset: UIEdgeInsets
let icon: ProfileIcon
let backgroundColor: ThemeValue?
let forcedBackgroundColor: ForcedThemeValue?
public init(
imageData: Data?,
renderingMode: UIImage.RenderingMode = .automatic,
themeTintColor: ThemeValue? = nil,
inset: UIEdgeInsets = .zero,
icon: ProfileIcon = .none,
backgroundColor: ThemeValue? = nil,
forcedBackgroundColor: ForcedThemeValue? = nil
) {
self.imageData = imageData
self.renderingMode = renderingMode
self.themeTintColor = themeTintColor
self.inset = inset
self.icon = icon
self.backgroundColor = backgroundColor
self.forcedBackgroundColor = forcedBackgroundColor
}
}
public enum Size {
case navigation
case message
case list
case hero
public var viewSize: CGFloat {
switch self {
case .navigation, .message: return 26
case .list: return 46
case .hero: return 110
}
}
public var imageSize: CGFloat {
switch self {
case .navigation, .message: return 26
case .list: return 46
case .hero: return 80
}
}
public var multiImageSize: CGFloat {
switch self {
case .navigation, .message: return 18 // Shouldn't be used
case .list: return 32
case .hero: return 80
}
}
var iconSize: CGFloat {
switch self {
case .navigation, .message: return 8
case .list: return 16
case .hero: return 24
}
}
}
public enum ProfileIcon: Equatable, Hashable {
case none
case crown
case rightPlus
func iconVerticalInset(for size: Size) -> CGFloat {
switch (self, size) {
case (.crown, .navigation), (.crown, .message): return 1
case (.crown, .list): return 3
case (.crown, .hero): return 5
case (.rightPlus, _): return 3
default: return 0
}
}
}
public var size: Size {
didSet {
widthConstraint.constant = (customWidth ?? size.viewSize)
heightConstraint.constant = size.viewSize
profileIconBackgroundWidthConstraint.constant = size.iconSize
profileIconBackgroundHeightConstraint.constant = size.iconSize
additionalProfileIconBackgroundWidthConstraint.constant = size.iconSize
additionalProfileIconBackgroundHeightConstraint.constant = size.iconSize
profileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
}
}
public var customWidth: CGFloat? {
didSet {
self.widthConstraint.constant = (customWidth ?? self.size.viewSize)
}
}
override public var clipsToBounds: Bool {
didSet {
imageContainerView.clipsToBounds = clipsToBounds
additionalImageContainerView.clipsToBounds = clipsToBounds
imageContainerView.layer.cornerRadius = (clipsToBounds ?
(additionalImageContainerView.isHidden ? (size.imageSize / 2) : (size.multiImageSize / 2)) :
0
)
imageContainerView.layer.cornerRadius = (clipsToBounds ? (size.multiImageSize / 2) : 0)
}
}
// MARK: - Constraints
private var widthConstraint: NSLayoutConstraint!
private var heightConstraint: NSLayoutConstraint!
private var imageViewTopConstraint: NSLayoutConstraint!
private var imageViewLeadingConstraint: NSLayoutConstraint!
private var imageViewCenterXConstraint: NSLayoutConstraint!
private var imageViewCenterYConstraint: NSLayoutConstraint!
private var imageViewWidthConstraint: NSLayoutConstraint!
private var imageViewHeightConstraint: NSLayoutConstraint!
private var additionalImageViewWidthConstraint: NSLayoutConstraint!
private var additionalImageViewHeightConstraint: NSLayoutConstraint!
private var profileIconTopConstraint: NSLayoutConstraint!
private var profileIconBottomConstraint: NSLayoutConstraint!
private var profileIconBackgroundLeftAlignConstraint: NSLayoutConstraint!
private var profileIconBackgroundRightAlignConstraint: NSLayoutConstraint!
private var profileIconBackgroundWidthConstraint: NSLayoutConstraint!
private var profileIconBackgroundHeightConstraint: NSLayoutConstraint!
private var additionalProfileIconTopConstraint: NSLayoutConstraint!
private var additionalProfileIconBottomConstraint: NSLayoutConstraint!
private var additionalProfileIconBackgroundLeftAlignConstraint: NSLayoutConstraint!
private var additionalProfileIconBackgroundRightAlignConstraint: NSLayoutConstraint!
private var additionalProfileIconBackgroundWidthConstraint: NSLayoutConstraint!
private var additionalProfileIconBackgroundHeightConstraint: NSLayoutConstraint!
private lazy var imageEdgeConstraints: [NSLayoutConstraint] = [ // MUST be in 'top, left, bottom, right' order
imageView.pin(.top, to: .top, of: imageContainerView, withInset: 0),
imageView.pin(.left, to: .left, of: imageContainerView, withInset: 0),
imageView.pin(.bottom, to: .bottom, of: imageContainerView, withInset: 0),
imageView.pin(.right, to: .right, of: imageContainerView, withInset: 0),
animatedImageView.pin(.top, to: .top, of: imageContainerView, withInset: 0),
animatedImageView.pin(.left, to: .left, of: imageContainerView, withInset: 0),
animatedImageView.pin(.bottom, to: .bottom, of: imageContainerView, withInset: 0),
animatedImageView.pin(.right, to: .right, of: imageContainerView, withInset: 0)
]
private lazy var additionalImageEdgeConstraints: [NSLayoutConstraint] = [ // MUST be in 'top, left, bottom, right' order
additionalImageView.pin(.top, to: .top, of: additionalImageContainerView, withInset: 0),
additionalImageView.pin(.left, to: .left, of: additionalImageContainerView, withInset: 0),
additionalImageView.pin(.bottom, to: .bottom, of: additionalImageContainerView, withInset: 0),
additionalImageView.pin(.right, to: .right, of: additionalImageContainerView, withInset: 0),
additionalAnimatedImageView.pin(.top, to: .top, of: additionalImageContainerView, withInset: 0),
additionalAnimatedImageView.pin(.left, to: .left, of: additionalImageContainerView, withInset: 0),
additionalAnimatedImageView.pin(.bottom, to: .bottom, of: additionalImageContainerView, withInset: 0),
additionalAnimatedImageView.pin(.right, to: .right, of: additionalImageContainerView, withInset: 0)
]
// MARK: - Components
private lazy var imageContainerView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true
result.themeBackgroundColor = .backgroundSecondary
return result
}()
private lazy var imageView: UIImageView = {
let result: UIImageView = UIImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.isHidden = true
return result
}()
private lazy var animatedImageView: YYAnimatedImageView = {
let result: YYAnimatedImageView = YYAnimatedImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.isHidden = true
return result
}()
private lazy var additionalImageContainerView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true
result.themeBackgroundColor = .primary
result.themeBorderColor = .backgroundPrimary
result.layer.borderWidth = 1
result.isHidden = true
return result
}()
private lazy var additionalImageView: UIImageView = {
let result: UIImageView = UIImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.themeTintColor = .textPrimary
result.isHidden = true
return result
}()
private lazy var additionalAnimatedImageView: YYAnimatedImageView = {
let result: YYAnimatedImageView = YYAnimatedImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.isHidden = true
return result
}()
private lazy var profileIconBackgroundView: UIView = {
let result: UIView = UIView()
result.isHidden = true
return result
}()
private lazy var profileIconImageView: UIImageView = {
let result: UIImageView = UIImageView()
result.contentMode = .scaleAspectFit
return result
}()
private lazy var additionalProfileIconBackgroundView: UIView = {
let result: UIView = UIView()
result.isHidden = true
return result
}()
private lazy var additionalProfileIconImageView: UIImageView = {
let result: UIImageView = UIImageView()
result.contentMode = .scaleAspectFit
return result
}()
// MARK: - Lifecycle
public init(size: Size) {
self.size = size
super.init(frame: CGRect(x: 0, y: 0, width: size.viewSize, height: size.viewSize))
clipsToBounds = true
setUpViewHierarchy()
}
public required init?(coder: NSCoder) {
preconditionFailure("Use init(size:) instead.")
}
private func setUpViewHierarchy() {
addSubview(imageContainerView)
addSubview(profileIconBackgroundView)
addSubview(additionalImageContainerView)
addSubview(additionalProfileIconBackgroundView)
profileIconBackgroundView.addSubview(profileIconImageView)
additionalProfileIconBackgroundView.addSubview(additionalProfileIconImageView)
widthConstraint = self.set(.width, to: self.size.viewSize)
heightConstraint = self.set(.height, to: self.size.viewSize)
imageViewTopConstraint = imageContainerView.pin(.top, to: .top, of: self)
imageViewLeadingConstraint = imageContainerView.pin(.leading, to: .leading, of: self)
imageViewCenterXConstraint = imageContainerView.center(.horizontal, in: self)
imageViewCenterXConstraint.isActive = false
imageViewCenterYConstraint = imageContainerView.center(.vertical, in: self)
imageViewCenterYConstraint.isActive = false
imageViewWidthConstraint = imageContainerView.set(.width, to: size.imageSize)
imageViewHeightConstraint = imageContainerView.set(.height, to: size.imageSize)
additionalImageContainerView.pin(.trailing, to: .trailing, of: self)
additionalImageContainerView.pin(.bottom, to: .bottom, of: self)
additionalImageViewWidthConstraint = additionalImageContainerView.set(.width, to: size.multiImageSize)
additionalImageViewHeightConstraint = additionalImageContainerView.set(.height, to: size.multiImageSize)
imageContainerView.addSubview(imageView)
imageContainerView.addSubview(animatedImageView)
additionalImageContainerView.addSubview(additionalImageView)
additionalImageContainerView.addSubview(additionalAnimatedImageView)
// Activate the image edge constraints
imageEdgeConstraints.forEach { $0.isActive = true }
additionalImageEdgeConstraints.forEach { $0.isActive = true }
profileIconTopConstraint = profileIconImageView.pin(
.top,
to: .top,
of: profileIconBackgroundView,
withInset: 0
)
profileIconImageView.pin(.left, to: .left, of: profileIconBackgroundView)
profileIconImageView.pin(.right, to: .right, of: profileIconBackgroundView)
profileIconBottomConstraint = profileIconImageView.pin(
.bottom,
to: .bottom,
of: profileIconBackgroundView,
withInset: 0
)
profileIconBackgroundLeftAlignConstraint = profileIconBackgroundView.pin(.leading, to: .leading, of: imageContainerView)
profileIconBackgroundRightAlignConstraint = profileIconBackgroundView.pin(.trailing, to: .trailing, of: imageContainerView)
profileIconBackgroundView.pin(.bottom, to: .bottom, of: imageContainerView)
profileIconBackgroundWidthConstraint = profileIconBackgroundView.set(.width, to: size.iconSize)
profileIconBackgroundHeightConstraint = profileIconBackgroundView.set(.height, to: size.iconSize)
profileIconBackgroundLeftAlignConstraint.isActive = false
profileIconBackgroundRightAlignConstraint.isActive = false
additionalProfileIconTopConstraint = additionalProfileIconImageView.pin(
.top,
to: .top,
of: additionalProfileIconBackgroundView,
withInset: 0
)
additionalProfileIconImageView.pin(.left, to: .left, of: additionalProfileIconBackgroundView)
additionalProfileIconImageView.pin(.right, to: .right, of: additionalProfileIconBackgroundView)
additionalProfileIconBottomConstraint = additionalProfileIconImageView.pin(
.bottom,
to: .bottom,
of: additionalProfileIconBackgroundView,
withInset: 0
)
additionalProfileIconBackgroundLeftAlignConstraint = additionalProfileIconBackgroundView.pin(.leading, to: .leading, of: additionalImageContainerView)
additionalProfileIconBackgroundRightAlignConstraint = additionalProfileIconBackgroundView.pin(.trailing, to: .trailing, of: additionalImageContainerView)
additionalProfileIconBackgroundView.pin(.bottom, to: .bottom, of: additionalImageContainerView)
additionalProfileIconBackgroundWidthConstraint = additionalProfileIconBackgroundView.set(.width, to: size.iconSize)
additionalProfileIconBackgroundHeightConstraint = additionalProfileIconBackgroundView.set(.height, to: size.iconSize)
additionalProfileIconBackgroundLeftAlignConstraint.isActive = false
additionalProfileIconBackgroundRightAlignConstraint.isActive = false
}
// MARK: - Content
private func updateIconView(
icon: ProfileIcon,
imageView: UIImageView,
backgroundView: UIView,
topConstraint: NSLayoutConstraint,
leftAlignConstraint: NSLayoutConstraint,
rightAlignConstraint: NSLayoutConstraint,
bottomConstraint: NSLayoutConstraint
) {
backgroundView.isHidden = (icon == .none)
leftAlignConstraint.isActive = (
icon == .none ||
icon == .crown
)
rightAlignConstraint.isActive = (
icon == .rightPlus
)
topConstraint.constant = icon.iconVerticalInset(for: size)
bottomConstraint.constant = -icon.iconVerticalInset(for: size)
switch icon {
case .none: imageView.image = nil
case .crown:
imageView.image = UIImage(systemName: "crown.fill")
backgroundView.themeBackgroundColor = .profileIcon_background
ThemeManager.onThemeChange(observer: imageView) { [weak imageView] _, primaryColor in
let targetColor: ThemeValue = (primaryColor == .green ?
.profileIcon_greenPrimaryColor :
.profileIcon
)
guard imageView?.themeTintColor != targetColor else { return }
imageView?.themeTintColor = targetColor
}
case .rightPlus:
imageView.image = UIImage(
systemName: "plus",
withConfiguration: UIImage.SymbolConfiguration(weight: .semibold)
)
imageView.themeTintColor = .black
backgroundView.themeBackgroundColor = .primary
}
}
// MARK: - Content
private func prepareForReuse() {
imageView.contentMode = .scaleAspectFill
imageView.isHidden = true
animatedImageView.contentMode = .scaleAspectFill
animatedImageView.isHidden = true
imageContainerView.clipsToBounds = clipsToBounds
imageContainerView.themeBackgroundColor = .backgroundSecondary
additionalImageContainerView.isHidden = true
animatedImageView.image = nil
additionalImageView.image = nil
additionalAnimatedImageView.image = nil
additionalImageView.isHidden = true
additionalAnimatedImageView.isHidden = true
additionalImageContainerView.clipsToBounds = clipsToBounds
imageViewTopConstraint.isActive = false
imageViewLeadingConstraint.isActive = false
imageViewCenterXConstraint.isActive = true
imageViewCenterYConstraint.isActive = true
profileIconBackgroundView.isHidden = true
profileIconBackgroundLeftAlignConstraint.isActive = false
profileIconBackgroundRightAlignConstraint.isActive = false
additionalProfileIconBackgroundView.isHidden = true
additionalProfileIconBackgroundLeftAlignConstraint.isActive = false
additionalProfileIconBackgroundRightAlignConstraint.isActive = false
imageEdgeConstraints.forEach { $0.constant = 0 }
additionalImageEdgeConstraints.forEach { $0.constant = 0 }
}
public func update(
_ info: Info,
additionalInfo: Info? = nil
) {
prepareForReuse()
// Sort out the icon first
updateIconView(
icon: info.icon,
imageView: profileIconImageView,
backgroundView: profileIconBackgroundView,
topConstraint: profileIconTopConstraint,
leftAlignConstraint: profileIconBackgroundLeftAlignConstraint,
rightAlignConstraint: profileIconBackgroundRightAlignConstraint,
bottomConstraint: profileIconBottomConstraint
)
// Populate the main imageView
switch info.imageData?.guessedImageFormat {
case .gif, .webp: animatedImageView.image = info.imageData.map { YYImage(data: $0) }
default:
imageView.image = info.imageData
.map {
guard info.renderingMode != .automatic else { return UIImage(data: $0) }
return UIImage(data: $0)?.withRenderingMode(info.renderingMode)
}
}
imageView.themeTintColor = info.themeTintColor
imageView.isHidden = (imageView.image == nil)
animatedImageView.themeTintColor = info.themeTintColor
animatedImageView.isHidden = (animatedImageView.image == nil)
imageContainerView.themeBackgroundColor = info.backgroundColor
imageContainerView.themeBackgroundColorForced = info.forcedBackgroundColor
profileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
imageEdgeConstraints.enumerated().forEach { index, constraint in
switch index % 4 {
case 0: constraint.constant = info.inset.top
case 1: constraint.constant = info.inset.left
case 2: constraint.constant = -info.inset.bottom
case 3: constraint.constant = -info.inset.right
default: break
}
}
// Check if there is a second image (if not then set the size and finish)
guard let additionalInfo: Info = additionalInfo else {
imageViewWidthConstraint.constant = size.imageSize
imageViewHeightConstraint.constant = size.imageSize
imageContainerView.layer.cornerRadius = (imageContainerView.clipsToBounds ? (size.imageSize / 2) : 0)
return
}
// Sort out the additional icon first
updateIconView(
icon: additionalInfo.icon,
imageView: additionalProfileIconImageView,
backgroundView: additionalProfileIconBackgroundView,
topConstraint: additionalProfileIconTopConstraint,
leftAlignConstraint: additionalProfileIconBackgroundLeftAlignConstraint,
rightAlignConstraint: additionalProfileIconBackgroundRightAlignConstraint,
bottomConstraint: additionalProfileIconBottomConstraint
)
// Set the additional image content and reposition the image views correctly
switch additionalInfo.imageData?.guessedImageFormat {
case .gif, .webp: additionalAnimatedImageView.image = additionalInfo.imageData.map { YYImage(data: $0) }
default:
additionalImageView.image = additionalInfo.imageData
.map {
guard additionalInfo.renderingMode != .automatic else { return UIImage(data: $0) }
return UIImage(data: $0)?.withRenderingMode(additionalInfo.renderingMode)
}
}
additionalImageView.themeTintColor = additionalInfo.themeTintColor
additionalImageView.isHidden = (additionalImageView.image == nil)
additionalAnimatedImageView.themeTintColor = additionalInfo.themeTintColor
additionalAnimatedImageView.isHidden = (additionalAnimatedImageView.image == nil)
additionalImageContainerView.isHidden = false
switch (info.backgroundColor, info.forcedBackgroundColor) {
case (_, .some(let color)): additionalImageContainerView.themeBackgroundColorForced = color
case (.some(let color), _): additionalImageContainerView.themeBackgroundColor = color
default: additionalImageContainerView.themeBackgroundColor = .primary
}
additionalImageEdgeConstraints.enumerated().forEach { index, constraint in
switch index % 4 {
case 0: constraint.constant = additionalInfo.inset.top
case 1: constraint.constant = additionalInfo.inset.left
case 2: constraint.constant = -additionalInfo.inset.bottom
case 3: constraint.constant = -additionalInfo.inset.right
default: break
}
}
imageViewTopConstraint.isActive = true
imageViewLeadingConstraint.isActive = true
imageViewCenterXConstraint.isActive = false
imageViewCenterYConstraint.isActive = false
imageViewWidthConstraint.constant = size.multiImageSize
imageViewHeightConstraint.constant = size.multiImageSize
imageContainerView.layer.cornerRadius = (imageContainerView.clipsToBounds ? (size.multiImageSize / 2) : 0)
additionalImageViewWidthConstraint.constant = size.multiImageSize
additionalImageViewHeightConstraint.constant = size.multiImageSize
additionalImageContainerView.layer.cornerRadius = (additionalImageContainerView.clipsToBounds ?
(size.multiImageSize / 2) :
0
)
additionalProfileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
}
}

View File

@ -110,6 +110,11 @@ internal enum Theme_ClassicDark: ThemeColors {
.reactions_contextMoreBackground: .classicDark1, .reactions_contextMoreBackground: .classicDark1,
// NewConversation // NewConversation
.newConversation_background: .classicDark1 .newConversation_background: .classicDark1,
// Profile
.profileIcon: .primary,
.profileIcon_greenPrimaryColor: .black,
.profileIcon_background: .white
] ]
} }

View File

@ -110,6 +110,11 @@ internal enum Theme_ClassicLight: ThemeColors {
.reactions_contextMoreBackground: .classicLight6, .reactions_contextMoreBackground: .classicLight6,
// NewConversation // NewConversation
.newConversation_background: .classicLight6 .newConversation_background: .classicLight6,
// Profile
.profileIcon: .primary,
.profileIcon_greenPrimaryColor: .primary,
.profileIcon_background: .black
] ]
} }

View File

@ -110,6 +110,11 @@ internal enum Theme_OceanDark: ThemeColors {
.reactions_contextMoreBackground: .oceanDark2, .reactions_contextMoreBackground: .oceanDark2,
// NewConversation // NewConversation
.newConversation_background: .oceanDark3 .newConversation_background: .oceanDark3,
// Profile
.profileIcon: .primary,
.profileIcon_greenPrimaryColor: .black,
.profileIcon_background: .white
] ]
} }

View File

@ -110,6 +110,11 @@ internal enum Theme_OceanLight: ThemeColors {
.reactions_contextMoreBackground: .oceanLight6, .reactions_contextMoreBackground: .oceanLight6,
// NewConversation // NewConversation
.newConversation_background: .oceanLight7 .newConversation_background: .oceanLight7,
// Profile
.profileIcon: .primary,
.profileIcon_greenPrimaryColor: .primary,
.profileIcon_background: .oceanLight1
] ]
} }

View File

@ -199,6 +199,11 @@ public indirect enum ThemeValue: Hashable {
// NewConversation // NewConversation
case newConversation_background case newConversation_background
// Profile
case profileIcon
case profileIcon_greenPrimaryColor
case profileIcon_background
} }
// MARK: - ForcedThemeValue // MARK: - ForcedThemeValue

View File

@ -25,11 +25,6 @@ public final class Values : NSObject {
@objc public static let accentLineThickness = CGFloat(4) @objc public static let accentLineThickness = CGFloat(4)
@objc public static let verySmallProfilePictureSize = CGFloat(26)
@objc public static let smallProfilePictureSize = CGFloat(33)
@objc public static let mediumProfilePictureSize = CGFloat(45)
@objc public static let largeProfilePictureSize = CGFloat(75)
@objc public static let searchBarHeight = CGFloat(36) @objc public static let searchBarHeight = CGFloat(36)
@objc public static var separatorThickness: CGFloat { return 1 / UIScreen.main.scale } @objc public static var separatorThickness: CGFloat { return 1 / UIScreen.main.scale }

View File

@ -1,57 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
@objc(LKIdenticon)
public final class Identicon: NSObject {
private static let placeholderCache: Atomic<NSCache<NSString, UIImage>> = {
let result = NSCache<NSString, UIImage>()
result.countLimit = 50
return Atomic(result)
}()
@objc public static func generatePlaceholderIcon(seed: String, text: String, size: CGFloat) -> UIImage {
let icon = PlaceholderIcon(seed: seed)
var content: String = (text.hasSuffix("\(String(seed.suffix(4))))") ?
(text.split(separator: "(")
.first
.map { String($0) })
.defaulting(to: text) :
text
)
if content.count > 2 && SessionId.Prefix(from: content) != nil {
content.removeFirst(2)
}
let initials: String = content
.split(separator: " ")
.compactMap { word in word.first.map { String($0) } }
.joined()
let cacheKey: String = "\(content)-\(Int(floor(size)))"
if let cachedIcon: UIImage = placeholderCache.wrappedValue.object(forKey: cacheKey as NSString) {
return cachedIcon
}
let layer = icon.generateLayer(
with: size,
text: (initials.count >= 2 ?
initials.substring(to: 2).uppercased() :
content.substring(to: 2).uppercased()
)
)
let rect = CGRect(origin: CGPoint.zero, size: layer.frame.size)
let renderer = UIGraphicsImageRenderer(size: rect.size)
let result = renderer.image { layer.render(in: $0.cgContext) }
placeholderCache.mutate { $0.setObject(result, forKey: cacheKey as NSString) }
return result
}
}

View File

@ -1,303 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import YYImage
import SessionUIKit
import SessionMessagingKit
public final class ProfilePictureView: UIView {
public var size: CGFloat = 0
// Constraints
private var imageViewWidthConstraint: NSLayoutConstraint!
private var imageViewHeightConstraint: NSLayoutConstraint!
private var additionalImageViewWidthConstraint: NSLayoutConstraint!
private var additionalImageViewHeightConstraint: NSLayoutConstraint!
// MARK: - Components
private lazy var imageContainerView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true
result.themeBackgroundColor = .backgroundSecondary
return result
}()
private lazy var imageView: UIImageView = {
let result: UIImageView = UIImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.isHidden = true
return result
}()
private lazy var animatedImageView: YYAnimatedImageView = {
let result: YYAnimatedImageView = YYAnimatedImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.isHidden = true
return result
}()
private lazy var additionalImageContainerView: UIView = {
let result: UIView = UIView()
result.translatesAutoresizingMaskIntoConstraints = false
result.clipsToBounds = true
result.themeBackgroundColor = .primary
result.themeBorderColor = .backgroundPrimary
result.layer.borderWidth = 1
result.layer.cornerRadius = (Values.smallProfilePictureSize / 2)
result.isHidden = true
return result
}()
private lazy var additionalProfilePlaceholderImageView: UIImageView = {
let result: UIImageView = UIImageView(
image: UIImage(systemName: "person.fill")?.withRenderingMode(.alwaysTemplate)
)
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.themeTintColor = .textPrimary
result.isHidden = true
return result
}()
private lazy var additionalImageView: UIImageView = {
let result: UIImageView = UIImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.themeTintColor = .textPrimary
result.isHidden = true
return result
}()
private lazy var additionalAnimatedImageView: YYAnimatedImageView = {
let result: YYAnimatedImageView = YYAnimatedImageView()
result.translatesAutoresizingMaskIntoConstraints = false
result.contentMode = .scaleAspectFill
result.isHidden = true
return result
}()
// MARK: - Lifecycle
public override init(frame: CGRect) {
super.init(frame: frame)
setUpViewHierarchy()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
setUpViewHierarchy()
}
private func setUpViewHierarchy() {
let imageViewSize = CGFloat(Values.mediumProfilePictureSize)
let additionalImageViewSize = CGFloat(Values.smallProfilePictureSize)
addSubview(imageContainerView)
addSubview(additionalImageContainerView)
imageContainerView.pin(.leading, to: .leading, of: self)
imageContainerView.pin(.top, to: .top, of: self)
imageViewWidthConstraint = imageContainerView.set(.width, to: imageViewSize)
imageViewHeightConstraint = imageContainerView.set(.height, to: imageViewSize)
additionalImageContainerView.pin(.trailing, to: .trailing, of: self)
additionalImageContainerView.pin(.bottom, to: .bottom, of: self)
additionalImageViewWidthConstraint = additionalImageContainerView.set(.width, to: additionalImageViewSize)
additionalImageViewHeightConstraint = additionalImageContainerView.set(.height, to: additionalImageViewSize)
imageContainerView.addSubview(imageView)
imageContainerView.addSubview(animatedImageView)
additionalImageContainerView.addSubview(additionalImageView)
additionalImageContainerView.addSubview(additionalAnimatedImageView)
additionalImageContainerView.addSubview(additionalProfilePlaceholderImageView)
imageView.pin(to: imageContainerView)
animatedImageView.pin(to: imageContainerView)
additionalImageView.pin(to: additionalImageContainerView)
additionalAnimatedImageView.pin(to: additionalImageContainerView)
additionalProfilePlaceholderImageView.pin(.top, to: .top, of: additionalImageContainerView, withInset: 3)
additionalProfilePlaceholderImageView.pin(.left, to: .left, of: additionalImageContainerView)
additionalProfilePlaceholderImageView.pin(.right, to: .right, of: additionalImageContainerView)
additionalProfilePlaceholderImageView.pin(.bottom, to: .bottom, of: additionalImageContainerView, withInset: 5)
}
private func prepareForReuse() {
imageView.contentMode = .scaleAspectFill
imageView.isHidden = true
animatedImageView.contentMode = .scaleAspectFill
animatedImageView.isHidden = true
imageContainerView.themeBackgroundColor = .backgroundSecondary
additionalImageContainerView.isHidden = true
animatedImageView.image = nil
additionalImageView.image = nil
additionalAnimatedImageView.image = nil
additionalImageView.isHidden = true
additionalAnimatedImageView.isHidden = true
additionalProfilePlaceholderImageView.isHidden = true
}
private func getProfilePicture(
of size: CGFloat,
for publicKey: String,
profile: Profile?,
threadVariant: SessionThread.Variant
) -> (image: UIImage?, animatedImage: YYImage?) {
guard let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile) else {
return (
Identicon.generatePlaceholderIcon(
seed: publicKey,
text: (profile?.displayName(for: threadVariant))
.defaulting(to: publicKey),
size: size
),
nil
)
}
switch profileData.guessedImageFormat {
case .gif, .webp: return (nil, YYImage(data: profileData))
default: return (UIImage(data: profileData), nil)
}
}
public func update(
publicKey: String,
threadVariant: SessionThread.Variant,
customImageData: Data?,
profile: Profile?,
additionalProfile: Profile?
) {
prepareForReuse()
// If we are given 'customImageData' then only use that
if let customImageData: Data = customImageData {
switch customImageData.guessedImageFormat {
case .gif, .webp:
animatedImageView.image = YYImage(data: customImageData)
animatedImageView.isHidden = false
default:
imageView.image = UIImage(data: customImageData)
imageView.isHidden = false
}
imageViewWidthConstraint.constant = self.size
imageViewHeightConstraint.constant = self.size
imageContainerView.layer.cornerRadius = (self.size / 2)
return
}
// Otherwise there are conversation-type-specific behaviours
switch threadVariant {
case .community:
switch self.size {
case Values.smallProfilePictureSize..<Values.mediumProfilePictureSize:
imageView.image = #imageLiteral(resourceName: "SessionWhite16")
case Values.mediumProfilePictureSize..<Values.largeProfilePictureSize:
imageView.image = #imageLiteral(resourceName: "SessionWhite24")
default: imageView.image = #imageLiteral(resourceName: "SessionWhite40")
}
imageView.contentMode = .center
imageView.isHidden = false
imageContainerView.themeBackgroundColorForced = .theme(.classicDark, color: .borderSeparator)
imageViewWidthConstraint.constant = self.size
imageViewHeightConstraint.constant = self.size
imageContainerView.layer.cornerRadius = (self.size / 2)
case .legacyGroup, .group:
guard !publicKey.isEmpty else { return }
// If the `publicKey` we were given matches the first profile id then we have
// provided a "ClosedGroupProfile" (which is essentially a profile object populated
// with `ClosedGroup` data) so we don't want to add the 'additionalProfile' content
let isCustomGroupImage: Bool = (publicKey == profile?.id)
let targetSize: CGFloat = {
guard !isCustomGroupImage else { return self.size }
switch self.size {
case 40: return 32
case 80: return 64
case Values.largeProfilePictureSize: return 56
default: return Values.smallProfilePictureSize
}
}()
// Set the content for the first `profile` object
let (image, animatedImage): (UIImage?, YYImage?) = getProfilePicture(
of: targetSize,
for: publicKey,
profile: profile,
threadVariant: threadVariant
)
imageView.image = image
imageView.isHidden = (animatedImage != nil)
animatedImageView.image = animatedImage
animatedImageView.isHidden = (animatedImage == nil)
imageViewWidthConstraint.constant = targetSize
imageViewHeightConstraint.constant = targetSize
imageContainerView.layer.cornerRadius = (targetSize / 2)
// If the `publicKey` we were given matches the first profile id then we have
// provided a "ClosedGroupProfile" (which is essentially a profile object populated
// with `ClosedGroup` data) so we don't want to add the 'additionalProfile' content
guard !isCustomGroupImage else { return }
additionalImageViewWidthConstraint.constant = targetSize
additionalImageViewHeightConstraint.constant = targetSize
additionalImageContainerView.layer.cornerRadius = (targetSize / 2)
additionalImageContainerView.isHidden = false
if let additionalProfile: Profile = additionalProfile {
let (image, animatedImage): (UIImage?, YYImage?) = getProfilePicture(
of: targetSize,
for: additionalProfile.id,
profile: additionalProfile,
threadVariant: threadVariant
)
// Set the images and show the appropriate imageView (non-animated should be
// visible if there is no image)
additionalImageView.image = image
additionalAnimatedImageView.image = animatedImage
additionalImageView.isHidden = (animatedImage != nil)
additionalAnimatedImageView.isHidden = (animatedImage == nil)
}
else {
additionalProfilePlaceholderImageView.isHidden = false
}
case .contact:
guard !publicKey.isEmpty else { return }
let (image, animatedImage): (UIImage?, YYImage?) = getProfilePicture(
of: self.size,
for: publicKey,
profile: profile,
threadVariant: threadVariant
)
imageView.image = image
imageView.isHidden = (animatedImage != nil)
animatedImageView.image = animatedImage
animatedImageView.isHidden = (animatedImage == nil)
imageViewWidthConstraint.constant = self.size
imageViewHeightConstraint.constant = self.size
imageContainerView.layer.cornerRadius = (self.size / 2)
}
}
}