Updated the profile picture modal

Moved the ProfilePictureView into SessionUIKit
Fixed a couple of minor ProfilePictureView bugs
This commit is contained in:
Morgan Pretty 2023-05-24 17:42:43 +10:00
parent 2d792e4e3e
commit cf2e198a64
31 changed files with 585 additions and 385 deletions

View File

@ -381,9 +381,6 @@
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 */; };
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 */; };
C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */; };
C38EF31C255B6DBF007E1867 /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F2255B6DBC007E1867 /* Searcher.swift */; };
@ -547,6 +544,9 @@
FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E728264937000CE219 /* MediaDetailViewController.swift */; };
FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */; };
FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5EB282B8F17000CE219 /* AttachmentError.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 */; };
FD17D79C27F40B2E00122BE0 /* SMKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacy.swift */; };
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; };
@ -1489,9 +1489,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; };
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; };
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 = "SignalUtilitiesKit/Profile Pictures/PlaceholderIcon.swift"; sourceTree = SOURCE_ROOT; };
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProfilePictureView.swift; path = "SignalUtilitiesKit/Profile Pictures/ProfilePictureView.swift"; sourceTree = SOURCE_ROOT; };
C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PlaceholderIcon.swift; path = SessionUIKit/Components/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; };
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; };
C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProximityMonitoringManager.swift; path = SessionMessagingKit/Utilities/ProximityMonitoringManager.swift; sourceTree = SOURCE_ROOT; };
@ -1673,6 +1672,7 @@
FD09C5E728264937000CE219 /* MediaDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDetailViewController.swift; sourceTree = "<group>"; };
FD09C5E9282A1BB2000CE219 /* ThreadTypingIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTypingIndicator.swift; sourceTree = "<group>"; };
FD09C5EB282B8F17000CE219 /* AttachmentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentError.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>"; };
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>"; };
@ -2857,6 +2857,8 @@
C38EF3EE255B6DF6007E1867 /* GradientView.swift */,
B86BD08323399ACF000F5AE3 /* Modal.swift */,
FD52090628B49738006098F6 /* ConfirmationModal.swift */,
C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */,
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */,
FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */,
);
path = Components;
@ -2867,7 +2869,7 @@
children = (
C33FD9B7255A54A300E217F9 /* Meta */,
C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */,
C36096EF25AD2268008B62B2 /* Profile Pictures */,
FD16AB5D2A1DD8E70083D849 /* Profile Pictures */,
C36096EE25AD21BC008B62B2 /* Screen Lock */,
C3851CD225624B060061EEB0 /* Shared Views */,
C360970125AD22D3008B62B2 /* Shared View Controllers */,
@ -3046,16 +3048,6 @@
path = "Screen Lock";
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 */ = {
isa = PBXGroup;
children = (
@ -3174,6 +3166,7 @@
FDC4386827B4E6B700C60D73 /* String+Utlities.swift */,
FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */,
FD772899284AF1BD0018502F /* Sodium+Utilities.swift */,
FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */,
C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */,
C3ECBF7A257056B700EA7FCE /* Threading.swift */,
);
@ -3558,6 +3551,13 @@
path = Models;
sourceTree = "<group>";
};
FD16AB5D2A1DD8E70083D849 /* Profile Pictures */ = {
isa = PBXGroup;
children = (
);
path = "Profile Pictures";
sourceTree = "<group>";
};
FD17D79427F3E03300122BE0 /* Migrations */ = {
isa = PBXGroup;
children = (
@ -5171,6 +5171,7 @@
C331FFB92558FA8D00070591 /* UIView+Constraints.swift in Sources */,
FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */,
C331FFE02558FB0000070591 /* SearchBar.swift in Sources */,
FD16AB5B2A1DD7CA0083D849 /* PlaceholderIcon.swift in Sources */,
FD71162C28E1451400B47552 /* Position.swift in Sources */,
FD52090328B4680F006098F6 /* RadioButton.swift in Sources */,
C331FFE82558FB0000070591 /* TextView.swift in Sources */,
@ -5181,6 +5182,7 @@
FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */,
C331FF9A2558FA6B00070591 /* Values.swift in Sources */,
FD37E9C628A1D4EC003AE748 /* Theme+ClassicDark.swift in Sources */,
FD16AB5F2A1DD98F0083D849 /* ProfilePictureView.swift in Sources */,
C331FFE42558FB0000070591 /* SessionButton.swift in Sources */,
C331FFE92558FB0000070591 /* Separator.swift in Sources */,
FD71163228E2C42A00B47552 /* IconSize.swift in Sources */,
@ -5199,7 +5201,6 @@
C38EF247255B6D67007E1867 /* NSAttributedString+OWS.m in Sources */,
C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */,
C38EF3C5255B6DE7007E1867 /* OWSViewController+ImageEditor.swift in Sources */,
C38EF2A5255B6D93007E1867 /* Identicon+ObjC.swift in Sources */,
C38EF385255B6DD2007E1867 /* AttachmentTextToolbar.swift in Sources */,
C33FDD23255A582000E217F9 /* FeatureFlags.swift in Sources */,
FD71161E28D9772700B47552 /* UIViewController+OWS.swift in Sources */,
@ -5220,12 +5221,10 @@
C3F0A530255C80BC007BE2A3 /* NoopNotificationsManager.swift in Sources */,
C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */,
C38EF388255B6DD2007E1867 /* AttachmentApprovalViewController.swift in Sources */,
C38EF2A7255B6D93007E1867 /* ProfilePictureView.swift in Sources */,
C38EF22C255B6D5D007E1867 /* OWSVideoPlayer.swift in Sources */,
C33FDC29255A581F00E217F9 /* ReachabilityManager.swift in Sources */,
C38EF407255B6DF7007E1867 /* Toast.swift in Sources */,
C38EF38C255B6DD2007E1867 /* ApprovalRailCellView.swift in Sources */,
C38EF2A6255B6D93007E1867 /* PlaceholderIcon.swift in Sources */,
C33FDD92255A582000E217F9 /* SignalIOS.pb.swift in Sources */,
C33FDC45255A581F00E217F9 /* AppVersion.m in Sources */,
C38EF3C7255B6DE7007E1867 /* ImageEditorCanvasView.swift in Sources */,
@ -5558,6 +5557,7 @@
C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */,
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */,
C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */,
FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */,
FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */,
B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */,
C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */,

View File

@ -5,6 +5,7 @@ import CallKit
import GRDB
import WebRTC
import PromiseKit
import SessionUIKit
import SignalUtilitiesKit
import SessionMessagingKit
@ -154,7 +155,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
self.contactName = Profile.displayName(db, id: sessionId, threadVariant: .contact)
self.profilePicture = ProfileManager.profileAvatar(db, id: sessionId)
.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))
WebRTCSession.current = self.webRTCSession
self.webRTCSession.delegate = self

View File

@ -32,7 +32,7 @@ import SignalUtilitiesKit
let srcImage: UIImage
let successCompletion: ((UIImage) -> Void)
let successCompletion: ((Data) -> Void)
var imageView: UIView!
@ -79,7 +79,7 @@ import SignalUtilitiesKit
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.
self.srcImage = srcImage.normalized()
self.successCompletion = successCompletion
@ -487,10 +487,9 @@ import SignalUtilitiesKit
@objc func donePressed(sender: UIButton) {
let successCompletion = self.successCompletion
dismiss(animated: true, completion: {
guard let dstImage = self.generateDstImage() else {
return
}
successCompletion(dstImage)
guard let dstImageData: Data = self.generateDstImageData() else { return }
successCompletion(dstImageData)
})
}
@ -517,4 +516,8 @@ import SignalUtilitiesKit
UIGraphicsEndImageContext()
return scaledImage
}
func generateDstImageData() -> Data? {
return generateDstImage().map { $0.pngData() }
}
}

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Êtes-vous sûr de vouloir supprimer votre conversation avec %@ ?";
"delete_conversation_confirmation_alert_title" = "Supprimer conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -624,5 +624,5 @@
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_upload" = "Upload";
"update_profile_modal_save" = "Save";
"update_profile_modal_remove" = "Remove";

View File

@ -52,7 +52,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
private var editedDisplayName: String?
private var editProfilePictureModal: ConfirmationModal?
private var editProfilePictureModalInfo: ConfirmationModal.Info?
private var editedProfilePicture: UIImage?
private var editedProfilePictureData: Data?
private var editedProfilePictureFileName: String?
// MARK: - Initialization
@ -166,7 +166,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
self?.oldDisplayName = updatedNickname
self?.updateProfile(
name: updatedNickname,
profilePicture: nil,
profilePictureData: nil,
profilePictureFilePath: ProfileManager.profileAvatarFilepath(id: userSessionId),
isUpdatingDisplayName: true,
isUpdatingProfilePicture: false
@ -396,27 +396,27 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
private func updateProfilePicture() {
let existingDisplayName: String = self.oldDisplayName
let existingImage: UIImage? = ProfileManager
let existingImageData: Data? = ProfileManager
.profileAvatar(id: self.userSessionId)
.map { UIImage(data: $0) }
let editProfilePictureModalInfo: ConfirmationModal.Info = ConfirmationModal.Info(
title: "update_profile_modal_title".localized(),
body: .image(
placeholder: UIImage(named: "profile_placeholder"),
value: existingImage,
placeholderData: UIImage(named: "profile_placeholder")?.pngData(),
valueData: existingImageData,
icon: .rightPlus,
style: .circular,
onClick: { [weak self] in self?.showPhotoLibraryForAvatar() }
),
confirmTitle: "update_profile_modal_upload".localized(),
confirmTitle: "update_profile_modal_save".localized(),
confirmEnabled: false,
cancelTitle: "update_profile_modal_remove".localized(),
cancelEnabled: (existingImage != nil),
cancelEnabled: (existingImageData != nil),
hasCloseButton: true,
dismissOnConfirm: false,
onConfirm: { [weak self] modal in
self?.updateProfile(
name: existingDisplayName,
profilePicture: self?.editedProfilePicture,
profilePictureData: self?.editedProfilePictureData,
profilePictureFilePath: self?.editedProfilePictureFileName,
isUpdatingDisplayName: false,
isUpdatingProfilePicture: true,
@ -426,7 +426,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
onCancel: { [weak self] modal in
self?.updateProfile(
name: existingDisplayName,
profilePicture: nil,
profilePictureData: nil,
profilePictureFilePath: nil,
isUpdatingDisplayName: false,
isUpdatingProfilePicture: true,
@ -434,7 +434,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
)
},
afterClosed: { [weak self] in
self?.editedProfilePicture = nil
self?.editedProfilePictureData = nil
self?.editedProfilePictureFileName = nil
self?.editProfilePictureModal = nil
self?.editProfilePictureModalInfo = nil
@ -447,18 +447,19 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
self.transitionToScreen(modal, transitionType: .present)
}
fileprivate func updatedProfilePictureSelected(image: UIImage?, filePath: String?) {
fileprivate func updatedProfilePictureSelected(imageData: Data?, filePath: String?) {
guard let info: ConfirmationModal.Info = self.editProfilePictureModalInfo else { return }
self.editedProfilePicture = image
self.editedProfilePictureData = imageData
self.editedProfilePictureFileName = filePath
if let image: UIImage = image {
if let imageData: Data = imageData {
self.editProfilePictureModal?.updateContent(
with: info.with(
body: .image(
placeholder: UIImage(named: "profile_placeholder"),
value: image,
placeholderData: UIImage(named: "profile_placeholder")?.pngData(),
valueData: imageData,
icon: .rightPlus,
style: .circular,
onClick: { [weak self] in self?.showPhotoLibraryForAvatar() }
),
@ -470,8 +471,9 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
self.editProfilePictureModal?.updateContent(
with: info.with(
body: .image(
placeholder: UIImage(named: "profile_placeholder"),
value: UIImage(contentsOfFile: filePath),
placeholderData: UIImage(named: "profile_placeholder")?.pngData(),
valueData: FileManager.default.contents(atPath: filePath),
icon: .rightPlus,
style: .circular,
onClick: { [weak self] in self?.showPhotoLibraryForAvatar() }
),
@ -496,7 +498,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
private func updateProfile(
name: String,
profilePicture: UIImage?,
profilePictureData: Data?,
profilePictureFilePath: String?,
isUpdatingDisplayName: Bool,
isUpdatingProfilePicture: Bool,
@ -506,7 +508,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
ProfileManager.updateLocal(
queue: DispatchQueue.global(qos: .default),
profileName: name,
image: profilePicture,
image: profilePictureData.map { UIImage(data: $0) },
imageFilePath: profilePictureFilePath,
success: { db, updatedProfile in
if isUpdatingDisplayName {
@ -642,9 +644,9 @@ class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigati
else {
let viewController: CropScaleImageViewController = CropScaleImageViewController(
srcImage: rawAvatar,
successCompletion: { resultImage in
successCompletion: { resultImageData in
self?.viewModel.updatedProfilePictureSelected(
image: resultImage,
imageData: resultImageData,
filePath: nil
)
}
@ -654,7 +656,7 @@ class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigati
}
self?.viewModel.updatedProfilePictureSelected(
image: nil,
imageData: nil,
filePath: imageUrl.path
)
}

View File

@ -0,0 +1,211 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUIKit
public extension ProfilePictureView {
// FIXME: Remove this in the UserConfig branch
func update(
publicKey: String = "",
profile: Profile? = nil,
icon: ProfileIcon = .none,
additionalProfile: Profile? = nil,
additionalIcon: ProfileIcon = .none,
threadVariant: SessionThread.Variant,
openGroupProfilePictureData: Data? = nil,
useFallbackPicture: Bool = false,
showMultiAvatarForClosedGroup: Bool = false
) {
guard !useFallbackPicture else {
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")
}
}()
return update(
Info(
imageData: placeholderImage.pngData(),
inset: UIEdgeInsets(
top: 12,
left: 12,
bottom: 12,
right: 12
),
forcedBackgroundColor: .theme(.classicDark, color: .borderSeparator)
)
)
}
guard openGroupProfilePictureData == nil else {
return update(Info(imageData: openGroupProfilePictureData))
}
switch (threadVariant, showMultiAvatarForClosedGroup) {
case (.closedGroup, true):
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()
)
),
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()
)
)
}
.defaulting(
to: Info(
imageData: UIImage(systemName: "person.fill")?.pngData(),
renderingMode: .alwaysTemplate,
themeTintColor: .white,
inset: UIEdgeInsets(
top: 3,
left: 0,
bottom: -5,
right: 0
)
)
)
)
default:
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()
)
)
)
}
}
func update(
publicKey: String,
threadVariant: SessionThread.Variant,
customImageData: Data?,
profile: Profile?,
additionalProfile: Profile?
) {
// 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 .openGroup:
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
),
forcedBackgroundColor: .theme(.classicDark, color: .borderSeparator)
)
)
case .closedGroup: //.legacyGroup, .group:
guard !publicKey.isEmpty else { return }
// TODO: Test that this doesn't call 'PlaceholderIcon.generate' when the original value exists
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()
)
),
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()
)
)
}
.defaulting(
to: Info(
imageData: UIImage(systemName: "person.fill")?.pngData(),
renderingMode: .alwaysTemplate,
themeTintColor: .white,
inset: UIEdgeInsets(
top: 3,
left: 0,
bottom: -5,
right: 0
)
)
)
)
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()
)
)
)
}
}
}

View File

@ -5,7 +5,6 @@ import SessionUtilitiesKit
// FIXME: Refactor as part of the Groups Rebuild
public class ConfirmationModal: Modal {
private static let imageSize: CGFloat = 80
private static let closeSize: CGFloat = 24
private var internalOnConfirm: ((ConfirmationModal) -> ())? = nil
@ -37,22 +36,7 @@ public class ConfirmationModal: Modal {
return result
}()
private lazy var imageViewContainer: UIView = {
let result: UIView = UIView()
result.isHidden = true
return result
}()
private lazy var imageView: UIImageView = {
let result: UIImageView = UIImageView()
result.clipsToBounds = true
result.contentMode = .scaleAspectFill
result.set(.width, to: ConfirmationModal.imageSize)
result.set(.height, to: ConfirmationModal.imageSize)
return result
}()
private lazy var profileView: ProfilePictureView = ProfilePictureView(size: .hero)
private lazy var confirmButton: UIButton = {
let result: UIButton = Modal.createButton(
@ -73,7 +57,7 @@ public class ConfirmationModal: Modal {
}()
private lazy var contentStackView: UIStackView = {
let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, imageViewContainer ])
let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, profileView ])
result.axis = .vertical
result.spacing = Values.smallSpacing
result.isLayoutMarginsRelativeArrangement = true
@ -141,11 +125,6 @@ public class ConfirmationModal: Modal {
contentView.addSubview(mainStackView)
contentView.addSubview(closeButton)
imageViewContainer.addSubview(imageView)
imageView.center(.horizontal, in: imageViewContainer)
imageView.pin(.top, to: .top, of: imageViewContainer, withInset: 15)
imageView.pin(.bottom, to: .bottom, of: imageViewContainer, withInset: -15)
mainStackView.pin(to: contentView)
closeButton.pin(.top, to: .top, of: contentView, withInset: 8)
closeButton.pin(.right, to: .right, of: contentView, withInset: -8)
@ -185,14 +164,16 @@ public class ConfirmationModal: Modal {
explanationLabel.attributedText = attributedText
explanationLabel.isHidden = false
case .image(let placeholder, let value, let style, let onClick):
case .image(let placeholder, let value, let icon, let style, let onClick):
mainStackView.spacing = 0
imageView.image = (value ?? placeholder)
imageView.layer.cornerRadius = (style == .circular ?
(ConfirmationModal.imageSize / 2) :
0
profileView.clipsToBounds = (style == .circular)
profileView.update(
ProfilePictureView.Info(
imageData: (value ?? placeholder),
icon: icon
)
)
imageViewContainer.isHidden = false
profileView.isHidden = false
internalOnBodyTap = onClick
}
@ -406,8 +387,9 @@ public extension ConfirmationModal.Info {
// case input(placeholder: String, value: String?)
// case radio(explanation: NSAttributedString?, options: [(title: String, selected: Bool)])
case image(
placeholder: UIImage?,
value: UIImage?,
placeholderData: Data?,
valueData: Data?,
icon: ProfilePictureView.ProfileIcon = .none,
style: ImageStyle,
onClick: (() -> ())
)
@ -432,10 +414,11 @@ public extension ConfirmationModal.Info {
// lhsOptions.map { "\($0.0)-\($0.1)" } == rhsValue.map { "\($0.0)-\($0.1)" }
// )
case (.image(let lhsPlaceholder, let lhsValue, let lhsStyle, _), .image(let rhsPlaceholder, let rhsValue, let rhsStyle, _)):
case (.image(let lhsPlaceholder, let lhsValue, let lhsIcon, let lhsStyle, _), .image(let rhsPlaceholder, let rhsValue, let rhsIcon, let rhsStyle, _)):
return (
lhsPlaceholder == rhsPlaceholder &&
lhsValue == rhsValue &&
lhsIcon == rhsIcon &&
lhsStyle == rhsStyle
)
@ -449,9 +432,10 @@ public extension ConfirmationModal.Info {
case .text(let text): text.hash(into: &hasher)
case .attributedText(let text): text.hash(into: &hasher)
case .image(let placeholder, let value, let style, _):
case .image(let placeholder, let value, let icon, let style, _):
placeholder.hash(into: &hasher)
value.hash(into: &hasher)
icon.hash(into: &hasher)
style.hash(into: &hasher)
}
}

View File

@ -2,14 +2,23 @@
import UIKit
import CryptoSwift
import SessionUIKit
import SessionUtilitiesKit
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
// Colour palette
private var colors: [UIColor] = Theme.PrimaryColor.allCases.map { $0.color }
// MARK: - Initialization
init(seed: Int, colors: [UIColor]? = nil) {
self.seed = seed
if let colors = colors { self.colors = colors }
@ -21,7 +30,7 @@ public class PlaceholderIcon {
if (hash.matches("^[0-9A-Fa-f]+$") && hash.count >= 12) { hash = seed.sha512() }
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)
return
}
@ -29,7 +38,53 @@ public class PlaceholderIcon {
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 base: CALayer = getTextLayer(with: diameter, color: color, text: text)
base.masksToBounds = true

View File

@ -3,10 +3,36 @@
import UIKit
import GRDB
import YYImage
import SessionUIKit
import SessionMessagingKit
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
@ -21,7 +47,7 @@ public final class ProfilePictureView: UIView {
}
}
var imageSize: CGFloat {
public var imageSize: CGFloat {
switch self {
case .navigation, .message: return 26
case .list: return 46
@ -29,7 +55,7 @@ public final class ProfilePictureView: UIView {
}
}
var multiImageSize: CGFloat {
public var multiImageSize: CGFloat {
switch self {
case .navigation, .message: return 18 // Shouldn't be used
case .list: return 32
@ -44,32 +70,31 @@ public final class ProfilePictureView: UIView {
case .hero: return 24
}
}
var iconVerticalInset: CGFloat {
switch self {
case .navigation, .message: return 1
case .list: return 3
case .hero: return 5
}
}
}
public enum ProfileIcon {
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
profileIconTopConstraint.constant = size.iconVerticalInset
profileIconBottomConstraint.constant = -size.iconVerticalInset
profileIconBackgroundWidthConstraint.constant = size.iconSize
profileIconBackgroundHeightConstraint.constant = size.iconSize
additionalProfileIconTopConstraint.constant = size.iconVerticalInset
additionalProfileIconBottomConstraint.constant = -size.iconVerticalInset
additionalProfileIconBackgroundWidthConstraint.constant = size.iconSize
additionalProfileIconBackgroundHeightConstraint.constant = size.iconSize
@ -82,7 +107,18 @@ public final class ProfilePictureView: UIView {
self.widthConstraint.constant = (customWidth ?? self.size.viewSize)
}
}
private var hasTappableProfilePicture: Bool = false
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
@ -108,6 +144,26 @@ public final class ProfilePictureView: UIView {
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
@ -144,18 +200,7 @@ public final class ProfilePictureView: UIView {
result.clipsToBounds = true
result.themeBackgroundColor = .primary
result.themeBorderColor = .backgroundPrimary
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.layer.borderWidth = 1
result.isHidden = true
return result
@ -215,6 +260,7 @@ public final class ProfilePictureView: UIView {
super.init(frame: CGRect(x: 0, y: 0, width: size.viewSize, height: size.viewSize))
clipsToBounds = true
setUpViewHierarchy()
}
@ -251,23 +297,16 @@ public final class ProfilePictureView: UIView {
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)
// Activate the image edge constraints
imageEdgeConstraints.forEach { $0.isActive = true }
additionalImageEdgeConstraints.forEach { $0.isActive = true }
profileIconTopConstraint = profileIconImageView.pin(
.top,
to: .top,
of: profileIconBackgroundView,
withInset: size.iconVerticalInset
withInset: 0
)
profileIconImageView.pin(.left, to: .left, of: profileIconBackgroundView)
profileIconImageView.pin(.right, to: .right, of: profileIconBackgroundView)
@ -275,7 +314,7 @@ public final class ProfilePictureView: UIView {
.bottom,
to: .bottom,
of: profileIconBackgroundView,
withInset: -size.iconVerticalInset
withInset: 0
)
profileIconBackgroundLeftAlignConstraint = profileIconBackgroundView.pin(.leading, to: .leading, of: imageContainerView)
profileIconBackgroundRightAlignConstraint = profileIconBackgroundView.pin(.trailing, to: .trailing, of: imageContainerView)
@ -289,7 +328,7 @@ public final class ProfilePictureView: UIView {
.top,
to: .top,
of: additionalProfileIconBackgroundView,
withInset: size.iconVerticalInset
withInset: 0
)
additionalProfileIconImageView.pin(.left, to: .left, of: additionalProfileIconBackgroundView)
additionalProfileIconImageView.pin(.right, to: .right, of: additionalProfileIconBackgroundView)
@ -297,7 +336,7 @@ public final class ProfilePictureView: UIView {
.bottom,
to: .bottom,
of: additionalProfileIconBackgroundView,
withInset: -size.iconVerticalInset
withInset: 0
)
additionalProfileIconBackgroundLeftAlignConstraint = additionalProfileIconBackgroundView.pin(.leading, to: .leading, of: additionalImageContainerView)
additionalProfileIconBackgroundRightAlignConstraint = additionalProfileIconBackgroundView.pin(.trailing, to: .trailing, of: additionalImageContainerView)
@ -314,8 +353,10 @@ public final class ProfilePictureView: UIView {
icon: ProfileIcon,
imageView: UIImageView,
backgroundView: UIView,
topConstraint: NSLayoutConstraint,
leftAlignConstraint: NSLayoutConstraint,
rightAlignConstraint: NSLayoutConstraint
rightAlignConstraint: NSLayoutConstraint,
bottomConstraint: NSLayoutConstraint
) {
backgroundView.isHidden = (icon == .none)
leftAlignConstraint.isActive = (
@ -325,6 +366,8 @@ public final class ProfilePictureView: UIView {
rightAlignConstraint.isActive = (
icon == .rightPlus
)
topConstraint.constant = icon.iconVerticalInset(for: size)
bottomConstraint.constant = -icon.iconVerticalInset(for: size)
switch icon {
case .none: imageView.image = nil
@ -345,201 +388,159 @@ public final class ProfilePictureView: UIView {
}
case .rightPlus:
imageView.image = UIImage(systemName: "plus")
imageView.image = UIImage(
systemName: "plus",
withConfiguration: UIImage.SymbolConfiguration(weight: .semibold)
)
imageView.themeTintColor = .black
backgroundView.themeBackgroundColor = .primary
backgroundView.themeBackgroundColorForced = .primary(.green)
}
}
public func update(
publicKey: String = "",
profile: Profile? = nil,
icon: ProfileIcon = .none,
additionalProfile: Profile? = nil,
additionalIcon: ProfileIcon = .none,
threadVariant: SessionThread.Variant,
openGroupProfilePictureData: Data? = nil,
useFallbackPicture: Bool = false,
showMultiAvatarForClosedGroup: Bool = false
) {
AssertIsOnMainThread()
// 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
// Sort out the profile icon first
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: icon,
icon: info.icon,
imageView: profileIconImageView,
backgroundView: profileIconBackgroundView,
topConstraint: profileIconTopConstraint,
leftAlignConstraint: profileIconBackgroundLeftAlignConstraint,
rightAlignConstraint: profileIconBackgroundRightAlignConstraint
rightAlignConstraint: profileIconBackgroundRightAlignConstraint,
bottomConstraint: profileIconBottomConstraint
)
guard !useFallbackPicture else {
switch self.size {
case .navigation, .message: imageView.image = #imageLiteral(resourceName: "SessionWhite16")
case .list: imageView.image = #imageLiteral(resourceName: "SessionWhite24")
case .hero: imageView.image = #imageLiteral(resourceName: "SessionWhite40")
// 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
}
imageView.contentMode = .center
imageView.isHidden = false
animatedImageView.isHidden = true
imageContainerView.themeBackgroundColorForced = .theme(.classicDark, color: .borderSeparator)
imageContainerView.layer.cornerRadius = (self.size.imageSize / 2)
imageViewWidthConstraint.constant = self.size.imageSize
imageViewHeightConstraint.constant = self.size.imageSize
profileIconBackgroundWidthConstraint.constant = self.size.iconSize
profileIconBackgroundHeightConstraint.constant = self.size.iconSize
profileIconBackgroundView.layer.cornerRadius = (self.size.iconSize / 2)
additionalProfileIconBackgroundWidthConstraint.constant = self.size.iconSize
additionalProfileIconBackgroundHeightConstraint.constant = self.size.iconSize
additionalProfileIconBackgroundView.layer.cornerRadius = (self.size.iconSize / 2)
additionalImageContainerView.isHidden = true
animatedImageView.image = nil
additionalImageView.image = nil
additionalAnimatedImageView.image = nil
additionalImageView.isHidden = true
additionalAnimatedImageView.isHidden = true
additionalProfilePlaceholderImageView.isHidden = true
}
// 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
}
guard !publicKey.isEmpty || openGroupProfilePictureData != nil else { return }
func getProfilePicture(of size: CGFloat, for publicKey: String, profile: Profile?) -> (image: UIImage?, animatedImage: YYImage?, isTappable: Bool) {
if let profile: Profile = profile, let profileData: Data = ProfileManager.profileAvatar(profile: profile) {
let format: ImageFormat = profileData.guessedImageFormat
let image: UIImage? = (format == .gif || format == .webp ?
nil :
UIImage(data: profileData)
)
let animatedImage: YYImage? = (format != .gif && format != .webp ?
nil :
YYImage(data: profileData)
)
if image != nil || animatedImage != nil {
return (image, animatedImage, true)
}
}
return (
Identicon.generatePlaceholderIcon(
seed: publicKey,
text: (profile?.displayName(for: threadVariant))
.defaulting(to: publicKey),
size: size
),
nil,
false
)
}
// Sort out the additional icon first
updateIconView(
icon: additionalInfo.icon,
imageView: additionalProfileIconImageView,
backgroundView: additionalProfileIconBackgroundView,
topConstraint: additionalProfileIconTopConstraint,
leftAlignConstraint: additionalProfileIconBackgroundLeftAlignConstraint,
rightAlignConstraint: additionalProfileIconBackgroundRightAlignConstraint,
bottomConstraint: additionalProfileIconBottomConstraint
)
// Calulate the sizes (and set the additional image content)
let targetSize: CGFloat
switch (threadVariant, showMultiAvatarForClosedGroup) {
case (.closedGroup, true):
targetSize = self.size.multiImageSize
additionalImageContainerView.isHidden = false
imageViewTopConstraint.isActive = true
imageViewLeadingConstraint.isActive = true
imageViewCenterXConstraint.isActive = false
imageViewCenterYConstraint.isActive = false
// Sort out the additinoal profile icon if needed
updateIconView(
icon: additionalIcon,
imageView: additionalProfileIconImageView,
backgroundView: additionalProfileIconBackgroundView,
leftAlignConstraint: additionalProfileIconBackgroundLeftAlignConstraint,
rightAlignConstraint: additionalProfileIconBackgroundRightAlignConstraint
)
if let additionalProfile: Profile = additionalProfile {
let (image, animatedImage, _): (UIImage?, YYImage?, Bool) = getProfilePicture(
of: self.size.multiImageSize,
for: additionalProfile.id,
profile: additionalProfile
)
// 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)
additionalProfilePlaceholderImageView.isHidden = true
}
else {
additionalImageView.isHidden = true
additionalAnimatedImageView.isHidden = true
additionalProfilePlaceholderImageView.isHidden = false
}
// 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:
targetSize = self.size.imageSize
additionalImageContainerView.isHidden = true
additionalProfileIconBackgroundView.isHidden = true
additionalImageView.image = nil
additionalImageView.isHidden = true
additionalAnimatedImageView.image = nil
additionalAnimatedImageView.isHidden = true
additionalProfilePlaceholderImageView.isHidden = true
imageViewTopConstraint.isActive = false
imageViewLeadingConstraint.isActive = false
imageViewCenterXConstraint.isActive = true
imageViewCenterYConstraint.isActive = true
additionalImageView.image = additionalInfo.imageData
.map {
guard additionalInfo.renderingMode != .automatic else { return UIImage(data: $0) }
return UIImage(data: $0)?.withRenderingMode(additionalInfo.renderingMode)
}
}
// Set the image
if let openGroupProfilePictureData: Data = openGroupProfilePictureData {
let format: ImageFormat = openGroupProfilePictureData.guessedImageFormat
let image: UIImage? = (format == .gif || format == .webp ?
nil :
UIImage(data: openGroupProfilePictureData)
)
let animatedImage: YYImage? = (format != .gif && format != .webp ?
nil :
YYImage(data: openGroupProfilePictureData)
)
imageView.image = image
animatedImageView.image = animatedImage
imageView.isHidden = (animatedImage != nil)
animatedImageView.isHidden = (animatedImage == nil)
hasTappableProfilePicture = true
}
else {
let (image, animatedImage, isTappable): (UIImage?, YYImage?, Bool) = getProfilePicture(
of: targetSize,
for: publicKey,
profile: profile
)
imageView.image = image
animatedImageView.image = animatedImage
imageView.isHidden = (animatedImage != nil)
animatedImageView.isHidden = (animatedImage == nil)
hasTappableProfilePicture = isTappable
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
}
imageView.contentMode = .scaleAspectFill
animatedImageView.contentMode = .scaleAspectFill
imageContainerView.themeBackgroundColor = .backgroundSecondary
imageViewWidthConstraint.constant = targetSize
imageViewHeightConstraint.constant = targetSize
imageContainerView.layer.cornerRadius = (targetSize / 2)
additionalImageViewWidthConstraint.constant = targetSize
additionalImageViewHeightConstraint.constant = targetSize
additionalImageContainerView.layer.cornerRadius = (targetSize / 2)
profileIconBackgroundView.layer.cornerRadius = (size.iconSize / 2)
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)
}
// MARK: - Convenience
@objc public func getProfilePicture() -> UIImage? {
return (hasTappableProfilePicture ? imageView.image : nil)
}
}

View File

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