mirror of https://github.com/oxen-io/session-ios
Merge branch 'charlesmchen/reworkUnreadIndicator'
commit
04da18c7fd
@ -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
|
@ -0,0 +1,43 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
// 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, 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
|
Loading…
Reference in New Issue