diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 73df8b4165..7facedb819 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -212,6 +212,8 @@ 34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */; }; 34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */; }; 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */; }; + 34D2015120DC160E00A6FD3A /* ContactCellView.h in Headers */ = {isa = PBXBuildFile; fileRef = 34D2014F20DC160D00A6FD3A /* ContactCellView.h */; }; + 34D2015220DC160E00A6FD3A /* ContactCellView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2015020DC160D00A6FD3A /* ContactCellView.m */; }; 34D2CCD220618B3000CB1A14 /* OWSBackupLazyRestoreJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */; }; 34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */; }; 34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCDB206939B100CB1A14 /* DebugUIMessagesAction.m */; }; @@ -858,6 +860,8 @@ 34D1F0BB1F8D108C0066283D /* AttachmentUploadView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AttachmentUploadView.h; sourceTree = ""; }; 34D1F0BC1F8D108C0066283D /* AttachmentUploadView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AttachmentUploadView.m; sourceTree = ""; }; 34D1F0BF1F8EC1760066283D /* MessageRecipientStatusUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRecipientStatusUtils.swift; sourceTree = ""; }; + 34D2014F20DC160D00A6FD3A /* ContactCellView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ContactCellView.h; path = SignalMessaging/Views/ContactCellView.h; sourceTree = SOURCE_ROOT; }; + 34D2015020DC160D00A6FD3A /* ContactCellView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ContactCellView.m; path = SignalMessaging/Views/ContactCellView.m; sourceTree = SOURCE_ROOT; }; 34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSBackupLazyRestoreJob.swift; sourceTree = ""; }; 34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSScreenLockUI.h; sourceTree = ""; }; 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSScreenLockUI.m; sourceTree = ""; }; @@ -1595,6 +1599,8 @@ children = ( 45F3AEB51DFDE7900080CE33 /* AvatarImageView.swift */, 346129D11FD2085A00532771 /* CommonStrings.swift */, + 34D2014F20DC160D00A6FD3A /* ContactCellView.h */, + 34D2015020DC160D00A6FD3A /* ContactCellView.m */, 340CB2221EAC155C0001CAA1 /* ContactsViewHelper.h */, 340CB2231EAC155C0001CAA1 /* ContactsViewHelper.m */, 76EB052E18170B33006006FC /* ContactTableViewCell.h */, @@ -2431,6 +2437,7 @@ 346129951FD1E30000532771 /* OWSDatabaseMigration.h in Headers */, 45194F961FD7226300333B2C /* SelectThreadViewController.h in Headers */, 346129B41FD1F7E800532771 /* OWSProfileManager.h in Headers */, + 34D2015120DC160E00A6FD3A /* ContactCellView.h in Headers */, 346129FA1FD5F31400532771 /* OWS100RemoveTSRecipientsMigration.h in Headers */, 346129E21FD5C0BE00532771 /* VersionMigrations.h in Headers */, 34480B611FD0A98800BC14EF /* UIColor+OWS.h in Headers */, @@ -3154,6 +3161,7 @@ 451F8A381FD7117E005CB9DA /* OWSViewController.m in Sources */, 450C801120AD1CDB00F3A091 /* ReturnToCallViewController.swift in Sources */, 450C800C20AD191E00F3A091 /* OWSNavigationController.m in Sources */, + 34D2015220DC160E00A6FD3A /* ContactCellView.m in Sources */, 346129721FD1D74C00532771 /* SignalKeyingStorage.m in Sources */, 34480B561FD0A7A400BC14EF /* DebugLogger.m in Sources */, 459B775C207BA46C0071D0AB /* OWSQuotedReplyModel.m in Sources */, diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 9fa1399699..51adce28b9 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -7,6 +7,7 @@ // Separate iOS Frameworks from other imports. #import "AppSettingsViewController.h" +#import "ContactCellView.h" #import "ContactTableViewCell.h" #import "ConversationViewItem.h" #import "DateUtil.h" diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index 5bd2ddd2d7..ceb30e9812 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -238,19 +238,15 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele addDivider() } - let cell = ContactTableViewCell() - cell.configure(withRecipientId: recipientId, contactsManager: self.contactsManager) - let statusLabel = UILabel() + // We use ContactCellView, not ContactTableViewCell. + // Table view cells don't layout properly outside the + // context of a table view. + let cellView = ContactCellView() + cellView.layoutMargins = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 20) // We use the "short" status message to avoid being redundant with the section title. - statusLabel.text = shortStatusMessage - statusLabel.textColor = UIColor.ows_darkGray - statusLabel.font = .ows_dynamicTypeFootnote - statusLabel.adjustsFontSizeToFitWidth = true - statusLabel.sizeToFit() - cell.accessoryView = statusLabel - cell.setContentHuggingLow() - cell.isUserInteractionEnabled = false - groupRows.append(cell) + cellView.accessoryMessage = shortStatusMessage + cellView.configure(withRecipientId: recipientId, contactsManager: self.contactsManager) + groupRows.append(cellView) } if groupRows.count > 0 { diff --git a/Signal/src/ViewControllers/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m index 20b67be6e8..2e1a73c5e2 100644 --- a/Signal/src/ViewControllers/NewGroupViewController.m +++ b/Signal/src/ViewControllers/NewGroupViewController.m @@ -236,8 +236,6 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; } else if (isBlocked) { cell.accessoryMessage = NSLocalizedString( @"CONTACT_CELL_IS_BLOCKED", @"An indicator that a contact has been blocked."); - } else { - OWSAssert(cell.accessoryMessage == nil); } if (signalAccount) { @@ -333,8 +331,6 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; } else if (isBlocked) { cell.accessoryMessage = NSLocalizedString( @"CONTACT_CELL_IS_BLOCKED", @"An indicator that a contact has been blocked."); - } else { - OWSAssert(cell.accessoryMessage == nil); } [cell configureWithSignalAccount:signalAccount diff --git a/Signal/src/ViewControllers/ThreadSettings/ShowGroupMembersViewController.m b/Signal/src/ViewControllers/ThreadSettings/ShowGroupMembersViewController.m index 414f844b4f..7a006f84af 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ShowGroupMembersViewController.m +++ b/Signal/src/ViewControllers/ThreadSettings/ShowGroupMembersViewController.m @@ -202,9 +202,9 @@ NS_ASSUME_NONNULL_BEGIN } if (isVerified) { - cell.subtitle.attributedText = cell.verifiedSubtitle; + [cell setAttributedSubtitle:cell.verifiedSubtitle]; } else { - cell.subtitle.attributedText = nil; + [cell setAttributedSubtitle:nil]; } return cell; diff --git a/Signal/src/views/GroupTableViewCell.swift b/Signal/src/views/GroupTableViewCell.swift index 861baa300a..c2272913ca 100644 --- a/Signal/src/views/GroupTableViewCell.swift +++ b/Signal/src/views/GroupTableViewCell.swift @@ -39,10 +39,10 @@ import SignalServiceKit avatarView.autoPinLeadingToSuperviewMargin() avatarView.autoVCenterInSuperview() - avatarView.autoSetDimension(.width, toSize: CGFloat(kContactTableViewCellAvatarSize)) + avatarView.autoSetDimension(.width, toSize: CGFloat(kContactCellAvatarSize)) avatarView.autoPinToSquareAspectRatio() - textContainer.autoPinEdge(.leading, to: .trailing, of: avatarView, withOffset: kContactTableViewCellAvatarTextMargin) + textContainer.autoPinEdge(.leading, to: .trailing, of: avatarView, withOffset: kContactCellAvatarTextMargin) textContainer.autoPinTrailingToSuperviewMargin() textContainer.autoVCenterInSuperview() } @@ -65,7 +65,7 @@ import SignalServiceKit }.joined(separator: ", ") self.subtitleLabel.text = groupMemberNames - self.avatarView.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: kContactTableViewCellAvatarSize, contactsManager: contactsManager) + self.avatarView.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: kContactCellAvatarSize, contactsManager: contactsManager) } } diff --git a/SignalMessaging/Views/ContactCellView.h b/SignalMessaging/Views/ContactCellView.h new file mode 100644 index 0000000000..72797009fb --- /dev/null +++ b/SignalMessaging/Views/ContactCellView.h @@ -0,0 +1,36 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +extern const NSUInteger kContactCellAvatarSize; +extern const CGFloat kContactCellAvatarTextMargin; + +@class OWSContactsManager; +@class SignalAccount; +@class TSThread; + +@interface ContactCellView : UIView + +@property (nonatomic, nullable) NSString *accessoryMessage; + +- (void)configureWithSignalAccount:(SignalAccount *)signalAccount contactsManager:(OWSContactsManager *)contactsManager; + +- (void)configureWithRecipientId:(NSString *)recipientId contactsManager:(OWSContactsManager *)contactsManager; + +- (void)configureWithThread:(TSThread *)thread contactsManager:(OWSContactsManager *)contactsManager; + +- (void)prepareForReuse; + +- (NSAttributedString *)verifiedSubtitle; + +- (void)setAttributedSubtitle:(nullable NSAttributedString *)attributedSubtitle; + +- (BOOL)hasAccessoryText; + +- (void)setAccessoryView:(UIView *)accessoryView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/Views/ContactCellView.m b/SignalMessaging/Views/ContactCellView.m new file mode 100644 index 0000000000..5b059aba4e --- /dev/null +++ b/SignalMessaging/Views/ContactCellView.m @@ -0,0 +1,309 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "ContactCellView.h" +#import "OWSContactAvatarBuilder.h" +#import "OWSContactsManager.h" +#import "UIFont+OWS.h" +#import "UIView+OWS.h" +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +const NSUInteger kContactCellAvatarSize = 48; +const CGFloat kContactCellAvatarTextMargin = 12; + +@interface ContactCellView () + +@property (nonatomic) UILabel *nameLabel; +@property (nonatomic) UILabel *profileNameLabel; +@property (nonatomic) UIImageView *avatarView; +@property (nonatomic) UILabel *subtitleLabel; +@property (nonatomic) UILabel *accessoryLabel; +@property (nonatomic) UIStackView *nameContainerView; +@property (nonatomic) UIView *accessoryViewContainer; + +@property (nonatomic) OWSContactsManager *contactsManager; +@property (nonatomic) NSString *recipientId; + +@end + +#pragma mark - + +@implementation ContactCellView + +- (instancetype)init +{ + if (self = [super init]) { + [self configure]; + } + return self; +} + +- (void)configure +{ + OWSAssert(!self.nameLabel); + + self.layoutMargins = UIEdgeInsetsZero; + + _avatarView = [AvatarImageView new]; + [_avatarView autoSetDimension:ALDimensionWidth toSize:kContactCellAvatarSize]; + [_avatarView autoSetDimension:ALDimensionHeight toSize:kContactCellAvatarSize]; + + self.nameLabel = [UILabel new]; + self.nameLabel.lineBreakMode = NSLineBreakByTruncatingTail; + self.nameLabel.textColor = [UIColor blackColor]; + + self.profileNameLabel = [UILabel new]; + self.profileNameLabel.lineBreakMode = NSLineBreakByTruncatingTail; + self.profileNameLabel.textColor = [UIColor grayColor]; + + self.subtitleLabel = [UILabel new]; + self.subtitleLabel.textColor = [UIColor ows_darkGrayColor]; + + self.accessoryLabel = [[UILabel alloc] init]; + self.accessoryLabel.textAlignment = NSTextAlignmentRight; + self.accessoryLabel.textColor = [UIColor colorWithWhite:0.5f alpha:1.f]; + + self.accessoryViewContainer = [UIView containerView]; + + self.nameContainerView = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.nameLabel, + self.profileNameLabel, + self.subtitleLabel, + ]]; + self.nameContainerView.axis = UILayoutConstraintAxisVertical; + self.nameContainerView.alignment = UIStackViewAlignmentFill; + + [self.avatarView setContentHuggingHorizontalHigh]; + [self.nameContainerView setContentHuggingHorizontalLow]; + [self.accessoryViewContainer setContentHuggingHorizontalHigh]; + + UIStackView *hStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.avatarView, + self.nameContainerView, + self.accessoryViewContainer, + ]]; + hStackView.axis = UILayoutConstraintAxisHorizontal; + hStackView.spacing = kContactCellAvatarTextMargin; + hStackView.distribution = UIStackViewDistributionFill; + [self addSubview:hStackView]; + [hStackView autoVCenterInSuperview]; + [hStackView autoPinLeadingToSuperviewMargin]; + [hStackView autoPinTrailingToSuperviewMargin]; + // Ensure that the cell's contents never overflow the cell bounds. + [hStackView autoPinEdgeToSuperviewMargin:ALEdgeTop relation:NSLayoutRelationGreaterThanOrEqual]; + [hStackView autoPinEdgeToSuperviewMargin:ALEdgeBottom relation:NSLayoutRelationGreaterThanOrEqual]; + + [self configureFonts]; +} + +- (void)configureFonts +{ + self.nameLabel.font = [UIFont ows_dynamicTypeBodyFont]; + self.profileNameLabel.font = [UIFont ows_regularFontWithSize:11.f]; + self.subtitleLabel.font = [UIFont ows_regularFontWithSize:11.f]; + self.accessoryLabel.font = [UIFont ows_mediumFontWithSize:13.f]; +} + +- (void)configureWithSignalAccount:(SignalAccount *)signalAccount contactsManager:(OWSContactsManager *)contactsManager +{ + [self configureWithRecipientId:signalAccount.recipientId contactsManager:contactsManager]; +} + +- (void)configureWithRecipientId:(NSString *)recipientId contactsManager:(OWSContactsManager *)contactsManager +{ + OWSAssert(recipientId.length > 0); + OWSAssert(contactsManager); + + // Update fonts to reflect changes to dynamic type. + [self configureFonts]; + + self.recipientId = recipientId; + self.contactsManager = contactsManager; + + self.nameLabel.attributedText = + [contactsManager formattedFullNameForRecipientId:recipientId font:self.nameLabel.font]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(otherUsersProfileDidChange:) + name:kNSNotificationName_OtherUsersProfileDidChange + object:nil]; + [self updateProfileName]; + [self updateAvatar]; + + if (self.accessoryMessage) { + self.accessoryLabel.text = self.accessoryMessage; + [self setAccessoryView:self.accessoryLabel]; + } + + // Force layout, since imageView isn't being initally rendered on App Store optimized build. + [self layoutSubviews]; +} + +- (void)configureWithThread:(TSThread *)thread contactsManager:(OWSContactsManager *)contactsManager +{ + OWSAssert(thread); + + // Update fonts to reflect changes to dynamic type. + [self configureFonts]; + + self.contactsManager = contactsManager; + + NSString *threadName = thread.name; + if (threadName.length == 0 && [thread isKindOfClass:[TSGroupThread class]]) { + threadName = [MessageStrings newGroupDefaultTitle]; + } + + NSAttributedString *attributedText = + [[NSAttributedString alloc] initWithString:threadName + attributes:@{ + NSForegroundColorAttributeName : [UIColor blackColor], + }]; + self.nameLabel.attributedText = attributedText; + + if ([thread isKindOfClass:[TSContactThread class]]) { + self.recipientId = thread.contactIdentifier; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(otherUsersProfileDidChange:) + name:kNSNotificationName_OtherUsersProfileDidChange + object:nil]; + [self updateProfileName]; + } + self.avatarView.image = + [OWSAvatarBuilder buildImageForThread:thread diameter:kContactCellAvatarSize contactsManager:contactsManager]; + + if (self.accessoryMessage) { + self.accessoryLabel.text = self.accessoryMessage; + [self setAccessoryView:self.accessoryLabel]; + } + + // Force layout, since imageView isn't being initally rendered on App Store optimized build. + [self layoutSubviews]; +} + +- (void)updateAvatar +{ + OWSContactsManager *contactsManager = self.contactsManager; + if (contactsManager == nil) { + OWSFail(@"%@ contactsManager should not be nil", self.logTag); + self.avatarView.image = nil; + return; + } + + NSString *recipientId = self.recipientId; + if (recipientId.length == 0) { + OWSFail(@"%@ recipientId should not be nil", self.logTag); + self.avatarView.image = nil; + return; + } + + self.avatarView.image = [[[OWSContactAvatarBuilder alloc] initWithSignalId:recipientId + diameter:kContactCellAvatarSize + contactsManager:contactsManager] build]; +} + +- (void)updateProfileName +{ + OWSContactsManager *contactsManager = self.contactsManager; + if (contactsManager == nil) { + OWSFail(@"%@ contactsManager should not be nil", self.logTag); + self.profileNameLabel.text = nil; + return; + } + + NSString *recipientId = self.recipientId; + if (recipientId.length == 0) { + OWSFail(@"%@ recipientId should not be nil", self.logTag); + self.profileNameLabel.text = nil; + return; + } + + if ([contactsManager hasNameInSystemContactsForRecipientId:recipientId]) { + // Don't display profile name when we have a veritas name in system Contacts + self.profileNameLabel.text = nil; + } else { + // Use profile name, if any is available + self.profileNameLabel.text = [contactsManager formattedProfileNameForRecipientId:recipientId]; + } + + [self.profileNameLabel setNeedsLayout]; +} + +- (void)prepareForReuse +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + self.accessoryMessage = nil; + self.nameLabel.text = nil; + self.subtitleLabel.text = nil; + self.profileNameLabel.text = nil; + self.accessoryLabel.text = nil; + for (UIView *subview in self.accessoryViewContainer.subviews) { + [subview removeFromSuperview]; + } +} + +- (void)otherUsersProfileDidChange:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + NSString *recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId]; + OWSAssert(recipientId.length > 0); + + if (recipientId.length > 0 && [self.recipientId isEqualToString:recipientId]) { + [self updateProfileName]; + [self updateAvatar]; + } +} + +- (NSAttributedString *)verifiedSubtitle +{ + NSMutableAttributedString *text = [NSMutableAttributedString new]; + // "checkmark" + [text appendAttributedString:[[NSAttributedString alloc] + initWithString:@"\uf00c " + attributes:@{ + NSFontAttributeName : + [UIFont ows_fontAwesomeFont:self.subtitleLabel.font.pointSize], + }]]; + [text appendAttributedString:[[NSAttributedString alloc] + initWithString:NSLocalizedString(@"PRIVACY_IDENTITY_IS_VERIFIED_BADGE", + @"Badge indicating that the user is verified.")]]; + return [text copy]; +} + +- (void)setAttributedSubtitle:(nullable NSAttributedString *)attributedSubtitle +{ + self.subtitleLabel.attributedText = attributedSubtitle; +} + +- (BOOL)hasAccessoryText +{ + return self.accessoryMessage.length > 0; +} + +- (void)setAccessoryView:(UIView *)accessoryView +{ + OWSAssert(accessoryView); + OWSAssert(self.accessoryViewContainer); + OWSAssert(self.accessoryViewContainer.subviews.count < 1); + + [self.accessoryViewContainer addSubview:accessoryView]; + + // Trailing-align the accessory view. + [accessoryView autoPinEdgeToSuperviewMargin:ALEdgeTop]; + [accessoryView autoPinEdgeToSuperviewMargin:ALEdgeBottom]; + [accessoryView autoPinEdgeToSuperviewMargin:ALEdgeTrailing]; + [accessoryView autoPinEdgeToSuperviewMargin:ALEdgeLeading relation:NSLayoutRelationGreaterThanOrEqual]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/Views/ContactTableViewCell.h b/SignalMessaging/Views/ContactTableViewCell.h index d0d206b1f7..0554b65210 100644 --- a/SignalMessaging/Views/ContactTableViewCell.h +++ b/SignalMessaging/Views/ContactTableViewCell.h @@ -4,26 +4,14 @@ #import "OWSContactsManager.h" -/** - * - * ContactTableViewCell displays a contact from a Contact object. - * - */ - NS_ASSUME_NONNULL_BEGIN -extern const NSUInteger kContactTableViewCellAvatarSize; -extern const CGFloat kContactTableViewCellAvatarTextMargin; - @class OWSContactsManager; @class SignalAccount; @class TSThread; @interface ContactTableViewCell : UITableViewCell -@property (nonatomic, nullable) NSString *accessoryMessage; -@property (nonatomic, readonly) UILabel *subtitle; - + (NSString *)reuseIdentifier; - (void)configureWithSignalAccount:(SignalAccount *)signalAccount contactsManager:(OWSContactsManager *)contactsManager; @@ -32,8 +20,18 @@ extern const CGFloat kContactTableViewCellAvatarTextMargin; - (void)configureWithThread:(TSThread *)thread contactsManager:(OWSContactsManager *)contactsManager; +// This method should be called _before_ the configure... methods. +- (void)setAccessoryMessage:(nullable NSString *)accessoryMessage; + +// This method should be called _after_ the configure... methods. +- (void)setAttributedSubtitle:(nullable NSAttributedString *)attributedSubtitle; + - (NSAttributedString *)verifiedSubtitle; +- (BOOL)hasAccessoryText; + +- (void)ows_setAccessoryView:(UIView *)accessoryView; + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/Views/ContactTableViewCell.m b/SignalMessaging/Views/ContactTableViewCell.m index aca1d301e4..5fa7c9cac8 100644 --- a/SignalMessaging/Views/ContactTableViewCell.m +++ b/SignalMessaging/Views/ContactTableViewCell.m @@ -3,43 +3,27 @@ // #import "ContactTableViewCell.h" -#import "Environment.h" -#import "OWSContactAvatarBuilder.h" -#import "OWSContactsManager.h" -#import "OWSUserProfile.h" +#import "ContactCellView.h" #import "UIFont+OWS.h" -#import "UIUtil.h" #import "UIView+OWS.h" -#import #import -#import -#import -#import NS_ASSUME_NONNULL_BEGIN -const NSUInteger kContactTableViewCellAvatarSize = 48; -const CGFloat kContactTableViewCellAvatarTextMargin = 12; - @interface ContactTableViewCell () -@property (nonatomic) UILabel *nameLabel; -@property (nonatomic) UILabel *profileNameLabel; -@property (nonatomic) UIImageView *avatarView; -@property (nonatomic) UILabel *subtitle; -@property (nonatomic) UIView *nameContainerView; - -@property (nonatomic) OWSContactsManager *contactsManager; -@property (nonatomic) NSString *recipientId; +@property (nonatomic) ContactCellView *cellView; @end +#pragma mark - + @implementation ContactTableViewCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier { if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { - [self configureProgrammatically]; + [self configure]; } return self; } @@ -49,105 +33,28 @@ const CGFloat kContactTableViewCellAvatarTextMargin = 12; return NSStringFromClass(self.class); } -- (void)configureProgrammatically +- (void)setAccessoryView:(nullable UIView *)accessoryView { - OWSAssert(!self.nameLabel); - - _avatarView = [AvatarImageView new]; - [self.contentView addSubview:_avatarView]; - - _nameContainerView = [UIView containerView]; - [self.contentView addSubview:_nameContainerView]; - - _nameLabel = [UILabel new]; - _nameLabel.lineBreakMode = NSLineBreakByTruncatingTail; - [_nameContainerView addSubview:_nameLabel]; - - _profileNameLabel = [UILabel new]; - _profileNameLabel.lineBreakMode = NSLineBreakByTruncatingTail; - _profileNameLabel.textColor = [UIColor grayColor]; - [_nameContainerView addSubview:_profileNameLabel]; - - _subtitle = [UILabel new]; - _subtitle.textColor = [UIColor ows_darkGrayColor]; - [_nameContainerView addSubview:self.subtitle]; - - [_avatarView autoVCenterInSuperview]; - [_avatarView autoPinLeadingToSuperviewMargin]; - [_avatarView autoSetDimension:ALDimensionWidth toSize:kContactTableViewCellAvatarSize]; - [_avatarView autoSetDimension:ALDimensionHeight toSize:kContactTableViewCellAvatarSize]; - - [_nameLabel autoPinEdgeToSuperviewEdge:ALEdgeTop]; - [_nameLabel autoPinWidthToSuperview]; - - // profileNameLabel can be zero sized, in which case nameLabel essentially occupies the totality of - // nameContainerView's frame. - [_profileNameLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:_nameLabel]; - [_profileNameLabel autoPinWidthToSuperview]; - - [_subtitle autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:_profileNameLabel]; - [_subtitle autoPinWidthToSuperview]; - [_subtitle autoPinEdgeToSuperviewEdge:ALEdgeBottom]; - - [_nameContainerView autoVCenterInSuperview]; - [_nameContainerView autoPinLeadingToTrailingEdgeOfView:_avatarView offset:kContactTableViewCellAvatarTextMargin]; - [_nameContainerView autoPinTrailingToSuperviewMargin]; - - // Ensure that the cell's contents never overflow the cell bounds. - [self.avatarView autoPinEdgeToSuperviewMargin:ALEdgeTop relation:NSLayoutRelationGreaterThanOrEqual]; - [self.avatarView autoPinEdgeToSuperviewMargin:ALEdgeBottom relation:NSLayoutRelationGreaterThanOrEqual]; - [self.nameContainerView autoPinEdgeToSuperviewMargin:ALEdgeTop relation:NSLayoutRelationGreaterThanOrEqual]; - [self.nameContainerView autoPinEdgeToSuperviewMargin:ALEdgeBottom relation:NSLayoutRelationGreaterThanOrEqual]; - - [self configureFonts]; - - // Force layout, since imageView isn't being initally rendered on App Store optimized build. - [self layoutSubviews]; + OWSFail(@"%@ use ows_setAccessoryView instead.", self.logTag); } -- (void)configureFonts +- (void)configure { - self.nameLabel.font = [UIFont ows_dynamicTypeBodyFont]; - self.profileNameLabel.font = [UIFont ows_regularFontWithSize:11.f]; - self.subtitle.font = [UIFont ows_regularFontWithSize:11.f]; + OWSAssert(!self.cellView); + + self.cellView = [ContactCellView new]; + [self.contentView addSubview:self.cellView]; + [self.cellView autoPinEdgesToSuperviewMargins]; } - (void)configureWithSignalAccount:(SignalAccount *)signalAccount contactsManager:(OWSContactsManager *)contactsManager { - [self configureWithRecipientId:signalAccount.recipientId contactsManager:contactsManager]; + [self.cellView configureWithRecipientId:signalAccount.recipientId contactsManager:contactsManager]; } - (void)configureWithRecipientId:(NSString *)recipientId contactsManager:(OWSContactsManager *)contactsManager { - OWSAssert(recipientId.length > 0); - OWSAssert(contactsManager); - - // Update fonts to reflect changes to dynamic type. - [self configureFonts]; - - self.recipientId = recipientId; - self.contactsManager = contactsManager; - - self.nameLabel.attributedText = - [contactsManager formattedFullNameForRecipientId:recipientId font:self.nameLabel.font]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(otherUsersProfileDidChange:) - name:kNSNotificationName_OtherUsersProfileDidChange - object:nil]; - [self updateProfileName]; - [self updateAvatar]; - - if (self.accessoryMessage) { - UILabel *blockedLabel = [[UILabel alloc] init]; - blockedLabel.textAlignment = NSTextAlignmentRight; - blockedLabel.text = self.accessoryMessage; - blockedLabel.font = [UIFont ows_mediumFontWithSize:13.f]; - blockedLabel.textColor = [UIColor colorWithWhite:0.5f alpha:1.f]; - [blockedLabel sizeToFit]; - - self.accessoryView = blockedLabel; - } + [self.cellView configureWithRecipientId:recipientId contactsManager:contactsManager]; // Force layout, since imageView isn't being initally rendered on App Store optimized build. [self layoutSubviews]; @@ -157,140 +64,47 @@ const CGFloat kContactTableViewCellAvatarTextMargin = 12; { OWSAssert(thread); - // Update fonts to reflect changes to dynamic type. - [self configureFonts]; + [self.cellView configureWithThread:thread contactsManager:contactsManager]; - self.contactsManager = contactsManager; - - NSString *threadName = thread.name; - if (threadName.length == 0 && [thread isKindOfClass:[TSGroupThread class]]) { - threadName = [MessageStrings newGroupDefaultTitle]; - } - - NSAttributedString *attributedText = - [[NSAttributedString alloc] initWithString:threadName - attributes:@{ - NSForegroundColorAttributeName : [UIColor blackColor], - }]; - self.nameLabel.attributedText = attributedText; - - if ([thread isKindOfClass:[TSContactThread class]]) { - self.recipientId = thread.contactIdentifier; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(otherUsersProfileDidChange:) - name:kNSNotificationName_OtherUsersProfileDidChange - object:nil]; - [self updateProfileName]; - } - self.avatarView.image = [OWSAvatarBuilder buildImageForThread:thread - diameter:kContactTableViewCellAvatarSize - contactsManager:contactsManager]; - - if (self.accessoryMessage) { - UILabel *blockedLabel = [[UILabel alloc] init]; - blockedLabel.textAlignment = NSTextAlignmentRight; - blockedLabel.text = self.accessoryMessage; - blockedLabel.font = [UIFont ows_mediumFontWithSize:13.f]; - blockedLabel.textColor = [UIColor colorWithWhite:0.5f alpha:1.f]; - [blockedLabel sizeToFit]; - - self.accessoryView = blockedLabel; - } - // Force layout, since imageView isn't being initally rendered on App Store optimized build. [self layoutSubviews]; } +- (void)setAccessoryMessage:(nullable NSString *)accessoryMessage +{ + OWSAssert(self.cellView); + + self.cellView.accessoryMessage = accessoryMessage; +} + - (NSAttributedString *)verifiedSubtitle { - NSMutableAttributedString *text = [NSMutableAttributedString new]; - // "checkmark" - [text appendAttributedString:[[NSAttributedString alloc] - initWithString:@"\uf00c " - attributes:@{ - NSFontAttributeName : - [UIFont ows_fontAwesomeFont:self.subtitle.font.pointSize], - }]]; - [text appendAttributedString:[[NSAttributedString alloc] - initWithString:NSLocalizedString(@"PRIVACY_IDENTITY_IS_VERIFIED_BADGE", - @"Badge indicating that the user is verified.")]]; - return [text copy]; + return self.cellView.verifiedSubtitle; } -- (void)updateAvatar +- (void)setAttributedSubtitle:(nullable NSAttributedString *)attributedSubtitle { - OWSContactsManager *contactsManager = self.contactsManager; - if (contactsManager == nil) { - OWSFail(@"%@ contactsManager should not be nil", self.logTag); - self.avatarView.image = nil; - return; - } - - NSString *recipientId = self.recipientId; - if (recipientId.length == 0) { - OWSFail(@"%@ recipientId should not be nil", self.logTag); - self.avatarView.image = nil; - return; - } - - self.avatarView.image = [[[OWSContactAvatarBuilder alloc] initWithSignalId:recipientId - diameter:kContactTableViewCellAvatarSize - contactsManager:contactsManager] build]; -} - -- (void)updateProfileName -{ - OWSContactsManager *contactsManager = self.contactsManager; - if (contactsManager == nil) { - OWSFail(@"%@ contactsManager should not be nil", self.logTag); - self.profileNameLabel.text = nil; - return; - } - - NSString *recipientId = self.recipientId; - if (recipientId.length == 0) { - OWSFail(@"%@ recipientId should not be nil", self.logTag); - self.profileNameLabel.text = nil; - return; - } - - if ([contactsManager hasNameInSystemContactsForRecipientId:recipientId]) { - // Don't display profile name when we have a veritas name in system Contacts - self.profileNameLabel.text = nil; - } else { - // Use profile name, if any is available - self.profileNameLabel.text = [contactsManager formattedProfileNameForRecipientId:recipientId]; - } - - [self.profileNameLabel setNeedsLayout]; + [self.cellView setAttributedSubtitle:attributedSubtitle]; } - (void)prepareForReuse { [super prepareForReuse]; - [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self.cellView prepareForReuse]; - self.accessoryMessage = nil; self.accessoryView = nil; self.accessoryType = UITableViewCellAccessoryNone; - self.nameLabel.text = nil; - self.subtitle.text = nil; - self.profileNameLabel.text = nil; } -- (void)otherUsersProfileDidChange:(NSNotification *)notification +- (BOOL)hasAccessoryText { - OWSAssertIsOnMainThread(); + return [self.cellView hasAccessoryText]; +} - NSString *recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId]; - OWSAssert(recipientId.length > 0); - - if (recipientId.length > 0 && [self.recipientId isEqualToString:recipientId]) { - [self updateProfileName]; - [self updateAvatar]; - } +- (void)ows_setAccessoryView:(UIView *)accessoryView +{ + return [self.cellView setAccessoryView:accessoryView]; } @end diff --git a/SignalMessaging/contacts/SelectThreadViewController.m b/SignalMessaging/contacts/SelectThreadViewController.m index a14e2d5c49..29f89bb591 100644 --- a/SignalMessaging/contacts/SelectThreadViewController.m +++ b/SignalMessaging/contacts/SelectThreadViewController.m @@ -208,7 +208,7 @@ NS_ASSUME_NONNULL_BEGIN [cell configureWithThread:thread contactsManager:helper.contactsManager]; - if (cell.accessoryView == nil) { + if (!cell.hasAccessoryText) { // Don't add a disappearing messages indicator if we've already added a "blocked" label. __block OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration; [self.uiDatabaseConnection @@ -227,7 +227,7 @@ NS_ASSUME_NONNULL_BEGIN disappearingTimerConfigurationView.tintColor = [UIColor colorWithWhite:0.5f alpha:1.f]; - cell.accessoryView = disappearingTimerConfigurationView; + [cell ows_setAccessoryView:disappearingTimerConfigurationView]; } } @@ -282,8 +282,6 @@ NS_ASSUME_NONNULL_BEGIN if (isBlocked) { cell.accessoryMessage = NSLocalizedString( @"CONTACT_CELL_IS_BLOCKED", @"An indicator that a contact has been blocked."); - } else { - OWSAssert(cell.accessoryMessage == nil); } [cell configureWithSignalAccount:signalAccount contactsManager:helper.contactsManager]; return cell;