Restore the date headers to the conversation view cells.

// FREEBIE
This commit is contained in:
Matthew Chen 2017-10-11 14:45:02 -04:00
parent 603a7d2639
commit f7bd813c9f
11 changed files with 162 additions and 34 deletions

View File

@ -43,6 +43,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark -
// TODO: Consider making this a protocol.
@interface ConversationViewCell : UICollectionViewCell
@property (nonatomic, nullable, weak) id<ConversationViewCellDelegate> delegate;
@ -51,10 +52,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) BOOL isCellVisible;
// If this is non-null, we should show the message date header.
@property (nonatomic, nullable) NSAttributedString *messageDateHeaderText;
- (void)loadForDisplay;
- (void)loadForDisplay:(int)contentWidth;
- (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth;

View File

@ -14,12 +14,11 @@ NS_ASSUME_NONNULL_BEGIN
[super prepareForReuse];
self.viewItem = nil;
self.messageDateHeaderText = nil;
self.delegate = nil;
self.isCellVisible = NO;
}
- (void)loadForDisplay
- (void)loadForDisplay:(int)contentWidth
{
OWSFail(@"%@ This method should be overridden.", self.logTag);
}

View File

@ -87,7 +87,7 @@ NS_ASSUME_NONNULL_BEGIN
return NSStringFromClass([self class]);
}
- (void)loadForDisplay
- (void)loadForDisplay:(int)contentWidth
{
OWSAssert(self.viewItem);
OWSAssert([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]);

View File

@ -6,12 +6,12 @@
#import "AttachmentSharing.h"
#import "AttachmentUploadView.h"
#import "ConversationViewItem.h"
#import "NSAttributedString+OWS.h"
#import "OWSAudioMessageView.h"
#import "OWSGenericAttachmentView.h"
#import "UIColor+OWS.h"
//#import <AssetsLibrary/AssetsLibrary.h>
#import "Signal-Swift.h"
#import "UIColor+OWS.h"
#import <JSQMessagesViewController/JSQMessagesTimestampFormatter.h>
#import <JSQMessagesViewController/UIColor+JSQMessages.h>
//#import "OWSExpirationTimerView.h"
@ -21,6 +21,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageCell ()
// The text label is used so frequently that we always keep one around.
@property (nonatomic) UIView *payloadView;
@property (nonatomic) UILabel *dateHeaderLabel;
@property (nonatomic) UILabel *textLabel;
@property (nonatomic, nullable) UIImageView *bubbleImageView;
@property (nonatomic, nullable) AttachmentUploadView *attachmentUploadView;
@ -30,6 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, nullable) AttachmentPointerView *attachmentPointerView;
@property (nonatomic, nullable) OWSGenericAttachmentView *attachmentView;
@property (nonatomic, nullable) OWSAudioMessageView *audioMessageView;
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *dateHeaderConstraints;
@property (nonatomic, nullable) NSArray<NSLayoutConstraint *> *contentConstraints;
//@property (strong, nonatomic) OWSExpirationTimerView *expirationTimerView;
@ -58,10 +61,19 @@ NS_ASSUME_NONNULL_BEGIN
self.contentView.backgroundColor = [UIColor whiteColor];
self.payloadView = [UIView containerView];
[self.contentView addSubview:self.payloadView];
self.dateHeaderLabel = [UILabel new];
self.dateHeaderLabel.font = [UIFont ows_regularFontWithSize:16.f];
self.dateHeaderLabel.textAlignment = NSTextAlignmentCenter;
self.dateHeaderLabel.textColor = [UIColor lightGrayColor];
[self.contentView addSubview:self.dateHeaderLabel];
self.bubbleImageView = [UIImageView new];
self.bubbleImageView.layoutMargins = UIEdgeInsetsZero;
self.bubbleImageView.userInteractionEnabled = NO;
[self.contentView addSubview:self.bubbleImageView];
[self.payloadView addSubview:self.bubbleImageView];
[self.bubbleImageView autoPinToSuperviewEdges];
self.textLabel = [UILabel new];
@ -75,6 +87,12 @@ NS_ASSUME_NONNULL_BEGIN
// Hide these views by default.
self.bubbleImageView.hidden = YES;
self.textLabel.hidden = YES;
self.dateHeaderLabel.hidden = YES;
[self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.payloadView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderLabel];
[self.payloadView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[self.payloadView autoPinWidthToSuperview];
UITapGestureRecognizer *tap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
@ -122,7 +140,7 @@ NS_ASSUME_NONNULL_BEGIN
return self.viewItem.contentSize;
}
- (void)loadForDisplay
- (void)loadForDisplay:(int)contentWidth
{
OWSAssert(self.viewItem);
OWSAssert(self.viewItem.interaction);
@ -135,6 +153,8 @@ NS_ASSUME_NONNULL_BEGIN
= isIncoming ? [self.bubbleFactory incoming] : [self.bubbleFactory outgoing];
self.bubbleImageView.image = bubbleImageData.messageBubbleImage;
[self updateDateHeader:contentWidth];
switch (self.cellType) {
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
@ -178,6 +198,80 @@ NS_ASSUME_NONNULL_BEGIN
// });
}
- (void)updateDateHeader:(int)contentWidth
{
static NSDateFormatter *dateHeaderDateFormatter = nil;
static NSDateFormatter *dateHeaderTimeFormatter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dateHeaderDateFormatter = [NSDateFormatter new];
[dateHeaderDateFormatter setLocale:[NSLocale currentLocale]];
[dateHeaderDateFormatter setDoesRelativeDateFormatting:YES];
[dateHeaderDateFormatter setDateStyle:NSDateFormatterMediumStyle];
[dateHeaderDateFormatter setTimeStyle:NSDateFormatterNoStyle];
dateHeaderTimeFormatter = [NSDateFormatter new];
[dateHeaderTimeFormatter setLocale:[NSLocale currentLocale]];
[dateHeaderTimeFormatter setDoesRelativeDateFormatting:YES];
[dateHeaderTimeFormatter setDateStyle:NSDateFormatterNoStyle];
[dateHeaderTimeFormatter setTimeStyle:NSDateFormatterShortStyle];
});
if (self.viewItem.shouldShowDate) {
NSDate *date = self.viewItem.interaction.dateForSorting;
NSString *dateString = [dateHeaderDateFormatter stringFromDate:date];
NSString *timeString = [dateHeaderTimeFormatter stringFromDate:date];
NSAttributedString *attributedText = [NSAttributedString new];
attributedText = [attributedText rtlSafeAppend:dateString
attributes:@{
NSFontAttributeName : self.dateHeaderDateFont,
NSForegroundColorAttributeName : [UIColor lightGrayColor],
}
referenceView:self];
attributedText = [attributedText rtlSafeAppend:@" "
attributes:@{
NSFontAttributeName : self.dateHeaderDateFont,
}
referenceView:self];
attributedText = [attributedText rtlSafeAppend:timeString
attributes:@{
NSFontAttributeName : self.dateHeaderTimeFont,
NSForegroundColorAttributeName : [UIColor lightGrayColor],
}
referenceView:self];
self.dateHeaderLabel.attributedText = attributedText;
self.dateHeaderLabel.hidden = NO;
self.dateHeaderConstraints = @[
// Date headers should be visually centered within the conversation view,
// so they need to extend outside the cell's boundaries.
[self.dateHeaderLabel autoSetDimension:ALDimensionWidth toSize:contentWidth],
(self.isIncoming ? [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeLeading]
: [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing]),
[self.dateHeaderLabel autoSetDimension:ALDimensionHeight toSize:self.dateHeaderHeight],
];
} else {
self.dateHeaderLabel.hidden = YES;
self.dateHeaderConstraints = @[
[self.dateHeaderLabel autoSetDimension:ALDimensionHeight toSize:0],
];
}
}
- (UIFont *)dateHeaderDateFont
{
// TODO: Refine.
return [UIFont boldSystemFontOfSize:12.0f];
}
- (UIFont *)dateHeaderTimeFont
{
// TODO: Refine.
return [UIFont systemFontOfSize:12.0f];
}
- (void)loadForTextDisplay
{
self.bubbleImageView.hidden = NO;
@ -313,7 +407,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(view);
view.userInteractionEnabled = NO;
[self.contentView addSubview:view];
[self.payloadView addSubview:view];
self.contentConstraints = [view autoPinToSuperviewEdges];
[self cropViewToBubbbleShape:view];
if (self.isMediaBeingSent) {
@ -378,7 +472,7 @@ NS_ASSUME_NONNULL_BEGIN
self.customView = [UIView new];
self.customView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f];
self.customView.userInteractionEnabled = NO;
[self.contentView addSubview:self.customView];
[self.payloadView addSubview:self.customView];
self.contentConstraints = [self.customView autoPinToSuperviewEdges];
[self cropViewToBubbbleShape:self.customView];
}
@ -390,6 +484,7 @@ NS_ASSUME_NONNULL_BEGIN
const int maxMessageWidth = (int)floor(contentWidth * 0.7f);
CGSize cellSize = CGSizeZero;
switch (self.cellType) {
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage: {
@ -401,9 +496,9 @@ NS_ASSUME_NONNULL_BEGIN
self.textLabel.text = self.textMessage;
CGSize textSize = [self.textLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)];
CGSize result = CGSizeMake((CGFloat)ceil(textSize.width + leftMargin + rightMargin),
cellSize = CGSizeMake((CGFloat)ceil(textSize.width + leftMargin + rightMargin),
(CGFloat)ceil(textSize.height + textVMargin * 2));
return result;
break;
}
case OWSMessageCellType_StillImage:
case OWSMessageCellType_AnimatedImage:
@ -422,18 +517,34 @@ NS_ASSUME_NONNULL_BEGIN
mediaWidth = (CGFloat)round(maxMediaHeight * self.contentSize.width / self.contentSize.height);
mediaHeight = (CGFloat)round(maxMediaHeight);
}
CGSize result = CGSizeMake(mediaWidth, mediaHeight);
return result;
cellSize = CGSizeMake(mediaWidth, mediaHeight);
break;
}
case OWSMessageCellType_Audio:
return CGSizeMake(maxMessageWidth, OWSAudioMessageView.bubbleHeight);
cellSize = CGSizeMake(maxMessageWidth, OWSAudioMessageView.bubbleHeight);
break;
case OWSMessageCellType_GenericAttachment:
return CGSizeMake(maxMessageWidth, [OWSGenericAttachmentView bubbleHeight]);
cellSize = CGSizeMake(maxMessageWidth, [OWSGenericAttachmentView bubbleHeight]);
break;
case OWSMessageCellType_DownloadingAttachment:
return CGSizeMake(200, 90);
cellSize = CGSizeMake(200, 90);
break;
}
return CGSizeMake(maxMessageWidth, maxMessageWidth);
OWSAssert(cellSize.width > 0 && cellSize.height > 0);
cellSize.height += self.dateHeaderHeight;
return cellSize;
}
- (CGFloat)dateHeaderHeight
{
if (self.viewItem.shouldShowDate) {
return MAX(self.dateHeaderDateFont.lineHeight, self.dateHeaderTimeFont.lineHeight);
} else {
return 0.f;
}
}
- (BOOL)isIncoming
@ -489,8 +600,12 @@ NS_ASSUME_NONNULL_BEGIN
[NSLayoutConstraint deactivateConstraints:self.contentConstraints];
self.contentConstraints = nil;
[NSLayoutConstraint deactivateConstraints:self.dateHeaderConstraints];
self.dateHeaderConstraints = nil;
// The text label is used so frequently that we always keep one around.
self.dateHeaderLabel.text = nil;
self.dateHeaderLabel.hidden = YES;
self.textLabel.text = nil;
self.textLabel.hidden = YES;
self.bubbleImageView.image = nil;

View File

@ -85,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN
return NSStringFromClass([self class]);
}
- (void)loadForDisplay
- (void)loadForDisplay:(int)contentWidth
{
OWSAssert(self.viewItem);

View File

@ -84,7 +84,7 @@ NS_ASSUME_NONNULL_BEGIN
return NSStringFromClass([self class]);
}
- (void)loadForDisplay
- (void)loadForDisplay:(int)contentWidth
{
OWSAssert(self.viewItem);
OWSAssert([self.viewItem.interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]);

View File

@ -156,6 +156,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
@property (nonatomic, readonly) ConversationInputToolbar *inputToolbar;
@property (nonatomic, readonly) ConversationCollectionView *collectionView;
@property (nonatomic, readonly) ConversationViewLayout *layout;
@property (nonatomic) NSArray<ConversationViewItem *> *viewItems;
@property (nonatomic) NSMutableDictionary<NSString *, ConversationViewItem *> *viewItemMap;
@ -462,11 +463,12 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
- (void)createContents
{
ConversationViewLayout *layout = [ConversationViewLayout new];
layout.delegate = self;
_layout = [ConversationViewLayout new];
self.layout.delegate = self;
// We use the root view bounds as the initial frame for the collection
// view so that its contents can be laid out immediately.
_collectionView = [[ConversationCollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];
_collectionView =
[[ConversationCollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.layout];
self.collectionView.layoutDelegate = self;
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
@ -3921,13 +3923,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
cell.viewItem = viewItem;
cell.delegate = self;
// TODO: Could we move this inside the cell base class?
if (viewItem.shouldShowDate) {
cell.messageDateHeaderText = [[JSQMessagesTimestampFormatter sharedFormatter]
attributedTimestampForDate:viewItem.interaction.dateForSorting];
}
[cell loadForDisplay];
[cell loadForDisplay:self.layout.contentWidth];
return cell;

View File

@ -36,6 +36,8 @@ typedef NS_ENUM(NSInteger, ConversationViewLayoutAlignment) {
@property (nonatomic, weak) id<ConversationViewLayoutDelegate> delegate;
@property (nonatomic, readonly) int contentWidth;
@end
NS_ASSUME_NONNULL_END

View File

@ -20,6 +20,8 @@ NS_ASSUME_NONNULL_BEGIN
// unnecessary layout pass.
@property (nonatomic) BOOL hasLayout;
@property (nonatomic) int contentWidth;
@end
#pragma mark -
@ -85,6 +87,7 @@ NS_ASSUME_NONNULL_BEGIN
const int vSpacing = 5;
const int viewWidth = (int)floor(self.collectionView.bounds.size.width);
const int contentWidth = (int)floor(viewWidth - 2 * hInset);
self.contentWidth = contentWidth;
NSArray<id<ConversationViewLayoutItem>> *layoutItems = self.delegate.layoutItems;

View File

@ -6,6 +6,9 @@ NS_ASSUME_NONNULL_BEGIN
@interface NSAttributedString (OWS)
- (NSAttributedString *)rtlSafeAppend:(NSString *)text
attributes:(NSDictionary *)attributes
referenceView:(UIView *)referenceView;
- (NSAttributedString *)rtlSafeAppend:(NSAttributedString *)string referenceView:(UIView *)referenceView;
@end

View File

@ -9,6 +9,18 @@ NS_ASSUME_NONNULL_BEGIN
@implementation NSAttributedString (OWS)
- (NSAttributedString *)rtlSafeAppend:(NSString *)text
attributes:(NSDictionary *)attributes
referenceView:(UIView *)referenceView
{
OWSAssert(text);
OWSAssert(attributes);
OWSAssert(referenceView);
NSAttributedString *substring = [[NSAttributedString alloc] initWithString:text attributes:attributes];
return [self rtlSafeAppend:substring referenceView:referenceView];
}
- (NSAttributedString *)rtlSafeAppend:(NSAttributedString *)string referenceView:(UIView *)referenceView
{
OWSAssert(string);