From f7bd813c9f35530011ebda1556ae59e1b58442c3 Mon Sep 17 00:00:00 2001 From: Matthew Chen <charlesmchen@gmail.com> Date: Wed, 11 Oct 2017 14:45:02 -0400 Subject: [PATCH] Restore the date headers to the conversation view cells. // FREEBIE --- .../Cells/ConversationViewCell.h | 6 +- .../Cells/ConversationViewCell.m | 3 +- .../Cells/OWSContactOffersCell.m | 2 +- .../ConversationView/Cells/OWSMessageCell.m | 145 ++++++++++++++++-- .../Cells/OWSSystemMessageCell.m | 2 +- .../Cells/OWSUnreadIndicatorCell.m | 2 +- .../ConversationViewController.m | 16 +- .../ConversationView/ConversationViewLayout.h | 2 + .../ConversationView/ConversationViewLayout.m | 3 + Signal/src/util/NSAttributedString+OWS.h | 3 + Signal/src/util/NSAttributedString+OWS.m | 12 ++ 11 files changed, 162 insertions(+), 34 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h index 8d6636efe..2cae4e9f4 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h @@ -43,6 +43,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - +// TODO: Consider making this a protocol. @interface ConversationViewCell : UICollectionViewCell @property (nonatomic, nullable, weak) id<ConversationViewCellDelegate> delegate; @@ -51,10 +52,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL isCellVisible; -// If this is non-null, we should show the message date header. -@property (nonatomic, nullable) NSAttributedString *messageDateHeaderText; - -- (void)loadForDisplay; +- (void)loadForDisplay:(int)contentWidth; - (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m index cc34c4248..a168467bc 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m @@ -14,12 +14,11 @@ NS_ASSUME_NONNULL_BEGIN [super prepareForReuse]; self.viewItem = nil; - self.messageDateHeaderText = nil; self.delegate = nil; self.isCellVisible = NO; } -- (void)loadForDisplay +- (void)loadForDisplay:(int)contentWidth { OWSFail(@"%@ This method should be overridden.", self.logTag); } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m index dfc21c6f1..5dcd6171e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m @@ -87,7 +87,7 @@ NS_ASSUME_NONNULL_BEGIN return NSStringFromClass([self class]); } -- (void)loadForDisplay +- (void)loadForDisplay:(int)contentWidth { OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]); diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 055440176..8da9f8309 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -6,12 +6,12 @@ #import "AttachmentSharing.h" #import "AttachmentUploadView.h" #import "ConversationViewItem.h" +#import "NSAttributedString+OWS.h" #import "OWSAudioMessageView.h" #import "OWSGenericAttachmentView.h" -#import "UIColor+OWS.h" - -//#import <AssetsLibrary/AssetsLibrary.h> #import "Signal-Swift.h" +#import "UIColor+OWS.h" +#import <JSQMessagesViewController/JSQMessagesTimestampFormatter.h> #import <JSQMessagesViewController/UIColor+JSQMessages.h> //#import "OWSExpirationTimerView.h" @@ -21,6 +21,8 @@ NS_ASSUME_NONNULL_BEGIN @interface OWSMessageCell () // The text label is used so frequently that we always keep one around. +@property (nonatomic) UIView *payloadView; +@property (nonatomic) UILabel *dateHeaderLabel; @property (nonatomic) UILabel *textLabel; @property (nonatomic, nullable) UIImageView *bubbleImageView; @property (nonatomic, nullable) AttachmentUploadView *attachmentUploadView; @@ -30,6 +32,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) AttachmentPointerView *attachmentPointerView; @property (nonatomic, nullable) OWSGenericAttachmentView *attachmentView; @property (nonatomic, nullable) OWSAudioMessageView *audioMessageView; +@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *dateHeaderConstraints; @property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *contentConstraints; //@property (strong, nonatomic) OWSExpirationTimerView *expirationTimerView; @@ -58,10 +61,19 @@ NS_ASSUME_NONNULL_BEGIN self.contentView.backgroundColor = [UIColor whiteColor]; + self.payloadView = [UIView containerView]; + [self.contentView addSubview:self.payloadView]; + + self.dateHeaderLabel = [UILabel new]; + self.dateHeaderLabel.font = [UIFont ows_regularFontWithSize:16.f]; + self.dateHeaderLabel.textAlignment = NSTextAlignmentCenter; + self.dateHeaderLabel.textColor = [UIColor lightGrayColor]; + [self.contentView addSubview:self.dateHeaderLabel]; + self.bubbleImageView = [UIImageView new]; self.bubbleImageView.layoutMargins = UIEdgeInsetsZero; self.bubbleImageView.userInteractionEnabled = NO; - [self.contentView addSubview:self.bubbleImageView]; + [self.payloadView addSubview:self.bubbleImageView]; [self.bubbleImageView autoPinToSuperviewEdges]; self.textLabel = [UILabel new]; @@ -75,6 +87,12 @@ NS_ASSUME_NONNULL_BEGIN // Hide these views by default. self.bubbleImageView.hidden = YES; self.textLabel.hidden = YES; + self.dateHeaderLabel.hidden = YES; + + [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [self.payloadView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderLabel]; + [self.payloadView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + [self.payloadView autoPinWidthToSuperview]; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; @@ -122,7 +140,7 @@ NS_ASSUME_NONNULL_BEGIN return self.viewItem.contentSize; } -- (void)loadForDisplay +- (void)loadForDisplay:(int)contentWidth { OWSAssert(self.viewItem); OWSAssert(self.viewItem.interaction); @@ -135,6 +153,8 @@ NS_ASSUME_NONNULL_BEGIN = isIncoming ? [self.bubbleFactory incoming] : [self.bubbleFactory outgoing]; self.bubbleImageView.image = bubbleImageData.messageBubbleImage; + [self updateDateHeader:contentWidth]; + switch (self.cellType) { case OWSMessageCellType_TextMessage: case OWSMessageCellType_OversizeTextMessage: @@ -178,6 +198,80 @@ NS_ASSUME_NONNULL_BEGIN // }); } +- (void)updateDateHeader:(int)contentWidth +{ + static NSDateFormatter *dateHeaderDateFormatter = nil; + static NSDateFormatter *dateHeaderTimeFormatter = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dateHeaderDateFormatter = [NSDateFormatter new]; + [dateHeaderDateFormatter setLocale:[NSLocale currentLocale]]; + [dateHeaderDateFormatter setDoesRelativeDateFormatting:YES]; + [dateHeaderDateFormatter setDateStyle:NSDateFormatterMediumStyle]; + [dateHeaderDateFormatter setTimeStyle:NSDateFormatterNoStyle]; + + dateHeaderTimeFormatter = [NSDateFormatter new]; + [dateHeaderTimeFormatter setLocale:[NSLocale currentLocale]]; + [dateHeaderTimeFormatter setDoesRelativeDateFormatting:YES]; + [dateHeaderTimeFormatter setDateStyle:NSDateFormatterNoStyle]; + [dateHeaderTimeFormatter setTimeStyle:NSDateFormatterShortStyle]; + }); + + if (self.viewItem.shouldShowDate) { + NSDate *date = self.viewItem.interaction.dateForSorting; + NSString *dateString = [dateHeaderDateFormatter stringFromDate:date]; + NSString *timeString = [dateHeaderTimeFormatter stringFromDate:date]; + + NSAttributedString *attributedText = [NSAttributedString new]; + attributedText = [attributedText rtlSafeAppend:dateString + attributes:@{ + NSFontAttributeName : self.dateHeaderDateFont, + NSForegroundColorAttributeName : [UIColor lightGrayColor], + } + referenceView:self]; + attributedText = [attributedText rtlSafeAppend:@" " + attributes:@{ + NSFontAttributeName : self.dateHeaderDateFont, + } + referenceView:self]; + attributedText = [attributedText rtlSafeAppend:timeString + attributes:@{ + NSFontAttributeName : self.dateHeaderTimeFont, + NSForegroundColorAttributeName : [UIColor lightGrayColor], + } + referenceView:self]; + + self.dateHeaderLabel.attributedText = attributedText; + self.dateHeaderLabel.hidden = NO; + + self.dateHeaderConstraints = @[ + // Date headers should be visually centered within the conversation view, + // so they need to extend outside the cell's boundaries. + [self.dateHeaderLabel autoSetDimension:ALDimensionWidth toSize:contentWidth], + (self.isIncoming ? [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeLeading] + : [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing]), + [self.dateHeaderLabel autoSetDimension:ALDimensionHeight toSize:self.dateHeaderHeight], + ]; + } else { + self.dateHeaderLabel.hidden = YES; + self.dateHeaderConstraints = @[ + [self.dateHeaderLabel autoSetDimension:ALDimensionHeight toSize:0], + ]; + } +} + +- (UIFont *)dateHeaderDateFont +{ + // TODO: Refine. + return [UIFont boldSystemFontOfSize:12.0f]; +} + +- (UIFont *)dateHeaderTimeFont +{ + // TODO: Refine. + return [UIFont systemFontOfSize:12.0f]; +} + - (void)loadForTextDisplay { self.bubbleImageView.hidden = NO; @@ -313,7 +407,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(view); view.userInteractionEnabled = NO; - [self.contentView addSubview:view]; + [self.payloadView addSubview:view]; self.contentConstraints = [view autoPinToSuperviewEdges]; [self cropViewToBubbbleShape:view]; if (self.isMediaBeingSent) { @@ -378,7 +472,7 @@ NS_ASSUME_NONNULL_BEGIN self.customView = [UIView new]; self.customView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f]; self.customView.userInteractionEnabled = NO; - [self.contentView addSubview:self.customView]; + [self.payloadView addSubview:self.customView]; self.contentConstraints = [self.customView autoPinToSuperviewEdges]; [self cropViewToBubbbleShape:self.customView]; } @@ -390,6 +484,7 @@ NS_ASSUME_NONNULL_BEGIN const int maxMessageWidth = (int)floor(contentWidth * 0.7f); + CGSize cellSize = CGSizeZero; switch (self.cellType) { case OWSMessageCellType_TextMessage: case OWSMessageCellType_OversizeTextMessage: { @@ -401,9 +496,9 @@ NS_ASSUME_NONNULL_BEGIN self.textLabel.text = self.textMessage; CGSize textSize = [self.textLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]; - CGSize result = CGSizeMake((CGFloat)ceil(textSize.width + leftMargin + rightMargin), + cellSize = CGSizeMake((CGFloat)ceil(textSize.width + leftMargin + rightMargin), (CGFloat)ceil(textSize.height + textVMargin * 2)); - return result; + break; } case OWSMessageCellType_StillImage: case OWSMessageCellType_AnimatedImage: @@ -422,18 +517,34 @@ NS_ASSUME_NONNULL_BEGIN mediaWidth = (CGFloat)round(maxMediaHeight * self.contentSize.width / self.contentSize.height); mediaHeight = (CGFloat)round(maxMediaHeight); } - CGSize result = CGSizeMake(mediaWidth, mediaHeight); - return result; + cellSize = CGSizeMake(mediaWidth, mediaHeight); + break; } case OWSMessageCellType_Audio: - return CGSizeMake(maxMessageWidth, OWSAudioMessageView.bubbleHeight); + cellSize = CGSizeMake(maxMessageWidth, OWSAudioMessageView.bubbleHeight); + break; case OWSMessageCellType_GenericAttachment: - return CGSizeMake(maxMessageWidth, [OWSGenericAttachmentView bubbleHeight]); + cellSize = CGSizeMake(maxMessageWidth, [OWSGenericAttachmentView bubbleHeight]); + break; case OWSMessageCellType_DownloadingAttachment: - return CGSizeMake(200, 90); + cellSize = CGSizeMake(200, 90); + break; } - return CGSizeMake(maxMessageWidth, maxMessageWidth); + OWSAssert(cellSize.width > 0 && cellSize.height > 0); + + cellSize.height += self.dateHeaderHeight; + + return cellSize; +} + +- (CGFloat)dateHeaderHeight +{ + if (self.viewItem.shouldShowDate) { + return MAX(self.dateHeaderDateFont.lineHeight, self.dateHeaderTimeFont.lineHeight); + } else { + return 0.f; + } } - (BOOL)isIncoming @@ -489,8 +600,12 @@ NS_ASSUME_NONNULL_BEGIN [NSLayoutConstraint deactivateConstraints:self.contentConstraints]; self.contentConstraints = nil; + [NSLayoutConstraint deactivateConstraints:self.dateHeaderConstraints]; + self.dateHeaderConstraints = nil; // The text label is used so frequently that we always keep one around. + self.dateHeaderLabel.text = nil; + self.dateHeaderLabel.hidden = YES; self.textLabel.text = nil; self.textLabel.hidden = YES; self.bubbleImageView.image = nil; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m index 9b811758f..f0a1cdc68 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m @@ -85,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN return NSStringFromClass([self class]); } -- (void)loadForDisplay +- (void)loadForDisplay:(int)contentWidth { OWSAssert(self.viewItem); diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m index e97d32e08..b078d2966 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m @@ -84,7 +84,7 @@ NS_ASSUME_NONNULL_BEGIN return NSStringFromClass([self class]); } -- (void)loadForDisplay +- (void)loadForDisplay:(int)contentWidth { OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]); diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 18e7132cc..183f6d1b7 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -156,6 +156,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { @property (nonatomic, readonly) ConversationInputToolbar *inputToolbar; @property (nonatomic, readonly) ConversationCollectionView *collectionView; +@property (nonatomic, readonly) ConversationViewLayout *layout; @property (nonatomic) NSArray<ConversationViewItem *> *viewItems; @property (nonatomic) NSMutableDictionary<NSString *, ConversationViewItem *> *viewItemMap; @@ -462,11 +463,12 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { - (void)createContents { - ConversationViewLayout *layout = [ConversationViewLayout new]; - layout.delegate = self; + _layout = [ConversationViewLayout new]; + self.layout.delegate = self; // We use the root view bounds as the initial frame for the collection // view so that its contents can be laid out immediately. - _collectionView = [[ConversationCollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; + _collectionView = + [[ConversationCollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.layout]; self.collectionView.layoutDelegate = self; self.collectionView.delegate = self; self.collectionView.dataSource = self; @@ -3921,13 +3923,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { cell.viewItem = viewItem; cell.delegate = self; - // TODO: Could we move this inside the cell base class? - if (viewItem.shouldShowDate) { - cell.messageDateHeaderText = [[JSQMessagesTimestampFormatter sharedFormatter] - attributedTimestampForDate:viewItem.interaction.dateForSorting]; - } - - [cell loadForDisplay]; + [cell loadForDisplay:self.layout.contentWidth]; return cell; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.h b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.h index 515629fac..5037be191 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.h @@ -36,6 +36,8 @@ typedef NS_ENUM(NSInteger, ConversationViewLayoutAlignment) { @property (nonatomic, weak) id<ConversationViewLayoutDelegate> delegate; +@property (nonatomic, readonly) int contentWidth; + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m index ae757d5de..7162ea378 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewLayout.m @@ -20,6 +20,8 @@ NS_ASSUME_NONNULL_BEGIN // unnecessary layout pass. @property (nonatomic) BOOL hasLayout; +@property (nonatomic) int contentWidth; + @end #pragma mark - @@ -85,6 +87,7 @@ NS_ASSUME_NONNULL_BEGIN const int vSpacing = 5; const int viewWidth = (int)floor(self.collectionView.bounds.size.width); const int contentWidth = (int)floor(viewWidth - 2 * hInset); + self.contentWidth = contentWidth; NSArray<id<ConversationViewLayoutItem>> *layoutItems = self.delegate.layoutItems; diff --git a/Signal/src/util/NSAttributedString+OWS.h b/Signal/src/util/NSAttributedString+OWS.h index a1b32e17e..4079d7a47 100644 --- a/Signal/src/util/NSAttributedString+OWS.h +++ b/Signal/src/util/NSAttributedString+OWS.h @@ -6,6 +6,9 @@ NS_ASSUME_NONNULL_BEGIN @interface NSAttributedString (OWS) +- (NSAttributedString *)rtlSafeAppend:(NSString *)text + attributes:(NSDictionary *)attributes + referenceView:(UIView *)referenceView; - (NSAttributedString *)rtlSafeAppend:(NSAttributedString *)string referenceView:(UIView *)referenceView; @end diff --git a/Signal/src/util/NSAttributedString+OWS.m b/Signal/src/util/NSAttributedString+OWS.m index 6ffea8956..107c2ee0f 100644 --- a/Signal/src/util/NSAttributedString+OWS.m +++ b/Signal/src/util/NSAttributedString+OWS.m @@ -9,6 +9,18 @@ NS_ASSUME_NONNULL_BEGIN @implementation NSAttributedString (OWS) +- (NSAttributedString *)rtlSafeAppend:(NSString *)text + attributes:(NSDictionary *)attributes + referenceView:(UIView *)referenceView +{ + OWSAssert(text); + OWSAssert(attributes); + OWSAssert(referenceView); + + NSAttributedString *substring = [[NSAttributedString alloc] initWithString:text attributes:attributes]; + return [self rtlSafeAppend:substring referenceView:referenceView]; +} + - (NSAttributedString *)rtlSafeAppend:(NSAttributedString *)string referenceView:(UIView *)referenceView { OWSAssert(string);