diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index a5e4ae058..6a568b8ef 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -232,6 +232,10 @@ NS_ASSUME_NONNULL_BEGIN actionBlock:^{ [DebugUIMessages createSystemMessagesInThread:thread]; }], + [OWSTableItem itemWithTitle:@"Create messages with variety of timestamps" + actionBlock:^{ + [DebugUIMessages createTimestampMessagesInThread:thread]; + }], [OWSTableItem itemWithTitle:@"Send 10 text and system messages" actionBlock:^{ @@ -1534,6 +1538,61 @@ NS_ASSUME_NONNULL_BEGIN }); } ++ (void)createTimestampMessagesInThread:(TSThread *)thread +{ + OWSAssert(thread); + + uint64_t now = [NSDate ows_millisecondTimeStamp]; + NSArray *timestamps = @[ + @(now + 1 * kHourInMs), + @(now), + @(now - 1 * kHourInMs), + @(now - 12 * kHourInMs), + @(now - 1 * kDayInMs), + @(now - 2 * kDayInMs), + @(now - 3 * kDayInMs), + @(now - 6 * kDayInMs), + @(now - 7 * kDayInMs), + @(now - 8 * kDayInMs), + @(now - 2 * kWeekInMs), + @(now - 1 * kMonthInMs), + @(now - 2 * kMonthInMs), + ]; + NSMutableArray *recipientIds = [thread.recipientIdentifiers mutableCopy]; + [recipientIds removeObject:[TSAccountManager localNumber]]; + NSString *recipientId = (recipientIds.count > 0 ? recipientIds.firstObject : @"+19174054215"); + + [TSStorageManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + for (NSNumber *timestamp in timestamps) { + NSString *randomText = [self randomText]; + { + TSIncomingMessage *message = + [[TSIncomingMessage alloc] initWithTimestamp:timestamp.unsignedLongLongValue + inThread:thread + authorId:recipientId + sourceDeviceId:0 + messageBody:randomText]; + [message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; + } + { + TSOutgoingMessage *message = + [[TSOutgoingMessage alloc] initWithTimestamp:timestamp.unsignedLongLongValue + inThread:thread + messageBody:randomText]; + [message saveWithTransaction:transaction]; + [message updateWithMessageState:TSOutgoingMessageStateSentToService transaction:transaction]; + [message updateWithSentRecipient:recipientId transaction:transaction]; + [message updateWithDeliveredToRecipientId:recipientId + deliveryTimestamp:timestamp + transaction:transaction]; + [message updateWithReadRecipientId:recipientId + readTimestamp:timestamp.unsignedLongLongValue + transaction:transaction]; + } + } + }]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index bbb029f2d..d8ee9307e 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -211,7 +211,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi } for recipientId in thread.recipientIdentifiers { - let (recipientStatus, statusMessage) = MessageRecipientStatusUtils.recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, recipientId: recipientId, referenceView: self.view) + let (recipientStatus, shortStatusMessage, longStatusMessage) = MessageRecipientStatusUtils.recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, recipientId: recipientId, referenceView: self.view) guard recipientStatus == recipientStatusGroup else { continue @@ -229,9 +229,11 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi let cell = ContactTableViewCell() cell.configure(withRecipientId: recipientId, contactsManager: self.contactsManager) let statusLabel = UILabel() - statusLabel.text = statusMessage + // We use the "short" status message to avoid being redundant with the section title. + statusLabel.text = shortStatusMessage statusLabel.textColor = UIColor.ows_darkGray statusLabel.font = UIFont.ows_footnote() + statusLabel.adjustsFontSizeToFitWidth = true statusLabel.sizeToFit() cell.accessoryView = statusLabel cell.autoSetDimension(.height, toSize: ContactTableViewCell.rowHeight()) @@ -258,12 +260,14 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_SENT_DATE_TIME", comment: "Label for the 'sent date & time' field of the 'message metadata' view."), - value: DateUtil.formatPastTimestampRelativeToNow(message.timestamp))) + value: DateUtil.formatPastTimestampRelativeToNow(message.timestamp, + isRTL:self.view.isRTL()))) if message as? TSIncomingMessage != nil { rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_RECEIVED_DATE_TIME", comment: "Label for the 'received date & time' field of the 'message metadata' view."), - value: DateUtil.formatPastTimestampRelativeToNow(message.timestampForSorting()))) + value: DateUtil.formatPastTimestampRelativeToNow(message.timestampForSorting(), + isRTL:self.view.isRTL()))) } rows += addAttachmentMetadataRows() diff --git a/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift b/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift index ba98a7c8b..a48e2f1e5 100644 --- a/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift +++ b/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation @@ -56,7 +56,7 @@ class MessageRecipientStatusUtils: NSObject { public class func recipientStatus(outgoingMessage: TSOutgoingMessage, recipientId: String, referenceView: UIView) -> MessageRecipientStatus { - let (messageRecipientStatus, _) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, + let (messageRecipientStatus, _, _) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, recipientId: recipientId, referenceView: referenceView) return messageRecipientStatus @@ -64,20 +64,31 @@ class MessageRecipientStatusUtils: NSObject { // This method is per-recipient and "biased towards success". // See comments above. - public class func statusMessage(outgoingMessage: TSOutgoingMessage, - recipientId: String, - referenceView: UIView) -> String { - let (_, statusMessage) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, - recipientId: recipientId, - referenceView: referenceView) - return statusMessage + public class func shortStatusMessage(outgoingMessage: TSOutgoingMessage, + recipientId: String, + referenceView: UIView) -> String { + let (_, shortStatusMessage, _) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, + recipientId: recipientId, + referenceView: referenceView) + return shortStatusMessage + } + + // This method is per-recipient and "biased towards success". + // See comments above. + public class func longStatusMessage(outgoingMessage: TSOutgoingMessage, + recipientId: String, + referenceView: UIView) -> String { + let (_, _, longStatusMessage) = recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, + recipientId: recipientId, + referenceView: referenceView) + return longStatusMessage } // This method is per-recipient and "biased towards success". // See comments above. public class func recipientStatusAndStatusMessage(outgoingMessage: TSOutgoingMessage, recipientId: String, - referenceView: UIView) -> (MessageRecipientStatus, String) { + referenceView: UIView) -> (status: MessageRecipientStatus, shortStatusMessage: String, longStatusMessage: String) { // Legacy messages don't have "recipient read" state or "per-recipient delivery" state, // so we fall back to `TSOutgoingMessageState` which is not per-recipient and therefore // might be misleading. @@ -85,49 +96,53 @@ class MessageRecipientStatusUtils: NSObject { let recipientReadMap = outgoingMessage.recipientReadMap if let readTimestamp = recipientReadMap[recipientId] { assert(outgoingMessage.messageState == .sentToService) - let statusMessage = NSLocalizedString("MESSAGE_STATUS_READ", comment:"message footer for read messages").rtlSafeAppend(" ", referenceView:referenceView) - .rtlSafeAppend( - DateUtil.formatPastTimestampRelativeToNow(readTimestamp.uint64Value), referenceView:referenceView) - return (.read, statusMessage) + let timestampString = DateUtil.formatPastTimestampRelativeToNow(readTimestamp.uint64Value, + isRTL:referenceView.isRTL()) + let shortStatusMessage = timestampString + let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_READ", comment:"message footer for read messages").rtlSafeAppend(" ", referenceView:referenceView) + .rtlSafeAppend(timestampString, referenceView:referenceView) + return (status:.read, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage) } let recipientDeliveryMap = outgoingMessage.recipientDeliveryMap if let deliveryTimestamp = recipientDeliveryMap[recipientId] { assert(outgoingMessage.messageState == .sentToService) - let statusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED", + let timestampString = DateUtil.formatPastTimestampRelativeToNow(deliveryTimestamp.uint64Value, + isRTL:referenceView.isRTL()) + let shortStatusMessage = timestampString + let longStatusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED", comment:"message status for message delivered to their recipient.").rtlSafeAppend(" ", referenceView:referenceView) - .rtlSafeAppend( - DateUtil.formatPastTimestampRelativeToNow(deliveryTimestamp.uint64Value), referenceView:referenceView) - return (.delivered, statusMessage) + .rtlSafeAppend(timestampString, referenceView:referenceView) + return (status:.delivered, shortStatusMessage:shortStatusMessage, longStatusMessage:longStatusMessage) } if outgoingMessage.wasDelivered { let statusMessage = NSLocalizedString("MESSAGE_STATUS_DELIVERED", comment:"message status for message delivered to their recipient.") - return (.delivered, statusMessage) + return (status:.delivered, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) } if outgoingMessage.messageState == .unsent { let statusMessage = NSLocalizedString("MESSAGE_STATUS_FAILED", comment:"message footer for failed messages") - return (.failed, statusMessage) + return (status:.failed, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) } else if outgoingMessage.messageState == .sentToService || outgoingMessage.wasSent(toRecipient:recipientId) { let statusMessage = NSLocalizedString("MESSAGE_STATUS_SENT", comment:"message footer for sent messages") - return (.sent, statusMessage) + return (status:.sent, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) } else if outgoingMessage.hasAttachments() { assert(outgoingMessage.messageState == .attemptingOut) let statusMessage = NSLocalizedString("MESSAGE_STATUS_UPLOADING", comment:"message footer while attachment is uploading") - return (.uploading, statusMessage) + return (status:.uploading, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) } else { assert(outgoingMessage.messageState == .attemptingOut) let statusMessage = NSLocalizedString("MESSAGE_STATUS_SENDING", comment:"message status while message is sending.") - return (.sending, statusMessage) + return (status:.sending, shortStatusMessage:statusMessage, longStatusMessage:statusMessage) } } diff --git a/Signal/src/util/DateUtil.h b/Signal/src/util/DateUtil.h index d6b830412..31b4e5c5d 100644 --- a/Signal/src/util/DateUtil.h +++ b/Signal/src/util/DateUtil.h @@ -1,7 +1,9 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // +NS_ASSUME_NONNULL_BEGIN + @interface DateUtil : NSObject + (NSDateFormatter *)dateFormatter; @@ -11,6 +13,8 @@ + (BOOL)dateIsToday:(NSDate *)date; + (NSString *)formatPastTimestampRelativeToNow:(uint64_t)pastTimestamp - NS_SWIFT_NAME(formatPastTimestampRelativeToNow(_:)); + isRTL:(BOOL)isRTL NS_SWIFT_NAME(formatPastTimestampRelativeToNow(_:isRTL:)); @end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/DateUtil.m b/Signal/src/util/DateUtil.m index f9d4d3c24..648b40202 100644 --- a/Signal/src/util/DateUtil.m +++ b/Signal/src/util/DateUtil.m @@ -1,10 +1,13 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "DateUtil.h" +#import #import +NS_ASSUME_NONNULL_BEGIN + static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; @implementation DateUtil @@ -65,7 +68,13 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return [self date:[NSDate date] isEqualToDateIgnoringTime:date]; } -+ (NSString *)formatPastTimestampRelativeToNow:(uint64_t)pastTimestamp ++ (BOOL)dateIsYesterday:(NSDate *)date +{ + NSDate *yesterday = [NSDate ows_dateWithMillisecondsSince1970:[NSDate ows_millisecondTimeStamp] - kDayInMs]; + return [self date:yesterday isEqualToDateIgnoringTime:date]; +} + ++ (NSString *)formatPastTimestampRelativeToNow:(uint64_t)pastTimestamp isRTL:(BOOL)isRTL { OWSCAssert(pastTimestamp > 0); @@ -73,13 +82,18 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; BOOL isFutureTimestamp = pastTimestamp >= nowTimestamp; NSDate *pastDate = [NSDate ows_dateWithMillisecondsSince1970:pastTimestamp]; + NSString *dateString; if (isFutureTimestamp || [self dateIsToday:pastDate]) { - return [[self timeFormatter] stringFromDate:pastDate]; - } else if (![self dateIsOlderThanOneWeek:pastDate]) { - return [[self weekdayFormatter] stringFromDate:pastDate]; + dateString = NSLocalizedString(@"DATE_TODAY", @"The current day."); + } else if ([self dateIsYesterday:pastDate]) { + dateString = NSLocalizedString(@"DATE_YESTERDAY", @"The day before today."); } else { - return [[self dateFormatter] stringFromDate:pastDate]; + dateString = [[self dateFormatter] stringFromDate:pastDate]; } + return [[dateString rtlSafeAppend:@" " isRTL:isRTL] rtlSafeAppend:[[self timeFormatter] stringFromDate:pastDate] + isRTL:isRTL]; } @end + +NS_ASSUME_NONNULL_END diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 7b624d9b1..8c651c7e0 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -493,6 +493,12 @@ /* Title shown while the app is updating its database. */ "DATABASE_VIEW_OVERLAY_TITLE" = "Updating Database"; +/* The current day. */ +"DATE_TODAY" = "Today"; + +/* The day before today. */ +"DATE_YESTERDAY" = "Yesterday"; + /* Message indicating that the debug log is being uploaded. */ "DEBUG_LOG_ACTIVITY_INDICATOR" = "Sending Debug Log..."; diff --git a/SignalMessaging/categories/NSString+OWS.h b/SignalMessaging/categories/NSString+OWS.h index 99ab9a420..9af84cf07 100644 --- a/SignalMessaging/categories/NSString+OWS.h +++ b/SignalMessaging/categories/NSString+OWS.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -9,6 +9,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)ows_stripped; - (NSString *)rtlSafeAppend:(NSString *)string referenceView:(UIView *)referenceView; +- (NSString *)rtlSafeAppend:(NSString *)string isRTL:(BOOL)isRTL; - (NSString *)digitsOnly; diff --git a/SignalMessaging/categories/NSString+OWS.m b/SignalMessaging/categories/NSString+OWS.m index afd59123e..130a76581 100644 --- a/SignalMessaging/categories/NSString+OWS.m +++ b/SignalMessaging/categories/NSString+OWS.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "NSString+OWS.h" @@ -19,7 +19,14 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(string); OWSAssert(referenceView); - if ([referenceView isRTL]) { + return [self rtlSafeAppend:string isRTL:referenceView.isRTL]; +} + +- (NSString *)rtlSafeAppend:(NSString *)string isRTL:(BOOL)isRTL +{ + OWSAssert(string); + + if (isRTL) { return [string stringByAppendingString:self]; } else { return [self stringByAppendingString:string]; @@ -28,7 +35,8 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)removeAllCharactersIn:(NSCharacterSet *)characterSet { - OWSAssert(characterSet != nil); + OWSAssert(characterSet); + return [[self componentsSeparatedByCharactersInSet:characterSet] componentsJoinedByString:@""]; }