Rework how dates are formatted in home view.

This commit is contained in:
Matthew Chen 2018-04-10 14:24:35 -04:00
parent b8f8a3017a
commit abba24988c
10 changed files with 266 additions and 17 deletions

View file

@ -45,6 +45,8 @@ extern const CGFloat kBubbleTextVInset;
- (void)updatePartnerViews;
+ (CGFloat)minWidth;
@end
NS_ASSUME_NONNULL_END

View file

@ -239,6 +239,11 @@ const CGFloat kBubbleTextVInset = 10.f;
}
}
+ (CGFloat)minWidth
{
return (kBubbleHRounding * 2 + kBubbleThornSideInset);
}
@end
NS_ASSUME_NONNULL_END

View file

@ -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) {

View file

@ -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

View file

@ -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,

View file

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

View file

@ -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

View file

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

View file

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

View file

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