diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.m b/Signal/src/ViewControllers/HomeView/HomeViewCell.m index efc8f5bcf..ffa39e5b1 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.m @@ -18,13 +18,15 @@ NS_ASSUME_NONNULL_BEGIN @interface HomeViewCell () @property (nonatomic) AvatarImageView *avatarView; -@property (nonatomic) UIStackView *payloadView; @property (nonatomic) UIStackView *topRowView; @property (nonatomic) UILabel *nameLabel; @property (nonatomic) UILabel *snippetLabel; @property (nonatomic) UILabel *dateTimeLabel; +@property (nonatomic) UIImageView *messageStatusView; + @property (nonatomic) UIView *unreadBadge; @property (nonatomic) UILabel *unreadLabel; +@property (nonatomic) UIView *unreadBadgeContainer; @property (nonatomic, nullable) ThreadViewModel *thread; @property (nonatomic, nullable) OWSContactsManager *contactsManager; @@ -75,16 +77,6 @@ NS_ASSUME_NONNULL_BEGIN [self.avatarView autoPinEdgeToSuperviewMargin:ALEdgeTop relation:NSLayoutRelationGreaterThanOrEqual]; [self.avatarView autoPinEdgeToSuperviewMargin:ALEdgeBottom relation:NSLayoutRelationGreaterThanOrEqual]; - self.payloadView = [UIStackView new]; - self.payloadView.axis = UILayoutConstraintAxisVertical; - [self.contentView addSubview:self.payloadView]; - [self.payloadView autoPinLeadingToTrailingEdgeOfView:self.avatarView offset:self.avatarHSpacing]; - [self.payloadView autoVCenterInSuperview]; - // Ensure that the cell's contents never overflow the cell bounds. - [self.payloadView autoPinEdgeToSuperviewMargin:ALEdgeTop relation:NSLayoutRelationGreaterThanOrEqual]; - [self.payloadView autoPinEdgeToSuperviewMargin:ALEdgeBottom relation:NSLayoutRelationGreaterThanOrEqual]; - // We pin the payloadView traillingEdge later, as part of the "Unread Badge" logic. - self.nameLabel = [UILabel new]; self.nameLabel.lineBreakMode = NSLineBreakByTruncatingTail; self.nameLabel.font = self.nameFont; @@ -95,35 +87,69 @@ NS_ASSUME_NONNULL_BEGIN [self.dateTimeLabel setContentHuggingHorizontalHigh]; [self.dateTimeLabel setCompressionResistanceHorizontalHigh]; + self.messageStatusView = [UIImageView new]; + [self.messageStatusView setContentHuggingHorizontalHigh]; + [self.messageStatusView setCompressionResistanceHorizontalHigh]; + self.topRowView = [[UIStackView alloc] initWithArrangedSubviews:@[ self.nameLabel, self.dateTimeLabel, + self.messageStatusView, ]]; self.topRowView.axis = UILayoutConstraintAxisHorizontal; self.topRowView.alignment = UIStackViewAlignmentLastBaseline; - [self.payloadView addArrangedSubview:self.topRowView]; + self.topRowView.spacing = 6.f; self.snippetLabel = [UILabel new]; self.snippetLabel.font = [self snippetFont]; self.snippetLabel.numberOfLines = 1; self.snippetLabel.lineBreakMode = NSLineBreakByTruncatingTail; - [self.payloadView addArrangedSubview:self.snippetLabel]; [self.snippetLabel setContentHuggingHorizontalLow]; [self.snippetLabel setCompressionResistanceHorizontalLow]; + UIStackView *vStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.topRowView, + self.snippetLabel, + ]]; + vStackView.axis = UILayoutConstraintAxisVertical; + self.unreadLabel = [UILabel new]; - self.unreadLabel.textColor = [UIColor whiteColor]; + self.unreadLabel.textColor = [UIColor ows_whiteColor]; self.unreadLabel.lineBreakMode = NSLineBreakByTruncatingTail; self.unreadLabel.textAlignment = NSTextAlignmentCenter; + [self.unreadLabel setContentHuggingHigh]; + [self.unreadLabel setCompressionResistanceHigh]; self.unreadBadge = [NeverClearView new]; self.unreadBadge.backgroundColor = [UIColor ows_materialBlueColor]; [self.unreadBadge addSubview:self.unreadLabel]; [self.unreadLabel autoCenterInSuperview]; - [self.unreadLabel setContentHuggingHigh]; - [self.unreadLabel setCompressionResistanceHigh]; + [self.unreadBadge setContentHuggingHigh]; + [self.unreadBadge setCompressionResistanceHigh]; + + self.unreadBadgeContainer = [UIView containerView]; + [self.unreadBadgeContainer addSubview:self.unreadBadge]; + [self.unreadBadge autoPinWidthToSuperview]; + [self.unreadBadgeContainer setContentHuggingHigh]; + [self.unreadBadgeContainer setCompressionResistanceHigh]; + + UIStackView *hStackView = [[UIStackView alloc] initWithArrangedSubviews:@[ + vStackView, + self.unreadBadgeContainer, + ]]; + hStackView.axis = UILayoutConstraintAxisHorizontal; + hStackView.spacing = 6.f; + [self.contentView addSubview:hStackView]; + [hStackView autoPinLeadingToTrailingEdgeOfView:self.avatarView offset:self.avatarHSpacing]; + [hStackView autoVCenterInSuperview]; + // Ensure that the cell's contents never overflow the cell bounds. + [hStackView autoPinEdgeToSuperviewMargin:ALEdgeTop relation:NSLayoutRelationGreaterThanOrEqual]; + [hStackView autoPinEdgeToSuperviewMargin:ALEdgeBottom relation:NSLayoutRelationGreaterThanOrEqual]; + [hStackView autoPinTrailingToSuperviewMargin]; + + [self.unreadBadge autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.nameLabel]; - self.payloadView.userInteractionEnabled = NO; + hStackView.userInteractionEnabled = NO; } - (void)dealloc @@ -180,9 +206,6 @@ NS_ASSUME_NONNULL_BEGIN [self updateNameLabel]; [self updateAvatarView]; - self.payloadView.spacing = 0.f; - self.topRowView.spacing = self.topRowHSpacing; - // We update the fonts every time this cell is configured to ensure that // changes to the dynamic type settings are reflected. self.snippetLabel.font = [self snippetFont]; @@ -197,19 +220,26 @@ NS_ASSUME_NONNULL_BEGIN self.dateTimeLabel.text = (overrideDate ? [self stringForDate:overrideDate] : [self stringForDate:thread.lastMessageDate]); + UIColor *textColor = [UIColor ows_light60Color]; if (hasUnreadMessages && overrideSnippet == nil) { - self.dateTimeLabel.textColor = [UIColor ows_blackColor]; - self.dateTimeLabel.font = self.unreadFont.ows_mediumWeight; + textColor = [UIColor ows_light90Color]; + self.dateTimeLabel.font = self.dateTimeFont.ows_mediumWeight; } else { - self.dateTimeLabel.textColor = [UIColor lightGrayColor]; - self.dateTimeLabel.font = self.unreadFont; + self.dateTimeLabel.font = self.dateTimeFont; } + self.dateTimeLabel.textColor = textColor; NSUInteger unreadCount = thread.unreadCount; - if (unreadCount == 0 || overrideSnippet != nil) { - [self.viewConstraints addObject:[self.payloadView autoPinTrailingToSuperviewMargin]]; - } else { - [self.contentView addSubview:self.unreadBadge]; + if (overrideSnippet) { + // If we're using the home view cell to render search results, + // don't show "unread badge" or "message status" indicator. + self.unreadBadgeContainer.hidden = YES; + self.messageStatusView.hidden = YES; + } else if (unreadCount > 0) { + // If there are unread messages, show the "unread badge." + // The "message status" indicators is redundant. + self.unreadBadgeContainer.hidden = NO; + self.messageStatusView.hidden = YES; self.unreadLabel.text = [OWSFormat formatInt:(int)unreadCount]; self.unreadLabel.font = self.unreadFont; @@ -236,28 +266,54 @@ NS_ASSUME_NONNULL_BEGIN [self.unreadBadge autoSetDimension:ALDimensionHeight toSize:unreadBadgeHeight], ]]; }]; - - const CGFloat kMinVMargin = 5; - [self.viewConstraints addObjectsFromArray:@[ - // Horizontally, badge is inserted after the tail of the payloadView, pushing back the date *and* snippet - // view - [self.payloadView autoPinEdge:ALEdgeTrailing - toEdge:ALEdgeLeading - ofView:self.unreadBadge - withOffset:-self.topRowHSpacing], - [self.unreadBadge autoPinTrailingToSuperviewMargin], - [self.unreadBadge autoPinEdgeToSuperviewEdge:ALEdgeTop - withInset:kMinVMargin - relation:NSLayoutRelationGreaterThanOrEqual], - [self.unreadBadge autoPinEdgeToSuperviewEdge:ALEdgeBottom - withInset:kMinVMargin - relation:NSLayoutRelationGreaterThanOrEqual], - - // Vertically, badge is positioned vertically by aligning it's label *subview's* baseline. - // This allows us a single visual baseline of text across the top row across [name, dateTime, - // optional(unread count)] - [self.unreadLabel autoAlignAxis:ALAxisBaseline toSameAxisOfView:self.dateTimeLabel] - ]]; + } else { + UIImage *_Nullable statusIndicatorImage = nil; + UIColor *messageStatusViewTintColor = textColor; + BOOL shouldAnimateStatusIcon = NO; + if ([self.thread.lastMessageForInbox isKindOfClass:[TSOutgoingMessage class]]) { + TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.thread.lastMessageForInbox; + + MessageReceiptStatus messageStatus = + [MessageRecipientStatusUtils recipientStatusWithOutgoingMessage:outgoingMessage]; + switch (messageStatus) { + case MessageReceiptStatusUploading: + case MessageReceiptStatusSending: + statusIndicatorImage = [UIImage imageNamed:@"message_status_sending"]; + shouldAnimateStatusIcon = YES; + break; + case MessageReceiptStatusSent: + case MessageReceiptStatusSkipped: + statusIndicatorImage = [UIImage imageNamed:@"message_status_sent"]; + break; + case MessageReceiptStatusDelivered: + case MessageReceiptStatusRead: + statusIndicatorImage = [UIImage imageNamed:@"message_status_delivered"]; + break; + case MessageReceiptStatusFailed: + // TODO: + statusIndicatorImage = [UIImage imageNamed:@"message_status_sending"]; + break; + } + if (messageStatus == MessageReceiptStatusRead) { + messageStatusViewTintColor = [UIColor ows_signalBlueColor]; + } + } + self.messageStatusView.image = [statusIndicatorImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + self.messageStatusView.tintColor = messageStatusViewTintColor; + self.messageStatusView.hidden = statusIndicatorImage == nil; + self.unreadBadgeContainer.hidden = YES; + if (shouldAnimateStatusIcon) { + CABasicAnimation *animation; + animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; + animation.toValue = @(M_PI * 2.0); + const CGFloat kPeriodSeconds = 1.f; + animation.duration = kPeriodSeconds; + animation.cumulative = YES; + animation.repeatCount = HUGE_VALF; + [self.messageStatusView.layer addAnimation:animation forKey:@"animation"]; + } else { + [self.messageStatusView.layer removeAllAnimations]; + } } } @@ -303,7 +359,7 @@ NS_ASSUME_NONNULL_BEGIN @"A label for conversations with blocked users.") attributes:@{ NSFontAttributeName : self.snippetFont.ows_mediumWeight, - NSForegroundColorAttributeName : [UIColor ows_blackColor], + NSForegroundColorAttributeName : [UIColor ows_light90Color], }]]; } else { if ([thread isMuted]) { @@ -313,7 +369,7 @@ NS_ASSUME_NONNULL_BEGIN NSFontAttributeName : [UIFont ows_elegantIconsFont:9.f], NSForegroundColorAttributeName : (hasUnreadMessages ? [UIColor colorWithWhite:0.1f alpha:1.f] - : [UIColor lightGrayColor]), + : [UIColor ows_light60Color]), }]]; } NSString *displayableText = thread.lastMessageText; @@ -325,8 +381,8 @@ NS_ASSUME_NONNULL_BEGIN (hasUnreadMessages ? self.snippetFont.ows_mediumWeight : self.snippetFont), NSForegroundColorAttributeName : - (hasUnreadMessages ? [UIColor ows_blackColor] - : [UIColor lightGrayColor]), + (hasUnreadMessages ? [UIColor ows_light90Color] + : [UIColor ows_light60Color]), }]]; } } @@ -353,6 +409,11 @@ NS_ASSUME_NONNULL_BEGIN return [UIFont ows_dynamicTypeCaption1Font].ows_mediumWeight; } +- (UIFont *)dateTimeFont +{ + return [UIFont ows_dynamicTypeCaption1Font]; +} + - (UIFont *)snippetFont { return [UIFont ows_dynamicTypeSubheadlineFont]; @@ -369,22 +430,6 @@ NS_ASSUME_NONNULL_BEGIN return [UIFont ows_dynamicTypeFootnoteFont]; } -// A simple function to scale dimensions to reflect dynamic type. Given a value -// we lerp it larger linearly to reflect size of dynamic type relative to a -// reference value for default dynamic type sizes. -// -// * We _NEVER_ scale values down. -// * We cap scaling. -+ (CGFloat)scaleValueWithDynamicType:(CGFloat)minValue -{ - // The default size of dynamic "body" type. - const NSUInteger kReferenceFontSizeMin = 17.f; - - CGFloat referenceFontSize = UIFont.ows_dynamicTypeBodyFont.pointSize; - CGFloat alpha = CGFloatClamp(referenceFontSize / kReferenceFontSizeMin, 1.f, 1.3f); - return minValue * alpha; -} - - (NSUInteger)avatarSize { return 48.f; @@ -395,12 +440,6 @@ NS_ASSUME_NONNULL_BEGIN return 12.f; } -// Using an NSUInteger precludes us from negating this value -- (CGFloat)topRowHSpacing -{ - return ceil([HomeViewCell scaleValueWithDynamicType:5]); -} - #pragma mark - Reuse - (void)prepareForReuse @@ -414,8 +453,6 @@ NS_ASSUME_NONNULL_BEGIN self.contactsManager = nil; self.avatarView.image = nil; - [self.unreadBadge removeFromSuperview]; - [[NSNotificationCenter defaultCenter] removeObserver:self]; } diff --git a/SignalMessaging/ViewModels/ThreadViewModel.swift b/SignalMessaging/ViewModels/ThreadViewModel.swift index 80267e368..36cbcd0df 100644 --- a/SignalMessaging/ViewModels/ThreadViewModel.swift +++ b/SignalMessaging/ViewModels/ThreadViewModel.swift @@ -14,11 +14,13 @@ public class ThreadViewModel: NSObject { @objc public let contactIdentifier: String? @objc public let name: String @objc public let isMuted: Bool + var isContactThread: Bool { return !isGroupThread } @objc public let lastMessageText: String? + @objc public let lastMessageForInbox: TSInteraction? @objc public init(thread: TSThread, transaction: YapDatabaseReadTransaction) { @@ -28,6 +30,7 @@ public class ThreadViewModel: NSObject { self.name = thread.name() self.isMuted = thread.isMuted self.lastMessageText = thread.lastMessageText(transaction: transaction) + self.lastMessageForInbox = thread.lastInteractionForInbox(transaction: transaction) if let contactThread = thread as? TSContactThread { self.contactIdentifier = contactThread.contactIdentifier() diff --git a/SignalServiceKit/src/Contacts/TSThread.h b/SignalServiceKit/src/Contacts/TSThread.h index e6bf79886..3adefacbf 100644 --- a/SignalServiceKit/src/Contacts/TSThread.h +++ b/SignalServiceKit/src/Contacts/TSThread.h @@ -84,6 +84,9 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)lastMessageTextWithTransaction:(YapDatabaseReadTransaction *)transaction NS_SWIFT_NAME(lastMessageText(transaction:)); +- (nullable TSInteraction *)lastInteractionForInboxWithTransaction:(YapDatabaseReadTransaction *)transaction + NS_SWIFT_NAME(lastInteractionForInbox(transaction:)); + /** * Updates the thread's caches of the latest interaction. * diff --git a/SignalServiceKit/src/Contacts/TSThread.m b/SignalServiceKit/src/Contacts/TSThread.m index 1c8cf546d..d0ef2ea89 100644 --- a/SignalServiceKit/src/Contacts/TSThread.m +++ b/SignalServiceKit/src/Contacts/TSThread.m @@ -246,8 +246,10 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert([self unseenMessagesWithTransaction:transaction].count < 1); } -- (TSInteraction *)lastInteractionForInboxWithTransaction:(YapDatabaseReadTransaction *)transaction +- (nullable TSInteraction *)lastInteractionForInboxWithTransaction:(YapDatabaseReadTransaction *)transaction { + OWSAssert(transaction); + __block NSUInteger missedCount = 0; __block TSInteraction *last = nil; [[transaction ext:TSMessageDatabaseViewExtensionName]