From ac458cc7ad4d0daaaec42e31926d10a248ca8db5 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 16 May 2017 11:26:01 -0400 Subject: [PATCH] Add unread indicator. // FREEBIE --- Signal.xcodeproj/project.pbxproj | 18 +++ .../TSMessageAdapaters/OWSMessageData.h | 1 + .../TSMessageAdapaters/TSMessageAdapter.m | 10 +- .../ViewControllers/MessagesViewController.m | 24 ++++ Signal/src/util/ThreadUtil.h | 2 + Signal/src/util/ThreadUtil.m | 53 +++++++++ Signal/src/views/OWSBezierPathView.h | 22 ++++ Signal/src/views/OWSBezierPathView.m | 110 ++++++++++++++++++ Signal/src/views/OWSUnreadIndicatorCell.h | 12 ++ Signal/src/views/OWSUnreadIndicatorCell.m | 77 ++++++++++++ .../src/views/TSUnreadIndicatorInteraction.h | 17 +++ .../src/views/TSUnreadIndicatorInteraction.m | 44 +++++++ .../translations/en.lproj/Localizable.strings | 3 + 13 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 Signal/src/views/OWSBezierPathView.h create mode 100644 Signal/src/views/OWSBezierPathView.m create mode 100644 Signal/src/views/OWSUnreadIndicatorCell.h create mode 100644 Signal/src/views/OWSUnreadIndicatorCell.m create mode 100644 Signal/src/views/TSUnreadIndicatorInteraction.h create mode 100644 Signal/src/views/TSUnreadIndicatorInteraction.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 49650e7f1..a915fd577 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -72,6 +72,9 @@ 34D5CCB11EAE7E7F005515DB /* SelectRecipientViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCB01EAE7E7F005515DB /* SelectRecipientViewController.m */; }; 34DFCB851E8E04B500053165 /* AddToBlockListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DFCB841E8E04B500053165 /* AddToBlockListViewController.m */; }; 34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */; }; + 34F3089C1ECA4CDB00BB7697 /* TSUnreadIndicatorInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F3089B1ECA4CDB00BB7697 /* TSUnreadIndicatorInteraction.m */; }; + 34F3089F1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F3089E1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m */; }; + 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; }; 34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */; }; 450573FE1E78A06D00615BB4 /* OWS103EnableVideoCalling.m in Sources */ = {isa = PBXBuildFile; fileRef = 450573FD1E78A06D00615BB4 /* OWS103EnableVideoCalling.m */; }; 4505C2BF1E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4505C2BE1E648EA300CEBF41 /* ExperienceUpgrade.swift */; }; @@ -472,6 +475,12 @@ 34DFCB831E8E04B400053165 /* AddToBlockListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddToBlockListViewController.h; sourceTree = ""; }; 34DFCB841E8E04B500053165 /* AddToBlockListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AddToBlockListViewController.m; sourceTree = ""; }; 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioProgressView.swift; sourceTree = ""; }; + 34F3089A1ECA4CDB00BB7697 /* TSUnreadIndicatorInteraction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSUnreadIndicatorInteraction.h; sourceTree = ""; }; + 34F3089B1ECA4CDB00BB7697 /* TSUnreadIndicatorInteraction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSUnreadIndicatorInteraction.m; sourceTree = ""; }; + 34F3089D1ECA580B00BB7697 /* OWSUnreadIndicatorCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUnreadIndicatorCell.h; sourceTree = ""; }; + 34F3089E1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUnreadIndicatorCell.m; sourceTree = ""; }; + 34F308A01ECB469700BB7697 /* OWSBezierPathView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBezierPathView.h; sourceTree = ""; }; + 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBezierPathView.m; sourceTree = ""; }; 34FD936E1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAnyTouchGestureRecognizer.h; path = views/OWSAnyTouchGestureRecognizer.h; sourceTree = ""; }; 34FD936F1E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSAnyTouchGestureRecognizer.m; path = views/OWSAnyTouchGestureRecognizer.m; sourceTree = ""; }; 450573FC1E78A06D00615BB4 /* OWS103EnableVideoCalling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWS103EnableVideoCalling.h; path = Migrations/OWS103EnableVideoCalling.h; sourceTree = ""; }; @@ -1307,6 +1316,8 @@ 4531C9C21DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.h */, 4531C9C31DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m */, 3453D8E91EC0D4ED003F9E6F /* OWSAlerts.swift */, + 34F308A01ECB469700BB7697 /* OWSBezierPathView.h */, + 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */, 45C681B91D305C080050903A /* OWSCallCollectionViewCell.h */, 45C681BA1D305C080050903A /* OWSCallCollectionViewCell.m */, 45C681C01D305C9E0050903A /* OWSCallCollectionViewCell.xib */, @@ -1327,7 +1338,11 @@ 45F2B1961D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib */, 34330AA11E79686200DF2FB9 /* OWSProgressView.h */, 34330AA21E79686200DF2FB9 /* OWSProgressView.m */, + 34F3089D1ECA580B00BB7697 /* OWSUnreadIndicatorCell.h */, + 34F3089E1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m */, 45A6DAD51EBBF85500893231 /* ReminderView.swift */, + 34F3089A1ECA4CDB00BB7697 /* TSUnreadIndicatorInteraction.h */, + 34F3089B1ECA4CDB00BB7697 /* TSUnreadIndicatorInteraction.m */, ); name = Views; path = views; @@ -2045,6 +2060,7 @@ 343D3D9B1E9283F100165CA4 /* BlockListUIUtils.m in Sources */, 34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */, 76EB063A18170B33006006FC /* FunctionalUtil.m in Sources */, + 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */, 76EB058A18170B33006006FC /* Release.m in Sources */, 45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */, 450873C71D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */, @@ -2074,6 +2090,7 @@ 45666F561D9B2827008FE134 /* OWSScrubbingLogFormatter.m in Sources */, 45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */, 45C681C61D305C9E0050903A /* OWSDisplayedMessageCollectionViewCell.m in Sources */, + 34F3089F1ECA580B00BB7697 /* OWSUnreadIndicatorCell.m in Sources */, 34B3F8861E8DF1700035BE1A /* NotificationSettingsOptionsViewController.m in Sources */, 452ECA4D1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */, 452EA0971EA662330078744B /* AttachmentPointerAdapter.swift in Sources */, @@ -2158,6 +2175,7 @@ 34B3F87D1E8DF1700035BE1A /* FullImageViewController.m in Sources */, 45666F7B1D9C0533008FE134 /* OWSDatabaseMigration.m in Sources */, B90418E6183E9DD40038554A /* DateUtil.m in Sources */, + 34F3089C1ECA4CDB00BB7697 /* TSUnreadIndicatorInteraction.m in Sources */, 45B201761DAECBFE00C461E0 /* HighlightableLabel.swift in Sources */, 459311FC1D75C948008DD4F0 /* OWSDeviceTableViewCell.m in Sources */, ); diff --git a/Signal/src/Models/TSMessageAdapaters/OWSMessageData.h b/Signal/src/Models/TSMessageAdapaters/OWSMessageData.h index 07f5175ae..697e58cf5 100644 --- a/Signal/src/Models/TSMessageAdapaters/OWSMessageData.h +++ b/Signal/src/Models/TSMessageAdapaters/OWSMessageData.h @@ -15,6 +15,7 @@ typedef NS_ENUM(NSInteger, TSMessageAdapterType) { TSErrorMessageAdapter, TSMediaAttachmentAdapter, TSGenericTextMessageAdapter, // Used when message direction is unknown (outgoing or incoming) + TSUnreadIndicatorAdapter, }; @protocol OWSMessageData diff --git a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m index 16229c5cb..60b245a60 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m +++ b/Signal/src/Models/TSMessageAdapaters/TSMessageAdapter.m @@ -2,6 +2,7 @@ // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // +#import "TSMessageAdapter.h" #import "AttachmentSharing.h" #import "OWSCall.h" #import "Signal-Swift.h" @@ -16,6 +17,7 @@ #import "TSIncomingMessage.h" #import "TSInfoMessage.h" #import "TSOutgoingMessage.h" +#import "TSUnreadIndicatorInteraction.h" #import NS_ASSUME_NONNULL_BEGIN @@ -119,6 +121,8 @@ NS_ASSUME_NONNULL_BEGIN adapter.senderDisplayName = NSLocalizedString(@"ME_STRING", @""); adapter.messageType = TSOutgoingMessageAdapter; } + } else { + OWSAssert(0); } if ([interaction isKindOfClass:[TSIncomingMessage class]] || @@ -216,11 +220,15 @@ NS_ASSUME_NONNULL_BEGIN displayString:@""]; return call; } - } else { + } else if ([interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]) { + adapter.messageType = TSUnreadIndicatorAdapter; + } else if ([interaction isKindOfClass:[TSErrorMessage class]]) { TSErrorMessage *errorMessage = (TSErrorMessage *)interaction; adapter.errorMessageType = errorMessage.errorType; adapter.messageBody = errorMessage.description; adapter.messageType = TSErrorMessageAdapter; + } else { + OWSAssert(0); } if ([interaction isKindOfClass:[TSOutgoingMessage class]]) { diff --git a/Signal/src/ViewControllers/MessagesViewController.m b/Signal/src/ViewControllers/MessagesViewController.m index 9d38a93a1..1aab585cb 100644 --- a/Signal/src/ViewControllers/MessagesViewController.m +++ b/Signal/src/ViewControllers/MessagesViewController.m @@ -27,6 +27,7 @@ #import "OWSMessagesBubblesSizeCalculator.h" #import "OWSOutgoingMessageCollectionViewCell.h" #import "OWSUnknownContactBlockOfferMessage.h" +#import "OWSUnreadIndicatorCell.h" #import "PropertyListPreferences.h" #import "Signal-Swift.h" #import "SignalKeyingStorage.h" @@ -707,6 +708,8 @@ typedef enum : NSUInteger { _composeOnOpen = keyboardOnViewAppearing; _callOnOpen = callOnViewAppearing; + [ThreadUtil createUnreadMessagesIndicatorIfNecessary:thread storageManager:self.storageManager]; + [self markAllMessagesAsRead]; [self.uiDatabaseConnection beginLongLivedReadTransaction]; @@ -813,6 +816,9 @@ typedef enum : NSUInteger { [self.collectionView registerNib:[OWSCallCollectionViewCell nib] forCellWithReuseIdentifier:[OWSCallCollectionViewCell cellReuseIdentifier]]; + [self.collectionView registerClass:[OWSUnreadIndicatorCell class] + forCellWithReuseIdentifier:[OWSUnreadIndicatorCell cellReuseIdentifier]]; + [self.collectionView registerNib:[OWSDisplayedMessageCollectionViewCell nib] forCellWithReuseIdentifier:[OWSDisplayedMessageCollectionViewCell cellReuseIdentifier]]; @@ -1685,6 +1691,9 @@ typedef enum : NSUInteger { case TSOutgoingMessageAdapter: { cell = [self loadOutgoingCellForMessage:message atIndexPath:indexPath]; } break; + case TSUnreadIndicatorAdapter: { + cell = [self loadUnreadIndicatorCell:indexPath]; + } break; default: { DDLogWarn(@"using default cell constructor for message: %@", message); cell = (JSQMessagesCollectionViewCell *)[super collectionView:collectionView cellForItemAtIndexPath:indexPath]; @@ -1712,6 +1721,7 @@ typedef enum : NSUInteger { if (![cell isKindOfClass:[OWSIncomingMessageCollectionViewCell class]]) { DDLogError(@"%@ Unexpected cell type: %@", self.tag, cell); + OWSAssert(0); return cell; } @@ -1732,6 +1742,7 @@ typedef enum : NSUInteger { if (![cell isKindOfClass:[OWSOutgoingMessageCollectionViewCell class]]) { DDLogError(@"%@ Unexpected cell type: %@", self.tag, cell); + OWSAssert(0); return cell; } @@ -1752,6 +1763,18 @@ typedef enum : NSUInteger { return cell; } +- (JSQMessagesCollectionViewCell *)loadUnreadIndicatorCell:(NSIndexPath *)indexPath +{ + OWSAssert(indexPath); + + OWSUnreadIndicatorCell *cell = + [self.collectionView dequeueReusableCellWithReuseIdentifier:[OWSUnreadIndicatorCell cellReuseIdentifier] + forIndexPath:indexPath]; + [cell configure]; + + return cell; +} + - (OWSCallCollectionViewCell *)loadCallCellForCall:(OWSCall *)call atIndexPath:(NSIndexPath *)indexPath { OWSCallCollectionViewCell *callCell = [self.collectionView dequeueReusableCellWithReuseIdentifier:[OWSCallCollectionViewCell cellReuseIdentifier] @@ -2222,6 +2245,7 @@ typedef enum : NSUInteger { [self handleErrorMessageTap:(TSErrorMessage *)interaction]; break; case TSCallAdapter: + case TSUnreadIndicatorAdapter: break; default: DDLogDebug(@"Unhandled bubble touch for interaction: %@.", interaction); diff --git a/Signal/src/util/ThreadUtil.h b/Signal/src/util/ThreadUtil.h index f62c10f07..7af2cbef1 100644 --- a/Signal/src/util/ThreadUtil.h +++ b/Signal/src/util/ThreadUtil.h @@ -27,6 +27,8 @@ NS_ASSUME_NONNULL_BEGIN contactsManager:(OWSContactsManager *)contactsManager blockingManager:(OWSBlockingManager *)blockingManager; ++ (void)createUnreadMessagesIndicatorIfNecessary:(TSThread *)thread storageManager:(TSStorageManager *)storageManager; + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/ThreadUtil.m b/Signal/src/util/ThreadUtil.m index cf1e61d7c..f712272ea 100644 --- a/Signal/src/util/ThreadUtil.m +++ b/Signal/src/util/ThreadUtil.m @@ -5,6 +5,7 @@ #import "ThreadUtil.h" #import "OWSContactsManager.h" #import "Signal-Swift.h" +#import "TSUnreadIndicatorInteraction.h" #import #import #import @@ -178,6 +179,58 @@ NS_ASSUME_NONNULL_BEGIN }]; } ++ (void)createUnreadMessagesIndicatorIfNecessary:(TSThread *)thread storageManager:(TSStorageManager *)storageManager +{ + OWSAssert(thread); + OWSAssert(storageManager); + + [storageManager.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + + NSMutableArray *indicators = [NSMutableArray new]; + __block TSMessage *firstUnreadMessage = nil; + // TODO: Will this approach be prohibitively expensive? + [[transaction ext:TSMessageDatabaseViewExtensionName] + enumerateRowsInGroup:thread.uniqueId + usingBlock:^( + NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { + + if ([object isKindOfClass:[TSUnreadIndicatorInteraction class]]) { + [indicators addObject:object]; + } else if ([object isKindOfClass:[TSIncomingMessage class]]) { + TSIncomingMessage *incomingMessage = (TSIncomingMessage *)object; + if (!incomingMessage.wasRead) { + if (!firstUnreadMessage) { + firstUnreadMessage = incomingMessage; + } else { + OWSAssert([[firstUnreadMessage receiptDateForSorting] + compare:[incomingMessage receiptDateForSorting]] + == NSOrderedAscending); + } + } + } + }]; + + for (TSUnreadIndicatorInteraction *indicator in indicators) { + [indicator removeWithTransaction:transaction]; + } + + BOOL shouldHaveIndicator = firstUnreadMessage != nil; + if (!shouldHaveIndicator) { + return; + } + + DDLogInfo(@"%@ Creating TSUnreadIndicatorInteraction", self.tag); + + // We want the block offer to appear just before the first unread incoming + // message in the conversation timeline. + uint64_t indicatorTimestamp = firstUnreadMessage.timestamp - 1; + + TSUnreadIndicatorInteraction *indicator = + [[TSUnreadIndicatorInteraction alloc] initWithTimestamp:indicatorTimestamp thread:thread]; + [indicator saveWithTransaction:transaction]; + }]; +} + #pragma mark - Logging + (NSString *)tag diff --git a/Signal/src/views/OWSBezierPathView.h b/Signal/src/views/OWSBezierPathView.h new file mode 100644 index 000000000..b9174d819 --- /dev/null +++ b/Signal/src/views/OWSBezierPathView.h @@ -0,0 +1,22 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +typedef void (^ConfigureShapeLayerBlock)(CAShapeLayer *layer, CGRect bounds); + +@interface OWSBezierPathView : UIView + +// Configure the view with this method if it uses a single Bezier path. +- (void)setConfigureShapeLayerBlock:(ConfigureShapeLayerBlock)configureShapeLayerBlock; + +// Configure the view with this method if it uses multiple Bezier paths. +// +// Paths will be rendered in back-to-front order. +- (void)setConfigureShapeLayerBlocks:(NSArray *)configureShapeLayerBlocks; + +// This method forces the view to reconstruct its layer content. It shouldn't +// be necessary to call this unless the ConfigureShapeLayerBlocks depend on external +// state which has changed. +- (void)updateLayers; + +@end diff --git a/Signal/src/views/OWSBezierPathView.m b/Signal/src/views/OWSBezierPathView.m new file mode 100644 index 000000000..9ee6f98fa --- /dev/null +++ b/Signal/src/views/OWSBezierPathView.m @@ -0,0 +1,110 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "OWSBezierPathView.h" + +@interface OWSBezierPathView () + +@property (nonatomic) NSArray *configureShapeLayerBlocks; + +@end + +@implementation OWSBezierPathView + +- (id)init +{ + self = [super init]; + if (self) { + [self initCommon]; + } + + return self; +} + +- (id)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + [self initCommon]; + } + return self; +} + +- (void)initCommon +{ + self.opaque = NO; + self.userInteractionEnabled = NO; + self.backgroundColor = [UIColor clearColor]; +} + +- (void)setFrame:(CGRect)frame +{ + BOOL didChangeSize = !CGSizeEqualToSize(frame.size, self.frame.size); + + [super setFrame:frame]; + + if (didChangeSize) { + [self updateLayers]; + } +} + +- (void)setBounds:(CGRect)bounds +{ + BOOL didChangeSize = !CGSizeEqualToSize(bounds.size, self.bounds.size); + + [super setBounds:bounds]; + + if (didChangeSize) { + [self updateLayers]; + } +} + +- (void)setConfigureShapeLayerBlock:(ConfigureShapeLayerBlock)configureShapeLayerBlock +{ + OWSAssert(configureShapeLayerBlock); + + [self setConfigureShapeLayerBlocks:@[ configureShapeLayerBlock ]]; +} + +- (void)setConfigureShapeLayerBlocks:(NSArray *)configureShapeLayerBlocks +{ + OWSAssert(configureShapeLayerBlocks.count > 0); + + _configureShapeLayerBlocks = configureShapeLayerBlocks; + + [self updateLayers]; +} + +- (void)updateLayers +{ + if (self.bounds.size.width <= 0.f || self.bounds.size.height <= 0.f) { + return; + } + + for (CALayer *layer in self.layer.sublayers) { + [layer removeFromSuperlayer]; + } + + for (ConfigureShapeLayerBlock configureShapeLayerBlock in self.configureShapeLayerBlocks) { + CAShapeLayer *shapeLayer = [CAShapeLayer new]; + configureShapeLayerBlock(shapeLayer, self.bounds); + [self.layer addSublayer:shapeLayer]; + } + + [self setNeedsDisplay]; +} + +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; +} + +@end diff --git a/Signal/src/views/OWSUnreadIndicatorCell.h b/Signal/src/views/OWSUnreadIndicatorCell.h new file mode 100644 index 000000000..5abb6b231 --- /dev/null +++ b/Signal/src/views/OWSUnreadIndicatorCell.h @@ -0,0 +1,12 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import +#import + +@interface OWSUnreadIndicatorCell : JSQMessagesCollectionViewCell + +- (void)configure; + +@end diff --git a/Signal/src/views/OWSUnreadIndicatorCell.m b/Signal/src/views/OWSUnreadIndicatorCell.m new file mode 100644 index 000000000..cb559e995 --- /dev/null +++ b/Signal/src/views/OWSUnreadIndicatorCell.m @@ -0,0 +1,77 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "OWSUnreadIndicatorCell.h" +#import "OWSBezierPathView.h" +#import "UIColor+OWS.h" +#import "UIFont+OWS.h" +#import + +@interface OWSUnreadIndicatorCell () + +@property (nonatomic) UILabel *label; +@property (nonatomic) OWSBezierPathView *leftPathView; +@property (nonatomic) OWSBezierPathView *rightPathView; + +@end + +#pragma mark - + +@implementation OWSUnreadIndicatorCell + ++ (NSString *)cellReuseIdentifier +{ + return NSStringFromClass([self class]); +} + +- (void)configure +{ + self.backgroundColor = [UIColor whiteColor]; + + if (!self.label) { + self.label = [UILabel new]; + self.label.text = NSLocalizedString( + @"MESSAGES_VIEW_UNREAD_INDICATOR", @"Indicator that separates read from unread messages."); + self.label.textColor = [UIColor ows_infoMessageBorderColor]; + self.label.font = [UIFont ows_mediumFontWithSize:12.f]; + [self.contentView addSubview:self.label]; + + CGFloat kLineThickness = 0.5f; + CGFloat kLineMargin = 5.f; + ConfigureShapeLayerBlock configureShapeLayerBlock = ^(CAShapeLayer *layer, CGRect bounds) { + OWSCAssert(layer); + + CGRect pathBounds + = CGRectMake(0, (bounds.size.height - kLineThickness) * 0.5f, bounds.size.width, kLineThickness); + pathBounds = CGRectInset(pathBounds, kLineMargin, 0); + UIBezierPath *path = [UIBezierPath bezierPathWithRect:pathBounds]; + layer.path = path.CGPath; + layer.fillColor = [UIColor ows_infoMessageBorderColor].CGColor; + }; + + self.leftPathView = [OWSBezierPathView new]; + self.leftPathView.configureShapeLayerBlock = configureShapeLayerBlock; + [self.contentView addSubview:self.leftPathView]; + + self.rightPathView = [OWSBezierPathView new]; + self.rightPathView.configureShapeLayerBlock = configureShapeLayerBlock; + [self.contentView addSubview:self.rightPathView]; + } +} + +- (void)layoutSubviews +{ + CGSize labelSize = [self.label sizeThatFits:CGSizeZero]; + self.label.frame = CGRectMake(round(self.bounds.origin.x + (self.bounds.size.width - labelSize.width) * 0.5f), + round(self.bounds.origin.y + (self.bounds.size.height - labelSize.height) * 0.5f), + labelSize.width, + labelSize.height); + self.leftPathView.frame = CGRectMake(0, 0, self.label.frame.origin.x, self.bounds.size.height); + self.rightPathView.frame = CGRectMake(self.label.frame.origin.x + self.label.frame.size.width, + 0, + self.bounds.size.width - (self.label.frame.origin.x + self.label.frame.size.width), + self.bounds.size.height); +} + +@end diff --git a/Signal/src/views/TSUnreadIndicatorInteraction.h b/Signal/src/views/TSUnreadIndicatorInteraction.h new file mode 100644 index 000000000..a408e7de6 --- /dev/null +++ b/Signal/src/views/TSUnreadIndicatorInteraction.h @@ -0,0 +1,17 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "TSMessage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface TSUnreadIndicatorInteraction : TSMessage + +- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithTimestamp:(uint64_t)timestamp thread:(TSThread *)thread NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/views/TSUnreadIndicatorInteraction.m b/Signal/src/views/TSUnreadIndicatorInteraction.m new file mode 100644 index 000000000..ad099a7cd --- /dev/null +++ b/Signal/src/views/TSUnreadIndicatorInteraction.m @@ -0,0 +1,44 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "TSUnreadIndicatorInteraction.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation TSUnreadIndicatorInteraction + +- (instancetype)initWithCoder:(NSCoder *)coder +{ + return [super initWithCoder:coder]; +} + +- (instancetype)initWithTimestamp:(uint64_t)timestamp thread:(TSThread *)thread +{ + self = [super initWithTimestamp:timestamp + inThread:thread + messageBody:nil + attachmentIds:@[] + expiresInSeconds:0 + expireStartedAt:0]; + + if (!self) { + return self; + } + + return self; +} + +- (nullable NSDate *)receiptDateForSorting +{ + // Always use date, since we're creating these interactions after the fact + // and back-dating them. + // + // By default [TSMessage receiptDateForSorting] will prefer to use receivedAtDate + // which is not back-dated. + return self.date; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 5cde7f3c1..9db7db35d 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -700,6 +700,9 @@ /* The subtitle for the messages view title indicates that the title can be tapped to access settings for this conversation. */ "MESSAGES_VIEW_TITLE_SUBTITLE" = "Tap here for settings"; +/* Indicator that separates read from unread messages. */ +"MESSAGES_VIEW_UNREAD_INDICATOR" = "Unread Messages"; + /* {{number of minutes}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{5 minutes}}'. See other *_TIME_AMOUNT strings */ "MINUTES_TIME_AMOUNT" = "%u minutes";