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:
commit
5d88db7a8a
|
@ -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 */,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue