Rework unread indicators.

pull/1/head
Matthew Chen 7 years ago
parent 1ce147e945
commit 8d72bb032e

@ -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 = "<group>"; };
3478506F1FDAEB16007B8332 /* OWSUserProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUserProfile.m; sourceTree = "<group>"; };
347850701FDAEB16007B8332 /* OWSUserProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUserProfile.h; sourceTree = "<group>"; };
348570A620F67574004FF32B /* OWSMessageHeaderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageHeaderView.m; sourceTree = "<group>"; };
348570A720F67574004FF32B /* OWSMessageHeaderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageHeaderView.h; sourceTree = "<group>"; };
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 = "<group>"; };
34B3F89D1E8DF5490035BE1A /* OWSTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSTableViewController.h; sourceTree = "<group>"; };
34B3F89E1E8DF5490035BE1A /* OWSTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSTableViewController.m; sourceTree = "<group>"; };
34B6D27220F664C800765BE2 /* OWSUnreadIndicator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUnreadIndicator.h; sourceTree = "<group>"; };
34B6D27320F664C800765BE2 /* OWSUnreadIndicator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUnreadIndicator.m; sourceTree = "<group>"; };
34BECE291F74C12700D7438D /* DebugUIStress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIStress.h; sourceTree = "<group>"; };
34BECE2A1F74C12700D7438D /* DebugUIStress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIStress.m; sourceTree = "<group>"; };
34BECE2D1F7ABCE000D7438D /* GifPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GifPickerViewController.swift; sourceTree = "<group>"; };
@ -857,8 +863,6 @@
34D1F0A21F867BFC0066283D /* OWSMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageCell.m; sourceTree = "<group>"; };
34D1F0A51F867BFC0066283D /* OWSSystemMessageCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSystemMessageCell.h; sourceTree = "<group>"; };
34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSystemMessageCell.m; sourceTree = "<group>"; };
34D1F0A71F867BFC0066283D /* OWSUnreadIndicatorCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUnreadIndicatorCell.h; sourceTree = "<group>"; };
34D1F0A81F867BFC0066283D /* OWSUnreadIndicatorCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUnreadIndicatorCell.m; sourceTree = "<group>"; };
34D1F0B21F86D31D0066283D /* ConversationCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConversationCollectionView.h; sourceTree = "<group>"; };
34D1F0B31F86D31D0066283D /* ConversationCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationCollectionView.m; sourceTree = "<group>"; };
34D1F0B51F87F8850066283D /* OWSGenericAttachmentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSGenericAttachmentView.h; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

@ -8,8 +8,6 @@
NS_ASSUME_NONNULL_BEGIN
extern const CGFloat OWSMessageCellDateHeaderVMargin;
@interface OWSMessageCell : ConversationViewCell
@property (nonatomic, readonly) OWSMessageBubbleView *messageBubbleView;

@ -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];

@ -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

@ -0,0 +1,183 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageHeaderView.h"
#import "ConversationViewItem.h"
#import "Signal-Swift.h"
#import <SignalMessaging/OWSUnreadIndicator.h>
#import <SignalMessaging/UIColor+OWS.h>
#import <SignalMessaging/UIFont+OWS.h>
#import <SignalMessaging/UIView+OWS.h>
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<NSLayoutConstraint *> *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

@ -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

@ -1,169 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSUnreadIndicatorCell.h"
#import "ConversationViewItem.h"
#import "Signal-Swift.h"
#import <SignalMessaging/TSUnreadIndicatorInteraction.h>
#import <SignalMessaging/UIColor+OWS.h>
#import <SignalMessaging/UIFont+OWS.h>
#import <SignalMessaging/UIView+OWS.h>
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<NSLayoutConstraint *> *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

@ -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 <SignalMessaging/OWSContactsManager.h>
#import <SignalMessaging/OWSFormat.h>
#import <SignalMessaging/OWSNavigationController.h>
#import <SignalMessaging/OWSUnreadIndicator.h>
#import <SignalMessaging/OWSUserProfile.h>
#import <SignalMessaging/TSUnreadIndicatorInteraction.h>
#import <SignalMessaging/ThreadUtil.h>
#import <SignalMessaging/UIUtil.h>
#import <SignalMessaging/UIViewController+OWS.h>
@ -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];

@ -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;

@ -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 <AssetsLibrary/AssetsLibrary.h>
#import <SignalMessaging/NSString+OWS.h>
#import <SignalMessaging/OWSUnreadIndicator.h>
#import <SignalServiceKit/OWSContact.h>
#import <SignalServiceKit/TSInteraction.h>
@ -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];

@ -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

@ -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,

@ -40,7 +40,6 @@ FOUNDATION_EXPORT const unsigned char SignalMessagingVersionString[];
#import <SignalMessaging/ScreenLockViewController.h>
#import <SignalMessaging/SharingThreadPickerViewController.h>
#import <SignalMessaging/SignalKeyingStorage.h>
#import <SignalMessaging/TSUnreadIndicatorInteraction.h>
#import <SignalMessaging/ThreadUtil.h>
#import <SignalMessaging/ThreadViewHelper.h>
#import <SignalMessaging/UIColor+OWS.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

@ -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

@ -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;

@ -6,6 +6,7 @@
#import "OWSContactOffersInteraction.h"
#import "OWSContactsManager.h"
#import "OWSQuotedReplyModel.h"
#import "OWSUnreadIndicator.h"
#import "TSUnreadIndicatorInteraction.h"
#import <SignalMessaging/OWSProfileManager.h>
#import <SignalMessaging/SignalMessaging-Swift.h>
@ -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<TSInvalidIdentityKeyErrorMessage *> *blockingSafetyNumberChanges = [NSMutableArray new];
NSMutableArray<TSInteraction *> *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<TSInvalidIdentityKeyErrorMessage *> *)blockingSafetyNumberChanges
nonBlockingSafetyNumberChanges:(NSArray<TSInteraction *> *)nonBlockingSafetyNumberChanges
existingUnreadIndicator:(nullable TSUnreadIndicatorInteraction *)existingUnreadIndicator
hideUnreadMessagesIndicator:(BOOL)hideUnreadMessagesIndicator
thread:(TSThread *)thread
transaction:(YapDatabaseReadWriteTransaction *)transaction
shouldHaveContactOffers:(BOOL)shouldHaveContactOffers
maxRangeSize:(int)maxRangeSize
blockingSafetyNumberChanges:(NSArray<TSInvalidIdentityKeyErrorMessage *> *)blockingSafetyNumberChanges
nonBlockingSafetyNumberChanges:(NSArray<TSInteraction *> *)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<NSData *> *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<NSData *> *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

@ -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,
};

Loading…
Cancel
Save