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