Merge branch 'charlesmchen/relativeTimestamps'

pull/1/head
Matthew Chen 7 years ago
commit 4cedce2635

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

@ -168,7 +168,7 @@ 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 formatMessageTimestamp:viewItem.interaction.timestamp];
}
}
@ -180,7 +180,34 @@ 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 _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);
// 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 exemplaryMinutesTimeFormat];
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);

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

@ -23,8 +23,16 @@ 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 *)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;
+ (BOOL)isSameDayWithTimestamp:(uint64_t)timestamp1 timestamp:(uint64_t)timestamp2;
+ (BOOL)isSameDayWithDate:(NSDate *)date1 date:(NSDate *)date2;

@ -4,6 +4,7 @@
#import "DateUtil.h"
#import <SignalMessaging/NSString+OWS.h>
#import <SignalMessaging/OWSFormat.h>
#import <SignalServiceKit/NSDate+OWS.h>
NS_ASSUME_NONNULL_BEGIN
@ -145,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);
@ -187,12 +206,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 +219,120 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE";
return dateTimeString.uppercaseString;
}
+ (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];
uint64_t nowTimestamp = [NSDate ows_millisecondTimeStamp];
NSDate *nowDate = [NSDate ows_dateWithMillisecondsSince1970:nowTimestamp];
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
= 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];
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 {
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)isTimestampFromLastHour:(uint64_t)timestamp
{
NSDate *date = [NSDate ows_dateWithMillisecondsSince1970:timestamp];
uint64_t nowTimestamp = [NSDate ows_millisecondTimeStamp];
NSDate *nowDate = [NSDate ows_dateWithMillisecondsSince1970:nowTimestamp];
NSCalendar *calendar = [NSCalendar currentCalendar];
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.").uppercaseString;
}
+ (NSString *)exemplaryMinutesTimeFormat
{
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]
.uppercaseString;
}
+ (BOOL)isSameDayWithTimestamp:(uint64_t)timestamp1 timestamp:(uint64_t)timestamp2
{
return [self isSameDayWithDate:[NSDate ows_dateWithMillisecondsSince1970:timestamp1]
@ -211,6 +344,7 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE";
NSInteger dayDifference = [self daysFromFirstDate:date1 toSecondDate:date2];
return dayDifference == 0;
}
@end
NS_ASSUME_NONNULL_END

@ -611,6 +611,15 @@
/* Title shown while the app is updating its database. */
"DATABASE_VIEW_OVERLAY_TITLE" = "Optimizing Database";
/* 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. */
"DATE_NOW" = "Now";
/* The current day. */
"DATE_TODAY" = "Today";

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

Loading…
Cancel
Save