From 712d6d89e10e228569dbdb8fc513af15e1bbeb3d Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 5 Jul 2018 11:56:46 -0400 Subject: [PATCH 1/4] Tweak relative timestamps. --- .../Cells/OWSMessageBubbleView.m | 1 + .../Cells/OWSMessageFooterView.m | 38 +++++++++++- Signal/src/util/DateUtil.h | 11 +++- Signal/src/util/DateUtil.m | 58 ++++++++++++++++++- .../translations/en.lproj/Localizable.strings | 6 ++ SignalMessaging/categories/UIView+OWS.h | 2 +- 6 files changed, 108 insertions(+), 8 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 6e794396e..daf958e8b 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -1343,6 +1343,7 @@ NS_ASSUME_NONNULL_BEGIN if (self.hasBottomFooter) { CGSize footerSize = [self.footerView measureWithConversationViewItem:self.viewItem]; + footerSize.width = MIN(footerSize.width, self.conversationStyle.maxMessageWidth); [textViewSizes addObject:[NSValue valueWithCGSize:footerSize]]; } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m index 094616ec0..86426276a 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m @@ -168,10 +168,16 @@ NS_ASSUME_NONNULL_BEGIN self.timestampLabel.text = NSLocalizedString(@"MESSAGE_STATUS_SEND_FAILED", @"Label indicating that a message failed to send."); } else { - self.timestampLabel.text = [DateUtil formatTimestampAsTimeShort:viewItem.interaction.timestamp]; + self.timestampLabel.text = [DateUtil formatTimestampAsTime:viewItem.interaction.timestamp + maxRelativeDurationMinutes:self.maxRelativeDurationMinutes]; } } +- (NSInteger)maxRelativeDurationMinutes +{ + return 59; +} + - (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem { OWSAssert(viewItem); @@ -180,7 +186,35 @@ NS_ASSUME_NONNULL_BEGIN CGSize result = CGSizeZero; result.height = MAX(self.timestampLabel.font.lineHeight, self.imageHeight); - result.width = [self.timestampLabel sizeThatFits:CGSizeZero].width; + + // Measure the actual current width, to be safe. + CGFloat timestampLabelWidth = [self.timestampLabel sizeThatFits:CGSizeZero].width; + + // Measuring the timestamp label's width is non-trivial since its + // contents can be relative the current time. We avoid having + // message bubbles' "visually vibrate" as their timestamp labels + // vary in width. So we try to leave enough space for all possible + // contents of this label. + if ([DateUtil isTimestampRelative:viewItem.interaction.timestamp + maxRelativeDurationMinutes:self.maxRelativeDurationMinutes]) { + // Measure the "now" case. + self.timestampLabel.text = [DateUtil exemplaryNowTimeFormat]; + timestampLabelWidth = MAX(timestampLabelWidth, [self.timestampLabel sizeThatFits:CGSizeZero].width); + // Measure the "relative time" case. + // Since this case varies with time, we multiply to leave + // space for the worst case (whose exact value, due to localization, + // is unpredictable). + self.timestampLabel.text = + [DateUtil exemplaryRelativeTimeFormatWithMaxRelativeDurationMinutes:self.maxRelativeDurationMinutes]; + timestampLabelWidth = MAX(timestampLabelWidth, + [self.timestampLabel sizeThatFits:CGSizeZero].width + self.timestampLabel.font.lineHeight * 0.5f); + + // Re-configure the labels with the current appropriate value in case + // we are configuring this view for display. + [self configureLabelsWithConversationViewItem:viewItem]; + } + + result.width = timestampLabelWidth; if (viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) { if (![self isFailedOutgoingMessage:viewItem]) { result.width += (self.maxImageWidth + self.hSpacing); diff --git a/Signal/src/util/DateUtil.h b/Signal/src/util/DateUtil.h index 50805fdc4..7a8522312 100644 --- a/Signal/src/util/DateUtil.h +++ b/Signal/src/util/DateUtil.h @@ -23,8 +23,15 @@ NS_ASSUME_NONNULL_BEGIN + (NSString *)formatTimestampShort:(uint64_t)timestamp; + (NSString *)formatDateShort:(NSDate *)date; -+ (NSString *)formatTimestampAsTimeShort:(uint64_t)timestamp; -+ (NSString *)formatDateAsTimeShort:(NSDate *)date; ++ (NSString *)formatTimestampAsTime:(uint64_t)timestamp; ++ (NSString *)formatDateAsTime:(NSDate *)date; + ++ (NSString *)formatTimestampAsTime:(uint64_t)timestamp + maxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes; + ++ (BOOL)isTimestampRelative:(uint64_t)timestamp maxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes; ++ (NSString *)exemplaryNowTimeFormat; ++ (NSString *)exemplaryRelativeTimeFormatWithMaxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes; + (BOOL)isSameDayWithTimestamp:(uint64_t)timestamp1 timestamp:(uint64_t)timestamp2; + (BOOL)isSameDayWithDate:(NSDate *)date1 date:(NSDate *)date2; diff --git a/Signal/src/util/DateUtil.m b/Signal/src/util/DateUtil.m index e3d1b6c96..841ea5ffa 100644 --- a/Signal/src/util/DateUtil.m +++ b/Signal/src/util/DateUtil.m @@ -4,6 +4,7 @@ #import "DateUtil.h" #import +#import #import NS_ASSUME_NONNULL_BEGIN @@ -187,12 +188,12 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return dateTimeString.uppercaseString; } -+ (NSString *)formatTimestampAsTimeShort:(uint64_t)timestamp ++ (NSString *)formatTimestampAsTime:(uint64_t)timestamp { - return [self formatDateAsTimeShort:[NSDate ows_dateWithMillisecondsSince1970:timestamp]]; + return [self formatDateAsTime:[NSDate ows_dateWithMillisecondsSince1970:timestamp]]; } -+ (NSString *)formatDateAsTimeShort:(NSDate *)date ++ (NSString *)formatDateAsTime:(NSDate *)date { OWSAssert(date); @@ -200,6 +201,56 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return dateTimeString.uppercaseString; } ++ (NSString *)formatTimestampAsTime:(uint64_t)timestamp maxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes +{ + NSDate *date = [NSDate ows_dateWithMillisecondsSince1970:timestamp]; + NSDate *now = [NSDate new]; + + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSInteger minutesDiff + = MAX(0, [[calendar components:NSCalendarUnitMinute fromDate:date toDate:now options:0] minute]); + + // Treat anything in the last two minutes as "now", so that we + // don't have to worry about pluralization while formatting. + if (minutesDiff <= 1) { + return NSLocalizedString(@"DATE_NOW", @"The present; the current time."); + } else if (minutesDiff <= maxRelativeDurationMinutes) { + NSString *minutesString = [OWSFormat formatInt:(int)minutesDiff]; + return [NSString stringWithFormat:NSLocalizedString(@"DATE_MINUTES_AGO_FORMAT", + @"Format string for a relative time, expressed as a certain number of " + @"minutes in the past. Embeds {{The number of minutes}}."), + minutesString]; + } else { + return [self formatDateAsTime:date]; + } +} + ++ (BOOL)isTimestampRelative:(uint64_t)timestamp maxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes +{ + NSDate *date = [NSDate ows_dateWithMillisecondsSince1970:timestamp]; + NSDate *now = [NSDate new]; + + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSInteger minutesDiff + = MAX(0, [[calendar components:NSCalendarUnitMinute fromDate:date toDate:now options:0] minute]); + + return minutesDiff <= maxRelativeDurationMinutes; +} + ++ (NSString *)exemplaryNowTimeFormat +{ + return NSLocalizedString(@"DATE_NOW", @"The present; the current time."); +} + ++ (NSString *)exemplaryRelativeTimeFormatWithMaxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes +{ + NSString *minutesString = [OWSFormat formatInt:(int)maxRelativeDurationMinutes]; + return [NSString stringWithFormat:NSLocalizedString(@"DATE_MINUTES_AGO_FORMAT", + @"Format string for a relative time, expressed as a certain number of " + @"minutes in the past. Embeds {{The number of minutes}}."), + minutesString]; +} + + (BOOL)isSameDayWithTimestamp:(uint64_t)timestamp1 timestamp:(uint64_t)timestamp2 { return [self isSameDayWithDate:[NSDate ows_dateWithMillisecondsSince1970:timestamp1] @@ -211,6 +262,7 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; NSInteger dayDifference = [self daysFromFirstDate:date1 toSecondDate:date2]; return dayDifference == 0; } + @end NS_ASSUME_NONNULL_END diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 055a0c775..5f4c4ea23 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -611,6 +611,12 @@ /* Title shown while the app is updating its database. */ "DATABASE_VIEW_OVERLAY_TITLE" = "Optimizing Database"; +/* Format string for a duration of time, expressed as a certain number of minutes. Embeds {{The number of minutes}}. */ +"DATE_MINUTES_AGO_FORMAT" = "%@ Min Ago"; + +/* The present; the current time. */ +"DATE_NOW" = "Now"; + /* The current day. */ "DATE_TODAY" = "Today"; diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index cd9a9b79f..1206a2e7b 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -168,6 +168,6 @@ CG_INLINE CGSize CGSizeMax(CGSize size1, CGSize size2) return CGSizeMake(MAX(size1.width, size2.width), MAX(size1.height, size2.height)); } -CGFloat CGHairlineWidth(); +CGFloat CGHairlineWidth(void); NS_ASSUME_NONNULL_END From 41e505fb6fb1b9535239f86e887c7d76d73cab6b Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 5 Jul 2018 12:36:50 -0400 Subject: [PATCH 2/4] Tweak relative timestamps. --- .../ConversationViewController.m | 42 ++++++++++++++++++- .../Messages/Attachments/TSAttachmentStream.m | 2 +- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 60b8311be..c174667be 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -237,6 +237,8 @@ typedef enum : NSUInteger { @property (nonatomic, nullable) NSNumber *previousLastTimestamp; @property (nonatomic, nullable) NSNumber *viewHorizonTimestamp; @property (nonatomic) ContactShareViewHelper *contactShareViewHelper; +@property (nonatomic) NSTimer *reloadTimer; +@property (nonatomic, nullable) NSDate *lastReloadDate; @end @@ -480,6 +482,39 @@ typedef enum : NSUInteger { }]; [self updateMessageMappingRangeOptions]; [self updateShouldObserveDBModifications]; + + self.reloadTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f + target:self + selector:@selector(reloadTimerDidFire) + userInfo:nil + repeats:YES]; +} + +- (void)dealloc +{ + [self.reloadTimer invalidate]; +} + +- (void)reloadTimerDidFire +{ + OWSAssertIsOnMainThread(); + + if (self.isUserScrolling || !self.isViewCompletelyAppeared || !self.isViewVisible + || !self.shouldObserveDBModifications || !self.viewHasEverAppeared) { + return; + } + + NSDate *now = [NSDate new]; + if (self.lastReloadDate) { + NSTimeInterval timeSinceLastReload = [now timeIntervalSinceDate:self.lastReloadDate]; + const NSTimeInterval kReloadFrequency = 60.f; + if (timeSinceLastReload < kReloadFrequency) { + return; + } + } + + DDLogVerbose(@"%@ reloading conversation view contents.", self.logTag); + [self resetContentAndLayout]; } - (BOOL)userLeftGroup @@ -795,6 +830,7 @@ typedef enum : NSUInteger { // Avoid layout corrupt issues and out-of-date message subtitles. [self.collectionView.collectionViewLayout invalidateLayout]; [self.collectionView reloadData]; + self.lastReloadDate = [NSDate new]; } - (void)setUserHasScrolled:(BOOL)userHasScrolled @@ -1669,8 +1705,7 @@ typedef enum : NSUInteger { self.loadMoreHeader.userInteractionEnabled = showLoadMoreHeader; if (valueChanged) { - [self.collectionView.collectionViewLayout invalidateLayout]; - [self.collectionView reloadData]; + [self resetContentAndLayout]; } } @@ -3341,6 +3376,7 @@ typedef enum : NSUInteger { OWSProdLogAndFail(@"%@ hasMalformedRowChange", self.logTag); [self reloadViewItems]; [self.collectionView reloadData]; + self.lastReloadDate = [NSDate new]; [self updateLastVisibleTimestamp]; [self cleanUpUnreadIndicatorIfNecessary]; return; @@ -3438,6 +3474,7 @@ typedef enum : NSUInteger { [self.collectionView performBatchUpdates:batchUpdates completion:batchUpdatesCompletion]; }]; } + self.lastReloadDate = [NSDate new]; } - (BOOL)shouldAnimateRowUpdates:(NSArray *)rowChanges @@ -4325,6 +4362,7 @@ typedef enum : NSUInteger { [self.conversationStyle updateProperties]; [self.headerView updateAvatar]; [self.collectionView reloadData]; + self.lastReloadDate = [NSDate new]; } - (void)groupWasUpdated:(TSGroupModel *)groupModel diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index a42666d52..eec26fb63 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -469,7 +469,7 @@ NS_ASSUME_NONNULL_BEGIN } if (result == nil) { - OWSFail(@"%@ Unable to build thumbnail for attachmentId: %@", self.logTag, self.uniqueId); + DDLogError(@"%@ Unable to build thumbnail for attachmentId: %@", self.logTag, self.uniqueId); return; } From d4fa7e5e68d2f06bf3ddab2be6e7a78ee42f15af Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 9 Jul 2018 13:55:25 -0400 Subject: [PATCH 3/4] Tweak relative timestamps. --- .../Cells/OWSMessageFooterView.m | 17 +-- Signal/src/util/DateUtil.h | 7 +- Signal/src/util/DateUtil.m | 127 ++++++++++++++---- .../translations/en.lproj/Localizable.strings | 5 +- 4 files changed, 116 insertions(+), 40 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m index 86426276a..a125c4dc0 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageFooterView.m @@ -168,16 +168,10 @@ NS_ASSUME_NONNULL_BEGIN self.timestampLabel.text = NSLocalizedString(@"MESSAGE_STATUS_SEND_FAILED", @"Label indicating that a message failed to send."); } else { - self.timestampLabel.text = [DateUtil formatTimestampAsTime:viewItem.interaction.timestamp - maxRelativeDurationMinutes:self.maxRelativeDurationMinutes]; + self.timestampLabel.text = [DateUtil formatMessageTimestamp:viewItem.interaction.timestamp]; } } -- (NSInteger)maxRelativeDurationMinutes -{ - return 59; -} - - (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem { OWSAssert(viewItem); @@ -194,9 +188,9 @@ NS_ASSUME_NONNULL_BEGIN // contents can be relative the current time. We avoid having // message bubbles' "visually vibrate" as their timestamp labels // vary in width. So we try to leave enough space for all possible - // contents of this label. - if ([DateUtil isTimestampRelative:viewItem.interaction.timestamp - maxRelativeDurationMinutes:self.maxRelativeDurationMinutes]) { + // contents of this label _for the first hour of its lifetime_, when + // the timestamp is particularly volatile. + if ([DateUtil isTimestampFromLastHour:viewItem.interaction.timestamp]) { // Measure the "now" case. self.timestampLabel.text = [DateUtil exemplaryNowTimeFormat]; timestampLabelWidth = MAX(timestampLabelWidth, [self.timestampLabel sizeThatFits:CGSizeZero].width); @@ -204,8 +198,7 @@ NS_ASSUME_NONNULL_BEGIN // Since this case varies with time, we multiply to leave // space for the worst case (whose exact value, due to localization, // is unpredictable). - self.timestampLabel.text = - [DateUtil exemplaryRelativeTimeFormatWithMaxRelativeDurationMinutes:self.maxRelativeDurationMinutes]; + self.timestampLabel.text = [DateUtil exemplaryMinutesTimeFormat]; timestampLabelWidth = MAX(timestampLabelWidth, [self.timestampLabel sizeThatFits:CGSizeZero].width + self.timestampLabel.font.lineHeight * 0.5f); diff --git a/Signal/src/util/DateUtil.h b/Signal/src/util/DateUtil.h index 7a8522312..ac2c13f96 100644 --- a/Signal/src/util/DateUtil.h +++ b/Signal/src/util/DateUtil.h @@ -26,12 +26,11 @@ NS_ASSUME_NONNULL_BEGIN + (NSString *)formatTimestampAsTime:(uint64_t)timestamp; + (NSString *)formatDateAsTime:(NSDate *)date; -+ (NSString *)formatTimestampAsTime:(uint64_t)timestamp - maxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes; ++ (NSString *)formatMessageTimestamp:(uint64_t)timestamp; -+ (BOOL)isTimestampRelative:(uint64_t)timestamp maxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes; ++ (BOOL)isTimestampFromLastHour:(uint64_t)timestamp; + (NSString *)exemplaryNowTimeFormat; -+ (NSString *)exemplaryRelativeTimeFormatWithMaxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes; ++ (NSString *)exemplaryMinutesTimeFormat; + (BOOL)isSameDayWithTimestamp:(uint64_t)timestamp1 timestamp:(uint64_t)timestamp2; + (BOOL)isSameDayWithDate:(NSDate *)date1 date:(NSDate *)date2; diff --git a/Signal/src/util/DateUtil.m b/Signal/src/util/DateUtil.m index 841ea5ffa..2c92f1c2d 100644 --- a/Signal/src/util/DateUtil.m +++ b/Signal/src/util/DateUtil.m @@ -146,6 +146,24 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return [[calendar components:NSCalendarUnitDay fromDate:date1 toDate:date2 options:0] day]; } +// Returns the difference in years, ignoring shorter units of time. +// If both dates fall in the same year, returns 0. +// If firstDate is from the year before secondDate, returns 1. +// +// Note: Assumes both dates use the "current" calendar. ++ (NSInteger)yearsFromFirstDate:(NSDate *)firstDate toSecondDate:(NSDate *)secondDate +{ + NSCalendar *calendar = [NSCalendar currentCalendar]; + NSCalendarUnit units = NSCalendarUnitEra | NSCalendarUnitYear; + NSDateComponents *comp1 = [calendar components:units fromDate:firstDate]; + NSDateComponents *comp2 = [calendar components:units fromDate:secondDate]; + [comp1 setHour:12]; + [comp2 setHour:12]; + NSDate *date1 = [calendar dateFromComponents:comp1]; + NSDate *date2 = [calendar dateFromComponents:comp2]; + return [[calendar components:NSCalendarUnitYear fromDate:date1 toDate:date2 options:0] year]; +} + + (NSString *)formatPastTimestampRelativeToNow:(uint64_t)pastTimestamp { OWSCAssert(pastTimestamp > 0); @@ -201,54 +219,117 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return dateTimeString.uppercaseString; } -+ (NSString *)formatTimestampAsTime:(uint64_t)timestamp maxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes ++ (NSDateFormatter *)otherYearMessageFormatter +{ + static NSDateFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [NSDateFormatter new]; + [formatter setLocale:[NSLocale currentLocale]]; + [formatter setDateFormat:@"MMM d, yyyy"]; + }); + return formatter; +} + ++ (NSDateFormatter *)thisYearMessageFormatter +{ + static NSDateFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [NSDateFormatter new]; + [formatter setLocale:[NSLocale currentLocale]]; + [formatter setDateFormat:@"MMM d"]; + }); + return formatter; +} + ++ (NSDateFormatter *)thisWeekMessageFormatter +{ + static NSDateFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [NSDateFormatter new]; + [formatter setLocale:[NSLocale currentLocale]]; + [formatter setDateFormat:@"E"]; + }); + return formatter; +} + ++ (NSString *)formatMessageTimestamp:(uint64_t)timestamp { NSDate *date = [NSDate ows_dateWithMillisecondsSince1970:timestamp]; - NSDate *now = [NSDate new]; + uint64_t nowTimestamp = [NSDate ows_millisecondTimeStamp]; + NSDate *nowDate = [NSDate ows_dateWithMillisecondsSince1970:nowTimestamp]; NSCalendar *calendar = [NSCalendar currentCalendar]; - NSInteger minutesDiff - = MAX(0, [[calendar components:NSCalendarUnitMinute fromDate:date toDate:now options:0] minute]); - // Treat anything in the last two minutes as "now", so that we - // don't have to worry about pluralization while formatting. - if (minutesDiff <= 1) { - return NSLocalizedString(@"DATE_NOW", @"The present; the current time."); - } else if (minutesDiff <= maxRelativeDurationMinutes) { + NSInteger yearsDiff = [self yearsFromFirstDate:date toSecondDate:nowDate]; + NSInteger daysDiff = [self daysFromFirstDate:date toSecondDate:nowDate]; + NSInteger minutesDiff + = MAX(0, [[calendar components:NSCalendarUnitMinute fromDate:date toDate:nowDate options:0] minute]); + NSInteger hoursDiff + = MAX(0, [[calendar components:NSCalendarUnitHour fromDate:date toDate:nowDate options:0] hour]); + + NSString *result; + if (yearsDiff > 0) { + // "Long date" + locale-specific "short" time format. + NSString *dayOfWeek = [self.otherYearMessageFormatter stringFromDate:date]; + NSString *formattedTime = [[self timeFormatter] stringFromDate:date]; + result = [[dayOfWeek rtlSafeAppend:@" "] rtlSafeAppend:formattedTime]; + } else if (daysDiff >= 7) { + // "Short date" + locale-specific "short" time format. + NSString *dayOfWeek = [self.thisYearMessageFormatter stringFromDate:date]; + NSString *formattedTime = [[self timeFormatter] stringFromDate:date]; + result = [[dayOfWeek rtlSafeAppend:@" "] rtlSafeAppend:formattedTime]; + } else if (daysDiff > 0) { + // "Day of week" + locale-specific "short" time format. + NSString *dayOfWeek = [self.thisWeekMessageFormatter stringFromDate:date]; + NSString *formattedTime = [[self timeFormatter] stringFromDate:date]; + result = [[dayOfWeek rtlSafeAppend:@" "] rtlSafeAppend:formattedTime]; + } else if (minutesDiff < 1) { + result = NSLocalizedString(@"DATE_NOW", @"The present; the current time."); + } else if (hoursDiff < 1) { NSString *minutesString = [OWSFormat formatInt:(int)minutesDiff]; - return [NSString stringWithFormat:NSLocalizedString(@"DATE_MINUTES_AGO_FORMAT", - @"Format string for a relative time, expressed as a certain number of " - @"minutes in the past. Embeds {{The number of minutes}}."), - minutesString]; + result = [NSString stringWithFormat:NSLocalizedString(@"DATE_MINUTES_AGO_FORMAT", + @"Format string for a relative time, expressed as a certain number of " + @"minutes in the past. Embeds {{The number of minutes}}."), + minutesString]; } else { - return [self formatDateAsTime:date]; + NSString *hoursString = [OWSFormat formatInt:(int)hoursDiff]; + result = [NSString stringWithFormat:NSLocalizedString(@"DATE_HOURS_AGO_FORMAT", + @"Format string for a relative time, expressed as a certain number of " + @"hours in the past. Embeds {{The number of hours}}."), + hoursString]; } + return result.uppercaseString; } -+ (BOOL)isTimestampRelative:(uint64_t)timestamp maxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes ++ (BOOL)isTimestampFromLastHour:(uint64_t)timestamp { NSDate *date = [NSDate ows_dateWithMillisecondsSince1970:timestamp]; - NSDate *now = [NSDate new]; + uint64_t nowTimestamp = [NSDate ows_millisecondTimeStamp]; + NSDate *nowDate = [NSDate ows_dateWithMillisecondsSince1970:nowTimestamp]; NSCalendar *calendar = [NSCalendar currentCalendar]; - NSInteger minutesDiff - = MAX(0, [[calendar components:NSCalendarUnitMinute fromDate:date toDate:now options:0] minute]); - return minutesDiff <= maxRelativeDurationMinutes; + NSInteger hoursDiff + = MAX(0, [[calendar components:NSCalendarUnitHour fromDate:date toDate:nowDate options:0] hour]); + return hoursDiff < 1; } + (NSString *)exemplaryNowTimeFormat { - return NSLocalizedString(@"DATE_NOW", @"The present; the current time."); + return NSLocalizedString(@"DATE_NOW", @"The present; the current time.").uppercaseString; } -+ (NSString *)exemplaryRelativeTimeFormatWithMaxRelativeDurationMinutes:(NSInteger)maxRelativeDurationMinutes ++ (NSString *)exemplaryMinutesTimeFormat { - NSString *minutesString = [OWSFormat formatInt:(int)maxRelativeDurationMinutes]; + NSString *minutesString = [OWSFormat formatInt:(int)59]; return [NSString stringWithFormat:NSLocalizedString(@"DATE_MINUTES_AGO_FORMAT", @"Format string for a relative time, expressed as a certain number of " @"minutes in the past. Embeds {{The number of minutes}}."), - minutesString]; + minutesString] + .uppercaseString; } + (BOOL)isSameDayWithTimestamp:(uint64_t)timestamp1 timestamp:(uint64_t)timestamp2 diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 5f4c4ea23..cc95d4978 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -611,7 +611,10 @@ /* Title shown while the app is updating its database. */ "DATABASE_VIEW_OVERLAY_TITLE" = "Optimizing Database"; -/* Format string for a duration of time, expressed as a certain number of minutes. Embeds {{The number of minutes}}. */ +/* Format string for a relative time, expressed as a certain number of hours in the past. Embeds {{The number of hours}}. */ +"DATE_HOURS_AGO_FORMAT" = "%@ Hr Ago"; + +/* Format string for a relative time, expressed as a certain number of minutes in the past. Embeds {{The number of minutes}}. */ "DATE_MINUTES_AGO_FORMAT" = "%@ Min Ago"; /* The present; the current time. */ From 5e71f3130b3e8d2e2ce0117c802e36820a06ce61 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 9 Jul 2018 16:39:48 -0400 Subject: [PATCH 4/4] Respond to CR. --- Signal/src/util/DateUtil.h | 2 ++ Signal/src/util/DateUtil.m | 1 + SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Signal/src/util/DateUtil.h b/Signal/src/util/DateUtil.h index ac2c13f96..3a5974a21 100644 --- a/Signal/src/util/DateUtil.h +++ b/Signal/src/util/DateUtil.h @@ -29,6 +29,8 @@ NS_ASSUME_NONNULL_BEGIN + (NSString *)formatMessageTimestamp:(uint64_t)timestamp; + (BOOL)isTimestampFromLastHour:(uint64_t)timestamp; +// These two "exemplary" values can be used by views to measure +// the likely size for recent values formatted using isTimestampFromLastHour:. + (NSString *)exemplaryNowTimeFormat; + (NSString *)exemplaryMinutesTimeFormat; diff --git a/Signal/src/util/DateUtil.m b/Signal/src/util/DateUtil.m index 2c92f1c2d..bda7dcaec 100644 --- a/Signal/src/util/DateUtil.m +++ b/Signal/src/util/DateUtil.m @@ -263,6 +263,7 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; NSCalendar *calendar = [NSCalendar currentCalendar]; + // Note: we are careful to treat "future" dates as "now". NSInteger yearsDiff = [self yearsFromFirstDate:date toSecondDate:nowDate]; NSInteger daysDiff = [self daysFromFirstDate:date toSecondDate:nowDate]; NSInteger minutesDiff diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index eec26fb63..a42666d52 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -469,7 +469,7 @@ NS_ASSUME_NONNULL_BEGIN } if (result == nil) { - DDLogError(@"%@ Unable to build thumbnail for attachmentId: %@", self.logTag, self.uniqueId); + OWSFail(@"%@ Unable to build thumbnail for attachmentId: %@", self.logTag, self.uniqueId); return; }