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, ); }; };
|
||||
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 */; };
|
||||
|
@ -520,6 +517,9 @@
|
|||
FD09C5EC282B8F18000CE219 /* AttachmentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09C5EB282B8F17000CE219 /* AttachmentError.swift */; };
|
||||
FD0B77B029B69A65009169BA /* TopBannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0B77AF29B69A65009169BA /* TopBannerController.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 */; };
|
||||
FD17D79C27F40B2E00122BE0 /* SMKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacy.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; };
|
||||
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; };
|
||||
|
@ -1692,6 +1691,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -2939,6 +2939,8 @@
|
|||
C38EF3EE255B6DF6007E1867 /* GradientView.swift */,
|
||||
B86BD08323399ACF000F5AE3 /* Modal.swift */,
|
||||
FD52090628B49738006098F6 /* ConfirmationModal.swift */,
|
||||
C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */,
|
||||
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */,
|
||||
FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */,
|
||||
FD0B77AF29B69A65009169BA /* TopBannerController.swift */,
|
||||
);
|
||||
|
@ -2950,7 +2952,7 @@
|
|||
children = (
|
||||
C33FD9B7255A54A300E217F9 /* Meta */,
|
||||
C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */,
|
||||
C36096EF25AD2268008B62B2 /* Profile Pictures */,
|
||||
FD16AB5D2A1DD8E70083D849 /* Profile Pictures */,
|
||||
C36096EE25AD21BC008B62B2 /* Screen Lock */,
|
||||
C3851CD225624B060061EEB0 /* Shared Views */,
|
||||
C360970125AD22D3008B62B2 /* Shared View Controllers */,
|
||||
|
@ -3128,16 +3130,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 = (
|
||||
|
@ -3253,6 +3245,7 @@
|
|||
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */,
|
||||
FDC4386827B4E6B700C60D73 /* String+Utlities.swift */,
|
||||
FD772899284AF1BD0018502F /* Sodium+Utilities.swift */,
|
||||
FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */,
|
||||
C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */,
|
||||
C3ECBF7A257056B700EA7FCE /* Threading.swift */,
|
||||
FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */,
|
||||
|
@ -3634,6 +3627,13 @@
|
|||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD16AB5D2A1DD8E70083D849 /* Profile Pictures */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = "Profile Pictures";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD17D79427F3E03300122BE0 /* Migrations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -5431,6 +5431,7 @@
|
|||
FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */,
|
||||
FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.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 */,
|
||||
|
@ -5441,6 +5442,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 */,
|
||||
|
@ -5460,7 +5462,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 */,
|
||||
|
@ -5480,12 +5481,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 */,
|
||||
|
@ -5859,6 +5858,7 @@
|
|||
FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */,
|
||||
FDA1E83B29A5F2D500C5C3BD /* SessionUtil+Shared.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 */,
|
||||
|
|
|
@ -6,6 +6,7 @@ import Combine
|
|||
import CallKit
|
||||
import GRDB
|
||||
import WebRTC
|
||||
import SessionUIKit
|
||||
import SignalUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
|
||||
|
@ -157,7 +158,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
|
|||
self.contactName = Profile.displayName(db, id: sessionId, threadVariant: .contact)
|
||||
self.profilePicture = avatarData
|
||||
.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
|
||||
.map { data in
|
||||
switch data.guessedImageFormat {
|
||||
|
|
|
@ -206,9 +206,9 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
|||
return
|
||||
}
|
||||
|
||||
guard CurrentAppContext().isMainAppAndActive else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard CurrentAppContext().isMainAppAndActive else { return }
|
||||
|
||||
guard let presentingVC = CurrentAppContext().frontmostViewController() else {
|
||||
preconditionFailure() // FIXME: Handle more gracefully
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import UIKit
|
|||
import WebRTC
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
|
||||
private static let swipeToOperateThreshold: CGFloat = 60
|
||||
|
@ -20,14 +21,7 @@ final class IncomingCallBanner: UIView, UIGestureRecognizerDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var profilePictureView: ProfilePictureView = {
|
||||
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 profilePictureView: ProfilePictureView = ProfilePictureView(size: .list)
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
|
|
|
@ -1243,8 +1243,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
|||
|
||||
switch threadData.threadVariant {
|
||||
case .contact:
|
||||
let profilePictureView = ProfilePictureView()
|
||||
profilePictureView.size = Values.verySmallProfilePictureSize
|
||||
let profilePictureView = ProfilePictureView(size: .navigation)
|
||||
profilePictureView.update(
|
||||
publicKey: threadData.threadId, // Contact thread uses the contactId
|
||||
threadVariant: threadData.threadVariant,
|
||||
|
@ -1252,9 +1251,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
|||
profile: threadData.profile,
|
||||
additionalProfile: nil
|
||||
)
|
||||
|
||||
profilePictureView.set(.width, to: (44 - 16)) // Width of the standard back button
|
||||
profilePictureView.set(.height, to: Values.verySmallProfilePictureSize)
|
||||
profilePictureView.customWidth = (44 - 16) // Width of the standard back button
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
|
||||
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
|||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, MentionSelectionViewDelegate {
|
||||
// MARK: - Variables
|
||||
|
@ -499,7 +500,7 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
|
|||
func showMentionsUI(for candidates: [MentionInfo]) {
|
||||
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
|
||||
layoutIfNeeded()
|
||||
|
||||
|
|
|
@ -116,9 +116,7 @@ private extension MentionSelectionView {
|
|||
final class Cell: UITableViewCell {
|
||||
// MARK: - UI
|
||||
|
||||
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView()
|
||||
|
||||
private lazy var moderatorIconImageView: UIImageView = UIImageView(image: #imageLiteral(resourceName: "Crown"))
|
||||
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView(size: .message)
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
|
@ -160,18 +158,12 @@ private extension MentionSelectionView {
|
|||
selectedBackgroundView.themeBackgroundColor = .highlighted(.settings_tabBackground)
|
||||
self.selectedBackgroundView = selectedBackgroundView
|
||||
|
||||
// Profile picture image view
|
||||
let profilePictureViewSize = Values.smallProfilePictureSize
|
||||
profilePictureView.set(.width, to: profilePictureViewSize)
|
||||
profilePictureView.set(.height, to: profilePictureViewSize)
|
||||
profilePictureView.size = profilePictureViewSize
|
||||
|
||||
// Main stack view
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel ])
|
||||
mainStackView.axis = .horizontal
|
||||
mainStackView.alignment = .center
|
||||
mainStackView.spacing = Values.mediumSpacing
|
||||
mainStackView.set(.height, to: profilePictureViewSize)
|
||||
mainStackView.set(.height, to: ProfilePictureView.Size.message.viewSize)
|
||||
contentView.addSubview(mainStackView)
|
||||
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing)
|
||||
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)
|
||||
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
|
||||
addSubview(separator)
|
||||
separator.pin(.leading, to: .leading, of: self)
|
||||
|
@ -204,12 +189,11 @@ private extension MentionSelectionView {
|
|||
displayNameLabel.text = profile.displayName(for: threadVariant)
|
||||
profilePictureView.update(
|
||||
publicKey: profile.id,
|
||||
threadVariant: .contact,
|
||||
threadVariant: .contact, // Always show the display picture in 'contact' mode
|
||||
customImageData: nil,
|
||||
profile: profile,
|
||||
additionalProfile: nil
|
||||
profileIcon: (isUserModeratorOrAdmin ? .crown : .none)
|
||||
)
|
||||
moderatorIconImageView.isHidden = !isUserModeratorOrAdmin
|
||||
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 authorLabelHeightConstraint = authorLabel.set(.height, to: 0)
|
||||
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 contentViewLeadingConstraint2 = snContentView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: VisibleMessageCell.gutterSize)
|
||||
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] = [
|
||||
snContentView,
|
||||
profilePictureView,
|
||||
moderatorIconImageView,
|
||||
replyButton,
|
||||
timerView,
|
||||
messageStatusImageView,
|
||||
reactionContainerView
|
||||
]
|
||||
|
||||
private lazy var profilePictureView: ProfilePictureView = {
|
||||
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"))
|
||||
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView(size: .message)
|
||||
|
||||
lazy var bubbleBackgroundView: UIView = {
|
||||
let result = UIView()
|
||||
|
@ -176,7 +166,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
private static let messageStatusImageViewSize: CGFloat = 12
|
||||
private static let authorLabelBottomSpacing: CGFloat = 4
|
||||
private static let groupThreadHSpacing: CGFloat = 12
|
||||
private static let profilePictureSize = Values.verySmallProfilePictureSize
|
||||
private static let authorLabelInset: CGFloat = 12
|
||||
private static let replyButtonSize: CGFloat = 24
|
||||
private static let maxBubbleTranslationX: CGFloat = 40
|
||||
|
@ -186,7 +175,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
static let contactThreadHSpacing = Values.mediumSpacing
|
||||
|
||||
static var gutterSize: CGFloat = {
|
||||
var result = groupThreadHSpacing + profilePictureSize + groupThreadHSpacing
|
||||
var result = groupThreadHSpacing + ProfilePictureView.Size.message.viewSize + groupThreadHSpacing
|
||||
|
||||
if UIDevice.current.isIPad {
|
||||
result += 168
|
||||
|
@ -195,7 +184,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
static var leftGutterSize: CGFloat { groupThreadHSpacing + profilePictureSize + groupThreadHSpacing }
|
||||
static var leftGutterSize: CGFloat { groupThreadHSpacing + ProfilePictureView.Size.message.viewSize + groupThreadHSpacing }
|
||||
|
||||
// MARK: Direction & Position
|
||||
|
||||
|
@ -214,21 +203,13 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
// Profile picture view
|
||||
addSubview(profilePictureView)
|
||||
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
|
||||
addSubview(snContentView)
|
||||
contentViewLeadingConstraint1.isActive = true
|
||||
contentViewTopConstraint.isActive = true
|
||||
contentViewTrailingConstraint1.isActive = true
|
||||
snContentView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: -1)
|
||||
snContentView.pin(.bottom, to: .bottom, of: profilePictureView)
|
||||
|
||||
// Bubble background view
|
||||
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)
|
||||
profilePictureViewLeadingConstraint.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : 0)
|
||||
profilePictureViewWidthConstraint.constant = (isGroupThread ? VisibleMessageCell.profilePictureSize : 0)
|
||||
profilePictureView.isHidden = (!cellViewModel.shouldShowProfile || cellViewModel.profile == nil)
|
||||
profilePictureView.update(
|
||||
publicKey: cellViewModel.authorId,
|
||||
threadVariant: .contact, // Should always be '.contact'
|
||||
threadVariant: .contact, // Always show the display picture in 'contact' mode
|
||||
customImageData: nil,
|
||||
profile: cellViewModel.profile,
|
||||
additionalProfile: nil
|
||||
profileIcon: (cellViewModel.isSenderOpenGroupModerator ? .crown : .none)
|
||||
)
|
||||
moderatorIconImageView.isHidden = (!cellViewModel.isSenderOpenGroupModerator || !cellViewModel.shouldShowProfile)
|
||||
|
||||
// Bubble view
|
||||
contentViewLeadingConstraint1.isActive = (
|
||||
|
|
|
@ -244,12 +244,13 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
id: .avatar,
|
||||
accessory: .profile(
|
||||
id: threadViewModel.id,
|
||||
size: .extraLarge,
|
||||
size: .hero,
|
||||
threadVariant: threadVariant,
|
||||
customImageData: threadViewModel.openGroupProfilePictureData,
|
||||
profile: threadViewModel.profile,
|
||||
profileIcon: .none,
|
||||
additionalProfile: threadViewModel.additionalProfile,
|
||||
cornerIcon: nil,
|
||||
additionalProfileIcon: .none,
|
||||
accessibility: nil
|
||||
),
|
||||
styling: SessionCell.StyleInfo(
|
||||
|
|
|
@ -484,12 +484,10 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
|
|||
|
||||
private func updateNavBarButtons() {
|
||||
// Profile picture view
|
||||
let profilePictureSize = Values.verySmallProfilePictureSize
|
||||
let profilePictureView = ProfilePictureView()
|
||||
let profilePictureView = ProfilePictureView(size: .navigation)
|
||||
profilePictureView.accessibilityIdentifier = "User settings"
|
||||
profilePictureView.accessibilityLabel = "User settings"
|
||||
profilePictureView.isAccessibilityElement = true
|
||||
profilePictureView.size = profilePictureSize
|
||||
profilePictureView.update(
|
||||
publicKey: getUserHexEncodedPublicKey(),
|
||||
threadVariant: .contact,
|
||||
|
@ -497,8 +495,6 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData
|
|||
profile: Profile.fetchOrCreateCurrentUser(),
|
||||
additionalProfile: nil
|
||||
)
|
||||
profilePictureView.set(.width, to: profilePictureSize)
|
||||
profilePictureView.set(.height, to: profilePictureSize)
|
||||
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
|
||||
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
class MessageRequestsCell: UITableViewCell {
|
||||
static let reuseIdentifier = "MessageRequestsCell"
|
||||
|
@ -29,7 +30,7 @@ class MessageRequestsCell: UITableViewCell {
|
|||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.themeBackgroundColor = .conversationButton_unreadBubbleBackground
|
||||
result.layer.cornerRadius = (Values.mediumProfilePictureSize / 2)
|
||||
result.layer.cornerRadius = (ProfilePictureView.Size.list.viewSize / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -100,8 +101,8 @@ class MessageRequestsCell: UITableViewCell {
|
|||
constant: (Values.accentLineThickness + Values.mediumSpacing)
|
||||
),
|
||||
iconContainerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
iconContainerView.widthAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize),
|
||||
iconContainerView.heightAnchor.constraint(equalToConstant: Values.mediumProfilePictureSize),
|
||||
iconContainerView.widthAnchor.constraint(equalToConstant: ProfilePictureView.Size.list.viewSize),
|
||||
iconContainerView.heightAnchor.constraint(equalToConstant: ProfilePictureView.Size.list.viewSize),
|
||||
|
||||
iconImageView.centerXAnchor.constraint(equalTo: iconContainerView.centerXAnchor),
|
||||
iconImageView.centerYAnchor.constraint(equalTo: iconContainerView.centerYAnchor),
|
||||
|
|
|
@ -31,7 +31,7 @@ import SignalCoreKit
|
|||
|
||||
let srcImage: UIImage
|
||||
|
||||
let successCompletion: ((UIImage) -> Void)
|
||||
let successCompletion: ((Data) -> Void)
|
||||
|
||||
var imageView: UIView!
|
||||
|
||||
|
@ -78,7 +78,7 @@ import SignalCoreKit
|
|||
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
|
||||
|
@ -486,10 +486,9 @@ import SignalCoreKit
|
|||
@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)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -516,4 +515,8 @@ import SignalCoreKit
|
|||
UIGraphicsEndImageContext()
|
||||
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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"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_message" = "Are you sure you want to hide %@?";
|
||||
"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_error_title" = "Unable to remove avatar image";
|
||||
"update_profile_modal_max_size_error_title" = "Maximum File Size Exceeded";
|
||||
|
|
|
@ -4,19 +4,16 @@ import Foundation
|
|||
|
||||
class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigationControllerDelegate {
|
||||
private let onTransition: (UIViewController, TransitionType) -> Void
|
||||
private let onImagePicked: (UIImage) -> Void
|
||||
private let onImageFilePicked: (String) -> Void
|
||||
private let onImageDataPicked: (Data) -> Void
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
onTransition: @escaping (UIViewController, TransitionType) -> Void,
|
||||
onImagePicked: @escaping (UIImage) -> Void,
|
||||
onImageFilePicked: @escaping (String) -> Void
|
||||
onImageDataPicked: @escaping (Data) -> Void
|
||||
) {
|
||||
self.onTransition = onTransition
|
||||
self.onImagePicked = onImagePicked
|
||||
self.onImageFilePicked = onImageFilePicked
|
||||
self.onImageDataPicked = onImageDataPicked
|
||||
}
|
||||
|
||||
// MARK: - UIImagePickerControllerDelegate
|
||||
|
@ -45,15 +42,17 @@ class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigati
|
|||
else {
|
||||
let viewController: CropScaleImageViewController = CropScaleImageViewController(
|
||||
srcImage: rawAvatar,
|
||||
successCompletion: { resultImage in
|
||||
self?.onImagePicked(resultImage)
|
||||
successCompletion: { resultImageData in
|
||||
self?.onImageDataPicked(resultImageData)
|
||||
}
|
||||
)
|
||||
self?.onTransition(viewController, .present)
|
||||
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 lazy var imagePickerHandler: ImagePickerHandler = ImagePickerHandler(
|
||||
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 }
|
||||
|
||||
self?.updatedProfilePictureSelected(
|
||||
name: oldDisplayName,
|
||||
avatarUpdate: .uploadImage(resultImage)
|
||||
)
|
||||
},
|
||||
onImageFilePicked: { [weak self] resultImagePath in
|
||||
guard let oldDisplayName: String = self?.oldDisplayName else { return }
|
||||
|
||||
self?.updatedProfilePictureSelected(
|
||||
name: oldDisplayName,
|
||||
avatarUpdate: .uploadFilePath(resultImagePath)
|
||||
avatarUpdate: .uploadImageData(resultImageData)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -249,7 +241,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
id: .avatar,
|
||||
accessory: .profile(
|
||||
id: profile.id,
|
||||
size: .extraLarge,
|
||||
size: .hero,
|
||||
profile: profile
|
||||
),
|
||||
styling: SessionCell.StyleInfo(
|
||||
|
@ -486,21 +478,14 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
|
||||
private func updateProfilePicture(currentFileName: String?) {
|
||||
let existingDisplayName: String = self.oldDisplayName
|
||||
|
||||
let existingImageData: (image: UIImage?, animatedImage: YYImage?)? = currentFileName
|
||||
.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 existingImageData: Data? = ProfileManager
|
||||
.profileAvatar(id: self.userSessionId)
|
||||
let editProfilePictureModalInfo: ConfirmationModal.Info = ConfirmationModal.Info(
|
||||
title: "update_profile_modal_title".localized(),
|
||||
body: .image(
|
||||
placeholder: UIImage(named: "profile_placeholder"),
|
||||
value: existingImageData?.image,
|
||||
animatedValue: existingImageData?.animatedImage,
|
||||
placeholderData: UIImage(named: "profile_placeholder")?.pngData(),
|
||||
valueData: existingImageData,
|
||||
icon: .rightPlus,
|
||||
style: .circular,
|
||||
accessibility: Accessibility(
|
||||
identifier: "Image picker",
|
||||
|
@ -508,7 +493,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
),
|
||||
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: (existingImageData != nil),
|
||||
|
@ -540,19 +525,14 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
self.editProfilePictureModal?.updateContent(
|
||||
with: info.with(
|
||||
body: .image(
|
||||
placeholder: UIImage(named: "profile_placeholder"),
|
||||
value: {
|
||||
placeholderData: UIImage(named: "profile_placeholder")?.pngData(),
|
||||
valueData: {
|
||||
switch avatarUpdate {
|
||||
case .uploadImage(let image): return image
|
||||
default: return nil
|
||||
}
|
||||
}(),
|
||||
animatedValue: {
|
||||
switch avatarUpdate {
|
||||
case .uploadFilePath(let filePath): return YYImage(contentsOfFile: filePath)
|
||||
case .uploadImageData(let imageData): return imageData
|
||||
default: return nil
|
||||
}
|
||||
}(),
|
||||
icon: .rightPlus,
|
||||
style: .circular,
|
||||
accessibility: Accessibility(
|
||||
identifier: "Image picker",
|
||||
|
|
|
@ -8,7 +8,7 @@ import SignalUtilitiesKit
|
|||
class BlockedContactCell: UITableViewCell {
|
||||
// MARK: - Components
|
||||
|
||||
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView()
|
||||
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView(size: .list)
|
||||
|
||||
private let selectionView: RadioButton = {
|
||||
let result: RadioButton = RadioButton(size: .medium)
|
||||
|
@ -61,9 +61,6 @@ class BlockedContactCell: UITableViewCell {
|
|||
.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -Values.mediumSpacing)
|
||||
.isActive = true
|
||||
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.topAnchor
|
||||
|
|
|
@ -19,7 +19,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC
|
|||
|
||||
private let accentLineView: UIView = UIView()
|
||||
|
||||
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView()
|
||||
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView(size: .list)
|
||||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
|
@ -204,12 +204,6 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC
|
|||
accentLineView.set(.width, to: Values.accentLineThickness)
|
||||
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
|
||||
unreadCountView.addSubview(unreadCountLabel)
|
||||
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
|
||||
// content accordingly
|
||||
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 }
|
||||
|
||||
|
|
|
@ -43,12 +43,13 @@ extension SessionCell {
|
|||
)
|
||||
case profile(
|
||||
id: String,
|
||||
size: IconSize,
|
||||
size: ProfilePictureView.Size,
|
||||
threadVariant: SessionThread.Variant,
|
||||
customImageData: Data?,
|
||||
profile: Profile?,
|
||||
profileIcon: ProfilePictureView.ProfileIcon,
|
||||
additionalProfile: Profile?,
|
||||
cornerIcon: UIImage?,
|
||||
additionalProfileIcon: ProfilePictureView.ProfileIcon,
|
||||
accessibility: Accessibility?
|
||||
)
|
||||
|
||||
|
@ -127,8 +128,9 @@ extension SessionCell {
|
|||
let threadVariant,
|
||||
let customImageData,
|
||||
let profile,
|
||||
let profileIcon,
|
||||
let additionalProfile,
|
||||
let cornerIcon,
|
||||
let additionalProfileIcon,
|
||||
let accessibility
|
||||
):
|
||||
profileId.hash(into: &hasher)
|
||||
|
@ -136,8 +138,9 @@ extension SessionCell {
|
|||
threadVariant.hash(into: &hasher)
|
||||
customImageData.hash(into: &hasher)
|
||||
profile.hash(into: &hasher)
|
||||
profileIcon.hash(into: &hasher)
|
||||
additionalProfile.hash(into: &hasher)
|
||||
cornerIcon.hash(into: &hasher)
|
||||
additionalProfileIcon.hash(into: &hasher)
|
||||
accessibility.hash(into: &hasher)
|
||||
|
||||
case .search(let placeholder, let accessibility, _):
|
||||
|
@ -204,20 +207,22 @@ extension SessionCell {
|
|||
let lhsProfileId,
|
||||
let lhsSize,
|
||||
let lhsThreadVariant,
|
||||
let lhsProfile,
|
||||
let lhsAdditionalProfile,
|
||||
let lhsCustomImageData,
|
||||
let lhsCornerIcon,
|
||||
let lhsProfile,
|
||||
let lhsProfileIcon,
|
||||
let lhsAdditionalProfile,
|
||||
let lhsAdditionalProfileIcon,
|
||||
let lhsAccessibility
|
||||
),
|
||||
.profile(
|
||||
let rhsProfileId,
|
||||
let rhsSize,
|
||||
let rhsThreadVariant,
|
||||
let rhsProfile,
|
||||
let rhsAdditionalProfile,
|
||||
let rhsCustomImageData,
|
||||
let rhsCornerIcon,
|
||||
let rhsProfile,
|
||||
let rhsProfileIcon,
|
||||
let rhsAdditionalProfile,
|
||||
let rhsAdditionalProfileIcon,
|
||||
let rhsAccessibility
|
||||
)
|
||||
):
|
||||
|
@ -225,10 +230,11 @@ extension SessionCell {
|
|||
lhsProfileId == rhsProfileId &&
|
||||
lhsSize == rhsSize &&
|
||||
lhsThreadVariant == rhsThreadVariant &&
|
||||
lhsProfile == rhsProfile &&
|
||||
lhsAdditionalProfile == rhsAdditionalProfile &&
|
||||
lhsCustomImageData == rhsCustomImageData &&
|
||||
lhsCornerIcon == rhsCornerIcon &&
|
||||
lhsProfile == rhsProfile &&
|
||||
lhsProfileIcon == rhsProfileIcon &&
|
||||
lhsAdditionalProfile == rhsAdditionalProfile &&
|
||||
lhsAdditionalProfileIcon == rhsAdditionalProfileIcon &&
|
||||
lhsAccessibility == rhsAccessibility
|
||||
)
|
||||
|
||||
|
@ -346,25 +352,27 @@ extension SessionCell.Accessory {
|
|||
public static func profile(id: String, profile: Profile?) -> SessionCell.Accessory {
|
||||
return .profile(
|
||||
id: id,
|
||||
size: .veryLarge,
|
||||
size: .list,
|
||||
threadVariant: .contact,
|
||||
customImageData: nil,
|
||||
profile: profile,
|
||||
profileIcon: .none,
|
||||
additionalProfile: nil,
|
||||
cornerIcon: nil,
|
||||
additionalProfileIcon: .none,
|
||||
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(
|
||||
id: id,
|
||||
size: size,
|
||||
threadVariant: .contact,
|
||||
customImageData: nil,
|
||||
profile: profile,
|
||||
profileIcon: .none,
|
||||
additionalProfile: nil,
|
||||
cornerIcon: nil,
|
||||
additionalProfileIcon: .none,
|
||||
accessibility: nil
|
||||
)
|
||||
}
|
||||
|
|
|
@ -61,8 +61,6 @@ extension SessionCell {
|
|||
profilePictureView.pin(.top, to: .top, 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] = [
|
||||
searchBar.pin(.top, to: .top, of: self),
|
||||
searchBar.pin(.leading, to: .leading, of: self, withInset: -8), // Removing default inset
|
||||
|
@ -164,32 +162,13 @@ extension SessionCell {
|
|||
}()
|
||||
|
||||
private lazy var profilePictureView: ProfilePictureView = {
|
||||
let result: ProfilePictureView = ProfilePictureView()
|
||||
let result: ProfilePictureView = ProfilePictureView(size: .list)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.isHidden = true
|
||||
|
||||
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 = {
|
||||
let result: ContactsSearchBar = ContactsSearchBar()
|
||||
result.themeTintColor = .textPrimary
|
||||
|
@ -233,7 +212,6 @@ extension SessionCell {
|
|||
addSubview(radioBorderView)
|
||||
addSubview(highlightingBackgroundLabel)
|
||||
addSubview(profilePictureView)
|
||||
addSubview(profileIconContainerView)
|
||||
addSubview(button)
|
||||
addSubview(searchBar)
|
||||
|
||||
|
@ -242,12 +220,6 @@ extension SessionCell {
|
|||
|
||||
radioBorderView.addSubview(radioView)
|
||||
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
|
||||
|
@ -277,7 +249,6 @@ extension SessionCell {
|
|||
radioView.isHidden = true
|
||||
highlightingBackgroundLabel.isHidden = true
|
||||
profilePictureView.isHidden = true
|
||||
profileIconContainerView.isHidden = true
|
||||
button.isHidden = true
|
||||
searchBar.isHidden = true
|
||||
|
||||
|
@ -300,8 +271,6 @@ extension SessionCell {
|
|||
highlightingBackgroundLabelConstraints.forEach { $0.isActive = false }
|
||||
profilePictureViewLeadingConstraint.isActive = false
|
||||
profilePictureViewTrailingConstraint.isActive = false
|
||||
profilePictureViewWidthConstraint.isActive = false
|
||||
profilePictureViewHeightConstraint.isActive = false
|
||||
profilePictureViewConstraints.forEach { $0.isActive = false }
|
||||
searchBarConstraints.forEach { $0.isActive = false }
|
||||
buttonConstraints.forEach { $0.isActive = false }
|
||||
|
@ -465,46 +434,34 @@ extension SessionCell {
|
|||
let threadVariant,
|
||||
let customImageData,
|
||||
let profile,
|
||||
let profileIcon,
|
||||
let additionalProfile,
|
||||
let cornerIcon,
|
||||
let additionalProfileIcon,
|
||||
let accessibility
|
||||
):
|
||||
// Note: We MUST set the 'size' property before triggering the 'update'
|
||||
// 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.accessibilityLabel = accessibility?.label
|
||||
profilePictureView.isAccessibilityElement = (accessibility != nil)
|
||||
profilePictureView.size = profileSize
|
||||
profilePictureView.update(
|
||||
publicKey: profileId,
|
||||
threadVariant: threadVariant,
|
||||
customImageData: customImageData,
|
||||
profile: profile,
|
||||
additionalProfile: additionalProfile
|
||||
profileIcon: profileIcon,
|
||||
additionalProfile: additionalProfile,
|
||||
additionalProfileIcon: additionalProfileIcon
|
||||
)
|
||||
profilePictureView.isHidden = false
|
||||
profileIconContainerView.isHidden = (cornerIcon == nil)
|
||||
profileIconImageView.image = cornerIcon
|
||||
|
||||
fixedWidthConstraint.constant = profilePictureViewWidthConstraint.constant
|
||||
fixedWidthConstraint.constant = profileSize.viewSize
|
||||
fixedWidthConstraint.isActive = true
|
||||
profilePictureViewLeadingConstraint.constant = (profilePictureView.size > AccessoryView.minWidth ? 0 : Values.smallSpacing)
|
||||
profilePictureViewTrailingConstraint.constant = (profilePictureView.size > AccessoryView.minWidth ? 0 : -Values.smallSpacing)
|
||||
profilePictureViewLeadingConstraint.constant = (profileSize.viewSize > AccessoryView.minWidth ? 0 : Values.smallSpacing)
|
||||
profilePictureViewTrailingConstraint.constant = (profileSize.viewSize > AccessoryView.minWidth ? 0 : -Values.smallSpacing)
|
||||
profilePictureViewLeadingConstraint.isActive = true
|
||||
profilePictureViewTrailingConstraint.isActive = true
|
||||
profilePictureViewWidthConstraint.isActive = true
|
||||
profilePictureViewHeightConstraint.isActive = true
|
||||
profilePictureViewConstraints.forEach { $0.isActive = true }
|
||||
|
||||
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
|
||||
let profile: Profile = Profile.fetchOrCreateCurrentUser()
|
||||
let profileFilePath: String? = profile.profilePictureFileName
|
||||
.map { ProfileManager.profileAvatarFilepath(filename: $0) }
|
||||
let profilePictureData: Data? = profile.profilePictureFileName
|
||||
.map { ProfileManager.loadProfileData(with: $0) }
|
||||
|
||||
ProfileManager.updateLocal(
|
||||
queue: queue,
|
||||
profileName: profile.name,
|
||||
avatarUpdate: (profileFilePath.map { .uploadFilePath($0) } ?? .none),
|
||||
avatarUpdate: (profilePictureData.map { .uploadImageData($0) } ?? .none),
|
||||
success: { db in
|
||||
// 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
|
||||
|
|
|
@ -11,23 +11,8 @@ public struct ProfileManager {
|
|||
public enum AvatarUpdate {
|
||||
case none
|
||||
case remove
|
||||
case uploadImage(UIImage)
|
||||
case uploadFilePath(String)
|
||||
case uploadImageData(Data)
|
||||
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.
|
||||
|
@ -339,11 +324,10 @@ public struct ProfileManager {
|
|||
try success?(db)
|
||||
}
|
||||
|
||||
case .uploadFilePath, .uploadImage:
|
||||
case .uploadImageData(let data):
|
||||
prepareAndUploadAvatarImage(
|
||||
queue: queue,
|
||||
image: avatarUpdate.image,
|
||||
imageFilePath: avatarUpdate.filePath,
|
||||
imageData: data,
|
||||
success: { downloadUrl, fileName, newProfileKey in
|
||||
Storage.shared.writeAsync { db in
|
||||
try ProfileManager.updateProfileIfNeeded(
|
||||
|
@ -365,8 +349,7 @@ public struct ProfileManager {
|
|||
|
||||
private static func prepareAndUploadAvatarImage(
|
||||
queue: DispatchQueue,
|
||||
image: UIImage?,
|
||||
imageFilePath: String?,
|
||||
imageData: Data,
|
||||
success: @escaping ((downloadUrl: String, fileName: String, profileKey: Data)) -> (),
|
||||
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
|
||||
// to ensure that other users know that our profile picture was updated
|
||||
let newProfileKey: Data
|
||||
let avatarImageData: Data?
|
||||
let avatarImageData: Data
|
||||
let fileExtension: String
|
||||
|
||||
do {
|
||||
newProfileKey = try Randomness.generateRandomBytes(numberBytes: ProfileManager.avatarAES256KeyByteLength)
|
||||
let guessedFormat: ImageFormat = imageData.guessedImageFormat
|
||||
|
||||
avatarImageData = try {
|
||||
guard var image: UIImage = image else {
|
||||
guard let imageFilePath: String = imageFilePath else {
|
||||
throw ProfileManagerError.invalidCall
|
||||
}
|
||||
switch guessedFormat {
|
||||
case .gif, .webp:
|
||||
// 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
|
||||
// be able to fit our profile photo (eg. generating pure noise at our resolution
|
||||
// compresses to ~200k)
|
||||
SNLog("Animated profile avatar was too large.")
|
||||
SNLog("Updating service with profile failed.")
|
||||
throw ProfileManagerError.avatarUploadMaxFileSizeExceeded
|
||||
}
|
||||
|
||||
let data: Data = try Data(contentsOf: URL(fileURLWithPath: imageFilePath))
|
||||
return imageData
|
||||
|
||||
guard data.count <= maxAvatarBytes else {
|
||||
// Our avatar dimensions are so small that it's incredibly unlikely we wouldn't
|
||||
// be able to fit our profile photo (eg. generating pure noise at our resolution
|
||||
// compresses to ~200k)
|
||||
SNLog("Animated profile avatar was too large.")
|
||||
SNLog("Updating service with profile failed.")
|
||||
throw ProfileManagerError.avatarUploadMaxFileSizeExceeded
|
||||
}
|
||||
|
||||
return data
|
||||
default: break
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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.
|
||||
|
@ -422,19 +408,19 @@ public struct ProfileManager {
|
|||
|
||||
return data
|
||||
}()
|
||||
}
|
||||
catch {
|
||||
if let profileManagerError: ProfileManagerError = error as? ProfileManagerError {
|
||||
failure?(profileManagerError)
|
||||
}
|
||||
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
|
||||
newProfileKey = try Randomness.generateRandomBytes(numberBytes: ProfileManager.avatarAES256KeyByteLength)
|
||||
fileExtension = {
|
||||
switch guessedFormat {
|
||||
case .gif: return "gif"
|
||||
case .webp: return "webp"
|
||||
default: return "jpg"
|
||||
}
|
||||
}()
|
||||
}
|
||||
// TODO: Test that this actually works
|
||||
catch let error as ProfileManagerError { return (failure?(error) ?? {}()) }
|
||||
catch { return (failure?(ProfileManagerError.invalidCall) ?? {}()) }
|
||||
|
||||
// If we have a new avatar image, we must first:
|
||||
//
|
||||
|
@ -444,16 +430,11 @@ public struct ProfileManager {
|
|||
// * Send asset service info to Signal Service
|
||||
OWSLogger.verbose("Updating local profile on service with new avatar.")
|
||||
|
||||
let fileName: String = UUID().uuidString
|
||||
.appendingFileExtension(
|
||||
imageFilePath
|
||||
.map { URL(fileURLWithPath: $0).pathExtension }
|
||||
.defaulting(to: "jpg")
|
||||
)
|
||||
let fileName: String = UUID().uuidString.appendingFileExtension(fileExtension)
|
||||
let filePath: String = ProfileManager.profileAvatarFilepath(filename: fileName)
|
||||
|
||||
// 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 {
|
||||
SNLog("Updating service with profile failed.")
|
||||
failure?(.avatarWriteFailed)
|
||||
|
@ -461,7 +442,7 @@ public struct ProfileManager {
|
|||
}
|
||||
|
||||
// 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.")
|
||||
failure?(.avatarEncryptionFailed)
|
||||
return
|
||||
|
@ -489,7 +470,7 @@ public struct ProfileManager {
|
|||
let downloadUrl: String = "\(FileServerAPI.server)/file/\(fileUploadResponse.id)"
|
||||
|
||||
// Update the cached avatar image value
|
||||
profileAvatarCache.mutate { $0[fileName] = data }
|
||||
profileAvatarCache.mutate { $0[fileName] = avatarImageData }
|
||||
UserDefaults.standard[.lastProfilePictureUpload] = Date()
|
||||
|
||||
SNLog("Successfully uploaded avatar image.")
|
||||
|
@ -545,7 +526,7 @@ public struct ProfileManager {
|
|||
if shouldUpdateAvatar {
|
||||
switch avatarUpdate {
|
||||
case .none: break
|
||||
case .uploadImage, .uploadFilePath: preconditionFailure("Invalid options for this function")
|
||||
case .uploadImageData: preconditionFailure("Invalid options for this function")
|
||||
|
||||
case .remove:
|
||||
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 = {
|
||||
let view: ProfilePictureView = ProfilePictureView()
|
||||
let view: ProfilePictureView = ProfilePictureView(size: .list)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
return view
|
||||
|
@ -79,10 +79,6 @@ final class SimplifiedConversationCell: UITableViewCell {
|
|||
accentLineView.set(.width, to: Values.accentLineThickness)
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import YYImage
|
||||
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
|
||||
|
@ -51,27 +49,7 @@ public class ConfirmationModal: Modal {
|
|||
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)
|
||||
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 profileView: ProfilePictureView = ProfilePictureView(size: .hero)
|
||||
|
||||
private lazy var confirmButton: UIButton = {
|
||||
let result: UIButton = Modal.createButton(
|
||||
|
@ -157,15 +135,10 @@ 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)
|
||||
|
||||
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)
|
||||
imageViewContainer.addSubview(profileView)
|
||||
profileView.center(.horizontal, in: imageViewContainer)
|
||||
profileView.pin(.top, to: .top, of: imageViewContainer)//, withInset: 15)
|
||||
profileView.pin(.bottom, to: .bottom, of: imageViewContainer)//, withInset: -15)
|
||||
|
||||
mainStackView.pin(to: contentView)
|
||||
closeButton.pin(.top, to: .top, of: contentView, withInset: 8)
|
||||
|
@ -206,32 +179,20 @@ public class ConfirmationModal: Modal {
|
|||
explanationLabel.attributedText = attributedText
|
||||
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.accessibilityIdentifier = accessibility?.identifier
|
||||
imageViewContainer.accessibilityLabel = accessibility?.label
|
||||
mainStackView.spacing = 0
|
||||
imageViewContainer.isHidden = false
|
||||
profileView.clipsToBounds = (style == .circular)
|
||||
profileView.update(
|
||||
ProfilePictureView.Info(
|
||||
imageData: (value ?? placeholder),
|
||||
icon: icon
|
||||
)
|
||||
)
|
||||
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
|
||||
|
@ -443,9 +404,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?,
|
||||
animatedValue: YYImage?,
|
||||
placeholderData: Data?,
|
||||
valueData: Data?,
|
||||
icon: ProfilePictureView.ProfileIcon = .none,
|
||||
style: ImageStyle,
|
||||
accessibility: Accessibility?,
|
||||
onClick: (() -> ())
|
||||
|
@ -471,11 +432,11 @@ public extension ConfirmationModal.Info {
|
|||
// 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 (
|
||||
lhsPlaceholder == rhsPlaceholder &&
|
||||
lhsValue == rhsValue &&
|
||||
lhsAnimatedValue == rhsAnimatedValue &&
|
||||
lhsIcon == rhsIcon &&
|
||||
lhsStyle == rhsStyle &&
|
||||
lhsAccessibility == rhsAccessibility
|
||||
)
|
||||
|
@ -490,10 +451,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 animatedValue, let style, let accessibility, _):
|
||||
case .image(let placeholder, let value, let icon, let style, let accessibility, _):
|
||||
placeholder.hash(into: &hasher)
|
||||
value.hash(into: &hasher)
|
||||
animatedValue.hash(into: &hasher)
|
||||
icon.hash(into: &hasher)
|
||||
style.hash(into: &hasher)
|
||||
accessibility.hash(into: &hasher)
|
||||
}
|
||||
|
|
|
@ -2,16 +2,23 @@
|
|||
|
||||
import UIKit
|
||||
import CryptoKit
|
||||
import SessionUIKit
|
||||
import SignalCoreKit
|
||||
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 }
|
||||
|
@ -26,7 +33,7 @@ public class PlaceholderIcon {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -34,7 +41,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
|
|
@ -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,
|
||||
|
||||
// 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,
|
||||
|
||||
// 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,
|
||||
|
||||
// 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,
|
||||
|
||||
// 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
|
||||
case newConversation_background
|
||||
|
||||
// Profile
|
||||
case profileIcon
|
||||
case profileIcon_greenPrimaryColor
|
||||
case profileIcon_background
|
||||
}
|
||||
|
||||
// MARK: - ForcedThemeValue
|
||||
|
|
|
@ -25,11 +25,6 @@ public final class Values : NSObject {
|
|||
|
||||
@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 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