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

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

View File

@ -373,9 +373,6 @@
C38EF24C255B6D67007E1867 /* NSAttributedString+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF23F255B6D67007E1867 /* NSAttributedString+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
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 */,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
private lazy var authorLabelTopConstraint = authorLabel.pin(.top, to: .top, of: self)
private lazy var 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 = (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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),
@ -533,26 +518,21 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
self.editProfilePictureModal = modal
self.transitionToScreen(modal, transitionType: .present)
}
fileprivate func updatedProfilePictureSelected(name: String, avatarUpdate: ProfileManager.AvatarUpdate) {
guard let info: ConfirmationModal.Info = self.editProfilePictureModalInfo else { return }
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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}
let data: Data = try Data(contentsOf: URL(fileURLWithPath: imageFilePath))
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
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
}
return imageData
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
}()
newProfileKey = try Randomness.generateRandomBytes(numberBytes: ProfileManager.avatarAES256KeyByteLength)
fileExtension = {
switch guessedFormat {
case .gif: return "gif"
case .webp: return "webp"
default: return "jpg"
}
}()
}
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
}
// 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 {

View File

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

View File

@ -39,7 +39,7 @@ final class SimplifiedConversationCell: UITableViewCell {
}()
private lazy var profilePictureView: ProfilePictureView = {
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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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