From b8f8a3017aef3f71ec6ddd63d7a28736fbc29f23 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 10 Apr 2018 13:02:33 -0400 Subject: [PATCH 1/4] Apply design changes to home view. --- Signal.xcodeproj/project.pbxproj | 32 +- .../ViewControllers/HomeView/HomeViewCell.h | 23 ++ .../HomeViewCell.m} | 323 ++++++++++-------- .../{ => HomeView}/HomeViewController.h | 0 .../{ => HomeView}/HomeViewController.m | 22 +- .../src/ViewControllers/InboxTableViewCell.h | 22 -- SignalMessaging/categories/UIFont+OWS.h | 1 + SignalMessaging/categories/UIFont+OWS.m | 5 + .../contacts/SelectThreadViewController.m | 2 +- 9 files changed, 251 insertions(+), 179 deletions(-) create mode 100644 Signal/src/ViewControllers/HomeView/HomeViewCell.h rename Signal/src/ViewControllers/{InboxTableViewCell.m => HomeView/HomeViewCell.m} (51%) rename Signal/src/ViewControllers/{ => HomeView}/HomeViewController.h (100%) rename Signal/src/ViewControllers/{ => HomeView}/HomeViewController.m (98%) delete mode 100644 Signal/src/ViewControllers/InboxTableViewCell.h diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 2524d09d6..acd652c0d 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -49,6 +49,8 @@ 34330A5E1E787BD800DF2FB9 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */; }; 34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; }; 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; }; + 34386A51207D0C01009F5D9C /* HomeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34386A4D207D0C01009F5D9C /* HomeViewController.m */; }; + 34386A52207D0C01009F5D9C /* HomeViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34386A50207D0C01009F5D9C /* HomeViewCell.m */; }; 343A65951FC47D5E000477A1 /* DebugUISyncMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 343A65941FC47D5E000477A1 /* DebugUISyncMessages.m */; }; 343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */; }; 34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */; }; @@ -163,12 +165,10 @@ 34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F83E1E8DF1700035BE1A /* ContactsPicker.swift */; }; 34B3F8781E8DF1700035BE1A /* ContactsPicker.xib in Resources */ = {isa = PBXBuildFile; fileRef = 34B3F83F1E8DF1700035BE1A /* ContactsPicker.xib */; }; 34B3F87B1E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8441E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift */; }; - 34B3F87E1E8DF1700035BE1A /* InboxTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */; }; 34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */; }; 34B3F8821E8DF1700035BE1A /* NewContactThreadViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8501E8DF1700035BE1A /* NewContactThreadViewController.m */; }; 34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8551E8DF1700035BE1A /* NewGroupViewController.m */; }; 34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */; }; - 34B3F8941E8DF1710035BE1A /* HomeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8701E8DF1700035BE1A /* HomeViewController.m */; }; 34BECE2B1F74C12700D7438D /* DebugUIStress.m in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2A1F74C12700D7438D /* DebugUIStress.m */; }; 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2D1F7ABCE000D7438D /* GifPickerViewController.swift */; }; 34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BECE2F1F7ABCF800D7438D /* GifPickerLayout.swift */; }; @@ -617,6 +617,10 @@ 34330AA11E79686200DF2FB9 /* OWSProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProgressView.h; sourceTree = ""; }; 34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = ""; }; 34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = ""; }; + 34386A4D207D0C01009F5D9C /* HomeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HomeViewController.m; sourceTree = ""; }; + 34386A4E207D0C01009F5D9C /* HomeViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeViewCell.h; sourceTree = ""; }; + 34386A4F207D0C01009F5D9C /* HomeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeViewController.h; sourceTree = ""; }; + 34386A50207D0C01009F5D9C /* HomeViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HomeViewCell.m; sourceTree = ""; }; 343A65931FC47D5D000477A1 /* DebugUISyncMessages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUISyncMessages.h; sourceTree = ""; }; 343A65941FC47D5E000477A1 /* DebugUISyncMessages.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUISyncMessages.m; sourceTree = ""; }; 343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationScrollButton.m; sourceTree = ""; }; @@ -748,8 +752,6 @@ 34B3F83E1E8DF1700035BE1A /* ContactsPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactsPicker.swift; sourceTree = ""; }; 34B3F83F1E8DF1700035BE1A /* ContactsPicker.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContactsPicker.xib; sourceTree = ""; }; 34B3F8441E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradesPageViewController.swift; sourceTree = ""; }; - 34B3F8491E8DF1700035BE1A /* InboxTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InboxTableViewCell.h; sourceTree = ""; }; - 34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InboxTableViewCell.m; sourceTree = ""; }; 34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InviteFlow.swift; sourceTree = ""; }; 34B3F84F1E8DF1700035BE1A /* NewContactThreadViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NewContactThreadViewController.h; sourceTree = ""; }; 34B3F8501E8DF1700035BE1A /* NewContactThreadViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NewContactThreadViewController.m; sourceTree = ""; }; @@ -757,8 +759,6 @@ 34B3F8551E8DF1700035BE1A /* NewGroupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NewGroupViewController.m; sourceTree = ""; }; 34B3F86D1E8DF1700035BE1A /* SignalsNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalsNavigationController.h; sourceTree = ""; }; 34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalsNavigationController.m; sourceTree = ""; }; - 34B3F86F1E8DF1700035BE1A /* HomeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeViewController.h; sourceTree = ""; }; - 34B3F8701E8DF1700035BE1A /* HomeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HomeViewController.m; sourceTree = ""; }; 34B3F89D1E8DF5490035BE1A /* OWSTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSTableViewController.h; sourceTree = ""; }; 34B3F89E1E8DF5490035BE1A /* OWSTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSTableViewController.m; sourceTree = ""; }; 34BECE291F74C12700D7438D /* DebugUIStress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIStress.h; sourceTree = ""; }; @@ -1352,6 +1352,17 @@ path = Fonts; sourceTree = ""; }; + 34386A4C207D0C01009F5D9C /* HomeView */ = { + isa = PBXGroup; + children = ( + 34386A4E207D0C01009F5D9C /* HomeViewCell.h */, + 34386A50207D0C01009F5D9C /* HomeViewCell.m */, + 34386A4F207D0C01009F5D9C /* HomeViewController.h */, + 34386A4D207D0C01009F5D9C /* HomeViewController.m */, + ); + path = HomeView; + sourceTree = ""; + }; 34480B2F1FD0921000BC14EF /* utils */ = { isa = PBXGroup; children = ( @@ -1583,10 +1594,7 @@ 34D8C0221ED3673300188D7C /* DebugUI */, 34B3F8441E8DF1700035BE1A /* ExperienceUpgradesPageViewController.swift */, 34BECE2C1F7ABCE000D7438D /* GifPicker */, - 34B3F86F1E8DF1700035BE1A /* HomeViewController.h */, - 34B3F8701E8DF1700035BE1A /* HomeViewController.m */, - 34B3F8491E8DF1700035BE1A /* InboxTableViewCell.h */, - 34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */, + 34386A4C207D0C01009F5D9C /* HomeView */, 34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */, 45B9EE9A200E91FB005D2F2D /* MediaDetailViewController.h */, 45B9EE9B200E91FB005D2F2D /* MediaDetailViewController.m */, @@ -3158,6 +3166,7 @@ 3461293E1FD1D72B00532771 /* ExperienceUpgradeFinder.swift in Sources */, 34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */, 452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */, + 34386A52207D0C01009F5D9C /* HomeViewCell.m in Sources */, 34DBF007206C3CB200025978 /* OWSBubbleStrokeView.m in Sources */, 34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */, 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */, @@ -3178,6 +3187,7 @@ 34D1F0AB1F867BFC0066283D /* OWSContactOffersCell.m in Sources */, 340FC8C7204DE64D007AEB0F /* OWSBackupAPI.swift in Sources */, 343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */, + 34386A51207D0C01009F5D9C /* HomeViewController.m in Sources */, 34D1F0A91F867BFC0066283D /* ConversationViewCell.m in Sources */, 4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */, 34CCAF381F0C0599004084F4 /* AppUpdateNag.m in Sources */, @@ -3298,14 +3308,12 @@ 340FC8A8204DAC8D007AEB0F /* CodeVerificationViewController.m in Sources */, 3461299C1FD1EA9E00532771 /* NotificationsManager.m in Sources */, 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, - 34B3F87E1E8DF1700035BE1A /* InboxTableViewCell.m in Sources */, 34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */, 340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */, 34D2CCD4206294B900CB1A14 /* OWSScreenLock.swift in Sources */, 340FC8C5204DE223007AEB0F /* DebugUIBackup.m in Sources */, 340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */, 4579431E1E7C8CE9008ED0C0 /* Pastelog.m in Sources */, - 34B3F8941E8DF1710035BE1A /* HomeViewController.m in Sources */, 340FC8B0204DAC8D007AEB0F /* AddToBlockListViewController.m in Sources */, 340FC8B3204DAC8D007AEB0F /* AppSettingsViewController.m in Sources */, 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */, diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.h b/Signal/src/ViewControllers/HomeView/HomeViewCell.h new file mode 100644 index 000000000..2063fdc14 --- /dev/null +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.h @@ -0,0 +1,23 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@class OWSContactsManager; +@class TSThread; + +@interface HomeViewCell : UITableViewCell + ++ (CGFloat)rowHeight; + ++ (NSString *)cellReuseIdentifier; + +- (void)configureWithThread:(TSThread *)thread + contactsManager:(OWSContactsManager *)contactsManager + blockedPhoneNumberSet:(NSSet *)blockedPhoneNumberSet + shouldHaveBottomSeparator:(BOOL)shouldHaveBottomSeparator; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/InboxTableViewCell.m b/Signal/src/ViewControllers/HomeView/HomeViewCell.m similarity index 51% rename from Signal/src/ViewControllers/InboxTableViewCell.m rename to Signal/src/ViewControllers/HomeView/HomeViewCell.m index 5b69b95c4..ebc89261e 100644 --- a/Signal/src/ViewControllers/InboxTableViewCell.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.m @@ -2,10 +2,9 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // -#import "InboxTableViewCell.h" +#import "HomeViewCell.h" #import "OWSAvatarBuilder.h" #import "Signal-Swift.h" -#import "ViewControllerUtils.h" #import #import #import @@ -15,17 +14,16 @@ NS_ASSUME_NONNULL_BEGIN -#define ARCHIVE_IMAGE_VIEW_WIDTH 22.0f -#define DELETE_IMAGE_VIEW_WIDTH 19.0f -#define TIME_LABEL_SIZE 11 -#define DATE_LABEL_SIZE 13 -#define SWIPE_ARCHIVE_OFFSET -50 +const NSUInteger kHomeViewCellHeight = 72; +const NSUInteger kHomeViewCellHMargin = 16; +const NSUInteger kHomeViewCellVMargin = 12; +const NSUInteger kHomeViewAvatarSize = kHomeViewCellHeight - kHomeViewCellVMargin * 2; +const NSUInteger kHomeViewAvatarHSpacing = 12; -const NSUInteger kAvatarViewDiameter = 52; - -@interface InboxTableViewCell () +@interface HomeViewCell () @property (nonatomic) AvatarImageView *avatarView; +@property (nonatomic) UIView *payloadView; @property (nonatomic) UILabel *nameLabel; @property (nonatomic) UILabel *snippetLabel; @property (nonatomic) UILabel *timeLabel; @@ -35,11 +33,13 @@ const NSUInteger kAvatarViewDiameter = 52; @property (nonatomic) TSThread *thread; @property (nonatomic) OWSContactsManager *contactsManager; +@property (nonatomic, readonly) NSMutableArray *viewConstraints; + @end #pragma mark - -@implementation InboxTableViewCell +@implementation HomeViewCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier { @@ -64,64 +64,84 @@ const NSUInteger kAvatarViewDiameter = 52; OWSAssert(!self.avatarView); [self setTranslatesAutoresizingMaskIntoConstraints:NO]; - self.preservesSuperviewLayoutMargins = YES; - self.contentView.preservesSuperviewLayoutMargins = YES; + self.layoutMargins = UIEdgeInsetsZero; + self.contentView.layoutMargins = UIEdgeInsetsZero; + self.preservesSuperviewLayoutMargins = NO; + self.contentView.preservesSuperviewLayoutMargins = NO; self.backgroundColor = [UIColor whiteColor]; + _viewConstraints = [NSMutableArray new]; + self.avatarView = [[AvatarImageView alloc] init]; [self.contentView addSubview:self.avatarView]; - [self.avatarView autoSetDimension:ALDimensionWidth toSize:self.avatarSize]; - [self.avatarView autoSetDimension:ALDimensionHeight toSize:self.avatarSize]; - [self.avatarView autoPinLeadingToSuperviewMargin]; + [self.avatarView autoSetDimension:ALDimensionWidth toSize:kHomeViewAvatarSize]; + [self.avatarView autoSetDimension:ALDimensionHeight toSize:kHomeViewAvatarSize]; + [self.avatarView autoPinLeadingToSuperviewMarginWithInset:kHomeViewCellHMargin]; [self.avatarView autoVCenterInSuperview]; + [self.avatarView setContentHuggingHigh]; + [self.avatarView setCompressionResistanceHigh]; + + self.payloadView = [UIView containerView]; + [self.contentView addSubview:self.payloadView]; + [self.payloadView autoPinLeadingToTrailingEdgeOfView:self.avatarView offset:kHomeViewAvatarHSpacing]; + [self.payloadView autoVCenterInSuperview]; self.nameLabel = [UILabel new]; self.nameLabel.lineBreakMode = NSLineBreakByTruncatingTail; - self.nameLabel.font = [UIFont ows_boldFontWithSize:14.0f]; - [self.contentView addSubview:self.nameLabel]; - [self.nameLabel autoPinLeadingToTrailingEdgeOfView:self.avatarView offset:13.f]; - [self.nameLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.avatarView]; + self.nameLabel.font = self.nameFont; [self.nameLabel setContentHuggingHorizontalLow]; + [self.nameLabel setCompressionResistanceHorizontalLow]; + + self.timeLabel = [UILabel new]; + [self.timeLabel setContentHuggingHorizontalHigh]; + [self.timeLabel setCompressionResistanceHorizontalHigh]; + + UIStackView *topRowView = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.nameLabel, + self.timeLabel, + ]]; + topRowView.axis = UILayoutConstraintAxisHorizontal; + topRowView.spacing = 4; + [self.payloadView addSubview:topRowView]; + [topRowView autoPinLeadingToSuperviewMargin]; + [topRowView autoPinTrailingToSuperviewMargin]; + [topRowView autoPinTopToSuperviewMargin]; self.snippetLabel = [UILabel new]; - self.snippetLabel.font = [UIFont ows_regularFontWithSize:14.f]; + self.snippetLabel.font = [self snippetFont]; + self.snippetLabel.numberOfLines = 1; self.snippetLabel.lineBreakMode = NSLineBreakByTruncatingTail; - self.snippetLabel.textColor = [UIColor colorWithWhite:2 / 3.f alpha:1.f]; - self.snippetLabel.numberOfLines = 2; - self.snippetLabel.lineBreakMode = NSLineBreakByTruncatingTail; - [self.contentView addSubview:self.snippetLabel]; - [self.snippetLabel autoPinLeadingToTrailingEdgeOfView:self.avatarView offset:13.f]; - [self.snippetLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.nameLabel withOffset:5.f]; - [self.snippetLabel autoPinEdge:ALEdgeTrailing toEdge:ALEdgeTrailing ofView:self.nameLabel]; + [self.payloadView addSubview:self.snippetLabel]; + [self.snippetLabel autoPinLeadingToSuperviewMargin]; + [self.snippetLabel autoPinTrailingToSuperviewMargin]; + [self.snippetLabel autoPinBottomToSuperviewMargin]; + [self.snippetLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:topRowView withOffset:5.f]; [self.snippetLabel setContentHuggingHorizontalLow]; + [self.snippetLabel setCompressionResistanceHorizontalLow]; - self.timeLabel = [UILabel new]; - self.timeLabel.font = [UIFont ows_lightFontWithSize:14.f]; - [self.contentView addSubview:self.timeLabel]; - [self.timeLabel autoPinTrailingToSuperviewMargin]; - [self.timeLabel autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.nameLabel]; - [self.timeLabel autoPinLeadingToTrailingEdgeOfView:self.nameLabel offset:10.f]; - [self.timeLabel setContentHuggingHorizontalHigh]; - [self.timeLabel setCompressionResistanceHigh]; + self.unreadLabel = [UILabel new]; + self.unreadLabel.font = [UIFont ows_dynamicTypeCaption1Font]; + self.unreadLabel.textColor = [UIColor whiteColor]; + self.unreadLabel.lineBreakMode = NSLineBreakByTruncatingTail; + self.unreadLabel.textAlignment = NSTextAlignmentCenter; - const int kunreadBadgeSize = 24; - self.unreadBadge = [[NeverClearView alloc] initWithFrame:CGRectMake(0, 0, kunreadBadgeSize, kunreadBadgeSize)]; - self.unreadBadge.layer.cornerRadius = kunreadBadgeSize / 2; + self.unreadBadge = [NeverClearView new]; self.unreadBadge.backgroundColor = [UIColor ows_materialBlueColor]; [self.contentView addSubview:self.unreadBadge]; - [self.unreadBadge autoSetDimension:ALDimensionWidth toSize:kunreadBadgeSize]; - [self.unreadBadge autoSetDimension:ALDimensionHeight toSize:kunreadBadgeSize]; - [self.unreadBadge autoPinTrailingToSuperviewMargin]; - [self.unreadBadge autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.avatarView]; - [self.unreadBadge setContentHuggingHorizontalHigh]; + [self.unreadBadge autoPinTrailingToSuperviewMarginWithInset:kHomeViewCellHMargin]; + [self.unreadBadge autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.timeLabel]; + [self.unreadBadge setContentHuggingHigh]; [self.unreadBadge setCompressionResistanceHigh]; - self.unreadLabel = [UILabel new]; - self.unreadLabel.font = [UIFont ows_regularFontWithSize:12.f]; - self.unreadLabel.textColor = [UIColor whiteColor]; - self.unreadLabel.lineBreakMode = NSLineBreakByTruncatingTail; - self.unreadLabel.textAlignment = NSTextAlignmentCenter; + // TODO: Will this localize? It assumes that the worst case + // unread count (99) will fit horizontally into some multiple + // N of the font's line height. + const int unreadBadgeSize = (int)ceil(self.unreadLabel.font.lineHeight * 1.5f); + self.unreadBadge.layer.cornerRadius = unreadBadgeSize / 2; + [self.unreadBadge autoSetDimension:ALDimensionWidth toSize:unreadBadgeSize]; + [self.unreadBadge autoSetDimension:ALDimensionHeight toSize:unreadBadgeSize]; + [self.unreadBadge addSubview:self.unreadLabel]; [self.unreadLabel autoVCenterInSuperview]; [self.unreadLabel autoPinWidthToSuperview]; @@ -134,15 +154,11 @@ const NSUInteger kAvatarViewDiameter = 52; + (CGFloat)rowHeight { - return 72.f; + return kHomeViewCellHeight; } -- (CGFloat)avatarSize +- (void)initializeLayout { - return 52.f; -} - -- (void)initializeLayout { self.selectionStyle = UITableViewCellSelectionStyleDefault; } @@ -152,61 +168,21 @@ const NSUInteger kAvatarViewDiameter = 52; } - (void)configureWithThread:(TSThread *)thread - contactsManager:(OWSContactsManager *)contactsManager - blockedPhoneNumberSet:(NSSet *)blockedPhoneNumberSet + contactsManager:(OWSContactsManager *)contactsManager + blockedPhoneNumberSet:(NSSet *)blockedPhoneNumberSet + shouldHaveBottomSeparator:(BOOL)shouldHaveBottomSeparator { OWSAssertIsOnMainThread(); OWSAssert(thread); OWSAssert(contactsManager); OWSAssert(blockedPhoneNumberSet); + // TODO: Honor shouldHaveBottomSeparator. + self.thread = thread; self.contactsManager = contactsManager; - - BOOL isBlocked = NO; - if (!thread.isGroupThread) { - NSString *contactIdentifier = thread.contactIdentifier; - isBlocked = [blockedPhoneNumberSet containsObject:contactIdentifier]; - } - - NSMutableAttributedString *snippetText = [NSMutableAttributedString new]; - if (isBlocked) { - // If thread is blocked, don't show a snippet or mute status. - [snippetText appendAttributedString:[[NSAttributedString alloc] initWithString:NSLocalizedString(@"HOME_VIEW_BLOCKED_CONTACT_CONVERSATION", - @"A label for conversations with blocked users.") - attributes:@{ - NSFontAttributeName : [UIFont ows_mediumFontWithSize:12], - NSForegroundColorAttributeName : [UIColor ows_blackColor], - }]]; - } else { - if ([thread isMuted]) { - [snippetText appendAttributedString:[[NSAttributedString alloc] - initWithString:@"\ue067 " - attributes:@{ - NSFontAttributeName : [UIFont ows_elegantIconsFont:9.f], - NSForegroundColorAttributeName : (thread.hasUnreadMessages - ? [UIColor colorWithWhite:0.1f alpha:1.f] - : [UIColor lightGrayColor]), - }]]; - } - NSString *displayableText = thread.lastMessageLabel.filterStringForDisplay; - if (displayableText) { - [snippetText appendAttributedString:[[NSAttributedString alloc] - initWithString:displayableText - attributes:@{ - NSFontAttributeName : (thread.hasUnreadMessages - ? [UIFont ows_mediumFontWithSize:12] - : [UIFont ows_regularFontWithSize:12]), - NSForegroundColorAttributeName : - (thread.hasUnreadMessages ? [UIColor ows_blackColor] - : [UIColor lightGrayColor]), - }]]; - } - } - - NSAttributedString *attributedDate = [self dateAttributedString:thread.lastMessageDate]; - NSUInteger unreadCount = [[OWSMessageUtils sharedManager] unreadMessagesInThread:thread]; + BOOL hasUnreadMessages = thread.hasUnreadMessages; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(otherUsersProfileDidChange:) @@ -215,20 +191,29 @@ const NSUInteger kAvatarViewDiameter = 52; [self updateNameLabel]; [self updateAvatarView]; - self.snippetLabel.attributedText = snippetText; - self.timeLabel.attributedText = attributedDate; + self.snippetLabel.attributedText = + [self attributedSnippetForThread:thread blockedPhoneNumberSet:blockedPhoneNumberSet]; + self.timeLabel.attributedText = [self attributedStringForDate:thread.lastMessageDate]; - self.separatorInset = UIEdgeInsetsMake(0, self.avatarSize * 1.5f, 0, 0); + self.separatorInset + = UIEdgeInsetsMake(0, kHomeViewAvatarSize + kHomeViewCellHMargin + kHomeViewAvatarHSpacing, 0, 0); - _timeLabel.textColor = thread.hasUnreadMessages ? [UIColor ows_materialBlueColor] : [UIColor ows_darkGrayColor]; + self.timeLabel.textColor = hasUnreadMessages ? [UIColor ows_materialBlueColor] : [UIColor ows_darkGrayColor]; + NSUInteger unreadCount = [[OWSMessageUtils sharedManager] unreadMessagesInThread:thread]; if (unreadCount > 0) { self.unreadBadge.hidden = NO; - self.unreadLabel.hidden = NO; self.unreadLabel.text = [OWSFormat formatInt:MIN(99, (int)unreadCount)]; + + [self.viewConstraints addObjectsFromArray:@[ + [self.unreadBadge autoPinLeadingToTrailingEdgeOfView:self.payloadView offset:4.f], + ]]; } else { self.unreadBadge.hidden = YES; - self.unreadLabel.hidden = YES; + + [self.viewConstraints addObjectsFromArray:@[ + [self.payloadView autoPinTrailingToSuperviewMarginWithInset:kHomeViewCellHMargin], + ]]; } } @@ -249,46 +234,115 @@ const NSUInteger kAvatarViewDiameter = 52; } self.avatarView.image = - [OWSAvatarBuilder buildImageForThread:thread diameter:kAvatarViewDiameter contactsManager:contactsManager]; + [OWSAvatarBuilder buildImageForThread:thread diameter:kHomeViewAvatarSize contactsManager:contactsManager]; +} + +- (NSAttributedString *)attributedSnippetForThread:(TSThread *)thread + blockedPhoneNumberSet:(NSSet *)blockedPhoneNumberSet +{ + OWSAssert(thread); + + BOOL isBlocked = NO; + if (!thread.isGroupThread) { + NSString *contactIdentifier = thread.contactIdentifier; + isBlocked = [blockedPhoneNumberSet containsObject:contactIdentifier]; + } + BOOL hasUnreadMessages = thread.hasUnreadMessages; + + NSMutableAttributedString *snippetText = [NSMutableAttributedString new]; + if (isBlocked) { + // If thread is blocked, don't show a snippet or mute status. + [snippetText + appendAttributedString:[[NSAttributedString alloc] + initWithString:NSLocalizedString(@"HOME_VIEW_BLOCKED_CONTACT_CONVERSATION", + @"A label for conversations with blocked users.") + attributes:@{ + NSFontAttributeName : self.snippetFont.ows_medium, + NSForegroundColorAttributeName : [UIColor ows_blackColor], + }]]; + } else { + if ([thread isMuted]) { + [snippetText appendAttributedString:[[NSAttributedString alloc] + initWithString:@"\ue067 " + attributes:@{ + NSFontAttributeName : [UIFont ows_elegantIconsFont:9.f], + NSForegroundColorAttributeName : (hasUnreadMessages + ? [UIColor colorWithWhite:0.1f alpha:1.f] + : [UIColor lightGrayColor]), + }]]; + } + NSString *displayableText = thread.lastMessageLabel.filterStringForDisplay; + if (displayableText) { + [snippetText appendAttributedString:[[NSAttributedString alloc] + initWithString:displayableText + attributes:@{ + NSFontAttributeName : + (hasUnreadMessages ? self.snippetFont.ows_medium + : self.snippetFont), + NSForegroundColorAttributeName : + (hasUnreadMessages ? [UIColor ows_blackColor] + : [UIColor lightGrayColor]), + }]]; + } + } + + return snippetText; } #pragma mark - Date formatting -- (NSAttributedString *)dateAttributedString:(nullable NSDate *)date +- (NSAttributedString *)attributedStringForDate:(nullable NSDate *)date { if (date == nil) { OWSProdLogAndFail(@"%@ date was unexpectedly nil", self.logTag); return [NSAttributedString new]; } - NSString *timeString; - - if ([DateUtil dateIsToday:date]) { - timeString = [[DateUtil timeFormatter] stringFromDate:date]; - } else { - timeString = [[DateUtil dateFormatter] stringFromDate:date]; - } - + NSDateFormatter *formatter = ([DateUtil dateIsToday:date] ? [DateUtil timeFormatter] : [DateUtil dateFormatter]); + NSString *timeString = [formatter stringFromDate:date]; OWSAssert(timeString); - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:timeString]; + return [[NSAttributedString alloc] initWithString:timeString + attributes:@{ + NSForegroundColorAttributeName : [UIColor ows_darkGrayColor], + NSFontAttributeName : self.dateTimeFont, + }]; +} - [attributedString addAttribute:NSForegroundColorAttributeName - value:[UIColor ows_darkGrayColor] - range:NSMakeRange(0, timeString.length)]; +#pragma mark - Constants +- (UIFont *)dateTimeFont +{ + return [UIFont ows_dynamicTypeFootnoteFont]; +} - [attributedString addAttribute:NSFontAttributeName - value:[UIFont ows_regularFontWithSize:TIME_LABEL_SIZE] - range:NSMakeRange(0, timeString.length)]; +- (UIFont *)snippetFont +{ + return [UIFont ows_dynamicTypeBodyFont]; +} +- (UIFont *)nameFont +{ + return [UIFont ows_dynamicTypeBodyFont].ows_medium; +} - return attributedString; +// Used for profile names. +- (UIFont *)nameSecondaryFont +{ + return [UIFont ows_dynamicTypeFootnoteFont]; } +#pragma mark - Reuse + - (void)prepareForReuse { [super prepareForReuse]; + [NSLayoutConstraint deactivateConstraints:self.viewConstraints]; + [self.viewConstraints removeAllObjects]; + + self.thread = nil; + self.contactsManager = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self]; } @@ -302,7 +356,7 @@ const NSUInteger kAvatarViewDiameter = 52; if (recipientId.length == 0) { return; } - + if (![self.thread isKindOfClass:[TSContactThread class]]) { return; } @@ -310,12 +364,12 @@ const NSUInteger kAvatarViewDiameter = 52; if (![self.thread.contactIdentifier isEqualToString:recipientId]) { return; } - + [self updateNameLabel]; [self updateAvatarView]; } --(void)updateNameLabel +- (void)updateNameLabel { OWSAssertIsOnMainThread(); @@ -325,14 +379,14 @@ const NSUInteger kAvatarViewDiameter = 52; self.nameLabel.attributedText = nil; return; } - + OWSContactsManager *contactsManager = self.contactsManager; if (contactsManager == nil) { OWSFail(@"%@ contacts manager should not be nil", self.logTag); self.nameLabel.attributedText = nil; return; } - + NSAttributedString *name; if (thread.isGroupThread) { if (thread.name.length == 0) { @@ -341,12 +395,11 @@ const NSUInteger kAvatarViewDiameter = 52; name = [[NSAttributedString alloc] initWithString:thread.name]; } } else { - name = [contactsManager - attributedStringForConversationTitleWithPhoneIdentifier:thread.contactIdentifier - primaryFont:self.nameLabel.font - secondaryFont:[UIFont ows_dynamicTypeFootnoteFont]]; + name = [contactsManager attributedStringForConversationTitleWithPhoneIdentifier:thread.contactIdentifier + primaryFont:self.nameFont + secondaryFont:self.nameSecondaryFont]; } - + self.nameLabel.attributedText = name; } diff --git a/Signal/src/ViewControllers/HomeViewController.h b/Signal/src/ViewControllers/HomeView/HomeViewController.h similarity index 100% rename from Signal/src/ViewControllers/HomeViewController.h rename to Signal/src/ViewControllers/HomeView/HomeViewController.h diff --git a/Signal/src/ViewControllers/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m similarity index 98% rename from Signal/src/ViewControllers/HomeViewController.m rename to Signal/src/ViewControllers/HomeView/HomeViewController.m index 993be1b9f..9d4ba834f 100644 --- a/Signal/src/ViewControllers/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -6,7 +6,7 @@ #import "AppDelegate.h" #import "AppSettingsViewController.h" #import "ConversationViewController.h" -#import "InboxTableViewCell.h" +#import "HomeViewCell.h" #import "NewContactThreadViewController.h" #import "OWSNavigationController.h" #import "OWSPrimaryStorage.h" @@ -210,8 +210,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; self.tableView.delegate = self; self.tableView.dataSource = self; - [self.tableView registerClass:[InboxTableViewCell class] - forCellReuseIdentifier:InboxTableViewCell.cellReuseIdentifier]; + [self.tableView registerClass:[HomeViewCell class] forCellReuseIdentifier:HomeViewCell.cellReuseIdentifier]; [self.view addSubview:self.tableView]; [self.tableView autoPinWidthToSuperview]; [self.tableView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; @@ -329,7 +328,8 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button]; } -- (void)settingsButtonPressed:(id)sender { +- (void)settingsButtonPressed:(id)sender +{ OWSNavigationController *navigationController = [AppSettingsViewController inModalNavigationController]; [self presentViewController:navigationController animated:YES completion:nil]; } @@ -578,13 +578,18 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - InboxTableViewCell *cell = - [self.tableView dequeueReusableCellWithIdentifier:InboxTableViewCell.cellReuseIdentifier]; + HomeViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:HomeViewCell.cellReuseIdentifier]; OWSAssert(cell); TSThread *thread = [self threadForIndexPath:indexPath]; - [cell configureWithThread:thread contactsManager:self.contactsManager blockedPhoneNumberSet:_blockedPhoneNumberSet]; + BOOL isLastCell = (indexPath.row == [self tableView:tableView numberOfRowsInSection:indexPath.section] - 1); + BOOL shouldHaveBottomSeparator = !isLastCell; + + [cell configureWithThread:thread + contactsManager:self.contactsManager + blockedPhoneNumberSet:self.blockedPhoneNumberSet + shouldHaveBottomSeparator:shouldHaveBottomSeparator]; if ((unsigned long)indexPath.row == [self.threadMappings numberOfItemsInSection:0] - 1) { cell.separatorInset = UIEdgeInsetsMake(0.f, cell.bounds.size.width, 0.f, 0.f); @@ -606,7 +611,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - return InboxTableViewCell.rowHeight; + return HomeViewCell.rowHeight; } - (void)pullToRefreshPerformed:(UIRefreshControl *)refreshControl @@ -728,7 +733,6 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; [self.editingDbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { viewingThreadsIn == kInboxState ? [thread archiveThreadWithTransaction:transaction] : [thread unarchiveThreadWithTransaction:transaction]; - }]; [self checkIfEmptyView]; } diff --git a/Signal/src/ViewControllers/InboxTableViewCell.h b/Signal/src/ViewControllers/InboxTableViewCell.h deleted file mode 100644 index 0ed13e554..000000000 --- a/Signal/src/ViewControllers/InboxTableViewCell.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -NS_ASSUME_NONNULL_BEGIN - -@class TSThread; -@class OWSContactsManager; - -@interface InboxTableViewCell : UITableViewCell - -+ (CGFloat)rowHeight; - -+ (NSString *)cellReuseIdentifier; - -- (void)configureWithThread:(TSThread *)thread - contactsManager:(OWSContactsManager *)contactsManager - blockedPhoneNumberSet:(NSSet *)blockedPhoneNumberSet; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/categories/UIFont+OWS.h b/SignalMessaging/categories/UIFont+OWS.h index d9379f32c..af28e1cd9 100644 --- a/SignalMessaging/categories/UIFont+OWS.h +++ b/SignalMessaging/categories/UIFont+OWS.h @@ -39,6 +39,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Styles - (UIFont *)ows_italic; +- (UIFont *)ows_bold; - (UIFont *)ows_medium; @end diff --git a/SignalMessaging/categories/UIFont+OWS.m b/SignalMessaging/categories/UIFont+OWS.m index fe67bbd87..780f86d44 100644 --- a/SignalMessaging/categories/UIFont+OWS.m +++ b/SignalMessaging/categories/UIFont+OWS.m @@ -104,6 +104,11 @@ NS_ASSUME_NONNULL_BEGIN return [self styleWithSymbolicTraits:UIFontDescriptorTraitItalic]; } +- (UIFont *)ows_bold +{ + return [self styleWithSymbolicTraits:UIFontDescriptorTraitBold]; +} + - (UIFont *)styleWithSymbolicTraits:(UIFontDescriptorSymbolicTraits)symbolicTraits { UIFontDescriptor *fontDescriptor = [self.fontDescriptor fontDescriptorWithSymbolicTraits:symbolicTraits]; diff --git a/SignalMessaging/contacts/SelectThreadViewController.m b/SignalMessaging/contacts/SelectThreadViewController.m index b7a0225aa..18e84e81a 100644 --- a/SignalMessaging/contacts/SelectThreadViewController.m +++ b/SignalMessaging/contacts/SelectThreadViewController.m @@ -197,7 +197,7 @@ NS_ASSUME_NONNULL_BEGIN OWSCAssert(strongSelf); // To be consistent with the threads (above), we use ContactTableViewCell - // instead of InboxTableViewCell to present contacts and threads. + // instead of HomeViewCell to present contacts and threads. ContactTableViewCell *cell = [ContactTableViewCell new]; if ([thread isKindOfClass:[TSContactThread class]]) { From abba24988c8c4b0fdbd651ed50425a0d1ab9cf93 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 10 Apr 2018 14:24:35 -0400 Subject: [PATCH 2/4] Rework how dates are formatted in home view. --- .../ConversationView/Cells/OWSBubbleView.h | 2 + .../ConversationView/Cells/OWSBubbleView.m | 5 + .../Cells/OWSMessageBubbleView.m | 3 + .../ViewControllers/DebugUI/DebugUIMessages.m | 74 +++++++++++++ .../ViewControllers/HomeView/HomeViewCell.m | 37 ++++--- Signal/src/util/DateUtil.h | 3 + Signal/src/util/DateUtil.m | 51 ++++++++- Signal/test/util/UtilTest.m | 104 ++++++++++++++++++ SignalServiceKit/src/Util/NSDate+OWS.h | 3 + SignalServiceKit/src/Util/NSDate+OWS.mm | 1 + 10 files changed, 266 insertions(+), 17 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h index 4ef469937..0882a50f2 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h @@ -45,6 +45,8 @@ extern const CGFloat kBubbleTextVInset; - (void)updatePartnerViews; ++ (CGFloat)minWidth; + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m index 4cb83dd3f..6d428768a 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -239,6 +239,11 @@ const CGFloat kBubbleTextVInset = 10.f; } } ++ (CGFloat)minWidth +{ + return (kBubbleHRounding * 2 + kBubbleThornSideInset); +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index bba575e0c..47e630db0 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -933,6 +933,9 @@ NS_ASSUME_NONNULL_BEGIN cellSize.width = MAX(cellSize.width, textContentSize.width); cellSize.height += textContentSize.height; + // Make sure the bubble is always wide enough to complete it's bubble shape. + cellSize.width = MAX(cellSize.width, OWSBubbleView.minWidth); + OWSAssert(cellSize.width > 0 && cellSize.height > 0); if (self.hasTapForMore) { diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 4ad1d0537..1f30ccc46 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -71,6 +71,7 @@ NS_ASSUME_NONNULL_BEGIN [DebugUIMessages allQuotedReplyAction:thread], // Exemplary [DebugUIMessages allFakeAction:thread], + [DebugUIMessages allFakeBackDatedAction:thread], ]) { [items addObject:[OWSTableItem itemWithTitle:action.label actionBlock:^{ @@ -108,6 +109,10 @@ NS_ASSUME_NONNULL_BEGIN actionBlock:^{ [DebugUIMessages selectQuotedReplyAction:thread]; }], + [OWSTableItem itemWithTitle:@"Select Back-Dated" + actionBlock:^{ + [DebugUIMessages selectBackDatedAction:thread]; + }], #pragma mark - Misc. @@ -2656,6 +2661,7 @@ NS_ASSUME_NONNULL_BEGIN [actions addObjectsFromArray:[self allFakeTextActions:thread includeLabels:includeLabels]]; [actions addObjectsFromArray:[self allFakeSequenceActions:thread includeLabels:includeLabels]]; [actions addObjectsFromArray:[self allFakeQuotedReplyActions:thread includeLabels:includeLabels]]; + [actions addObjectsFromArray:[self allFakeBackDatedActions:thread includeLabels:includeLabels]]; return actions; } @@ -2827,6 +2833,74 @@ NS_ASSUME_NONNULL_BEGIN subactions:[self allFakeSequenceActions:thread includeLabels:YES]]; } +#pragma mark - Back-dated + ++ (DebugUIMessagesAction *)fakeBackDatedMessageAction:(TSThread *)thread + label:(NSString *)label + dateOffset:(int64_t)dateOffset +{ + OWSAssert(thread); + + return [DebugUIMessagesSingleAction + actionWithLabel:[NSString stringWithFormat:@"Fake Back-Date Message (%@)", label] + unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { + NSString *messageBody = + [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:self.randomText]; + TSOutgoingMessage *message = [self createFakeOutgoingMessage:thread + messageBody:messageBody + fakeAssetLoader:nil + messageState:TSOutgoingMessageStateSentToService + isDelivered:NO + isRead:NO + quotedMessage:nil + transaction:transaction]; + [message setReceivedAtTimestamp:(uint64_t)((int64_t)[NSDate ows_millisecondTimeStamp] + dateOffset)]; + [message saveWithTransaction:transaction]; + }]; +} + ++ (NSArray *)allFakeBackDatedActions:(TSThread *)thread includeLabels:(BOOL)includeLabels +{ + OWSAssert(thread); + + NSMutableArray *actions = [NSMutableArray new]; + + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Back-Dated ⚠️"]]; + } + + [actions + addObject:[self fakeBackDatedMessageAction:thread label:@"One Minute Ago" dateOffset:-(int64_t)kMinuteInMs]]; + [actions addObject:[self fakeBackDatedMessageAction:thread label:@"One Hour Ago" dateOffset:-(int64_t)kHourInMs]]; + [actions addObject:[self fakeBackDatedMessageAction:thread label:@"One Day Ago" dateOffset:-(int64_t)kDayInMs]]; + [actions + addObject:[self fakeBackDatedMessageAction:thread label:@"Two Days Ago" dateOffset:-(int64_t)kDayInMs * 2]]; + [actions + addObject:[self fakeBackDatedMessageAction:thread label:@"Ten Days Ago" dateOffset:-(int64_t)kDayInMs * 10]]; + [actions + addObject:[self fakeBackDatedMessageAction:thread label:@"400 Days Ago" dateOffset:-(int64_t)kDayInMs * 400]]; + + return actions; +} + ++ (DebugUIMessagesAction *)allFakeBackDatedAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Back-Dated" + subactions:[self allFakeBackDatedActions:thread includeLabels:YES]]; +} + ++ (void)selectBackDatedAction:(TSThread *)thread +{ + OWSAssertIsOnMainThread(); + OWSAssert(thread); + + [self selectActionUI:[self allFakeBackDatedActions:thread includeLabels:NO] label:@"Select Back-Dated"]; +} + #pragma mark - + (NSString *)randomOversizeText diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.m b/Signal/src/ViewControllers/HomeView/HomeViewCell.m index ebc89261e..2b3c06948 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.m @@ -7,6 +7,7 @@ #import "Signal-Swift.h" #import #import +#import #import #import #import @@ -26,12 +27,12 @@ const NSUInteger kHomeViewAvatarHSpacing = 12; @property (nonatomic) UIView *payloadView; @property (nonatomic) UILabel *nameLabel; @property (nonatomic) UILabel *snippetLabel; -@property (nonatomic) UILabel *timeLabel; +@property (nonatomic) UILabel *dateTimeLabel; @property (nonatomic) UIView *unreadBadge; @property (nonatomic) UILabel *unreadLabel; -@property (nonatomic) TSThread *thread; -@property (nonatomic) OWSContactsManager *contactsManager; +@property (nonatomic, nullable) TSThread *thread; +@property (nonatomic, nullable) OWSContactsManager *contactsManager; @property (nonatomic, readonly) NSMutableArray *viewConstraints; @@ -93,13 +94,13 @@ const NSUInteger kHomeViewAvatarHSpacing = 12; [self.nameLabel setContentHuggingHorizontalLow]; [self.nameLabel setCompressionResistanceHorizontalLow]; - self.timeLabel = [UILabel new]; - [self.timeLabel setContentHuggingHorizontalHigh]; - [self.timeLabel setCompressionResistanceHorizontalHigh]; + self.dateTimeLabel = [UILabel new]; + [self.dateTimeLabel setContentHuggingHorizontalHigh]; + [self.dateTimeLabel setCompressionResistanceHorizontalHigh]; UIStackView *topRowView = [[UIStackView alloc] initWithArrangedSubviews:@[ self.nameLabel, - self.timeLabel, + self.dateTimeLabel, ]]; topRowView.axis = UILayoutConstraintAxisHorizontal; topRowView.spacing = 4; @@ -130,7 +131,7 @@ const NSUInteger kHomeViewAvatarHSpacing = 12; self.unreadBadge.backgroundColor = [UIColor ows_materialBlueColor]; [self.contentView addSubview:self.unreadBadge]; [self.unreadBadge autoPinTrailingToSuperviewMarginWithInset:kHomeViewCellHMargin]; - [self.unreadBadge autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.timeLabel]; + [self.unreadBadge autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.dateTimeLabel]; [self.unreadBadge setContentHuggingHigh]; [self.unreadBadge setCompressionResistanceHigh]; @@ -193,12 +194,12 @@ const NSUInteger kHomeViewAvatarHSpacing = 12; self.snippetLabel.attributedText = [self attributedSnippetForThread:thread blockedPhoneNumberSet:blockedPhoneNumberSet]; - self.timeLabel.attributedText = [self attributedStringForDate:thread.lastMessageDate]; + self.dateTimeLabel.attributedText = [self attributedStringForDate:thread.lastMessageDate]; self.separatorInset = UIEdgeInsetsMake(0, kHomeViewAvatarSize + kHomeViewCellHMargin + kHomeViewAvatarHSpacing, 0, 0); - self.timeLabel.textColor = hasUnreadMessages ? [UIColor ows_materialBlueColor] : [UIColor ows_darkGrayColor]; + self.dateTimeLabel.textColor = hasUnreadMessages ? [UIColor ows_materialBlueColor] : [UIColor ows_darkGrayColor]; NSUInteger unreadCount = [[OWSMessageUtils sharedManager] unreadMessagesInThread:thread]; if (unreadCount > 0) { @@ -298,10 +299,18 @@ const NSUInteger kHomeViewAvatarHSpacing = 12; return [NSAttributedString new]; } - NSDateFormatter *formatter = ([DateUtil dateIsToday:date] ? [DateUtil timeFormatter] : [DateUtil dateFormatter]); - NSString *timeString = [formatter stringFromDate:date]; - OWSAssert(timeString); - return [[NSAttributedString alloc] initWithString:timeString + NSString *dateTimeString; + if (![DateUtil dateIsThisYear:date]) { + dateTimeString = [[DateUtil dateFormatter] stringFromDate:date]; + } else if ([DateUtil dateIsOlderThanOneWeek:date]) { + dateTimeString = [[DateUtil monthAndDayFormatter] stringFromDate:date]; + } else if ([DateUtil dateIsOlderThanOneDay:date]) { + dateTimeString = [[DateUtil shortDayOfWeekFormatter] stringFromDate:date]; + } else { + dateTimeString = [[DateUtil timeFormatter] stringFromDate:date]; + } + + return [[NSAttributedString alloc] initWithString:dateTimeString attributes:@{ NSForegroundColorAttributeName : [UIColor ows_darkGrayColor], NSFontAttributeName : self.dateTimeFont, diff --git a/Signal/src/util/DateUtil.h b/Signal/src/util/DateUtil.h index 8291a2aa0..2ffeed30c 100644 --- a/Signal/src/util/DateUtil.h +++ b/Signal/src/util/DateUtil.h @@ -8,10 +8,13 @@ NS_ASSUME_NONNULL_BEGIN + (NSDateFormatter *)dateFormatter; + (NSDateFormatter *)timeFormatter; ++ (NSDateFormatter *)monthAndDayFormatter; ++ (NSDateFormatter *)shortDayOfWeekFormatter; + (BOOL)dateIsOlderThanOneDay:(NSDate *)date; + (BOOL)dateIsOlderThanOneWeek:(NSDate *)date; + (BOOL)dateIsToday:(NSDate *)date; ++ (BOOL)dateIsThisYear:(NSDate *)date; + (NSString *)formatPastTimestampRelativeToNow:(uint64_t)pastTimestamp isRTL:(BOOL)isRTL NS_SWIFT_NAME(formatPastTimestampRelativeToNow(_:isRTL:)); diff --git a/Signal/src/util/DateUtil.m b/Signal/src/util/DateUtil.m index 648b40202..696377915 100644 --- a/Signal/src/util/DateUtil.m +++ b/Signal/src/util/DateUtil.m @@ -47,12 +47,46 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return formatter; } ++ (NSDateFormatter *)monthAndDayFormatter +{ + static NSDateFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [NSDateFormatter new]; + [formatter setLocale:[NSLocale currentLocale]]; + formatter.dateFormat = @"MMM d"; + }); + return formatter; +} + ++ (NSDateFormatter *)shortDayOfWeekFormatter +{ + static NSDateFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [NSDateFormatter new]; + [formatter setLocale:[NSLocale currentLocale]]; + formatter.dateFormat = @"E"; + }); + return formatter; +} + + (BOOL)dateIsOlderThanOneDay:(NSDate *)date { - return [[NSDate date] timeIntervalSinceDate:date] > kDayInterval; + NSDate *now = [NSDate date]; + NSCalendar *calendar = [NSCalendar currentCalendar]; + + NSUInteger dateDayOfEra = [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:date]; + NSUInteger nowDayOfEra = [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:now]; + return dateDayOfEra < nowDayOfEra; } + (BOOL)dateIsOlderThanOneWeek:(NSDate *)date { - return [[NSDate date] timeIntervalSinceDate:date] > kWeekInterval; + NSDate *now = [NSDate date]; + NSCalendar *calendar = [NSCalendar currentCalendar]; + + NSUInteger dateDayOfEra = [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:date]; + NSUInteger nowDayOfEra = [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:now]; + return dateDayOfEra < (nowDayOfEra - 6); } + (BOOL)date:(NSDate *)date isEqualToDateIgnoringTime:(NSDate *)anotherDate { @@ -65,7 +99,18 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; } + (BOOL)dateIsToday:(NSDate *)date { - return [self date:[NSDate date] isEqualToDateIgnoringTime:date]; + NSDate *now = [NSDate date]; + NSCalendar *calendar = [NSCalendar currentCalendar]; + return ([calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:date] == + [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:now]); +} + ++ (BOOL)dateIsThisYear:(NSDate *)date +{ + NSDate *now = [NSDate date]; + NSCalendar *calendar = [NSCalendar currentCalendar]; + return ( + [calendar component:NSCalendarUnitYear fromDate:date] == [calendar component:NSCalendarUnitYear fromDate:now]); } + (BOOL)dateIsYesterday:(NSDate *)date diff --git a/Signal/test/util/UtilTest.m b/Signal/test/util/UtilTest.m index 55f20bd70..bb002afe2 100644 --- a/Signal/test/util/UtilTest.m +++ b/Signal/test/util/UtilTest.m @@ -3,6 +3,7 @@ // #import "UtilTest.h" +#import "DateUtil.h" #import "TestUtil.h" #import #import @@ -76,6 +77,109 @@ XCTAssertTrue([laterDate isAfterDate:firstDate]); } +- (void)testDateComparators +{ + NSDate *now = [NSDate new]; + + NSDate *oneSecondAgo = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kSecondInterval]; + NSDate *oneMinuteAgo = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kMinuteInterval]; + NSDate *oneDayAgo = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kDayInterval]; + NSDate *threeDaysAgo = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kDayInterval * 3]; + NSDate *tenDaysAgo = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kDayInterval * 10]; + NSDate *oneYearAgo = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kYearInterval]; + NSDate *twoYearsAgo = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kYearInterval * 2]; + + NSDate *oneSecondAhead = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kSecondInterval]; + NSDate *oneMinuteAhead = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kMinuteInterval]; + NSDate *oneDayAhead = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kDayInterval]; + NSDate *threeDaysAhead = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kDayInterval * 3]; + NSDate *tenDaysAhead = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kDayInterval * 10]; + NSDate *oneYearAhead = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kYearInterval]; + NSDate *twoYearsAhead = + [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kYearInterval * 2]; + + // These might fail around midnight. + XCTAssertTrue([DateUtil dateIsToday:oneSecondAgo]); + XCTAssertTrue([DateUtil dateIsToday:oneMinuteAgo]); + XCTAssertFalse([DateUtil dateIsToday:oneDayAgo]); + XCTAssertFalse([DateUtil dateIsToday:threeDaysAgo]); + XCTAssertFalse([DateUtil dateIsToday:tenDaysAgo]); + XCTAssertFalse([DateUtil dateIsToday:oneYearAgo]); + XCTAssertFalse([DateUtil dateIsToday:twoYearsAgo]); + + // These might fail around midnight. + XCTAssertTrue([DateUtil dateIsToday:oneSecondAhead]); + XCTAssertTrue([DateUtil dateIsToday:oneMinuteAhead]); + XCTAssertFalse([DateUtil dateIsToday:oneDayAhead]); + XCTAssertFalse([DateUtil dateIsToday:threeDaysAhead]); + XCTAssertFalse([DateUtil dateIsToday:tenDaysAhead]); + XCTAssertFalse([DateUtil dateIsToday:oneYearAhead]); + XCTAssertFalse([DateUtil dateIsToday:twoYearsAhead]); + + // These might fail around midnight. + XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneSecondAgo]); + XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneMinuteAgo]); + XCTAssertTrue([DateUtil dateIsOlderThanOneDay:oneDayAgo]); + XCTAssertTrue([DateUtil dateIsOlderThanOneDay:threeDaysAgo]); + XCTAssertTrue([DateUtil dateIsOlderThanOneDay:tenDaysAgo]); + XCTAssertTrue([DateUtil dateIsOlderThanOneDay:oneYearAgo]); + XCTAssertTrue([DateUtil dateIsOlderThanOneDay:twoYearsAgo]); + + // These might fail around midnight. + XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneSecondAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneMinuteAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneDayAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneDay:threeDaysAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneDay:tenDaysAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneYearAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneDay:twoYearsAhead]); + + // These might fail around midnight. + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneSecondAgo]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneMinuteAgo]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneDayAgo]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:threeDaysAgo]); + XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:tenDaysAgo]); + XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:oneYearAgo]); + XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:twoYearsAgo]); + + // These might fail around midnight. + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneSecondAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneMinuteAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneDayAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:threeDaysAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:tenDaysAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneYearAhead]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:twoYearsAhead]); + + // These might fail around new year's. + XCTAssertTrue([DateUtil dateIsThisYear:oneSecondAgo]); + XCTAssertTrue([DateUtil dateIsThisYear:oneMinuteAgo]); + XCTAssertTrue([DateUtil dateIsThisYear:oneDayAgo]); + XCTAssertFalse([DateUtil dateIsThisYear:oneYearAgo]); + XCTAssertFalse([DateUtil dateIsThisYear:twoYearsAgo]); + + // These might fail around new year's. + XCTAssertTrue([DateUtil dateIsThisYear:oneSecondAhead]); + XCTAssertTrue([DateUtil dateIsThisYear:oneMinuteAhead]); + XCTAssertTrue([DateUtil dateIsThisYear:oneDayAhead]); + XCTAssertFalse([DateUtil dateIsThisYear:oneYearAhead]); + XCTAssertFalse([DateUtil dateIsThisYear:twoYearsAhead]); +} + - (void)testObjectComparison { XCTAssertTrue([NSObject isNullableObject:nil equalTo:nil]); diff --git a/SignalServiceKit/src/Util/NSDate+OWS.h b/SignalServiceKit/src/Util/NSDate+OWS.h index 2455b7439..024924a9b 100755 --- a/SignalServiceKit/src/Util/NSDate+OWS.h +++ b/SignalServiceKit/src/Util/NSDate+OWS.h @@ -5,12 +5,15 @@ NS_ASSUME_NONNULL_BEGIN // These NSTimeInterval constants provide simplified durations for readability. +// +// These approximations should never be used for strict date/time calcuations. extern const NSTimeInterval kSecondInterval; extern const NSTimeInterval kMinuteInterval; extern const NSTimeInterval kHourInterval; extern const NSTimeInterval kDayInterval; extern const NSTimeInterval kWeekInterval; extern const NSTimeInterval kMonthInterval; +extern const NSTimeInterval kYearInterval; #define kSecondInMs ((uint64_t)1000) #define kMinuteInMs (kSecondInMs * 60) diff --git a/SignalServiceKit/src/Util/NSDate+OWS.mm b/SignalServiceKit/src/Util/NSDate+OWS.mm index 751b1a8f2..f6238a7e0 100644 --- a/SignalServiceKit/src/Util/NSDate+OWS.mm +++ b/SignalServiceKit/src/Util/NSDate+OWS.mm @@ -14,6 +14,7 @@ const NSTimeInterval kHourInterval = 60 * kMinuteInterval; const NSTimeInterval kDayInterval = 24 * kHourInterval; const NSTimeInterval kWeekInterval = 7 * kDayInterval; const NSTimeInterval kMonthInterval = 30 * kDayInterval; +const NSTimeInterval kYearInterval = 365 * kDayInterval; @implementation NSDate (OWS) From 45cb1ec51040db814c49ccb5f087ae10a0d770fb Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 10 Apr 2018 14:53:40 -0400 Subject: [PATCH 3/4] Clean up ahead of PR. --- .../ViewControllers/HomeView/HomeViewCell.m | 8 +- Signal/src/util/DateUtil.h | 3 +- Signal/src/util/DateUtil.m | 48 +++-- Signal/test/util/UtilTest.m | 170 +++++++++++------- 4 files changed, 143 insertions(+), 86 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.m b/Signal/src/ViewControllers/HomeView/HomeViewCell.m index 2b3c06948..ce114e472 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.m @@ -192,6 +192,9 @@ const NSUInteger kHomeViewAvatarHSpacing = 12; [self updateNameLabel]; [self updateAvatarView]; + // We update the fonts every time this cell is configured to ensure that + // changes to the dynamic type settings are reflected. + self.snippetLabel.font = [self snippetFont]; self.snippetLabel.attributedText = [self attributedSnippetForThread:thread blockedPhoneNumberSet:blockedPhoneNumberSet]; self.dateTimeLabel.attributedText = [self attributedStringForDate:thread.lastMessageDate]; @@ -204,6 +207,7 @@ const NSUInteger kHomeViewAvatarHSpacing = 12; NSUInteger unreadCount = [[OWSMessageUtils sharedManager] unreadMessagesInThread:thread]; if (unreadCount > 0) { self.unreadBadge.hidden = NO; + self.unreadLabel.font = [UIFont ows_dynamicTypeCaption1Font]; self.unreadLabel.text = [OWSFormat formatInt:MIN(99, (int)unreadCount)]; [self.viewConstraints addObjectsFromArray:@[ @@ -304,7 +308,7 @@ const NSUInteger kHomeViewAvatarHSpacing = 12; dateTimeString = [[DateUtil dateFormatter] stringFromDate:date]; } else if ([DateUtil dateIsOlderThanOneWeek:date]) { dateTimeString = [[DateUtil monthAndDayFormatter] stringFromDate:date]; - } else if ([DateUtil dateIsOlderThanOneDay:date]) { + } else if ([DateUtil dateIsOlderThanToday:date]) { dateTimeString = [[DateUtil shortDayOfWeekFormatter] stringFromDate:date]; } else { dateTimeString = [[DateUtil timeFormatter] stringFromDate:date]; @@ -382,6 +386,8 @@ const NSUInteger kHomeViewAvatarHSpacing = 12; { OWSAssertIsOnMainThread(); + self.nameLabel.font = self.nameFont; + TSThread *thread = self.thread; if (thread == nil) { OWSFail(@"%@ thread should not be nil", self.logTag); diff --git a/Signal/src/util/DateUtil.h b/Signal/src/util/DateUtil.h index 2ffeed30c..0ef6bf2d1 100644 --- a/Signal/src/util/DateUtil.h +++ b/Signal/src/util/DateUtil.h @@ -11,10 +11,11 @@ NS_ASSUME_NONNULL_BEGIN + (NSDateFormatter *)monthAndDayFormatter; + (NSDateFormatter *)shortDayOfWeekFormatter; -+ (BOOL)dateIsOlderThanOneDay:(NSDate *)date; ++ (BOOL)dateIsOlderThanToday:(NSDate *)date; + (BOOL)dateIsOlderThanOneWeek:(NSDate *)date; + (BOOL)dateIsToday:(NSDate *)date; + (BOOL)dateIsThisYear:(NSDate *)date; ++ (BOOL)dateIsYesterday:(NSDate *)date; + (NSString *)formatPastTimestampRelativeToNow:(uint64_t)pastTimestamp isRTL:(BOOL)isRTL NS_SWIFT_NAME(formatPastTimestampRelativeToNow(_:isRTL:)); diff --git a/Signal/src/util/DateUtil.m b/Signal/src/util/DateUtil.m index 696377915..d22255f41 100644 --- a/Signal/src/util/DateUtil.m +++ b/Signal/src/util/DateUtil.m @@ -71,8 +71,13 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return formatter; } -+ (BOOL)dateIsOlderThanOneDay:(NSDate *)date { - NSDate *now = [NSDate date]; ++ (BOOL)dateIsOlderThanToday:(NSDate *)date +{ + return [self dateIsOlderThanToday:date now:[NSDate date]]; +} + ++ (BOOL)dateIsOlderThanToday:(NSDate *)date now:(NSDate *)now +{ NSCalendar *calendar = [NSCalendar currentCalendar]; NSUInteger dateDayOfEra = [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:date]; @@ -80,8 +85,13 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return dateDayOfEra < nowDayOfEra; } -+ (BOOL)dateIsOlderThanOneWeek:(NSDate *)date { - NSDate *now = [NSDate date]; ++ (BOOL)dateIsOlderThanOneWeek:(NSDate *)date +{ + return [self dateIsOlderThanOneWeek:date now:[NSDate date]]; +} + ++ (BOOL)dateIsOlderThanOneWeek:(NSDate *)date now:(NSDate *)now +{ NSCalendar *calendar = [NSCalendar currentCalendar]; NSUInteger dateDayOfEra = [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:date]; @@ -89,17 +99,13 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; return dateDayOfEra < (nowDayOfEra - 6); } -+ (BOOL)date:(NSDate *)date isEqualToDateIgnoringTime:(NSDate *)anotherDate { - static const unsigned componentFlags = (NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay); - NSDateComponents *components1 = [[NSCalendar autoupdatingCurrentCalendar] components:componentFlags fromDate:date]; - NSDateComponents *components2 = - [[NSCalendar autoupdatingCurrentCalendar] components:componentFlags fromDate:anotherDate]; - return ((components1.year == components2.year) && (components1.month == components2.month) && - (components1.day == components2.day)); ++ (BOOL)dateIsToday:(NSDate *)date +{ + return [self dateIsToday:date now:[NSDate date]]; } -+ (BOOL)dateIsToday:(NSDate *)date { - NSDate *now = [NSDate date]; ++ (BOOL)dateIsToday:(NSDate *)date now:(NSDate *)now +{ NSCalendar *calendar = [NSCalendar currentCalendar]; return ([calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:date] == [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:now]); @@ -107,7 +113,11 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; + (BOOL)dateIsThisYear:(NSDate *)date { - NSDate *now = [NSDate date]; + return [self dateIsThisYear:date now:[NSDate date]]; +} + ++ (BOOL)dateIsThisYear:(NSDate *)date now:(NSDate *)now +{ NSCalendar *calendar = [NSCalendar currentCalendar]; return ( [calendar component:NSCalendarUnitYear fromDate:date] == [calendar component:NSCalendarUnitYear fromDate:now]); @@ -115,8 +125,14 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; + (BOOL)dateIsYesterday:(NSDate *)date { - NSDate *yesterday = [NSDate ows_dateWithMillisecondsSince1970:[NSDate ows_millisecondTimeStamp] - kDayInMs]; - return [self date:yesterday isEqualToDateIgnoringTime:date]; + return [self dateIsYesterday:date now:[NSDate date]]; +} + ++ (BOOL)dateIsYesterday:(NSDate *)date now:(NSDate *)now +{ + NSCalendar *calendar = [NSCalendar currentCalendar]; + return ([calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:date] == + [calendar ordinalityOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitEra forDate:now] - 1); } + (NSString *)formatPastTimestampRelativeToNow:(uint64_t)pastTimestamp isRTL:(BOOL)isRTL diff --git a/Signal/test/util/UtilTest.m b/Signal/test/util/UtilTest.m index bb002afe2..4091725f9 100644 --- a/Signal/test/util/UtilTest.m +++ b/Signal/test/util/UtilTest.m @@ -10,6 +10,18 @@ #import #import +@interface DateUtil (Test) + ++ (BOOL)dateIsOlderThanToday:(NSDate *)date now:(NSDate *)now; ++ (BOOL)dateIsOlderThanOneWeek:(NSDate *)date now:(NSDate *)now; ++ (BOOL)dateIsToday:(NSDate *)date now:(NSDate *)now; ++ (BOOL)dateIsThisYear:(NSDate *)date now:(NSDate *)now; ++ (BOOL)dateIsYesterday:(NSDate *)date now:(NSDate *)now; + +@end + +#pragma mark - + @interface NSString (OWS_Test) - (NSString *)removeAllCharactersIn:(NSCharacterSet *)characterSet; @@ -79,7 +91,19 @@ - (void)testDateComparators { - NSDate *now = [NSDate new]; + // Use a specific reference date to make this test deterministic, + // and to avoid failing around midnight, new year's, etc. + NSDateComponents *nowDateComponents = [NSDateComponents new]; + nowDateComponents.year = 2015; + nowDateComponents.month = 8; + nowDateComponents.day = 31; + nowDateComponents.hour = 8; + NSDate *now = [[NSCalendar currentCalendar] dateFromComponents:nowDateComponents]; + + NSDateFormatter *formatter = [NSDateFormatter new]; + formatter.dateStyle = NSDateFormatterLongStyle; + formatter.timeStyle = NSDateFormatterLongStyle; + NSLog(@"now: %@", [formatter stringFromDate:now]); NSDate *oneSecondAgo = [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] - kSecondInterval]; @@ -111,73 +135,83 @@ NSDate *twoYearsAhead = [NSDate dateWithTimeIntervalSinceReferenceDate:[now timeIntervalSinceReferenceDate] + kYearInterval * 2]; - // These might fail around midnight. - XCTAssertTrue([DateUtil dateIsToday:oneSecondAgo]); - XCTAssertTrue([DateUtil dateIsToday:oneMinuteAgo]); - XCTAssertFalse([DateUtil dateIsToday:oneDayAgo]); - XCTAssertFalse([DateUtil dateIsToday:threeDaysAgo]); - XCTAssertFalse([DateUtil dateIsToday:tenDaysAgo]); - XCTAssertFalse([DateUtil dateIsToday:oneYearAgo]); - XCTAssertFalse([DateUtil dateIsToday:twoYearsAgo]); - - // These might fail around midnight. - XCTAssertTrue([DateUtil dateIsToday:oneSecondAhead]); - XCTAssertTrue([DateUtil dateIsToday:oneMinuteAhead]); - XCTAssertFalse([DateUtil dateIsToday:oneDayAhead]); - XCTAssertFalse([DateUtil dateIsToday:threeDaysAhead]); - XCTAssertFalse([DateUtil dateIsToday:tenDaysAhead]); - XCTAssertFalse([DateUtil dateIsToday:oneYearAhead]); - XCTAssertFalse([DateUtil dateIsToday:twoYearsAhead]); - - // These might fail around midnight. - XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneSecondAgo]); - XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneMinuteAgo]); - XCTAssertTrue([DateUtil dateIsOlderThanOneDay:oneDayAgo]); - XCTAssertTrue([DateUtil dateIsOlderThanOneDay:threeDaysAgo]); - XCTAssertTrue([DateUtil dateIsOlderThanOneDay:tenDaysAgo]); - XCTAssertTrue([DateUtil dateIsOlderThanOneDay:oneYearAgo]); - XCTAssertTrue([DateUtil dateIsOlderThanOneDay:twoYearsAgo]); - - // These might fail around midnight. - XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneSecondAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneMinuteAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneDayAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneDay:threeDaysAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneDay:tenDaysAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneDay:oneYearAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneDay:twoYearsAhead]); - - // These might fail around midnight. - XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneSecondAgo]); - XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneMinuteAgo]); - XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneDayAgo]); - XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:threeDaysAgo]); - XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:tenDaysAgo]); - XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:oneYearAgo]); - XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:twoYearsAgo]); - - // These might fail around midnight. - XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneSecondAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneMinuteAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneDayAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:threeDaysAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:tenDaysAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneYearAhead]); - XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:twoYearsAhead]); - - // These might fail around new year's. - XCTAssertTrue([DateUtil dateIsThisYear:oneSecondAgo]); - XCTAssertTrue([DateUtil dateIsThisYear:oneMinuteAgo]); - XCTAssertTrue([DateUtil dateIsThisYear:oneDayAgo]); - XCTAssertFalse([DateUtil dateIsThisYear:oneYearAgo]); - XCTAssertFalse([DateUtil dateIsThisYear:twoYearsAgo]); - - // These might fail around new year's. - XCTAssertTrue([DateUtil dateIsThisYear:oneSecondAhead]); - XCTAssertTrue([DateUtil dateIsThisYear:oneMinuteAhead]); - XCTAssertTrue([DateUtil dateIsThisYear:oneDayAhead]); - XCTAssertFalse([DateUtil dateIsThisYear:oneYearAhead]); - XCTAssertFalse([DateUtil dateIsThisYear:twoYearsAhead]); + NSLog(@"oneSecondAgo: %@", [formatter stringFromDate:oneSecondAgo]); + + XCTAssertTrue([DateUtil dateIsToday:oneSecondAgo now:now]); + XCTAssertTrue([DateUtil dateIsToday:oneMinuteAgo now:now]); + XCTAssertFalse([DateUtil dateIsToday:oneDayAgo now:now]); + XCTAssertFalse([DateUtil dateIsToday:threeDaysAgo now:now]); + XCTAssertFalse([DateUtil dateIsToday:tenDaysAgo now:now]); + XCTAssertFalse([DateUtil dateIsToday:oneYearAgo now:now]); + XCTAssertFalse([DateUtil dateIsToday:twoYearsAgo now:now]); + + XCTAssertTrue([DateUtil dateIsToday:oneSecondAhead now:now]); + XCTAssertTrue([DateUtil dateIsToday:oneMinuteAhead now:now]); + XCTAssertFalse([DateUtil dateIsToday:oneDayAhead now:now]); + XCTAssertFalse([DateUtil dateIsToday:threeDaysAhead now:now]); + XCTAssertFalse([DateUtil dateIsToday:tenDaysAhead now:now]); + XCTAssertFalse([DateUtil dateIsToday:oneYearAhead now:now]); + XCTAssertFalse([DateUtil dateIsToday:twoYearsAhead now:now]); + + XCTAssertFalse([DateUtil dateIsYesterday:oneSecondAgo now:now]); + XCTAssertFalse([DateUtil dateIsYesterday:oneMinuteAgo now:now]); + XCTAssertTrue([DateUtil dateIsYesterday:oneDayAgo now:now]); + XCTAssertFalse([DateUtil dateIsYesterday:threeDaysAgo now:now]); + XCTAssertFalse([DateUtil dateIsYesterday:tenDaysAgo now:now]); + XCTAssertFalse([DateUtil dateIsYesterday:oneYearAgo now:now]); + XCTAssertFalse([DateUtil dateIsYesterday:twoYearsAgo now:now]); + + XCTAssertFalse([DateUtil dateIsYesterday:oneSecondAhead now:now]); + XCTAssertFalse([DateUtil dateIsYesterday:oneMinuteAhead now:now]); + XCTAssertFalse([DateUtil dateIsYesterday:oneDayAhead now:now]); + XCTAssertFalse([DateUtil dateIsYesterday:threeDaysAhead now:now]); + XCTAssertFalse([DateUtil dateIsYesterday:tenDaysAhead now:now]); + XCTAssertFalse([DateUtil dateIsYesterday:oneYearAhead now:now]); + XCTAssertFalse([DateUtil dateIsYesterday:twoYearsAhead now:now]); + + XCTAssertFalse([DateUtil dateIsOlderThanToday:oneSecondAgo now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanToday:oneMinuteAgo now:now]); + XCTAssertTrue([DateUtil dateIsOlderThanToday:oneDayAgo now:now]); + XCTAssertTrue([DateUtil dateIsOlderThanToday:threeDaysAgo now:now]); + XCTAssertTrue([DateUtil dateIsOlderThanToday:tenDaysAgo now:now]); + XCTAssertTrue([DateUtil dateIsOlderThanToday:oneYearAgo now:now]); + XCTAssertTrue([DateUtil dateIsOlderThanToday:twoYearsAgo now:now]); + + XCTAssertFalse([DateUtil dateIsOlderThanToday:oneSecondAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanToday:oneMinuteAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanToday:oneDayAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanToday:threeDaysAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanToday:tenDaysAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanToday:oneYearAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanToday:twoYearsAhead now:now]); + + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneSecondAgo now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneMinuteAgo now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneDayAgo now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:threeDaysAgo now:now]); + XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:tenDaysAgo now:now]); + XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:oneYearAgo now:now]); + XCTAssertTrue([DateUtil dateIsOlderThanOneWeek:twoYearsAgo now:now]); + + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneSecondAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneMinuteAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneDayAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:threeDaysAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:tenDaysAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:oneYearAhead now:now]); + XCTAssertFalse([DateUtil dateIsOlderThanOneWeek:twoYearsAhead now:now]); + + XCTAssertTrue([DateUtil dateIsThisYear:oneSecondAgo now:now]); + XCTAssertTrue([DateUtil dateIsThisYear:oneMinuteAgo now:now]); + XCTAssertTrue([DateUtil dateIsThisYear:oneDayAgo now:now]); + XCTAssertFalse([DateUtil dateIsThisYear:oneYearAgo now:now]); + XCTAssertFalse([DateUtil dateIsThisYear:twoYearsAgo now:now]); + + XCTAssertTrue([DateUtil dateIsThisYear:oneSecondAhead now:now]); + XCTAssertTrue([DateUtil dateIsThisYear:oneMinuteAhead now:now]); + XCTAssertTrue([DateUtil dateIsThisYear:oneDayAhead now:now]); + XCTAssertFalse([DateUtil dateIsThisYear:oneYearAhead now:now]); + XCTAssertFalse([DateUtil dateIsThisYear:twoYearsAhead now:now]); } - (void)testObjectComparison From c0578b31fe05d31df920a380614869fe6e84f5b1 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 10 Apr 2018 15:54:32 -0400 Subject: [PATCH 4/4] Clean up ahead of CR. --- .../ViewControllers/HomeView/HomeViewCell.m | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.m b/Signal/src/ViewControllers/HomeView/HomeViewCell.m index ce114e472..fbdefd232 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.m @@ -135,14 +135,6 @@ const NSUInteger kHomeViewAvatarHSpacing = 12; [self.unreadBadge setContentHuggingHigh]; [self.unreadBadge setCompressionResistanceHigh]; - // TODO: Will this localize? It assumes that the worst case - // unread count (99) will fit horizontally into some multiple - // N of the font's line height. - const int unreadBadgeSize = (int)ceil(self.unreadLabel.font.lineHeight * 1.5f); - self.unreadBadge.layer.cornerRadius = unreadBadgeSize / 2; - [self.unreadBadge autoSetDimension:ALDimensionWidth toSize:unreadBadgeSize]; - [self.unreadBadge autoSetDimension:ALDimensionHeight toSize:unreadBadgeSize]; - [self.unreadBadge addSubview:self.unreadLabel]; [self.unreadLabel autoVCenterInSuperview]; [self.unreadLabel autoPinWidthToSuperview]; @@ -206,12 +198,21 @@ const NSUInteger kHomeViewAvatarHSpacing = 12; NSUInteger unreadCount = [[OWSMessageUtils sharedManager] unreadMessagesInThread:thread]; if (unreadCount > 0) { + self.unreadBadge.hidden = NO; self.unreadLabel.font = [UIFont ows_dynamicTypeCaption1Font]; self.unreadLabel.text = [OWSFormat formatInt:MIN(99, (int)unreadCount)]; + // TODO: Will this localize? It assumes that the worst case + // unread count (99) will fit horizontally into some multiple + // N of the font's line height. + const int unreadBadgeSize = (int)ceil(self.unreadLabel.font.lineHeight * 1.5f); + self.unreadBadge.layer.cornerRadius = unreadBadgeSize / 2; + [self.viewConstraints addObjectsFromArray:@[ - [self.unreadBadge autoPinLeadingToTrailingEdgeOfView:self.payloadView offset:4.f], + [self.unreadBadge autoSetDimension:ALDimensionWidth toSize:unreadBadgeSize], + [self.unreadBadge autoSetDimension:ALDimensionHeight toSize:unreadBadgeSize], + [self.unreadBadge autoPinLeadingToTrailingEdgeOfView:self.payloadView offset:4.f], ]]; } else { self.unreadBadge.hidden = YES;