From abba24988c8c4b0fdbd651ed50425a0d1ab9cf93 Mon Sep 17 00:00:00 2001
From: Matthew Chen <matthew@signal.org>
Date: Tue, 10 Apr 2018 14:24:35 -0400
Subject: [PATCH] Rework how dates are formatted in home view.

---
 .../ConversationView/Cells/OWSBubbleView.h    |   2 +
 .../ConversationView/Cells/OWSBubbleView.m    |   5 +
 .../Cells/OWSMessageBubbleView.m              |   3 +
 .../ViewControllers/DebugUI/DebugUIMessages.m |  74 +++++++++++++
 .../ViewControllers/HomeView/HomeViewCell.m   |  37 ++++---
 Signal/src/util/DateUtil.h                    |   3 +
 Signal/src/util/DateUtil.m                    |  51 ++++++++-
 Signal/test/util/UtilTest.m                   | 104 ++++++++++++++++++
 SignalServiceKit/src/Util/NSDate+OWS.h        |   3 +
 SignalServiceKit/src/Util/NSDate+OWS.mm       |   1 +
 10 files changed, 266 insertions(+), 17 deletions(-)

diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h
index 4ef469937..0882a50f2 100644
--- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h
+++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h
@@ -45,6 +45,8 @@ extern const CGFloat kBubbleTextVInset;
 
 - (void)updatePartnerViews;
 
++ (CGFloat)minWidth;
+
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m
index 4cb83dd3f..6d428768a 100644
--- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m
+++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m
@@ -239,6 +239,11 @@ const CGFloat kBubbleTextVInset = 10.f;
     }
 }
 
++ (CGFloat)minWidth
+{
+    return (kBubbleHRounding * 2 + kBubbleThornSideInset);
+}
+
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m
index bba575e0c..47e630db0 100644
--- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m
+++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m
@@ -933,6 +933,9 @@ NS_ASSUME_NONNULL_BEGIN
     cellSize.width = MAX(cellSize.width, textContentSize.width);
     cellSize.height += textContentSize.height;
 
+    // Make sure the bubble is always wide enough to complete it's bubble shape.
+    cellSize.width = MAX(cellSize.width, OWSBubbleView.minWidth);
+
     OWSAssert(cellSize.width > 0 && cellSize.height > 0);
 
     if (self.hasTapForMore) {
diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m
index 4ad1d0537..1f30ccc46 100644
--- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m
+++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m
@@ -71,6 +71,7 @@ NS_ASSUME_NONNULL_BEGIN
              [DebugUIMessages allQuotedReplyAction:thread],
              // Exemplary
              [DebugUIMessages allFakeAction:thread],
+             [DebugUIMessages allFakeBackDatedAction:thread],
          ]) {
         [items addObject:[OWSTableItem itemWithTitle:action.label
                                          actionBlock:^{
@@ -108,6 +109,10 @@ NS_ASSUME_NONNULL_BEGIN
                         actionBlock:^{
                             [DebugUIMessages selectQuotedReplyAction:thread];
                         }],
+        [OWSTableItem itemWithTitle:@"Select Back-Dated"
+                        actionBlock:^{
+                            [DebugUIMessages selectBackDatedAction:thread];
+                        }],
 
 #pragma mark - Misc.
 
@@ -2656,6 +2661,7 @@ NS_ASSUME_NONNULL_BEGIN
     [actions addObjectsFromArray:[self allFakeTextActions:thread includeLabels:includeLabels]];
     [actions addObjectsFromArray:[self allFakeSequenceActions:thread includeLabels:includeLabels]];
     [actions addObjectsFromArray:[self allFakeQuotedReplyActions:thread includeLabels:includeLabels]];
+    [actions addObjectsFromArray:[self allFakeBackDatedActions:thread includeLabels:includeLabels]];
     return actions;
 }
 
@@ -2827,6 +2833,74 @@ NS_ASSUME_NONNULL_BEGIN
                                                     subactions:[self allFakeSequenceActions:thread includeLabels:YES]];
 }
 
+#pragma mark - Back-dated
+
++ (DebugUIMessagesAction *)fakeBackDatedMessageAction:(TSThread *)thread
+                                                label:(NSString *)label
+                                           dateOffset:(int64_t)dateOffset
+{
+    OWSAssert(thread);
+
+    return [DebugUIMessagesSingleAction
+               actionWithLabel:[NSString stringWithFormat:@"Fake Back-Date Message (%@)", label]
+        unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) {
+            NSString *messageBody =
+                [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:self.randomText];
+            TSOutgoingMessage *message = [self createFakeOutgoingMessage:thread
+                                                             messageBody:messageBody
+                                                         fakeAssetLoader:nil
+                                                            messageState:TSOutgoingMessageStateSentToService
+                                                             isDelivered:NO
+                                                                  isRead:NO
+                                                           quotedMessage:nil
+                                                             transaction:transaction];
+            [message setReceivedAtTimestamp:(uint64_t)((int64_t)[NSDate ows_millisecondTimeStamp] + dateOffset)];
+            [message saveWithTransaction:transaction];
+        }];
+}
+
++ (NSArray<DebugUIMessagesAction *> *)allFakeBackDatedActions:(TSThread *)thread includeLabels:(BOOL)includeLabels
+{
+    OWSAssert(thread);
+
+    NSMutableArray<DebugUIMessagesAction *> *actions = [NSMutableArray new];
+
+    if (includeLabels) {
+        [actions addObject:[self fakeOutgoingTextMessageAction:thread
+                                                  messageState:TSOutgoingMessageStateSentToService
+                                                          text:@"⚠️ Back-Dated ⚠️"]];
+    }
+
+    [actions
+        addObject:[self fakeBackDatedMessageAction:thread label:@"One Minute Ago" dateOffset:-(int64_t)kMinuteInMs]];
+    [actions addObject:[self fakeBackDatedMessageAction:thread label:@"One Hour Ago" dateOffset:-(int64_t)kHourInMs]];
+    [actions addObject:[self fakeBackDatedMessageAction:thread label:@"One Day Ago" dateOffset:-(int64_t)kDayInMs]];
+    [actions
+        addObject:[self fakeBackDatedMessageAction:thread label:@"Two Days Ago" dateOffset:-(int64_t)kDayInMs * 2]];
+    [actions
+        addObject:[self fakeBackDatedMessageAction:thread label:@"Ten Days Ago" dateOffset:-(int64_t)kDayInMs * 10]];
+    [actions
+        addObject:[self fakeBackDatedMessageAction:thread label:@"400 Days Ago" dateOffset:-(int64_t)kDayInMs * 400]];
+
+    return actions;
+}
+
++ (DebugUIMessagesAction *)allFakeBackDatedAction:(TSThread *)thread
+{
+    OWSAssert(thread);
+
+    return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Back-Dated"
+                                                    subactions:[self allFakeBackDatedActions:thread includeLabels:YES]];
+}
+
++ (void)selectBackDatedAction:(TSThread *)thread
+{
+    OWSAssertIsOnMainThread();
+    OWSAssert(thread);
+
+    [self selectActionUI:[self allFakeBackDatedActions:thread includeLabels:NO] label:@"Select Back-Dated"];
+}
+
 #pragma mark -
 
 + (NSString *)randomOversizeText
diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.m b/Signal/src/ViewControllers/HomeView/HomeViewCell.m
index ebc89261e..2b3c06948 100644
--- a/Signal/src/ViewControllers/HomeView/HomeViewCell.m
+++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.m
@@ -7,6 +7,7 @@
 #import "Signal-Swift.h"
 #import <SignalMessaging/OWSFormat.h>
 #import <SignalMessaging/OWSUserProfile.h>
+#import <SignalMessaging/SignalMessaging-Swift.h>
 #import <SignalServiceKit/OWSMessageManager.h>
 #import <SignalServiceKit/TSContactThread.h>
 #import <SignalServiceKit/TSGroupThread.h>
@@ -26,12 +27,12 @@ const NSUInteger kHomeViewAvatarHSpacing = 12;
 @property (nonatomic) UIView *payloadView;
 @property (nonatomic) UILabel *nameLabel;
 @property (nonatomic) UILabel *snippetLabel;
-@property (nonatomic) UILabel *timeLabel;
+@property (nonatomic) UILabel *dateTimeLabel;
 @property (nonatomic) UIView *unreadBadge;
 @property (nonatomic) UILabel *unreadLabel;
 
-@property (nonatomic) TSThread *thread;
-@property (nonatomic) OWSContactsManager *contactsManager;
+@property (nonatomic, nullable) TSThread *thread;
+@property (nonatomic, nullable) OWSContactsManager *contactsManager;
 
 @property (nonatomic, readonly) NSMutableArray<NSLayoutConstraint *> *viewConstraints;
 
@@ -93,13 +94,13 @@ const NSUInteger kHomeViewAvatarHSpacing = 12;
     [self.nameLabel setContentHuggingHorizontalLow];
     [self.nameLabel setCompressionResistanceHorizontalLow];
 
-    self.timeLabel = [UILabel new];
-    [self.timeLabel setContentHuggingHorizontalHigh];
-    [self.timeLabel setCompressionResistanceHorizontalHigh];
+    self.dateTimeLabel = [UILabel new];
+    [self.dateTimeLabel setContentHuggingHorizontalHigh];
+    [self.dateTimeLabel setCompressionResistanceHorizontalHigh];
 
     UIStackView *topRowView = [[UIStackView alloc] initWithArrangedSubviews:@[
         self.nameLabel,
-        self.timeLabel,
+        self.dateTimeLabel,
     ]];
     topRowView.axis = UILayoutConstraintAxisHorizontal;
     topRowView.spacing = 4;
@@ -130,7 +131,7 @@ const NSUInteger kHomeViewAvatarHSpacing = 12;
     self.unreadBadge.backgroundColor = [UIColor ows_materialBlueColor];
     [self.contentView addSubview:self.unreadBadge];
     [self.unreadBadge autoPinTrailingToSuperviewMarginWithInset:kHomeViewCellHMargin];
-    [self.unreadBadge autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.timeLabel];
+    [self.unreadBadge autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.dateTimeLabel];
     [self.unreadBadge setContentHuggingHigh];
     [self.unreadBadge setCompressionResistanceHigh];
 
@@ -193,12 +194,12 @@ const NSUInteger kHomeViewAvatarHSpacing = 12;
 
     self.snippetLabel.attributedText =
         [self attributedSnippetForThread:thread blockedPhoneNumberSet:blockedPhoneNumberSet];
-    self.timeLabel.attributedText = [self attributedStringForDate:thread.lastMessageDate];
+    self.dateTimeLabel.attributedText = [self attributedStringForDate:thread.lastMessageDate];
 
     self.separatorInset
         = UIEdgeInsetsMake(0, kHomeViewAvatarSize + kHomeViewCellHMargin + kHomeViewAvatarHSpacing, 0, 0);
 
-    self.timeLabel.textColor = hasUnreadMessages ? [UIColor ows_materialBlueColor] : [UIColor ows_darkGrayColor];
+    self.dateTimeLabel.textColor = hasUnreadMessages ? [UIColor ows_materialBlueColor] : [UIColor ows_darkGrayColor];
 
     NSUInteger unreadCount = [[OWSMessageUtils sharedManager] unreadMessagesInThread:thread];
     if (unreadCount > 0) {
@@ -298,10 +299,18 @@ const NSUInteger kHomeViewAvatarHSpacing = 12;
         return [NSAttributedString new];
     }
 
-    NSDateFormatter *formatter = ([DateUtil dateIsToday:date] ? [DateUtil timeFormatter] : [DateUtil dateFormatter]);
-    NSString *timeString = [formatter stringFromDate:date];
-    OWSAssert(timeString);
-    return [[NSAttributedString alloc] initWithString:timeString
+    NSString *dateTimeString;
+    if (![DateUtil dateIsThisYear:date]) {
+        dateTimeString = [[DateUtil dateFormatter] stringFromDate:date];
+    } else if ([DateUtil dateIsOlderThanOneWeek:date]) {
+        dateTimeString = [[DateUtil monthAndDayFormatter] stringFromDate:date];
+    } else if ([DateUtil dateIsOlderThanOneDay:date]) {
+        dateTimeString = [[DateUtil shortDayOfWeekFormatter] stringFromDate:date];
+    } else {
+        dateTimeString = [[DateUtil timeFormatter] stringFromDate:date];
+    }
+
+    return [[NSAttributedString alloc] initWithString:dateTimeString
                                            attributes:@{
                                                NSForegroundColorAttributeName : [UIColor ows_darkGrayColor],
                                                NSFontAttributeName : self.dateTimeFont,
diff --git a/Signal/src/util/DateUtil.h b/Signal/src/util/DateUtil.h
index 8291a2aa0..2ffeed30c 100644
--- a/Signal/src/util/DateUtil.h
+++ b/Signal/src/util/DateUtil.h
@@ -8,10 +8,13 @@ NS_ASSUME_NONNULL_BEGIN
 
 + (NSDateFormatter *)dateFormatter;
 + (NSDateFormatter *)timeFormatter;
++ (NSDateFormatter *)monthAndDayFormatter;
++ (NSDateFormatter *)shortDayOfWeekFormatter;
 
 + (BOOL)dateIsOlderThanOneDay:(NSDate *)date;
 + (BOOL)dateIsOlderThanOneWeek:(NSDate *)date;
 + (BOOL)dateIsToday:(NSDate *)date;
++ (BOOL)dateIsThisYear:(NSDate *)date;
 
 + (NSString *)formatPastTimestampRelativeToNow:(uint64_t)pastTimestamp
                                          isRTL:(BOOL)isRTL NS_SWIFT_NAME(formatPastTimestampRelativeToNow(_:isRTL:));
diff --git a/Signal/src/util/DateUtil.m b/Signal/src/util/DateUtil.m
index 648b40202..696377915 100644
--- a/Signal/src/util/DateUtil.m
+++ b/Signal/src/util/DateUtil.m
@@ -47,12 +47,46 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE";
     return formatter;
 }
 
++ (NSDateFormatter *)monthAndDayFormatter
+{
+    static NSDateFormatter *formatter;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        formatter = [NSDateFormatter new];
+        [formatter setLocale:[NSLocale currentLocale]];
+        formatter.dateFormat = @"MMM d";
+    });
+    return formatter;
+}
+
++ (NSDateFormatter *)shortDayOfWeekFormatter
+{
+    static NSDateFormatter *formatter;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        formatter = [NSDateFormatter new];
+        [formatter setLocale:[NSLocale currentLocale]];
+        formatter.dateFormat = @"E";
+    });
+    return formatter;
+}
+
 + (BOOL)dateIsOlderThanOneDay:(NSDate *)date {
-    return [[NSDate date] timeIntervalSinceDate:date] > kDayInterval;
+    NSDate *now = [NSDate date];
+    NSCalendar *calendar = [NSCalendar currentCalendar];
+
+    NSUInteger dateDayOfEra = [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:date];
+    NSUInteger nowDayOfEra = [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:now];
+    return dateDayOfEra < nowDayOfEra;
 }
 
 + (BOOL)dateIsOlderThanOneWeek:(NSDate *)date {
-    return [[NSDate date] timeIntervalSinceDate:date] > kWeekInterval;
+    NSDate *now = [NSDate date];
+    NSCalendar *calendar = [NSCalendar currentCalendar];
+
+    NSUInteger dateDayOfEra = [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:date];
+    NSUInteger nowDayOfEra = [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:now];
+    return dateDayOfEra < (nowDayOfEra - 6);
 }
 
 + (BOOL)date:(NSDate *)date isEqualToDateIgnoringTime:(NSDate *)anotherDate {
@@ -65,7 +99,18 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE";
 }
 
 + (BOOL)dateIsToday:(NSDate *)date {
-    return [self date:[NSDate date] isEqualToDateIgnoringTime:date];
+    NSDate *now = [NSDate date];
+    NSCalendar *calendar = [NSCalendar currentCalendar];
+    return ([calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:date] ==
+        [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:now]);
+}
+
++ (BOOL)dateIsThisYear:(NSDate *)date
+{
+    NSDate *now = [NSDate date];
+    NSCalendar *calendar = [NSCalendar currentCalendar];
+    return (
+        [calendar component:NSCalendarUnitYear fromDate:date] == [calendar component:NSCalendarUnitYear fromDate:now]);
 }
 
 + (BOOL)dateIsYesterday:(NSDate *)date
diff --git a/Signal/test/util/UtilTest.m b/Signal/test/util/UtilTest.m
index 55f20bd70..bb002afe2 100644
--- a/Signal/test/util/UtilTest.m
+++ b/Signal/test/util/UtilTest.m
@@ -3,6 +3,7 @@
 //
 
 #import "UtilTest.h"
+#import "DateUtil.h"
 #import "TestUtil.h"
 #import <SignalMessaging/NSString+OWS.h>
 #import <SignalServiceKit/NSDate+OWS.h>
@@ -76,6 +77,109 @@
     XCTAssertTrue([laterDate isAfterDate:firstDate]);
 }
 
+- (void)testDateComparators
+{
+    NSDate *now = [NSDate new];
+
+    NSDate *oneSecondAgo =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kSecondInterval];
+    NSDate *oneMinuteAgo =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kMinuteInterval];
+    NSDate *oneDayAgo =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kDayInterval];
+    NSDate *threeDaysAgo =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kDayInterval * 3];
+    NSDate *tenDaysAgo =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kDayInterval * 10];
+    NSDate *oneYearAgo =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kYearInterval];
+    NSDate *twoYearsAgo =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kYearInterval * 2];
+
+    NSDate *oneSecondAhead =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kSecondInterval];
+    NSDate *oneMinuteAhead =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kMinuteInterval];
+    NSDate *oneDayAhead =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kDayInterval];
+    NSDate *threeDaysAhead =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kDayInterval * 3];
+    NSDate *tenDaysAhead =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kDayInterval * 10];
+    NSDate *oneYearAhead =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kYearInterval];
+    NSDate *twoYearsAhead =
+        [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kYearInterval * 2];
+
+    // These might fail around midnight.
+    XCTAssertTrue([DateUtil dateIsToday:oneSecondAgo]);
+    XCTAssertTrue([DateUtil dateIsToday:oneMinuteAgo]);
+    XCTAssertFalse([DateUtil dateIsToday:oneDayAgo]);
+    XCTAssertFalse([DateUtil dateIsToday:threeDaysAgo]);
+    XCTAssertFalse([DateUtil dateIsToday:tenDaysAgo]);
+    XCTAssertFalse([DateUtil dateIsToday:oneYearAgo]);
+    XCTAssertFalse([DateUtil dateIsToday:twoYearsAgo]);
+
+    // These might fail around midnight.
+    XCTAssertTrue([DateUtil dateIsToday:oneSecondAhead]);
+    XCTAssertTrue([DateUtil dateIsToday:oneMinuteAhead]);
+    XCTAssertFalse([DateUtil dateIsToday:oneDayAhead]);
+    XCTAssertFalse([DateUtil dateIsToday:threeDaysAhead]);
+    XCTAssertFalse([DateUtil dateIsToday:tenDaysAhead]);
+    XCTAssertFalse([DateUtil dateIsToday:oneYearAhead]);
+    XCTAssertFalse([DateUtil dateIsToday:twoYearsAhead]);
+
+    // These might fail around midnight.
+    XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneSecondAgo]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneMinuteAgo]);
+    XCTAssertTrue([DateUtil dateIsOlderThanOneDay:oneDayAgo]);
+    XCTAssertTrue([DateUtil dateIsOlderThanOneDay:threeDaysAgo]);
+    XCTAssertTrue([DateUtil dateIsOlderThanOneDay:tenDaysAgo]);
+    XCTAssertTrue([DateUtil dateIsOlderThanOneDay:oneYearAgo]);
+    XCTAssertTrue([DateUtil dateIsOlderThanOneDay:twoYearsAgo]);
+
+    // These might fail around midnight.
+    XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneSecondAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneMinuteAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneDayAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneDay:threeDaysAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneDay:tenDaysAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneYearAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneDay:twoYearsAhead]);
+
+    // These might fail around midnight.
+    XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneSecondAgo]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneMinuteAgo]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneDayAgo]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:threeDaysAgo]);
+    XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:tenDaysAgo]);
+    XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:oneYearAgo]);
+    XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:twoYearsAgo]);
+
+    // These might fail around midnight.
+    XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneSecondAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneMinuteAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneDayAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:threeDaysAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:tenDaysAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneYearAhead]);
+    XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:twoYearsAhead]);
+
+    // These might fail around new year's.
+    XCTAssertTrue([DateUtil dateIsThisYear:oneSecondAgo]);
+    XCTAssertTrue([DateUtil dateIsThisYear:oneMinuteAgo]);
+    XCTAssertTrue([DateUtil dateIsThisYear:oneDayAgo]);
+    XCTAssertFalse([DateUtil dateIsThisYear:oneYearAgo]);
+    XCTAssertFalse([DateUtil dateIsThisYear:twoYearsAgo]);
+
+    // These might fail around new year's.
+    XCTAssertTrue([DateUtil dateIsThisYear:oneSecondAhead]);
+    XCTAssertTrue([DateUtil dateIsThisYear:oneMinuteAhead]);
+    XCTAssertTrue([DateUtil dateIsThisYear:oneDayAhead]);
+    XCTAssertFalse([DateUtil dateIsThisYear:oneYearAhead]);
+    XCTAssertFalse([DateUtil dateIsThisYear:twoYearsAhead]);
+}
+
 - (void)testObjectComparison
 {
     XCTAssertTrue([NSObject isNullableObject:nil equalTo:nil]);
diff --git a/SignalServiceKit/src/Util/NSDate+OWS.h b/SignalServiceKit/src/Util/NSDate+OWS.h
index 2455b7439..024924a9b 100755
--- a/SignalServiceKit/src/Util/NSDate+OWS.h
+++ b/SignalServiceKit/src/Util/NSDate+OWS.h
@@ -5,12 +5,15 @@
 NS_ASSUME_NONNULL_BEGIN
 
 // These NSTimeInterval constants provide simplified durations for readability.
+//
+// These approximations should never be used for strict date/time calcuations.
 extern const NSTimeInterval kSecondInterval;
 extern const NSTimeInterval kMinuteInterval;
 extern const NSTimeInterval kHourInterval;
 extern const NSTimeInterval kDayInterval;
 extern const NSTimeInterval kWeekInterval;
 extern const NSTimeInterval kMonthInterval;
+extern const NSTimeInterval kYearInterval;
 
 #define kSecondInMs ((uint64_t)1000)
 #define kMinuteInMs (kSecondInMs * 60)
diff --git a/SignalServiceKit/src/Util/NSDate+OWS.mm b/SignalServiceKit/src/Util/NSDate+OWS.mm
index 751b1a8f2..f6238a7e0 100644
--- a/SignalServiceKit/src/Util/NSDate+OWS.mm
+++ b/SignalServiceKit/src/Util/NSDate+OWS.mm
@@ -14,6 +14,7 @@ const NSTimeInterval kHourInterval = 60 * kMinuteInterval;
 const NSTimeInterval kDayInterval = 24 * kHourInterval;
 const NSTimeInterval kWeekInterval = 7 * kDayInterval;
 const NSTimeInterval kMonthInterval = 30 * kDayInterval;
+const NSTimeInterval kYearInterval = 365 * kDayInterval;
 
 @implementation NSDate (OWS)