From 712d6d89e10e228569dbdb8fc513af15e1bbeb3d Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 5 Jul 2018 11:56:46 -0400 Subject: [PATCH] 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