diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 7cd70287e9..17cdabca28 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ 344A761324B36C8C009D69A5 /* TestingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344A761224B36C8C009D69A5 /* TestingViewController.swift */; }; 344DAEDF248A7526000AD4FA /* DebugUIStress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344DAEDE248A7526000AD4FA /* DebugUIStress.swift */; }; 344F248D2007CCD600CFB4F4 /* DisplayableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248C2007CCD600CFB4F4 /* DisplayableText.swift */; }; + 3452851B26DE890400824983 /* DateUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3452851A26DE890300824983 /* DateUtil.swift */; }; 3452C15625348F1D00F69070 /* GroupsV2Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3452C15525348F1D00F69070 /* GroupsV2Migration.swift */; }; 34546F502649989D007C4958 /* ChatColorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34546F4F2649989C007C4958 /* ChatColorViewController.swift */; }; 3456A73223D63EBE00947219 /* GroupsV2Protos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3456A73123D63EBE00947219 /* GroupsV2Protos.swift */; }; @@ -1221,6 +1222,7 @@ 344A761224B36C8C009D69A5 /* TestingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestingViewController.swift; sourceTree = ""; }; 344DAEDE248A7526000AD4FA /* DebugUIStress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugUIStress.swift; sourceTree = ""; }; 344F248C2007CCD600CFB4F4 /* DisplayableText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayableText.swift; sourceTree = ""; }; + 3452851A26DE890300824983 /* DateUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateUtil.swift; sourceTree = ""; }; 3452C15525348F1D00F69070 /* GroupsV2Migration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupsV2Migration.swift; sourceTree = ""; }; 34546F4F2649989C007C4958 /* ChatColorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatColorViewController.swift; sourceTree = ""; }; 3456A73123D63EBE00947219 /* GroupsV2Protos.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupsV2Protos.swift; sourceTree = ""; }; @@ -2543,9 +2545,11 @@ 883A7FD0269F640D00841DF9 /* Avatars */, 343D3D991E9283F100165CA4 /* BlockListUIUtils.h */, 343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */, + 88D7BA9D266809F50088D1C2 /* CallMessageRelay.swift */, 3466087120E550F300AFFE73 /* ConversationStyle.swift */, 3464451022B7F97100A957B1 /* DateUtil.h */, 3464450F22B7F97000A957B1 /* DateUtil.m */, + 3452851A26DE890300824983 /* DateUtil.swift */, 34480B4D1FD0A7A300BC14EF /* DebugLogger.h */, 34480B4E1FD0A7A300BC14EF /* DebugLogger.m */, 348ECD8B25F278450083C47D /* DebugLogger.swift */, @@ -2579,7 +2583,6 @@ 340872BE22393CF900CB25B0 /* UIGestureRecognizer+OWS.swift */, B97940251832BD2400BD66CB /* UIUtil.h */, B97940261832BD2400BD66CB /* UIUtil.m */, - 88D7BA9D266809F50088D1C2 /* CallMessageRelay.swift */, ); path = utils; sourceTree = ""; @@ -5457,6 +5460,7 @@ 889C8A8D248831AC00C3D3D8 /* BlurTooltip.swift in Sources */, 34BBC859220C7ADA00857249 /* ImageEditorStrokeItem.swift in Sources */, 451F8A351FD710DE005CB9DA /* Searcher.swift in Sources */, + 3452851B26DE890400824983 /* DateUtil.swift in Sources */, 3464450D22B7F93600A957B1 /* OWSOrphanDataCleaner.m in Sources */, 348D8E72262DF67A0019CAA8 /* ConversationAvatarView.swift in Sources */, 3474C57226111605006723D2 /* PaymentsFormat.swift in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentDateHeader.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentDateHeader.swift index d264075772..8fa97ee7cf 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentDateHeader.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentDateHeader.swift @@ -130,7 +130,7 @@ public class CVComponentDateHeader: CVComponentBase, CVRootComponent { static func buildState(interaction: TSInteraction) -> State { let date = Date(millisecondsSince1970: interaction.timestamp) - let text = DateUtil.formatDate(forConversationDateBreaks: date) + let text = DateUtil.formatDateHeaderForCVC(date) return State(text: text) } diff --git a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentFooter.swift b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentFooter.swift index 06a40933bc..241e04717c 100644 --- a/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentFooter.swift +++ b/Signal/src/ViewControllers/ConversationView/CV/CVComponents/CVComponentFooter.swift @@ -199,7 +199,8 @@ public class CVComponentFooter: CVComponentBase, CVComponent { comment: "Label indicating that a message failed to send.") } } else { - return DateUtil.formatMessageTimestamp(interaction.timestamp, shouldUseLongFormat: shouldUseLongFormat) + return DateUtil.formatMessageTimestampForCVC(interaction.timestamp, + shouldUseLongFormat: shouldUseLongFormat) } } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 7c505a6f36..1b808ee5ca 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -2885,8 +2885,7 @@ typedef NS_CLOSED_ENUM(NSUInteger, MessageContentType) { return [DebugUIMessagesSingleAction actionWithLabel:[NSString stringWithFormat:@"Fake Back-Date Message (%@)", label] unstaggeredActionBlock:^(NSUInteger index, SDSAnyWriteTransaction *transaction) { - NSString *messageBody = - [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:self.randomText]; + NSString *messageBody = [@[ @(index).stringValue, self.randomText, label ] componentsJoinedByString:@" "]; TSOutgoingMessage *message = [self createFakeOutgoingMessage:thread messageBody:messageBody fakeAssetLoader:nil @@ -2898,8 +2897,9 @@ typedef NS_CLOSED_ENUM(NSUInteger, MessageContentType) { linkPreview:nil messageSticker:nil transaction:transaction]; - [message replaceReceivedAtTimestamp:(uint64_t)((int64_t)[NSDate ows_millisecondTimeStamp] + dateOffset) - transaction:transaction]; + uint64_t timestamp = (uint64_t)((int64_t)[NSDate ows_millisecondTimeStamp] + dateOffset); + [message replaceTimestamp:timestamp transaction:transaction]; + [message replaceReceivedAtTimestamp:timestamp transaction:transaction]; }]; } @@ -2926,6 +2926,12 @@ typedef NS_CLOSED_ENUM(NSUInteger, MessageContentType) { [actions addObject:[self fakeBackDatedMessageAction:thread label:@"Ten Days Ago" dateOffset:-(int64_t)kDayInMs * 10]]; + [actions addObject:[self fakeBackDatedMessageAction:thread + label:@"5 Months Ago" + dateOffset:-(int64_t)kMonthInMs * 5]]; + [actions addObject:[self fakeBackDatedMessageAction:thread + label:@"7 Months Ago" + dateOffset:-(int64_t)kMonthInMs * 7]]; [actions addObject:[self fakeBackDatedMessageAction:thread label:@"400 Days Ago" dateOffset:-(int64_t)kDayInMs * 400]]; diff --git a/SignalMessaging/utils/DateUtil.h b/SignalMessaging/utils/DateUtil.h index 41eb0f30db..40f8860662 100644 --- a/SignalMessaging/utils/DateUtil.h +++ b/SignalMessaging/utils/DateUtil.h @@ -7,7 +7,6 @@ NS_ASSUME_NONNULL_BEGIN @interface DateUtil : NSObject + (NSDateFormatter *)dateFormatter; -+ (NSDateFormatter *)timeFormatter; + (NSDateFormatter *)monthAndDayFormatter; + (NSDateFormatter *)shortDayOfWeekFormatter; @@ -29,9 +28,6 @@ NS_ASSUME_NONNULL_BEGIN + (NSString *)formatTimestampAsDate:(uint64_t)timestamp; + (NSString *)formatDateAsDate:(NSDate *)date; -+ (NSString *)formatMessageTimestamp:(uint64_t)timestamp - shouldUseLongFormat:(BOOL)shouldUseLongFormat; - + (BOOL)isTimestampFromLastHour:(uint64_t)timestamp; // These two "exemplary" values can be used by views to measure @@ -39,11 +35,11 @@ NS_ASSUME_NONNULL_BEGIN + (NSString *)exemplaryNowTimeFormat; + (NSString *)exemplaryMinutesTimeFormat; -+ (NSString *)formatDateForConversationDateBreaks:(NSDate *)date; - + (BOOL)isSameDayWithTimestamp:(uint64_t)timestamp1 timestamp:(uint64_t)timestamp2; + (BOOL)isSameDayWithDate:(NSDate *)date1 date:(NSDate *)date2; ++ (BOOL)dateIsOlderThanYesterday:(NSDate *)date; + @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/utils/DateUtil.m b/SignalMessaging/utils/DateUtil.m index 96578fdae3..625604e1e8 100644 --- a/SignalMessaging/utils/DateUtil.m +++ b/SignalMessaging/utils/DateUtil.m @@ -26,64 +26,6 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return formatter; } -+ (NSDateFormatter *)dateBreakRelativeDateFormatter -{ - static NSDateFormatter *formatter; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - formatter = [NSDateFormatter new]; - formatter.locale = [NSLocale currentLocale]; - formatter.dateStyle = NSDateFormatterShortStyle; - formatter.timeStyle = NSDateFormatterNoStyle; - formatter.doesRelativeDateFormatting = YES; - }); - - return formatter; -} - -+ (NSDateFormatter *)dateBreakThisWeekDateFormatter -{ - static NSDateFormatter *formatter; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - formatter = [NSDateFormatter new]; - formatter.locale = [NSLocale currentLocale]; - // "Monday", "Tuesday", etc. - formatter.dateFormat = @"EEEE"; - }); - - return formatter; -} - -+ (NSDateFormatter *)dateBreakThisYearDateFormatter -{ - static NSDateFormatter *formatter; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - formatter = [NSDateFormatter new]; - formatter.locale = [NSLocale currentLocale]; - // Tue, Jun 6 - [formatter setLocalizedDateFormatFromTemplate:@"EE, MMM d"]; - }); - - return formatter; -} - -+ (NSDateFormatter *)dateBreakOldDateFormatter -{ - static NSDateFormatter *formatter; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - formatter = [NSDateFormatter new]; - formatter.locale = [NSLocale currentLocale]; - formatter.dateStyle = NSDateFormatterMediumStyle; - formatter.timeStyle = NSDateFormatterNoStyle; - formatter.doesRelativeDateFormatting = YES; - }); - - return formatter; -} - + (NSDateFormatter *)weekdayFormatter { static NSDateFormatter *formatter; @@ -96,19 +38,6 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return formatter; } -+ (NSDateFormatter *)timeFormatter -{ - static NSDateFormatter *formatter; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - formatter = [NSDateFormatter new]; - [formatter setLocale:[NSLocale currentLocale]]; - [formatter setTimeStyle:NSDateFormatterShortStyle]; - [formatter setDateStyle:NSDateFormatterNoStyle]; - }); - return formatter; -} - + (NSDateFormatter *)monthAndDayFormatter { static NSDateFormatter *formatter; @@ -200,42 +129,6 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return dayDifference == 1; } -// Returns the difference in days, ignoring hours, minutes, seconds. -// If both dates are the same date, returns 0. -// If firstDate is a day before secondDate, returns 1. -// -// Note: Assumes both dates use the "current" calendar. -+ (NSInteger)daysFromFirstDate:(NSDate *)firstDate toSecondDate:(NSDate *)secondDate -{ - NSCalendar *calendar = [NSCalendar currentCalendar]; - NSCalendarUnit units = NSCalendarUnitEra | NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay; - 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: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 { OWSCAssertDebug(pastTimestamp > 0); @@ -284,25 +177,6 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return dateTimeString; } -+ (NSString *)formatDateForConversationDateBreaks:(NSDate *)date -{ - OWSAssertDebug(date); - - if (![self dateIsThisYear:date]) { - // last year formatter: Nov 11, 2017 - return [self.dateBreakOldDateFormatter stringFromDate:date]; - } else if ([self dateIsOlderThanOneWeek:date]) { - // this year formatter: Tue, Jun 23 - return [self.dateBreakThisYearDateFormatter stringFromDate:date]; - } else if ([self dateIsOlderThanYesterday:date]) { - // day of week formatter: Thursday - return [self.dateBreakThisWeekDateFormatter stringFromDate:date]; - } else { - // relative format: Today / Yesterday - return [self.dateBreakRelativeDateFormatter stringFromDate:date]; - } -} - + (NSString *)formatTimestampAsTime:(uint64_t)timestamp { return [self formatDateAsTime:[NSDate ows_dateWithMillisecondsSince1970:timestamp]]; @@ -385,76 +259,6 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return formatter; } -+ (NSString *)formatMessageTimestamp:(uint64_t)timestamp - shouldUseLongFormat:(BOOL)shouldUseLongFormat -{ - NSDate *date = [NSDate ows_dateWithMillisecondsSince1970:timestamp]; - uint64_t nowTimestamp = [NSDate ows_millisecondTimeStamp]; - NSDate *nowDate = [NSDate ows_dateWithMillisecondsSince1970:nowTimestamp]; - - NSCalendar *calendar = [NSCalendar currentCalendar]; - - NSDateComponents *relativeDiffComponents = - [calendar components:NSCalendarUnitMinute | NSCalendarUnitHour fromDate:date toDate:nowDate options:0]; - - // 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 hoursDiff = MAX(0, [relativeDiffComponents hour]); - NSInteger minutesDiff = MAX(0, [relativeDiffComponents minute]); - - if (yearsDiff > 0) { - // "Long date" + locale-specific "short" time format. - NSString *dayOfWeek = [self.otherYearMessageFormatter stringFromDate:date]; - NSString *formattedTime = [[self timeFormatter] stringFromDate:date]; - return [[dayOfWeek stringByAppendingString:@" "] stringByAppendingString:formattedTime]; - - } else if (daysDiff >= 7) { - // "Short date" + locale-specific "short" time format. - NSString *dayOfWeek = [self.thisYearMessageFormatter stringFromDate:date]; - NSString *formattedTime = [[self timeFormatter] stringFromDate:date]; - return [[dayOfWeek stringByAppendingString:@" "] stringByAppendingString:formattedTime]; - - } else if (daysDiff > 0) { - // "Day of week" + locale-specific "short" time format. - NSDateFormatter *thisWeekMessageFormatter = (shouldUseLongFormat - ? self.thisWeekMessageFormatterLong - : self.thisWeekMessageFormatterShort); - NSString *dayOfWeek = [thisWeekMessageFormatter stringFromDate:date]; - NSString *formattedTime = [[self timeFormatter] stringFromDate:date]; - return [[dayOfWeek stringByAppendingString:@" "] stringByAppendingString:formattedTime]; - - } else if (hoursDiff > 0) { - if (shouldUseLongFormat && hoursDiff == 1) { - // Long format has a distinction between singular and plural - return NSLocalizedString(@"DATE_ONE_HOUR_AGO_LONG", @"Full string for a relative time of one hour ago."); - } - - NSString *shortFormat = 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}}."); - NSString *longFormat = NSLocalizedString(@"DATE_HOURS_AGO_LONG_FORMAT", @"Full format string for a relative time, expressed as a certain number of hours in the past. Embeds {{The number of hours}}."); - - NSString *formatString = shouldUseLongFormat ? longFormat : shortFormat; - NSString *hoursString = [OWSFormat formatInt:hoursDiff]; - return [NSString stringWithFormat:formatString, hoursString]; - - } else if (minutesDiff > 0) { - if (shouldUseLongFormat && minutesDiff == 1) { - // Long format has a distinction between singular and plural - return NSLocalizedString(@"DATE_ONE_MINUTE_AGO_LONG", @"Full string for a relative time of one minute ago."); - } - - NSString *shortFormat = 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}}."); - NSString *longFormat = NSLocalizedString(@"DATE_MINUTES_AGO_LONG_FORMAT", @"Full format string for a relative time, expressed as a certain number of minutes in the past. Embeds {{The number of minutes}}."); - - NSString *formatString = shouldUseLongFormat ? longFormat : shortFormat; - NSString *minutesString = [OWSFormat formatInt:minutesDiff]; - return [NSString stringWithFormat:formatString, minutesString]; - - } else { - return NSLocalizedString(@"DATE_NOW", @"The present; the current time."); - } -} - + (BOOL)isTimestampFromLastHour:(uint64_t)timestamp { NSDate *date = [NSDate ows_dateWithMillisecondsSince1970:timestamp]; diff --git a/SignalMessaging/utils/DateUtil.swift b/SignalMessaging/utils/DateUtil.swift new file mode 100644 index 0000000000..f59d30c6a7 --- /dev/null +++ b/SignalMessaging/utils/DateUtil.swift @@ -0,0 +1,145 @@ +// +// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// + +import Foundation + +extension DateUtil { + + @objc + public static let timeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = .current + formatter.timeStyle = .short + formatter.dateStyle = .none + return formatter + }() + + // Returns the difference in days, ignoring hours, minutes, seconds. + // If both dates are the same date, returns 0. + // If firstDate is a day before secondDate, returns 1. + // + // Note: Assumes both dates use the "current" calendar. + @objc + public static func daysFrom(firstDate: Date, toSecondDate secondDate: Date) -> Int { + let calendar = Calendar.current + let units: Set = [.era, .year, .month, .day] + var components1 = calendar.dateComponents(units, from: firstDate) + var components2 = calendar.dateComponents(units, from: secondDate) + components1.hour = 12 + components2.hour = 12 + guard let date1 = calendar.date(from: components1), + let date2 = calendar.date(from: components2) else { + owsFailDebug("Invalid date.") + return 0 + } + guard let result = calendar.dateComponents([.day], from: date1, to: date2).day else { + owsFailDebug("Missing result.") + return 0 + } + return result + } + + // 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. + @objc + public static func yearsFrom(firstDate: Date, toSecondDate secondDate: Date) -> Int { + let calendar = Calendar.current + let units: Set = [.era, .year] + var components1 = calendar.dateComponents(units, from: firstDate) + var components2 = calendar.dateComponents(units, from: secondDate) + components1.hour = 12 + components2.hour = 12 + guard let date1 = calendar.date(from: components1), + let date2 = calendar.date(from: components2) else { + owsFailDebug("Invalid date.") + return 0 + } + guard let result = calendar.dateComponents([.year], from: date1, to: date2).year else { + owsFailDebug("Missing result.") + return 0 + } + return result + } + + // We might receive a message "from the future" due to a bug or + // malicious sender or a sender whose device time is misconfigured, + // etc. Clamp message and date headers dates to the past & present. + private static func safeDateForCVC(_ date: Date) -> Date { + let nowDate = Date() + return date < nowDate ? date : nowDate + } + + @objc + public static func formatMessageTimestampForCVC(_ timestamp: UInt64, + shouldUseLongFormat: Bool) -> String { + let date = safeDateForCVC(Date(millisecondsSince1970: timestamp)) + let calendar = Calendar.current + let minutesDiff = calendar.dateComponents([.minute], from: date, to: Date()).minute ?? 0 + if minutesDiff < 1 { + return NSLocalizedString("DATE_NOW", + comment: "The present the current time.") + } else if shouldUseLongFormat && minutesDiff == 1 { + // Long format has a distinction between singular and plural + return NSLocalizedString("DATE_ONE_MINUTE_AGO_LONG", + comment: "Full string for a relative time of one minute ago.") + } else if minutesDiff <= 60 { + let shortFormat = NSLocalizedString("DATE_MINUTES_AGO_FORMAT", + comment: "Format string for a relative time, expressed as a certain number of minutes in the past. Embeds {{The number of minutes}}.") + let longFormat = NSLocalizedString("DATE_MINUTES_AGO_LONG_FORMAT", + comment: "Full format string for a relative time, expressed as a certain number of minutes in the past. Embeds {{The number of minutes}}.") + let format = shouldUseLongFormat ? longFormat : shortFormat + let minutesString = OWSFormat.formatInt(minutesDiff) + return String(format: format, minutesString) + } else { + return timeFormatter.string(from: date) + } + } + + @objc + public static func formatDateHeaderForCVC(_ date: Date) -> String { + let date = safeDateForCVC(date) + let calendar = Calendar.current + let monthsDiff = calendar.dateComponents([.month], from: date, to: Date()).month ?? 0 + if monthsDiff >= 6 { + // Mar 8, 2017 + return dateHeaderOldDateFormatter.string(from: date) + } else if dateIsOlderThanYesterday(date) { + // Wed, Mar 3 + return dateHeaderRecentDateFormatter.string(from: date) + } else { + // Today / Yesterday + return dateHeaderRelativeDateFormatter.string(from: date) + } + } + + private static let dateHeaderRecentDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = .current + // Tue, Jun 6 + formatter.setLocalizedDateFormatFromTemplate("EE, MMM d") + return formatter + }() + + private static let dateHeaderOldDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = .current + formatter.timeStyle = .none + // Mar 8, 2017 + formatter.dateStyle = .medium + return formatter + }() + + private static let dateHeaderRelativeDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = .current + formatter.timeStyle = .none + formatter.dateStyle = .short + // Today / Yesterday + formatter.doesRelativeDateFormatting = true + return formatter + }() +} diff --git a/SignalServiceKit/src/Messages/Interactions/TSInteraction.h b/SignalServiceKit/src/Messages/Interactions/TSInteraction.h index c0598ef357..b612e2863f 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInteraction.h +++ b/SignalServiceKit/src/Messages/Interactions/TSInteraction.h @@ -104,6 +104,7 @@ NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:receivedAtTimestamp - (void)replaceSortId:(uint64_t)sortId; #if TESTABLE_BUILD +- (void)replaceTimestamp:(uint64_t)timestamp transaction:(SDSAnyWriteTransaction *)transaction; - (void)replaceReceivedAtTimestamp:(uint64_t)receivedAtTimestamp NS_SWIFT_NAME(replaceReceivedAtTimestamp(_:)); - (void)replaceReceivedAtTimestamp:(uint64_t)receivedAtTimestamp transaction:(SDSAnyWriteTransaction *)transaction; #endif diff --git a/SignalServiceKit/src/Messages/Interactions/TSInteraction.m b/SignalServiceKit/src/Messages/Interactions/TSInteraction.m index ba2bf459bc..78916c5045 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInteraction.m +++ b/SignalServiceKit/src/Messages/Interactions/TSInteraction.m @@ -315,6 +315,13 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value) } #if TESTABLE_BUILD + +- (void)replaceTimestamp:(uint64_t)timestamp transaction:(SDSAnyWriteTransaction *)transaction +{ + [self anyUpdateWithTransaction:transaction + block:^(TSInteraction *interaction) { interaction.timestamp = timestamp; }]; +} + - (void)replaceReceivedAtTimestamp:(uint64_t)receivedAtTimestamp { self.receivedAtTimestamp = receivedAtTimestamp;