diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h index 44df14689..8bda025ca 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h @@ -8,6 +8,7 @@ NS_ASSUME_NONNULL_BEGIN @class ConversationViewCell; @class ConversationViewItem; @class OWSContactOffersInteraction; +@class OWSContactsManager; @class TSAttachmentPointer; @class TSAttachmentStream; @class TSInteraction; @@ -47,6 +48,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)didTapFailedOutgoingMessage:(TSOutgoingMessage *)message; +#pragma mark - Contacts + +- (OWSContactsManager *)contactsManager; + @end #pragma mark - diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 07a8606a1..39729d483 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -3,6 +3,7 @@ // #import "OWSMessageCell.h" +#import "OWSContactAvatarBuilder.h" #import "OWSExpirationTimerView.h" #import "OWSMessageBubbleView.h" #import "Signal-Swift.h" @@ -24,6 +25,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) OWSMessageBubbleView *messageBubbleView; @property (nonatomic) UILabel *dateHeaderLabel; @property (nonatomic) UIView *footerView; +@property (nonatomic) AvatarImageView *avatarView; @property (nonatomic) UILabel *footerLabel; @property (nonatomic, nullable) OWSExpirationTimerView *expirationTimerView; @@ -71,9 +73,15 @@ NS_ASSUME_NONNULL_BEGIN self.footerLabel.textColor = [UIColor lightGrayColor]; [self.footerView addSubview:self.footerLabel]; + self.avatarView = [[AvatarImageView alloc] init]; + [self.contentView addSubview:self.avatarView]; + [self.avatarView autoSetDimension:ALDimensionWidth toSize:self.avatarSize]; + [self.avatarView autoSetDimension:ALDimensionHeight toSize:self.avatarSize]; + // Hide these views by default. self.dateHeaderLabel.hidden = YES; self.footerLabel.hidden = YES; + self.avatarView.hidden = YES; [self.messageBubbleView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderLabel]; @@ -95,6 +103,11 @@ NS_ASSUME_NONNULL_BEGIN [self addGestureRecognizer:panGesture]; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + - (void)setLayoutInfo:(nullable ConversationLayoutInfo *)layoutInfo { [super setLayoutInfo:layoutInfo]; @@ -168,6 +181,22 @@ NS_ASSUME_NONNULL_BEGIN [self updateDateHeader]; [self updateFooter]; + + if ([self updateAvatarView]) { + CGFloat avatarBottomMargin = round(self.layoutInfo.lastTextLineAxis - self.avatarSize * 0.5f); + [self.viewConstraints addObjectsFromArray:@[ + // V-align the "group sender" avatar with the + // last line of the text (if any, or where it + // would be). + [self.messageBubbleView autoPinLeadingToTrailingEdgeOfView:self.avatarView offset:8], + [self.messageBubbleView autoPinEdge:ALEdgeBottom + toEdge:ALEdgeBottom + ofView:self.avatarView + withOffset:avatarBottomMargin], + ]]; + [self.messageBubbleView logFrameLaterWithLabel:@"messageBubbleView"]; + [self.avatarView logFrameLaterWithLabel:@"avatarView"]; + } } // * If cell is visible, lazy-load (expensive) view contents. @@ -382,6 +411,74 @@ NS_ASSUME_NONNULL_BEGIN return UIFont.ows_dynamicTypeCaption1Font; } +#pragma mark - Avatar + +// Returns YES IFF the avatar view is appropriate and configured. +- (BOOL)updateAvatarView +{ + if (!self.viewItem.isGroupThread) { + return NO; + } + if (self.viewItem.interaction.interactionType != OWSInteractionType_IncomingMessage) { + return NO; + } + if (self.viewItem.shouldHideAvatar) { + return NO; + } + + OWSContactsManager *contactsManager = self.delegate.contactsManager; + if (contactsManager == nil) { + OWSFail(@"%@ contactsManager should not be nil", self.logTag); + return NO; + } + + TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.viewItem.interaction; + OWSAvatarBuilder *avatarBuilder = [[OWSContactAvatarBuilder alloc] initWithSignalId:incomingMessage.authorId + diameter:self.avatarSize + contactsManager:contactsManager]; + self.avatarView.image = [avatarBuilder build]; + self.avatarView.hidden = NO; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(otherUsersProfileDidChange:) + name:kNSNotificationName_OtherUsersProfileDidChange + object:nil]; + + return YES; +} + +- (NSUInteger)avatarSize +{ + return 24.f; +} + +- (void)otherUsersProfileDidChange:(NSNotification *)notification +{ + OWSAssertIsOnMainThread(); + + if (!self.viewItem.isGroupThread) { + return; + } + if (self.viewItem.interaction.interactionType != OWSInteractionType_IncomingMessage) { + return; + } + if (self.viewItem.shouldHideAvatar) { + return; + } + + NSString *recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId]; + if (recipientId.length == 0) { + return; + } + TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.viewItem.interaction; + + if (![incomingMessage.authorId isEqualToString:recipientId]) { + return; + } + + [self updateAvatarView]; +} + #pragma mark - Measurement - (CGSize)cellSizeWithTransaction:(YapDatabaseReadTransaction *)transaction @@ -421,7 +518,7 @@ NS_ASSUME_NONNULL_BEGIN } } -#pragma mark - +#pragma mark - Reuse - (void)prepareForReuse { @@ -437,12 +534,16 @@ NS_ASSUME_NONNULL_BEGIN self.dateHeaderLabel.hidden = YES; self.footerLabel.text = nil; self.footerLabel.hidden = YES; + self.avatarView.image = nil; + self.avatarView.hidden = YES; [self.expirationTimerView clearAnimations]; [self.expirationTimerView removeFromSuperview]; self.expirationTimerView = nil; [self hideMenuControllerIfNecessary]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Notifications @@ -673,11 +774,6 @@ NS_ASSUME_NONNULL_BEGIN self.isPresentingMenuController = NO; } -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationLayoutInfo.swift b/Signal/src/ViewControllers/ConversationView/ConversationLayoutInfo.swift index e77b8b15a..4ff22fa21 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationLayoutInfo.swift +++ b/Signal/src/ViewControllers/ConversationView/ConversationLayoutInfo.swift @@ -70,6 +70,14 @@ public class ConversationLayoutInfo: NSObject { @objc public var textInsets = OWSDirectionalEdgeInsets.zero + // We want to align "group sender" avatars with the v-center of the + // "last line" of the message body text - or where it would be for + // non-text content. + // + // This is the distance from that v-center to the bottom of the + // message bubble. + @objc public var lastTextLineAxis: CGFloat = 0 + @objc public required init(thread: TSThread) { @@ -131,5 +139,6 @@ public class ConversationLayoutInfo: NSObject { leading: 12, bottom: textInsetBottom, trailing: 12) + lastTextLineAxis = CGFloat(round(12 + messageTextFont.capHeight * 0.5)) } } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 5f85bf309..f866e5a90 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -4882,7 +4882,7 @@ typedef enum : NSUInteger { NSString *_Nullable lastIncomingSenderId = nil; for (ConversationViewItem *viewItem in viewItems.reverseObjectEnumerator) { BOOL shouldHideRecipientStatus = NO; - BOOL shouldHideBubbleTail = NO; + BOOL shouldHideAvatar = NO; OWSInteractionType interactionType = viewItem.interaction.interactionType; if (interactionType == OWSInteractionType_OutgoingMessage) { @@ -4899,14 +4899,12 @@ typedef enum : NSUInteger { = (interactionType == lastInteractionType && receiptStatus == lastReceiptStatus); } - shouldHideBubbleTail = interactionType == lastInteractionType; - lastReceiptStatus = receiptStatus; } else if (interactionType == OWSInteractionType_IncomingMessage) { TSIncomingMessage *incomingMessage = (TSIncomingMessage *)viewItem.interaction; NSString *incomingSenderId = incomingMessage.authorId; OWSAssert(incomingSenderId.length > 0); - shouldHideBubbleTail = (interactionType == lastInteractionType && + shouldHideAvatar = (interactionType == lastInteractionType && [NSObject isNullableObject:lastIncomingSenderId equalTo:incomingSenderId]); lastIncomingSenderId = incomingSenderId; } @@ -4919,7 +4917,7 @@ typedef enum : NSUInteger { [rowsThatChangedSize addObject:@(viewItem.previousRow)]; } viewItem.shouldHideRecipientStatus = shouldHideRecipientStatus; - viewItem.shouldHideBubbleTail = shouldHideBubbleTail; + viewItem.shouldHideAvatar = shouldHideAvatar; } self.viewItems = viewItems; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index bfa67c8a6..cf3b0de71 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -56,7 +56,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @property (nonatomic) BOOL shouldShowDate; @property (nonatomic) BOOL shouldHideRecipientStatus; -@property (nonatomic) BOOL shouldHideBubbleTail; +// Used to suppress "group sender" avatars. +@property (nonatomic) BOOL shouldHideAvatar; @property (nonatomic) NSInteger row; // During updates, we sometimes need the previous row index diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 757e4e201..cf4b9eea2 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -162,13 +162,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self clearCachedLayoutState]; } -- (void)setShouldHideBubbleTail:(BOOL)shouldHideBubbleTail +- (void)setShouldHideAvatar:(BOOL)shouldHideAvatar { - if (_shouldHideBubbleTail == shouldHideBubbleTail) { + if (_shouldHideAvatar == shouldHideAvatar) { return; } - _shouldHideBubbleTail = shouldHideBubbleTail; + _shouldHideAvatar = shouldHideAvatar; [self clearCachedLayoutState]; } diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.m b/Signal/src/ViewControllers/HomeView/HomeViewCell.m index 38471b801..abc1af63c 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.m @@ -64,7 +64,6 @@ NS_ASSUME_NONNULL_BEGIN _viewConstraints = [NSMutableArray new]; self.avatarView = [[AvatarImageView alloc] init]; - [self.contentView addSubview:self.avatarView]; [self.avatarView autoSetDimension:ALDimensionWidth toSize:self.avatarSize]; [self.avatarView autoSetDimension:ALDimensionHeight toSize:self.avatarSize]; @@ -125,6 +124,11 @@ NS_ASSUME_NONNULL_BEGIN [self.unreadLabel setCompressionResistanceHigh]; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + + (NSString *)cellReuseIdentifier { return NSStringFromClass([self class]); @@ -418,6 +422,7 @@ NS_ASSUME_NONNULL_BEGIN self.thread = nil; self.contactsManager = nil; + self.avatarView.image = nil; [self.unreadBadge removeFromSuperview];