From 8d72bb032ee880d3cf4c5c14dcffa3bafb57b141 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 11 Jul 2018 14:12:58 -0400 Subject: [PATCH] Rework unread indicators. --- Signal.xcodeproj/project.pbxproj | 20 +- .../ConversationView/Cells/OWSMessageCell.h | 2 - .../ConversationView/Cells/OWSMessageCell.m | 90 ++---- .../Cells/OWSMessageHeaderView.h | 22 ++ .../Cells/OWSMessageHeaderView.m | 183 +++++++++++ .../Cells/OWSUnreadIndicatorCell.h | 17 -- .../Cells/OWSUnreadIndicatorCell.m | 169 ----------- .../ConversationViewController.m | 52 ++-- .../ConversationView/ConversationViewItem.h | 4 + .../ConversationView/ConversationViewItem.m | 31 +- .../Models/TSUnreadIndicatorInteraction.h | 13 +- .../Models/TSUnreadIndicatorInteraction.m | 25 -- SignalMessaging/SignalMessaging.h | 1 - SignalMessaging/utils/OWSUnreadIndicator.h | 35 +++ SignalMessaging/utils/OWSUnreadIndicator.m | 50 +++ SignalMessaging/utils/ThreadUtil.h | 25 +- SignalMessaging/utils/ThreadUtil.m | 286 ++++++++---------- .../src/Messages/Interactions/TSInteraction.h | 1 + 18 files changed, 514 insertions(+), 512 deletions(-) create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHeaderView.h create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHeaderView.m delete mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.h delete mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m create mode 100644 SignalMessaging/utils/OWSUnreadIndicator.h create mode 100644 SignalMessaging/utils/OWSUnreadIndicator.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 896c0ba8b..7f8870d99 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -161,6 +161,7 @@ 3478506C1FD9B78A007B8332 /* NoopNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850681FD9B78A007B8332 /* NoopNotificationsManager.swift */; }; 347850711FDAEB17007B8332 /* OWSUserProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 3478506F1FDAEB16007B8332 /* OWSUserProfile.m */; }; 347850721FDAEB17007B8332 /* OWSUserProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 347850701FDAEB16007B8332 /* OWSUserProfile.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 348570A820F67575004FF32B /* OWSMessageHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = 348570A620F67574004FF32B /* OWSMessageHeaderView.m */; }; 348BB254209CD4B80047AEC2 /* ContactFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348BB253209CD4B80047AEC2 /* ContactFieldView.swift */; }; 348BB25A209CF8E50047AEC2 /* TappableStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348BB258209CF8E40047AEC2 /* TappableStackView.swift */; }; 348BB25B209CF8E50047AEC2 /* TappableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348BB259209CF8E50047AEC2 /* TappableView.swift */; }; @@ -180,6 +181,8 @@ 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 */; }; + 34B6D27420F664C900765BE2 /* OWSUnreadIndicator.h in Headers */ = {isa = PBXBuildFile; fileRef = 34B6D27220F664C800765BE2 /* OWSUnreadIndicator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34B6D27520F664C900765BE2 /* OWSUnreadIndicator.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B6D27320F664C800765BE2 /* OWSUnreadIndicator.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 */; }; @@ -208,7 +211,6 @@ 34D1F0AB1F867BFC0066283D /* OWSContactOffersCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */; }; 34D1F0AE1F867BFC0066283D /* OWSMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A21F867BFC0066283D /* OWSMessageCell.m */; }; 34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */; }; - 34D1F0B11F867BFC0066283D /* OWSUnreadIndicatorCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0A81F867BFC0066283D /* OWSUnreadIndicatorCell.m */; }; 34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */; }; 34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */; }; 34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */; }; @@ -775,6 +777,8 @@ 347850681FD9B78A007B8332 /* NoopNotificationsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoopNotificationsManager.swift; sourceTree = ""; }; 3478506F1FDAEB16007B8332 /* OWSUserProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUserProfile.m; sourceTree = ""; }; 347850701FDAEB16007B8332 /* OWSUserProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUserProfile.h; sourceTree = ""; }; + 348570A620F67574004FF32B /* OWSMessageHeaderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageHeaderView.m; sourceTree = ""; }; + 348570A720F67574004FF32B /* OWSMessageHeaderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageHeaderView.h; sourceTree = ""; }; 348BB253209CD4B80047AEC2 /* ContactFieldView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ContactFieldView.swift; path = SignalMessaging/attachments/ContactFieldView.swift; sourceTree = SOURCE_ROOT; }; 348BB258209CF8E40047AEC2 /* TappableStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TappableStackView.swift; path = SignalMessaging/Views/TappableStackView.swift; sourceTree = SOURCE_ROOT; }; 348BB259209CF8E50047AEC2 /* TappableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TappableView.swift; path = SignalMessaging/Views/TappableView.swift; sourceTree = SOURCE_ROOT; }; @@ -808,6 +812,8 @@ 34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalsNavigationController.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 = ""; }; + 34B6D27220F664C800765BE2 /* OWSUnreadIndicator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUnreadIndicator.h; sourceTree = ""; }; + 34B6D27320F664C800765BE2 /* OWSUnreadIndicator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUnreadIndicator.m; sourceTree = ""; }; 34BECE291F74C12700D7438D /* DebugUIStress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIStress.h; sourceTree = ""; }; 34BECE2A1F74C12700D7438D /* DebugUIStress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIStress.m; sourceTree = ""; }; 34BECE2D1F7ABCE000D7438D /* GifPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifPickerViewController.swift; sourceTree = ""; }; @@ -857,8 +863,6 @@ 34D1F0A21F867BFC0066283D /* OWSMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageCell.m; sourceTree = ""; }; 34D1F0A51F867BFC0066283D /* OWSSystemMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSystemMessageCell.h; sourceTree = ""; }; 34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSystemMessageCell.m; sourceTree = ""; }; - 34D1F0A71F867BFC0066283D /* OWSUnreadIndicatorCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUnreadIndicatorCell.h; sourceTree = ""; }; - 34D1F0A81F867BFC0066283D /* OWSUnreadIndicatorCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUnreadIndicatorCell.m; sourceTree = ""; }; 34D1F0B21F86D31D0066283D /* ConversationCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationCollectionView.h; sourceTree = ""; }; 34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationCollectionView.m; sourceTree = ""; }; 34D1F0B51F87F8850066283D /* OWSGenericAttachmentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSGenericAttachmentView.h; sourceTree = ""; }; @@ -1467,6 +1471,8 @@ 34641E172088D7E900E2EDE5 /* OWSScreenLock.swift */, 34480B4F1FD0A7A300BC14EF /* OWSScrubbingLogFormatter.h */, 34480B511FD0A7A400BC14EF /* OWSScrubbingLogFormatter.m */, + 34B6D27220F664C800765BE2 /* OWSUnreadIndicator.h */, + 34B6D27320F664C800765BE2 /* OWSUnreadIndicator.m */, 34641E1120878FB000E2EDE5 /* OWSWindowManager.h */, 34641E1020878FAF00E2EDE5 /* OWSWindowManager.m */, 45360B8C1F9521F800FA666C /* Searcher.swift */, @@ -1772,6 +1778,8 @@ 34D1F0A21F867BFC0066283D /* OWSMessageCell.m */, 34D920E520E179C100D51158 /* OWSMessageFooterView.h */, 34D920E620E179C200D51158 /* OWSMessageFooterView.m */, + 348570A720F67574004FF32B /* OWSMessageHeaderView.h */, + 348570A620F67574004FF32B /* OWSMessageHeaderView.m */, 34DBF000206BD5A400025978 /* OWSMessageTextView.h */, 34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */, 3427C64120F500DE00EEC730 /* OWSMessageTimerView.h */, @@ -1780,8 +1788,6 @@ 34277A5C20751BDC006049F2 /* OWSQuotedMessageView.m */, 34D1F0A51F867BFC0066283D /* OWSSystemMessageCell.h */, 34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */, - 34D1F0A71F867BFC0066283D /* OWSUnreadIndicatorCell.h */, - 34D1F0A81F867BFC0066283D /* OWSUnreadIndicatorCell.m */, ); path = Cells; sourceTree = ""; @@ -2456,6 +2462,7 @@ 451F8A491FD715CF005CB9DA /* OWSAvatarBuilder.h in Headers */, 346129951FD1E30000532771 /* OWSDatabaseMigration.h in Headers */, 45194F961FD7226300333B2C /* SelectThreadViewController.h in Headers */, + 34B6D27420F664C900765BE2 /* OWSUnreadIndicator.h in Headers */, 346129B41FD1F7E800532771 /* OWSProfileManager.h in Headers */, 34D2015120DC160E00A6FD3A /* ContactCellView.h in Headers */, 346129FA1FD5F31400532771 /* OWS100RemoveTSRecipientsMigration.h in Headers */, @@ -3165,6 +3172,7 @@ 34382266209A4E400094FEB7 /* ContactShareApprovalViewController.swift in Sources */, 4503F1C3204711D300CEE724 /* OWS107LegacySounds.m in Sources */, 3438226A209B63500094FEB7 /* EditContactShareNameViewController.swift in Sources */, + 34B6D27520F664C900765BE2 /* OWSUnreadIndicator.m in Sources */, 346129A61FD1F09100532771 /* OWSContactsManager.m in Sources */, 4541B71D209D3B7A0008608F /* ContactShareViewModel.swift in Sources */, 4598198F204E2F28009414F2 /* OWS108CallLoggingPreference.m in Sources */, @@ -3221,6 +3229,7 @@ 34D1F0871F8678AA0066283D /* ConversationViewItem.m in Sources */, B6DA6B071B8A2F9A00CA6F98 /* AppStoreRating.m in Sources */, 451A13B11E13DED2000A50FD /* CallNotificationsAdapter.swift in Sources */, + 348570A820F67575004FF32B /* OWSMessageHeaderView.m in Sources */, 450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */, 34D1F0AB1F867BFC0066283D /* OWSContactOffersCell.m in Sources */, 340FC8C7204DE64D007AEB0F /* OWSBackupAPI.swift in Sources */, @@ -3345,7 +3354,6 @@ 76EB054018170B33006006FC /* AppDelegate.m in Sources */, 34D1F0831F8678AA0066283D /* ConversationInputTextView.m in Sources */, 340FC8B6204DAC8D007AEB0F /* OWSQRCodeScanningViewController.m in Sources */, - 34D1F0B11F867BFC0066283D /* OWSUnreadIndicatorCell.m in Sources */, 340FC8B5204DAC8D007AEB0F /* AboutTableViewController.m in Sources */, 34BECE2B1F74C12700D7438D /* DebugUIStress.m in Sources */, 340FC8B9204DAC8D007AEB0F /* UpdateGroupViewController.m in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h index 1ab9e9def..4d52718b5 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h @@ -8,8 +8,6 @@ NS_ASSUME_NONNULL_BEGIN -extern const CGFloat OWSMessageCellDateHeaderVMargin; - @interface OWSMessageCell : ConversationViewCell @property (nonatomic, readonly) OWSMessageBubbleView *messageBubbleView; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index c75ccab31..72de49af2 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -5,21 +5,19 @@ #import "OWSMessageCell.h" #import "OWSContactAvatarBuilder.h" #import "OWSMessageBubbleView.h" +#import "OWSMessageHeaderView.h" #import "Signal-Swift.h" NS_ASSUME_NONNULL_BEGIN -const CGFloat OWSMessageCellDateHeaderVMargin = 23; - @interface OWSMessageCell () // The nullable properties are created as needed. // The non-nullable properties are so frequently used that it's easier // to always keep one around. +@property (nonatomic) OWSMessageHeaderView *headerView; @property (nonatomic) OWSMessageBubbleView *messageBubbleView; -@property (nonatomic) UIView *dateHeaderView; -@property (nonatomic) UILabel *dateHeaderLabel; @property (nonatomic) AvatarImageView *avatarView; @property (nonatomic, nullable) UIImageView *sendFailureBadgeView; @@ -55,15 +53,7 @@ const CGFloat OWSMessageCellDateHeaderVMargin = 23; self.messageBubbleView = [OWSMessageBubbleView new]; [self.contentView addSubview:self.messageBubbleView]; - self.dateHeaderLabel = [UILabel new]; - self.dateHeaderLabel.font = self.dateHeaderFont; - self.dateHeaderLabel.textAlignment = NSTextAlignmentCenter; - self.dateHeaderLabel.textColor = [UIColor ows_light60Color]; - - self.dateHeaderView = [UIView new]; - self.dateHeaderView.layoutMargins = UIEdgeInsetsMake(0, 0, OWSMessageCellDateHeaderVMargin, 0); - [self.dateHeaderView addSubview:self.dateHeaderLabel]; - [self.dateHeaderLabel autoPinToSuperviewMargins]; + self.headerView = [OWSMessageHeaderView new]; self.avatarView = [[AvatarImageView alloc] init]; [self.avatarView autoSetDimension:ALDimensionWidth toSize:self.avatarSize]; @@ -153,8 +143,24 @@ const CGFloat OWSMessageCellDateHeaderVMargin = 23; [self.messageBubbleView configureViews]; [self.messageBubbleView loadContent]; - // Update label fonts to honor dynamic type size. - self.dateHeaderLabel.font = self.dateHeaderFont; + if (self.viewItem.hasCellHeader) { + CGFloat headerHeight = + [self.headerView measureWithConversationViewItem:self.viewItem conversationStyle:self.conversationStyle] + .height; + [self.headerView loadForDisplayWithViewItem:self.viewItem conversationStyle:self.conversationStyle]; + [self.contentView addSubview:self.headerView]; + [self.viewConstraints addObjectsFromArray:@[ + [self.headerView autoSetDimension:ALDimensionHeight toSize:headerHeight], + [self.headerView autoPinLeadingToSuperviewMarginWithInset:self.conversationStyle.fullWidthGutterLeading], + [self.headerView autoPinTrailingToSuperviewMarginWithInset:self.conversationStyle.fullWidthGutterTrailing], + [self.headerView autoPinEdgeToSuperviewEdge:ALEdgeTop], + [self.messageBubbleView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.headerView], + ]]; + } else { + [self.viewConstraints addObjectsFromArray:@[ + [self.messageBubbleView autoPinEdgeToSuperviewEdge:ALEdgeTop], + ]]; + } if (self.isIncoming) { [self.viewConstraints addObjectsFromArray:@[ @@ -203,8 +209,6 @@ const CGFloat OWSMessageCellDateHeaderVMargin = 23; } } - [self updateDateHeader]; - if ([self updateAvatarView]) { CGFloat avatarBottomMargin = round(self.conversationStyle.lastTextLineAxis - self.avatarSize * 0.5f); [self.viewConstraints addObjectsFromArray:@[ @@ -251,40 +255,6 @@ const CGFloat OWSMessageCellDateHeaderVMargin = 23; } } -- (void)updateDateHeader -{ - OWSAssert(self.conversationStyle); - - if (self.viewItem.shouldShowDate) { - - self.dateHeaderLabel.font = self.dateHeaderFont; - self.dateHeaderLabel.textColor = self.conversationStyle.dateBreakTextColor; - - NSDate *date = self.viewItem.interaction.dateForSorting; - NSString *dateString = [DateUtil formatDateForConversationDateBreaks:date]; - self.dateHeaderLabel.text = dateString.localizedUppercaseString; - - [self.contentView addSubview:self.dateHeaderView]; - [self.viewConstraints addObjectsFromArray:@[ - [self.dateHeaderView - autoPinLeadingToSuperviewMarginWithInset:self.conversationStyle.fullWidthGutterLeading], - [self.dateHeaderView - autoPinTrailingToSuperviewMarginWithInset:self.conversationStyle.fullWidthGutterTrailing], - [self.dateHeaderView autoPinEdgeToSuperviewEdge:ALEdgeTop], - [self.messageBubbleView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderView], - ]]; - } else { - [self.viewConstraints addObjectsFromArray:@[ - [self.messageBubbleView autoPinEdgeToSuperviewEdge:ALEdgeTop], - ]]; - } -} - -- (UIFont *)dateHeaderFont -{ - return UIFont.ows_dynamicTypeCaption1Font; -} - #pragma mark - Avatar // Returns YES IFF the avatar view is appropriate and configured. @@ -376,7 +346,11 @@ const CGFloat OWSMessageCellDateHeaderVMargin = 23; OWSAssert(cellSize.width > 0 && cellSize.height > 0); - cellSize.height += self.dateHeaderHeight; + if (self.viewItem.hasCellHeader) { + cellSize.height += + [self.headerView measureWithConversationViewItem:self.viewItem conversationStyle:self.conversationStyle] + .height; + } if (self.shouldHaveSendFailureBadge) { cellSize.width += self.sendFailureBadgeSize + self.sendFailureBadgeSpacing; @@ -387,16 +361,6 @@ const CGFloat OWSMessageCellDateHeaderVMargin = 23; return cellSize; } -- (CGFloat)dateHeaderHeight -{ - if (self.viewItem.shouldShowDate) { - CGFloat textHeight = self.dateHeaderFont.lineHeight; - return (CGFloat)ceil(textHeight + OWSMessageCellDateHeaderVMargin); - } else { - return 0.f; - } -} - #pragma mark - Reuse - (void)prepareForReuse @@ -409,7 +373,7 @@ const CGFloat OWSMessageCellDateHeaderVMargin = 23; [self.messageBubbleView prepareForReuse]; [self.messageBubbleView unloadContent]; - [self.dateHeaderView removeFromSuperview]; + [self.headerView removeFromSuperview]; self.avatarView.image = nil; [self.avatarView removeFromSuperview]; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHeaderView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHeaderView.h new file mode 100644 index 000000000..797120d27 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHeaderView.h @@ -0,0 +1,22 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +extern const CGFloat OWSMessageHeaderViewDateHeaderVMargin; + +@class ConversationStyle; +@class ConversationViewItem; + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSMessageHeaderView : UIStackView + +- (void)loadForDisplayWithViewItem:(ConversationViewItem *)viewItem + conversationStyle:(ConversationStyle *)conversationStyle; + +- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem + conversationStyle:(ConversationStyle *)conversationStyle; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHeaderView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHeaderView.m new file mode 100644 index 000000000..dae87095a --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageHeaderView.m @@ -0,0 +1,183 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSMessageHeaderView.h" +#import "ConversationViewItem.h" +#import "Signal-Swift.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +const CGFloat OWSMessageHeaderViewDateHeaderVMargin = 23; + +@interface OWSMessageHeaderView () + +@property (nonatomic) UILabel *titleLabel; +@property (nonatomic) UILabel *subtitleLabel; +@property (nonatomic) UIView *strokeView; +@property (nonatomic) NSArray *layoutConstraints; +@property (nonatomic) UIStackView *stackView; + +@end + +#pragma mark - + +@implementation OWSMessageHeaderView + +// `[UIView init]` invokes `[self initWithFrame:...]`. +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [self commontInit]; + } + + return self; +} + +- (void)commontInit +{ + OWSAssert(!self.titleLabel); + + self.layoutMargins = UIEdgeInsetsZero; + + // Intercept touches. + // Date breaks and unread indicators are not interactive. + self.userInteractionEnabled = YES; + + self.strokeView = [UIView new]; + [self.strokeView setContentHuggingHigh]; + + self.titleLabel = [UILabel new]; + self.titleLabel.textColor = [UIColor ows_light90Color]; + self.titleLabel.textAlignment = NSTextAlignmentCenter; + self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; + + self.subtitleLabel = [UILabel new]; + self.subtitleLabel.textColor = [UIColor ows_light90Color]; + // The subtitle may wrap to a second line. + self.subtitleLabel.numberOfLines = 0; + self.subtitleLabel.lineBreakMode = NSLineBreakByWordWrapping; + self.subtitleLabel.textAlignment = NSTextAlignmentCenter; + + self.stackView = [[UIStackView alloc] initWithArrangedSubviews:@[ + self.strokeView, + self.titleLabel, + self.subtitleLabel, + ]]; + self.stackView.axis = NSTextLayoutOrientationVertical; + self.stackView.spacing = 2; + [self addSubview:self.stackView]; +} + +- (void)loadForDisplayWithViewItem:(ConversationViewItem *)viewItem + conversationStyle:(ConversationStyle *)conversationStyle +{ + OWSAssert(viewItem); + OWSAssert(conversationStyle); + + [self configureLabelsWithViewItem:viewItem]; + + CGFloat strokeThickness = [self strokeThicknessWithViewItem:viewItem]; + self.strokeView.layer.cornerRadius = strokeThickness * 0.5f; + self.strokeView.backgroundColor = [self strokeColorWithViewItem:viewItem]; + + self.subtitleLabel.hidden = self.subtitleLabel.text.length < 1; + + [NSLayoutConstraint deactivateConstraints:self.layoutConstraints]; + self.layoutConstraints = @[ + [self.strokeView autoSetDimension:ALDimensionHeight toSize:strokeThickness], + + [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTop], + [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:conversationStyle.fullWidthGutterLeading], + [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:conversationStyle.fullWidthGutterTrailing], + ]; +} + +- (CGFloat)strokeThicknessWithViewItem:(ConversationViewItem *)viewItem +{ + OWSAssert(viewItem); + + if (viewItem.unreadIndicator) { + return 4.f; + } else { + return 1.f; + } +} + +- (UIColor *)strokeColorWithViewItem:(ConversationViewItem *)viewItem +{ + OWSAssert(viewItem); + + if (viewItem.unreadIndicator) { + return UIColor.ows_light60Color; + } else { + return UIColor.ows_light45Color; + } +} + +- (void)configureLabelsWithViewItem:(ConversationViewItem *)viewItem +{ + OWSAssert(viewItem); + + NSDate *date = viewItem.interaction.dateForSorting; + NSString *dateString = [DateUtil formatDateForConversationDateBreaks:date].localizedUppercaseString; + + // Update cell to reflect changes in dynamic text. + if (viewItem.unreadIndicator) { + self.titleLabel.font = UIFont.ows_dynamicTypeCaption1Font.ows_mediumWeight; + + NSString *unreadTitle = NSLocalizedString( + @"MESSAGES_VIEW_UNREAD_INDICATOR", @"Indicator that separates read from unread messages."); + self.titleLabel.text = [[dateString rtlSafeAppend:@" • "] rtlSafeAppend:unreadTitle].localizedUppercaseString; + + if (!viewItem.unreadIndicator.hasMoreUnseenMessages) { + self.subtitleLabel.text = nil; + } else { + self.subtitleLabel.text = (viewItem.unreadIndicator.missingUnseenSafetyNumberChangeCount > 0 + ? NSLocalizedString(@"MESSAGES_VIEW_UNREAD_INDICATOR_HAS_MORE_UNSEEN_MESSAGES", + @"Messages that indicates that there are more unseen messages.") + : NSLocalizedString( + @"MESSAGES_VIEW_UNREAD_INDICATOR_HAS_MORE_UNSEEN_MESSAGES_AND_SAFETY_NUMBER_CHANGES", + @"Messages that indicates that there are more unseen messages including safety number " + @"changes.")); + } + } else { + self.titleLabel.font = UIFont.ows_dynamicTypeCaption1Font; + self.titleLabel.text = dateString; + self.subtitleLabel.text = nil; + } +} + +- (CGSize)measureWithConversationViewItem:(ConversationViewItem *)viewItem + conversationStyle:(ConversationStyle *)conversationStyle +{ + OWSAssert(viewItem); + OWSAssert(conversationStyle); + + [self configureLabelsWithViewItem:viewItem]; + + CGSize result = CGSizeMake(conversationStyle.viewWidth, 0); + + CGFloat strokeThickness = [self strokeThicknessWithViewItem:viewItem]; + result.height += strokeThickness; + + CGFloat maxTextWidth = conversationStyle.fullWidthContentWidth; + CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]; + result.height += titleSize.height + self.stackView.spacing; + + if (self.subtitleLabel.text.length > 0) { + CGSize subtitleSize = [self.subtitleLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]; + result.height += subtitleSize.height + self.stackView.spacing; + } + result.height += OWSMessageHeaderViewDateHeaderVMargin; + + return CGSizeCeil(result); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.h deleted file mode 100644 index e0f9b3af8..000000000 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import "ConversationViewCell.h" - -NS_ASSUME_NONNULL_BEGIN - -@class TSUnreadIndicatorInteraction; - -@interface OWSUnreadIndicatorCell : ConversationViewCell - -+ (NSString *)cellReuseIdentifier; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m deleted file mode 100644 index 9b9c9c066..000000000 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m +++ /dev/null @@ -1,169 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSUnreadIndicatorCell.h" -#import "ConversationViewItem.h" -#import "Signal-Swift.h" -#import -#import -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface OWSUnreadIndicatorCell () - -@property (nonatomic, nullable) TSUnreadIndicatorInteraction *interaction; - -@property (nonatomic) UILabel *titleLabel; -@property (nonatomic) UILabel *subtitleLabel; -@property (nonatomic) UIView *strokeView; -@property (nonatomic) NSArray *layoutConstraints; -@property (nonatomic) UIStackView *stackView; - -@end - -#pragma mark - - -@implementation OWSUnreadIndicatorCell - -// `[UIView init]` invokes `[self initWithFrame:...]`. -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - [self commontInit]; - } - - return self; -} - -- (void)commontInit -{ - OWSAssert(!self.titleLabel); - - self.layoutMargins = UIEdgeInsetsZero; - self.contentView.layoutMargins = UIEdgeInsetsZero; - - self.strokeView = [UIView new]; - self.strokeView.backgroundColor = [UIColor ows_darkSkyBlueColor]; - [self.strokeView autoSetDimension:ALDimensionHeight toSize:self.strokeThickness]; - self.strokeView.layer.cornerRadius = self.strokeThickness * 0.5f; - [self.strokeView setContentHuggingHigh]; - - self.titleLabel = [UILabel new]; - self.titleLabel.textColor = [UIColor ows_light90Color]; - self.titleLabel.textAlignment = NSTextAlignmentCenter; - - self.subtitleLabel = [UILabel new]; - self.subtitleLabel.textColor = [UIColor ows_light90Color]; - // The subtitle may wrap to a second line. - self.subtitleLabel.numberOfLines = 0; - self.subtitleLabel.lineBreakMode = NSLineBreakByWordWrapping; - self.subtitleLabel.textAlignment = NSTextAlignmentCenter; - - self.stackView = [[UIStackView alloc] initWithArrangedSubviews:@[ - self.strokeView, - self.titleLabel, - self.subtitleLabel, - ]]; - self.stackView.axis = NSTextLayoutOrientationVertical; - self.stackView.spacing = 2; - [self.contentView addSubview:self.stackView]; - - [self configureFonts]; -} - -- (void)configureFonts -{ - // Update cell to reflect changes in dynamic text. - self.titleLabel.font = UIFont.ows_dynamicTypeCaption1Font.ows_mediumWeight; - self.subtitleLabel.font = UIFont.ows_dynamicTypeCaption1Font; -} - -+ (NSString *)cellReuseIdentifier -{ - return NSStringFromClass([self class]); -} - -- (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction -{ - OWSAssert(self.conversationStyle); - OWSAssert(self.viewItem); - OWSAssert([self.viewItem.interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]); - - [self configureFonts]; - - TSUnreadIndicatorInteraction *interaction = (TSUnreadIndicatorInteraction *)self.viewItem.interaction; - - self.titleLabel.text = [self titleForInteraction:interaction]; - self.subtitleLabel.text = [self subtitleForInteraction:interaction]; - - self.subtitleLabel.hidden = self.subtitleLabel.text.length < 1; - - [NSLayoutConstraint deactivateConstraints:self.layoutConstraints]; - self.layoutConstraints = @[ - [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTop], - [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeBottom], - [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeLeading - withInset:self.conversationStyle.fullWidthGutterLeading], - [self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing - withInset:self.conversationStyle.fullWidthGutterTrailing], - ]; -} - -- (NSString *)titleForInteraction:(TSUnreadIndicatorInteraction *)interaction -{ - return NSLocalizedString(@"MESSAGES_VIEW_UNREAD_INDICATOR", @"Indicator that separates read from unread messages.") - .localizedUppercaseString; -} - -- (NSString *)subtitleForInteraction:(TSUnreadIndicatorInteraction *)interaction -{ - if (!interaction.hasMoreUnseenMessages) { - return nil; - } - return (interaction.missingUnseenSafetyNumberChangeCount > 0 - ? NSLocalizedString(@"MESSAGES_VIEW_UNREAD_INDICATOR_HAS_MORE_UNSEEN_MESSAGES", - @"Messages that indicates that there are more unseen messages.") - : NSLocalizedString(@"MESSAGES_VIEW_UNREAD_INDICATOR_HAS_MORE_UNSEEN_MESSAGES_AND_SAFETY_NUMBER_CHANGES", - @"Messages that indicates that there are more unseen messages including safety number changes.")); -} - -- (CGFloat)strokeThickness -{ - return 4.f; -} - -- (CGSize)cellSizeWithTransaction:(YapDatabaseReadTransaction *)transaction -{ - OWSAssert(self.conversationStyle); - OWSAssert(self.viewItem); - OWSAssert([self.viewItem.interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]); - - [self configureFonts]; - - CGSize result = CGSizeMake( - self.conversationStyle.fullWidthContentWidth, self.strokeThickness + self.titleLabel.font.lineHeight); - - TSUnreadIndicatorInteraction *interaction = (TSUnreadIndicatorInteraction *)self.viewItem.interaction; - self.subtitleLabel.text = [self subtitleForInteraction:interaction]; - if (self.subtitleLabel.text.length > 0) { - result.height += ceil( - [self.subtitleLabel sizeThatFits:CGSizeMake(self.conversationStyle.fullWidthContentWidth, CGFLOAT_MAX)] - .height); - } - - return CGSizeCeil(result); -} - -- (void)prepareForReuse -{ - [super prepareForReuse]; - - self.interaction = nil; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index ef8e6b0fd..2783c19fd 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -27,7 +27,6 @@ #import "OWSMath.h" #import "OWSMessageCell.h" #import "OWSSystemMessageCell.h" -#import "OWSUnreadIndicatorCell.h" #import "Signal-Swift.h" #import "SignalKeyingStorage.h" #import "TSAttachmentPointer.h" @@ -53,8 +52,8 @@ #import #import #import +#import #import -#import #import #import #import @@ -641,8 +640,6 @@ typedef enum : NSUInteger { { [self.collectionView registerClass:[OWSSystemMessageCell class] forCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]]; - [self.collectionView registerClass:[OWSUnreadIndicatorCell class] - forCellWithReuseIdentifier:[OWSUnreadIndicatorCell cellReuseIdentifier]]; [self.collectionView registerClass:[OWSContactOffersCell class] forCellWithReuseIdentifier:[OWSContactOffersCell cellReuseIdentifier]]; [self.collectionView registerClass:[OWSMessageCell class] @@ -1641,7 +1638,7 @@ typedef enum : NSUInteger { - (void)loadAnotherPageOfMessages { - BOOL hasEarlierUnseenMessages = self.dynamicInteractions.hasMoreUnseenMessages; + BOOL hasEarlierUnseenMessages = self.dynamicInteractions.unreadIndicator.hasMoreUnseenMessages; [self loadNMoreMessages:kYapDatabasePageSize]; @@ -1746,9 +1743,9 @@ typedef enum : NSUInteger { } } - if (self.dynamicInteractions.unreadIndicatorPosition) { + if (self.dynamicInteractions.unreadIndicator) { NSUInteger unreadIndicatorPosition - = (NSUInteger)[self.dynamicInteractions.unreadIndicatorPosition longValue]; + = (NSUInteger)self.dynamicInteractions.unreadIndicator.unreadIndicatorPosition; // If there is an unread indicator, increase the initial load window // to include it. @@ -2479,15 +2476,14 @@ typedef enum : NSUInteger { const int currentMaxRangeSize = (int)self.lastRangeLength; const int maxRangeSize = MAX(kConversationInitialMaxRangeSize, currentMaxRangeSize); - self.dynamicInteractions = - [ThreadUtil ensureDynamicInteractionsForThread:self.thread - contactsManager:self.contactsManager - blockingManager:self.blockingManager - dbConnection:self.editingDatabaseConnection - hideUnreadMessagesIndicator:self.hasClearedUnreadMessagesIndicator - firstUnseenInteractionTimestamp:self.dynamicInteractions.firstUnseenInteractionTimestamp - focusMessageId:self.focusMessageIdOnOpen - maxRangeSize:maxRangeSize]; + self.dynamicInteractions = [ThreadUtil ensureDynamicInteractionsForThread:self.thread + contactsManager:self.contactsManager + blockingManager:self.blockingManager + dbConnection:self.editingDatabaseConnection + hideUnreadMessagesIndicator:self.hasClearedUnreadMessagesIndicator + lastUnreadIndicator:self.dynamicInteractions.unreadIndicator + focusMessageId:self.focusMessageIdOnOpen + maxRangeSize:maxRangeSize]; } - (void)clearUnreadMessagesIndicator @@ -2504,7 +2500,7 @@ typedef enum : NSUInteger { // make sure we don't show it again. self.hasClearedUnreadMessagesIndicator = YES; - if (self.dynamicInteractions.unreadIndicatorPosition) { + if (self.dynamicInteractions.unreadIndicator) { // If we've just cleared the "unread messages" indicator, // update the dynamic interactions. [self ensureDynamicInteractions]; @@ -3793,18 +3789,10 @@ typedef enum : NSUInteger { - (void)cleanUpUnreadIndicatorIfNecessary { - BOOL hasUnreadIndicator = self.dynamicInteractions.unreadIndicatorPosition != nil; + BOOL hasUnreadIndicator = self.dynamicInteractions.unreadIndicator != nil; if (!hasUnreadIndicator) { return; } - __block BOOL hasUnseenInteractions = NO; - [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - hasUnseenInteractions = - [[transaction ext:TSUnreadDatabaseViewExtensionName] numberOfItemsInGroup:self.thread.uniqueId] > 0; - }]; - if (hasUnseenInteractions) { - return; - } // If the last unread message was deleted (manually or due to disappearing messages) // we may need to clean up an obsolete unread indicator. [self ensureDynamicInteractions]; @@ -3918,7 +3906,7 @@ typedef enum : NSUInteger { __block NSString *draft; [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - draft = [_thread currentDraftWithTransaction:transaction]; + draft = [self.thread currentDraftWithTransaction:transaction]; }]; [self.inputToolbar setMessageText:draft animated:NO]; } @@ -4790,6 +4778,7 @@ typedef enum : NSUInteger { // Update the properties of the view items. // // NOTE: This logic uses shouldShowDate which is set in the previous pass. + OWSUnreadIndicator *_Nullable unreadIndicator = self.dynamicInteractions.unreadIndicator; for (NSUInteger i = 0; i < viewItems.count; i++) { ConversationViewItem *viewItem = viewItems[i]; ConversationViewItem *_Nullable previousViewItem = (i > 0 ? viewItems[i - 1] : nil); @@ -4800,6 +4789,15 @@ typedef enum : NSUInteger { BOOL isLastInCluster = YES; NSAttributedString *_Nullable senderName = nil; + // Place the unread indicator onto the first appropriate view item, + // if any. + if (unreadIndicator && viewItem.interaction.timestampForSorting >= unreadIndicator.timestamp) { + viewItem.unreadIndicator = unreadIndicator; + unreadIndicator = nil; + } else { + viewItem.unreadIndicator = nil; + } + OWSInteractionType interactionType = viewItem.interaction.interactionType; NSString *timestampText = [DateUtil formatTimestampShort:viewItem.interaction.timestamp]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index cb0c4aa69..90d374089 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -29,6 +29,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @class DisplayableText; @class OWSAudioMessageView; @class OWSQuotedReplyModel; +@class OWSUnreadIndicator; @class TSAttachmentPointer; @class TSAttachmentStream; @class TSInteraction; @@ -53,6 +54,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @property (nonatomic, readonly) BOOL isQuotedReply; @property (nonatomic, readonly) BOOL hasQuotedAttachment; @property (nonatomic, readonly) BOOL hasQuotedText; +@property (nonatomic, readonly) BOOL hasCellHeader; @property (nonatomic, readonly) BOOL isExpiringMessage; @@ -63,6 +65,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @property (nonatomic) BOOL isFirstInCluster; @property (nonatomic) BOOL isLastInCluster; +@property (nonatomic, nullable) OWSUnreadIndicator *unreadIndicator; + @property (nonatomic, readonly) ConversationStyle *conversationStyle; - (instancetype)init NS_UNAVAILABLE; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index e84e4ce5f..7379479fe 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -6,11 +6,12 @@ #import "OWSAudioMessageView.h" #import "OWSContactOffersCell.h" #import "OWSMessageCell.h" +#import "OWSMessageHeaderView.h" #import "OWSSystemMessageCell.h" -#import "OWSUnreadIndicatorCell.h" #import "Signal-Swift.h" #import #import +#import #import #import @@ -149,6 +150,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return message.isExpiringMessage; } +- (BOOL)hasCellHeader +{ + return self.shouldShowDate || self.unreadIndicator; +} + - (void)setShouldShowDate:(BOOL)shouldShowDate { if (_shouldShowDate == shouldShowDate) { @@ -193,6 +199,17 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self clearCachedLayoutState]; } +- (void)setUnreadIndicator:(nullable OWSUnreadIndicator *)unreadIndicator +{ + if ([NSObject isNullableObject:_unreadIndicator equalTo:unreadIndicator]) { + return; + } + + _unreadIndicator = unreadIndicator; + + [self clearCachedLayoutState]; +} + - (void)clearCachedLayoutState { self.cachedCellSize = nil; @@ -245,8 +262,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) measurementCell = [OWSSystemMessageCell new]; break; case OWSInteractionType_UnreadIndicator: - measurementCell = [OWSUnreadIndicatorCell new]; - break; + OWSFail(@"%@ unexpected unread indicator.", self.logTag); + return nil; case OWSInteractionType_Offer: measurementCell = [OWSContactOffersCell new]; break; @@ -268,8 +285,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return 20.f; } - if (self.shouldShowDate) { - return OWSMessageCellDateHeaderVMargin; + if (self.hasCellHeader) { + return OWSMessageHeaderViewDateHeaderVMargin; } // "Bubble Collapse". Adjacent messages with the same author should be close together. @@ -310,8 +327,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier] forIndexPath:indexPath]; case OWSInteractionType_UnreadIndicator: - return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSUnreadIndicatorCell cellReuseIdentifier] - forIndexPath:indexPath]; + OWSFail(@"%@ unexpected unread indicator.", self.logTag); + return nil; case OWSInteractionType_Offer: return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSContactOffersCell cellReuseIdentifier] forIndexPath:indexPath]; diff --git a/SignalMessaging/Models/TSUnreadIndicatorInteraction.h b/SignalMessaging/Models/TSUnreadIndicatorInteraction.h index b9c6f50dd..e1972b783 100644 --- a/SignalMessaging/Models/TSUnreadIndicatorInteraction.h +++ b/SignalMessaging/Models/TSUnreadIndicatorInteraction.h @@ -6,22 +6,11 @@ NS_ASSUME_NONNULL_BEGIN +// This class is vestigial. @interface TSUnreadIndicatorInteraction : TSInteraction -@property (atomic, readonly) BOOL hasMoreUnseenMessages; - -@property (atomic, readonly) NSUInteger missingUnseenSafetyNumberChangeCount; - -- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE; - - (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; -- (instancetype)initUnreadIndicatorWithTimestamp:(uint64_t)timestamp - thread:(TSThread *)thread - hasMoreUnseenMessages:(BOOL)hasMoreUnseenMessages - missingUnseenSafetyNumberChangeCount:(NSUInteger)missingUnseenSafetyNumberChangeCount - NS_DESIGNATED_INITIALIZER; - @end NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/Models/TSUnreadIndicatorInteraction.m b/SignalMessaging/Models/TSUnreadIndicatorInteraction.m index aed872e47..acde4df42 100644 --- a/SignalMessaging/Models/TSUnreadIndicatorInteraction.m +++ b/SignalMessaging/Models/TSUnreadIndicatorInteraction.m @@ -6,14 +6,6 @@ NS_ASSUME_NONNULL_BEGIN -@interface TSUnreadIndicatorInteraction () - -@property (atomic) BOOL hasMoreUnseenMessages; - -@end - -#pragma mark - - @implementation TSUnreadIndicatorInteraction - (instancetype)initWithCoder:(NSCoder *)coder @@ -21,23 +13,6 @@ NS_ASSUME_NONNULL_BEGIN return [super initWithCoder:coder]; } -- (instancetype)initUnreadIndicatorWithTimestamp:(uint64_t)timestamp - thread:(TSThread *)thread - hasMoreUnseenMessages:(BOOL)hasMoreUnseenMessages - missingUnseenSafetyNumberChangeCount:(NSUInteger)missingUnseenSafetyNumberChangeCount -{ - self = [super initInteractionWithTimestamp:timestamp inThread:thread]; - - if (!self) { - return self; - } - - _hasMoreUnseenMessages = hasMoreUnseenMessages; - _missingUnseenSafetyNumberChangeCount = missingUnseenSafetyNumberChangeCount; - - return self; -} - - (BOOL)shouldUseReceiptDateForSorting { // Use the timestamp, not the "received at" timestamp to sort, diff --git a/SignalMessaging/SignalMessaging.h b/SignalMessaging/SignalMessaging.h index c8c35d0b4..1a6d3cdbd 100644 --- a/SignalMessaging/SignalMessaging.h +++ b/SignalMessaging/SignalMessaging.h @@ -40,7 +40,6 @@ FOUNDATION_EXPORT const unsigned char SignalMessagingVersionString[]; #import #import #import -#import #import #import #import diff --git a/SignalMessaging/utils/OWSUnreadIndicator.h b/SignalMessaging/utils/OWSUnreadIndicator.h new file mode 100644 index 000000000..5ce002456 --- /dev/null +++ b/SignalMessaging/utils/OWSUnreadIndicator.h @@ -0,0 +1,35 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSUnreadIndicator : NSObject + +@property (nonatomic, readonly) uint64_t timestamp; + +@property (nonatomic, readonly) BOOL hasMoreUnseenMessages; + +@property (nonatomic, readonly) NSUInteger missingUnseenSafetyNumberChangeCount; + +@property (nonatomic, readonly) uint64_t firstUnseenInteractionTimestamp; + +// The index of the unseen indicator, counting from the _end_ of the conversation +// history. +// +// This is used by MessageViewController to increase the +// range size of the mappings (the load window of the conversation) +// to include the unread indicator. +@property (nonatomic, readonly) NSInteger unreadIndicatorPosition; + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initUnreadIndicatorWithTimestamp:(uint64_t)timestamp + hasMoreUnseenMessages:(BOOL)hasMoreUnseenMessages + missingUnseenSafetyNumberChangeCount:(NSUInteger)missingUnseenSafetyNumberChangeCount + unreadIndicatorPosition:(NSInteger)unreadIndicatorPosition + firstUnseenInteractionTimestamp:(uint64_t)firstUnseenInteractionTimestamp NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/utils/OWSUnreadIndicator.m b/SignalMessaging/utils/OWSUnreadIndicator.m new file mode 100644 index 000000000..a62190245 --- /dev/null +++ b/SignalMessaging/utils/OWSUnreadIndicator.m @@ -0,0 +1,50 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSUnreadIndicator.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSUnreadIndicator + +- (instancetype)initUnreadIndicatorWithTimestamp:(uint64_t)timestamp + hasMoreUnseenMessages:(BOOL)hasMoreUnseenMessages + missingUnseenSafetyNumberChangeCount:(NSUInteger)missingUnseenSafetyNumberChangeCount + unreadIndicatorPosition:(NSInteger)unreadIndicatorPosition + firstUnseenInteractionTimestamp:(uint64_t)firstUnseenInteractionTimestamp +{ + self = [super init]; + + if (!self) { + return self; + } + + _timestamp = timestamp; + _hasMoreUnseenMessages = hasMoreUnseenMessages; + _missingUnseenSafetyNumberChangeCount = missingUnseenSafetyNumberChangeCount; + _unreadIndicatorPosition = unreadIndicatorPosition; + _firstUnseenInteractionTimestamp = firstUnseenInteractionTimestamp; + + return self; +} + +- (BOOL)isEqual:(id)object +{ + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[OWSUnreadIndicator class]]) { + return NO; + } + + OWSUnreadIndicator *other = object; + return (self.timestamp == other.timestamp && self.hasMoreUnseenMessages == other.hasMoreUnseenMessages + && self.missingUnseenSafetyNumberChangeCount == other.missingUnseenSafetyNumberChangeCount + && self.unreadIndicatorPosition == other.unreadIndicatorPosition); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/utils/ThreadUtil.h b/SignalMessaging/utils/ThreadUtil.h index dde4de03b..c2216697f 100644 --- a/SignalMessaging/utils/ThreadUtil.h +++ b/SignalMessaging/utils/ThreadUtil.h @@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN @class OWSBlockingManager; @class OWSContactsManager; @class OWSMessageSender; +@class OWSUnreadIndicator; @class SignalAttachment; @class TSContactThread; @class TSInteraction; @@ -16,15 +17,6 @@ NS_ASSUME_NONNULL_BEGIN @interface ThreadDynamicInteractions : NSObject -// If there are unseen messages in the thread, this is the index -// of the unseen indicator, counting from the _end_ of the conversation -// history. -// -// This is used by MessageViewController to increase the -// range size of the mappings (the load window of the conversation) -// to include the unread indicator. -@property (nonatomic, nullable, readonly) NSNumber *unreadIndicatorPosition; - // Represents the "reverse index" of the focus message, if any. // The "reverse index" is the distance of this interaction from // the last interaction in the thread. Therefore the last interaction @@ -34,18 +26,7 @@ NS_ASSUME_NONNULL_BEGIN // determine the initial load window size. @property (nonatomic, nullable, readonly) NSNumber *focusMessagePosition; -// If there are unseen messages in the thread, this is the timestamp -// of the oldest unseen message. -// -// Once we enter messages view, we mark all messages read, so we need -// a snapshot of what the first unread message was when we entered the -// view so that we can call ensureDynamicInteractionsForThread:... -// repeatedly. The unread indicator should continue to show up until -// it has been cleared, at which point hideUnreadMessagesIndicator is -// YES in ensureDynamicInteractionsForThread:... -@property (nonatomic, nullable, readonly) NSNumber *firstUnseenInteractionTimestamp; - -@property (nonatomic, readonly) BOOL hasMoreUnseenMessages; +@property (nonatomic, nullable, readonly) OWSUnreadIndicator *unreadIndicator; - (void)clearUnreadIndicatorState; @@ -113,7 +94,7 @@ NS_ASSUME_NONNULL_BEGIN blockingManager:(OWSBlockingManager *)blockingManager dbConnection:(YapDatabaseConnection *)dbConnection hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator - firstUnseenInteractionTimestamp:(nullable NSNumber *)firstUnseenInteractionTimestamp + lastUnreadIndicator:(nullable OWSUnreadIndicator *)lastUnreadIndicator focusMessageId:(nullable NSString *)focusMessageId maxRangeSize:(int)maxRangeSize; diff --git a/SignalMessaging/utils/ThreadUtil.m b/SignalMessaging/utils/ThreadUtil.m index 8603fd001..f0b1ce729 100644 --- a/SignalMessaging/utils/ThreadUtil.m +++ b/SignalMessaging/utils/ThreadUtil.m @@ -6,6 +6,7 @@ #import "OWSContactOffersInteraction.h" #import "OWSContactsManager.h" #import "OWSQuotedReplyModel.h" +#import "OWSUnreadIndicator.h" #import "TSUnreadIndicatorInteraction.h" #import #import @@ -29,13 +30,9 @@ NS_ASSUME_NONNULL_BEGIN @interface ThreadDynamicInteractions () -@property (nonatomic, nullable) NSNumber *unreadIndicatorPosition; - @property (nonatomic, nullable) NSNumber *focusMessagePosition; -@property (nonatomic, nullable) NSNumber *firstUnseenInteractionTimestamp; - -@property (nonatomic) BOOL hasMoreUnseenMessages; +@property (nonatomic, nullable) OWSUnreadIndicator *unreadIndicator; @end @@ -45,9 +42,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)clearUnreadIndicatorState { - self.unreadIndicatorPosition = nil; - self.firstUnseenInteractionTimestamp = nil; - self.hasMoreUnseenMessages = NO; + self.unreadIndicator = nil; } @end @@ -221,8 +216,7 @@ NS_ASSUME_NONNULL_BEGIN blockingManager:(OWSBlockingManager *)blockingManager dbConnection:(YapDatabaseConnection *)dbConnection hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator - firstUnseenInteractionTimestamp: - (nullable NSNumber *)firstUnseenInteractionTimestampParameter + lastUnreadIndicator:(nullable OWSUnreadIndicator *)lastUnreadIndicator focusMessageId:(nullable NSString *)focusMessageId maxRangeSize:(int)maxRangeSize { @@ -256,7 +250,6 @@ NS_ASSUME_NONNULL_BEGIN // Find any "dynamic" interactions and safety number changes. // // We use different views for performance reasons. - __block TSUnreadIndicatorInteraction *existingUnreadIndicator = nil; __block OWSContactOffersInteraction *existingContactOffers = nil; NSMutableArray *blockingSafetyNumberChanges = [NSMutableArray new]; NSMutableArray *nonBlockingSafetyNumberChanges = [NSMutableArray new]; @@ -280,13 +273,8 @@ NS_ASSUME_NONNULL_BEGIN // the OWSContactOffersInteraction. [interactionsToDelete addObject:object]; } else if ([object isKindOfClass:[TSUnreadIndicatorInteraction class]]) { - OWSAssert(!existingUnreadIndicator); - if (existingUnreadIndicator) { - // There should never be more than one unread indicator in - // a given thread, but if there is, discard all but one. - [interactionsToDelete addObject:existingUnreadIndicator]; - } - existingUnreadIndicator = (TSUnreadIndicatorInteraction *)object; + // Remove obsolete unread indicator interactions; + [interactionsToDelete addObject:object]; } else if ([object isKindOfClass:[OWSContactOffersInteraction class]]) { OWSAssert(!existingContactOffers); if (existingContactOffers) { @@ -318,13 +306,14 @@ NS_ASSUME_NONNULL_BEGIN // have been marked as read. // // IFF this variable is non-null, there are unseen messages in the thread. - if (firstUnseenInteractionTimestampParameter) { - result.firstUnseenInteractionTimestamp = firstUnseenInteractionTimestampParameter; + NSNumber *_Nullable firstUnseenInteractionTimestamp = nil; + if (lastUnreadIndicator) { + firstUnseenInteractionTimestamp = @(lastUnreadIndicator.firstUnseenInteractionTimestamp); } else { TSInteraction *_Nullable firstUnseenInteraction = [[TSDatabaseView unseenDatabaseViewExtension:transaction] firstObjectInGroup:thread.uniqueId]; if (firstUnseenInteraction) { - result.firstUnseenInteractionTimestamp = @(firstUnseenInteraction.timestampForSorting); + firstUnseenInteractionTimestamp = @(firstUnseenInteraction.timestampForSorting); } } @@ -481,14 +470,14 @@ NS_ASSUME_NONNULL_BEGIN } [self ensureUnreadIndicator:result - thread:thread - transaction:transaction - shouldHaveContactOffers:shouldHaveContactOffers - maxRangeSize:maxRangeSize - blockingSafetyNumberChanges:blockingSafetyNumberChanges - nonBlockingSafetyNumberChanges:nonBlockingSafetyNumberChanges - existingUnreadIndicator:existingUnreadIndicator - hideUnreadMessagesIndicator:hideUnreadMessagesIndicator]; + thread:thread + transaction:transaction + shouldHaveContactOffers:shouldHaveContactOffers + maxRangeSize:maxRangeSize + blockingSafetyNumberChanges:blockingSafetyNumberChanges + nonBlockingSafetyNumberChanges:nonBlockingSafetyNumberChanges + hideUnreadMessagesIndicator:hideUnreadMessagesIndicator + firstUnseenInteractionTimestamp:firstUnseenInteractionTimestamp]; // Determine the position of the focus message _after_ performing any mutations // around dynamic interactions. @@ -502,14 +491,14 @@ NS_ASSUME_NONNULL_BEGIN } + (void)ensureUnreadIndicator:(ThreadDynamicInteractions *)dynamicInteractions - thread:(TSThread *)thread - transaction:(YapDatabaseReadWriteTransaction *)transaction - shouldHaveContactOffers:(BOOL)shouldHaveContactOffers - maxRangeSize:(int)maxRangeSize - blockingSafetyNumberChanges:(NSArray *)blockingSafetyNumberChanges - nonBlockingSafetyNumberChanges:(NSArray *)nonBlockingSafetyNumberChanges - existingUnreadIndicator:(nullable TSUnreadIndicatorInteraction *)existingUnreadIndicator - hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator + thread:(TSThread *)thread + transaction:(YapDatabaseReadWriteTransaction *)transaction + shouldHaveContactOffers:(BOOL)shouldHaveContactOffers + maxRangeSize:(int)maxRangeSize + blockingSafetyNumberChanges:(NSArray *)blockingSafetyNumberChanges + nonBlockingSafetyNumberChanges:(NSArray *)nonBlockingSafetyNumberChanges + hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator + firstUnseenInteractionTimestamp:(nullable NSNumber *)firstUnseenInteractionTimestamp { OWSAssert(dynamicInteractions); OWSAssert(thread); @@ -517,6 +506,17 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(blockingSafetyNumberChanges); OWSAssert(nonBlockingSafetyNumberChanges); + if (hideUnreadMessagesIndicator) { + return; + } + if (!firstUnseenInteractionTimestamp) { + // If there are no unseen interactions, don't show an unread indicator. + return; + } + + YapDatabaseViewTransaction *threadMessagesTransaction = [transaction ext:TSMessageDatabaseViewExtensionName]; + OWSAssert([threadMessagesTransaction isKindOfClass:[YapDatabaseViewTransaction class]]); + // Determine unread indicator position, if necessary. // // Enumerate in reverse to count the number of messages @@ -527,137 +527,101 @@ NS_ASSUME_NONNULL_BEGIN // the unread indicator. __block long visibleUnseenMessageCount = 0; __block TSInteraction *interactionAfterUnreadIndicator = nil; - NSUInteger missingUnseenSafetyNumberChangeCount = 0; - if (dynamicInteractions.firstUnseenInteractionTimestamp != nil) { - [[transaction ext:TSMessageDatabaseViewExtensionName] - enumerateRowsInGroup:thread.uniqueId - withOptions:NSEnumerationReverse - usingBlock:^( - NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { - if (![object isKindOfClass:[TSInteraction class]]) { - OWSFail(@"Expected a TSInteraction: %@", [object class]); - return; - } - - TSInteraction *interaction = (TSInteraction *)object; - - if (interaction.isDynamicInteraction) { - // Ignore dynamic interactions, if any. - return; - } - - if (interaction.timestampForSorting - < dynamicInteractions.firstUnseenInteractionTimestamp.unsignedLongLongValue) { - // By default we want the unread indicator to appear just before - // the first unread message. - *stop = YES; - return; - } - - visibleUnseenMessageCount++; - - interactionAfterUnreadIndicator = interaction; - - if (visibleUnseenMessageCount + 1 >= maxRangeSize) { - // If there are more unseen messages than can be displayed in the - // messages view, show the unread indicator at the top of the - // displayed messages. - *stop = YES; - dynamicInteractions.hasMoreUnseenMessages = YES; - } - }]; - - if (!interactionAfterUnreadIndicator) { - // If we can't find an interaction after the unread indicator, - // remove it. All unread messages may have been deleted or - // expired. - dynamicInteractions.firstUnseenInteractionTimestamp = nil; - } else if (dynamicInteractions.hasMoreUnseenMessages) { - NSMutableSet *missingUnseenSafetyNumberChanges = [NSMutableSet set]; - for (TSInvalidIdentityKeyErrorMessage *safetyNumberChange in blockingSafetyNumberChanges) { - BOOL isUnseen = safetyNumberChange.timestampForSorting - >= dynamicInteractions.firstUnseenInteractionTimestamp.unsignedLongLongValue; - if (!isUnseen) { - continue; - } - BOOL isMissing - = safetyNumberChange.timestampForSorting < interactionAfterUnreadIndicator.timestampForSorting; - if (!isMissing) { - continue; - } - - NSData *_Nullable newIdentityKey = safetyNumberChange.newIdentityKey; - if (newIdentityKey == nil) { - OWSFail(@"Safety number change was missing it's new identity key."); - continue; - } - - [missingUnseenSafetyNumberChanges addObject:newIdentityKey]; - } - - // Count the de-duplicated "blocking" safety number changes and all - // of the "non-blocking" safety number changes. - missingUnseenSafetyNumberChangeCount - = (missingUnseenSafetyNumberChanges.count + nonBlockingSafetyNumberChanges.count); - } - } - if (dynamicInteractions.firstUnseenInteractionTimestamp) { - // The unread indicator is _before_ the last visible unseen message. - NSInteger unreadIndicatorPosition = visibleUnseenMessageCount + 1; - if (shouldHaveContactOffers) { - unreadIndicatorPosition++; - } - dynamicInteractions.unreadIndicatorPosition = @(unreadIndicatorPosition); + __block BOOL hasMoreUnseenMessages = NO; + [threadMessagesTransaction + enumerateRowsInGroup:thread.uniqueId + withOptions:NSEnumerationReverse + usingBlock:^( + NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) { + if (![object isKindOfClass:[TSInteraction class]]) { + OWSFail(@"Expected a TSInteraction: %@", [object class]); + return; + } + + TSInteraction *interaction = (TSInteraction *)object; + + if (interaction.isDynamicInteraction) { + // Ignore dynamic interactions, if any. + return; + } + + if (interaction.timestampForSorting < firstUnseenInteractionTimestamp.unsignedLongLongValue) { + // By default we want the unread indicator to appear just before + // the first unread message. + *stop = YES; + return; + } + + visibleUnseenMessageCount++; + + interactionAfterUnreadIndicator = interaction; + + if (visibleUnseenMessageCount + 1 >= maxRangeSize) { + // If there are more unseen messages than can be displayed in the + // messages view, show the unread indicator at the top of the + // displayed messages. + *stop = YES; + hasMoreUnseenMessages = YES; + } + }]; + + if (!interactionAfterUnreadIndicator) { + // If we can't find an interaction after the unread indicator, + // remove it. All unread messages may have been deleted or + // expired. + return; } - OWSAssert((dynamicInteractions.firstUnseenInteractionTimestamp != nil) - == (dynamicInteractions.unreadIndicatorPosition != nil)); + OWSAssert(visibleUnseenMessageCount > 0); - // Ensure unread indicator. - // - // We use this offset to control the ordering of the indicator. - const int kUnreadIndicatorOffset = -1; - NSUInteger threadMessageCount = - [[transaction ext:TSMessageDatabaseViewExtensionName] numberOfItemsInGroup:thread.uniqueId]; - BOOL shouldHaveUnreadIndicator - = (interactionAfterUnreadIndicator && !hideUnreadMessagesIndicator && threadMessageCount > 1); - if (!shouldHaveUnreadIndicator) { - if (existingUnreadIndicator) { - DDLogInfo(@"%@ Removing obsolete TSUnreadIndicatorInteraction: %@", - self.logTag, - existingUnreadIndicator.uniqueId); - [existingUnreadIndicator removeWithTransaction:transaction]; - } - } else { - // We want the unread indicator to appear just before the first unread incoming - // message in the conversation timeline... - // - // ...unless we have a fixed timestamp for the unread indicator. - uint64_t indicatorTimestamp - = (uint64_t)((long long)interactionAfterUnreadIndicator.timestampForSorting + kUnreadIndicatorOffset); - - if (indicatorTimestamp && existingUnreadIndicator.timestampForSorting == indicatorTimestamp) { - // Keep the existing indicator; it is in the correct position. - } else { - if (existingUnreadIndicator) { - DDLogInfo(@"%@ Removing TSUnreadIndicatorInteraction due to changed timestamp: %@", - self.logTag, - existingUnreadIndicator.uniqueId); - [existingUnreadIndicator removeWithTransaction:transaction]; + NSUInteger missingUnseenSafetyNumberChangeCount = 0; + if (hasMoreUnseenMessages) { + NSMutableSet *missingUnseenSafetyNumberChanges = [NSMutableSet set]; + for (TSInvalidIdentityKeyErrorMessage *safetyNumberChange in blockingSafetyNumberChanges) { + BOOL isUnseen + = safetyNumberChange.timestampForSorting >= firstUnseenInteractionTimestamp.unsignedLongLongValue; + if (!isUnseen) { + continue; + } + BOOL isMissing + = safetyNumberChange.timestampForSorting < interactionAfterUnreadIndicator.timestampForSorting; + if (!isMissing) { + continue; } - TSUnreadIndicatorInteraction *indicator = [[TSUnreadIndicatorInteraction alloc] - initUnreadIndicatorWithTimestamp:indicatorTimestamp - thread:thread - hasMoreUnseenMessages:dynamicInteractions.hasMoreUnseenMessages - missingUnseenSafetyNumberChangeCount:missingUnseenSafetyNumberChangeCount]; - [indicator saveWithTransaction:transaction]; + NSData *_Nullable newIdentityKey = safetyNumberChange.newIdentityKey; + if (newIdentityKey == nil) { + OWSFail(@"Safety number change was missing it's new identity key."); + continue; + } - DDLogInfo(@"%@ Creating TSUnreadIndicatorInteraction: %@ (%llu)", - self.logTag, - indicator.uniqueId, - indicator.timestampForSorting); + [missingUnseenSafetyNumberChanges addObject:newIdentityKey]; } + + // Count the de-duplicated "blocking" safety number changes and all + // of the "non-blocking" safety number changes. + missingUnseenSafetyNumberChangeCount + = (missingUnseenSafetyNumberChanges.count + nonBlockingSafetyNumberChanges.count); } + + // TODO: + NSInteger unreadIndicatorPosition = visibleUnseenMessageCount; + // TODO: + // if (dynamicInteractions.firstUnseenInteractionTimestamp) { + // // The unread indicator is _before_ the last visible unseen message. + // NSInteger unreadIndicatorPosition = visibleUnseenMessageCount + 1; + // if (shouldHaveContactOffers) { + // unreadIndicatorPosition++; + // } + // dynamicInteractions.unreadIndicatorPosition = @(unreadIndicatorPosition); + // } + + dynamicInteractions.unreadIndicator = [[OWSUnreadIndicator alloc] + initUnreadIndicatorWithTimestamp:interactionAfterUnreadIndicator.timestampForSorting + hasMoreUnseenMessages:hasMoreUnseenMessages + missingUnseenSafetyNumberChangeCount:missingUnseenSafetyNumberChangeCount + unreadIndicatorPosition:unreadIndicatorPosition + firstUnseenInteractionTimestamp:firstUnseenInteractionTimestamp.unsignedLongLongValue]; + DDLogInfo(@"%@ Creating TSUnreadIndicator: %llu", self.logTag, dynamicInteractions.unreadIndicator.timestamp); } + (nullable NSNumber *)focusMessagePositionForThread:(TSThread *)thread diff --git a/SignalServiceKit/src/Messages/Interactions/TSInteraction.h b/SignalServiceKit/src/Messages/Interactions/TSInteraction.h index 16d6bff2c..dc44f1461 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInteraction.h +++ b/SignalServiceKit/src/Messages/Interactions/TSInteraction.h @@ -15,6 +15,7 @@ typedef NS_ENUM(NSInteger, OWSInteractionType) { OWSInteractionType_Error, OWSInteractionType_Call, OWSInteractionType_Info, + // TODO: Obsolete, consider replacing with OWSInteractionType_Unknown. OWSInteractionType_UnreadIndicator, OWSInteractionType_Offer, };