Merge branch 'charlesmchen/conversationCellPerf'

This commit is contained in:
Matthew Chen 2017-10-18 12:13:33 -07:00
commit 7d3df0bf0a
25 changed files with 289 additions and 165 deletions

View File

@ -5,6 +5,7 @@
#import "ContactsViewHelper.h"
#import "ContactTableViewCell.h"
#import "Environment.h"
#import "NSString+OWS.h"
#import "OWSProfileManager.h"
#import "Signal-Swift.h"
#import <SignalServiceKit/Contact.h>
@ -205,7 +206,7 @@ NS_ASSUME_NONNULL_BEGIN
- (NSArray<NSString *> *)searchTermsForSearchString:(NSString *)searchText
{
return [[[searchText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]
return [[[searchText ows_stripped]
componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]
filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *_Nullable searchTerm,
NSDictionary<NSString *, id> *_Nullable bindings) {

View File

@ -43,6 +43,10 @@ NS_ASSUME_NONNULL_BEGIN
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId;
#pragma mark - Caching
- (NSCache *)cellMediaCache;
@end
#pragma mark -
@ -54,9 +58,22 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, nullable) ConversationViewItem *viewItem;
// Cells are prefetched but expensive cells (e.g. media) should only load
// when visible and unload when no longer visible. Non-visible cells can
// cache their contents on their ConversationViewItem, but that cache may
// be evacuated before the cell becomes visible again.
//
// ConversationViewController also uses this property to evacuate the cell's
// meda views when:
//
// * App enters background.
// * Users enters another view (e.g. conversation settings view, call screen, etc.).
@property (nonatomic) BOOL isCellVisible;
- (void)loadForDisplay:(int)contentWidth;
// The width of the collection view.
@property (nonatomic) int contentWidth;
- (void)loadForDisplay;
- (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth;

View File

@ -16,9 +16,10 @@ NS_ASSUME_NONNULL_BEGIN
self.viewItem = nil;
self.delegate = nil;
self.isCellVisible = NO;
self.contentWidth = 0;
}
- (void)loadForDisplay:(int)contentWidth
- (void)loadForDisplay
{
OWSFail(@"%@ This method should be overridden.", self.logTag);
}

View File

@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN
isIncoming:(BOOL)isIncoming
viewItem:(ConversationViewItem *)viewItem;
- (void)createContentsForSize:(CGSize)viewSize;
- (void)createContents;
+ (CGFloat)bubbleHeight;

View File

@ -193,34 +193,45 @@ NS_ASSUME_NONNULL_BEGIN
return (self.attachmentStream.isVoiceMessage || self.attachmentStream.sourceFilename.length < 1);
}
- (void)createContentsForSize:(CGSize)viewSize
- (void)createContents
{
UIColor *textColor = [self audioTextColor];
self.backgroundColor = self.bubbleBackgroundColor;
self.layoutMargins = UIEdgeInsetsZero;
// TODO: Verify that this layout works in RTL.
const CGFloat kBubbleTailWidth = 6.f;
CGRect contentFrame = CGRectMake(self.isIncoming ? kBubbleTailWidth : 0.f,
self.audioIconVMargin,
viewSize.width - kBubbleTailWidth - self.audioIconHMargin,
viewSize.height - self.audioIconVMargin * 2);
CGRect iconFrame = CGRectMake((CGFloat)round(contentFrame.origin.x + self.audioIconHMargin),
(CGFloat)round(contentFrame.origin.y + (contentFrame.size.height - self.iconSize) * 0.5f),
self.iconSize,
self.iconSize);
_audioPlayPauseButton = [[UIButton alloc] initWithFrame:iconFrame];
_audioPlayPauseButton.enabled = NO;
[self addSubview:_audioPlayPauseButton];
UIView *contentView = [UIView containerView];
[self addSubview:contentView];
[contentView autoPinLeadingToSuperviewWithMargin:self.isIncoming ? kBubbleTailWidth : 0.f];
[contentView autoPinTrailingToSuperviewWithMargin:self.isIncoming ? 0.f : kBubbleTailWidth];
[contentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.audioIconVMargin];
[contentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.audioIconVMargin];
_audioPlayPauseButton = [UIButton buttonWithType:UIButtonTypeCustom];
self.audioPlayPauseButton.enabled = NO;
[contentView addSubview:self.audioPlayPauseButton];
[self.audioPlayPauseButton autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.audioIconHMargin];
[self.audioPlayPauseButton autoVCenterInSuperview];
[self.audioPlayPauseButton autoSetDimension:ALDimensionWidth toSize:self.iconSize];
[self.audioPlayPauseButton autoSetDimension:ALDimensionHeight toSize:self.iconSize];
const CGFloat kLabelHSpacing = self.audioIconHSpacing;
UIView *labelsView = [UIView containerView];
[contentView addSubview:labelsView];
[labelsView autoPinLeadingToTrailingOfView:self.audioPlayPauseButton margin:kLabelHSpacing];
[labelsView autoPinEdgeToSuperviewEdge:ALEdgeRight];
[labelsView autoVCenterInSuperview];
const CGFloat kLabelVSpacing = 2;
NSString *filename = self.attachmentStream.sourceFilename;
if (!filename) {
filename = [[self.attachmentStream filePath] lastPathComponent];
}
NSString *topText = [[filename stringByDeletingPathExtension]
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *topText = [[filename stringByDeletingPathExtension] ows_stripped];
if (topText.length < 1) {
topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachmentStream.contentType].uppercaseString;
}
@ -235,13 +246,18 @@ NS_ASSUME_NONNULL_BEGIN
topLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
[topLabel sizeToFit];
[self addSubview:topLabel];
[labelsView addSubview:topLabel];
[topLabel autoPinEdgeToSuperviewEdge:ALEdgeTop];
[topLabel autoPinWidthToSuperview];
const CGFloat kAudioProgressViewHeight = 12.f;
AudioProgressView *audioProgressView = [AudioProgressView new];
self.audioProgressView = audioProgressView;
[self updateAudioProgressView];
[self addSubview:audioProgressView];
[labelsView addSubview:audioProgressView];
[audioProgressView autoPinWidthToSuperview];
[audioProgressView autoSetDimension:ALDimensionHeight toSize:kAudioProgressViewHeight];
[audioProgressView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:topLabel withOffset:kLabelVSpacing];
UILabel *bottomLabel = [UILabel new];
self.audioBottomLabel = bottomLabel;
@ -249,25 +265,10 @@ NS_ASSUME_NONNULL_BEGIN
bottomLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
bottomLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
[bottomLabel sizeToFit];
[self addSubview:bottomLabel];
const CGFloat topLabelHeight = (CGFloat)ceil(topLabel.font.lineHeight);
const CGFloat kAudioProgressViewHeight = 12.f;
const CGFloat bottomLabelHeight = (CGFloat)ceil(bottomLabel.font.lineHeight);
CGRect labelsBounds = CGRectZero;
labelsBounds.origin.x = (CGFloat)round(iconFrame.origin.x + iconFrame.size.width + kLabelHSpacing);
labelsBounds.size.width = contentFrame.origin.x + contentFrame.size.width - labelsBounds.origin.x;
labelsBounds.size.height = topLabelHeight + kAudioProgressViewHeight + bottomLabelHeight + kLabelVSpacing * 2;
labelsBounds.origin.y
= (CGFloat)round(contentFrame.origin.y + (contentFrame.size.height - labelsBounds.size.height) * 0.5f);
CGFloat y = labelsBounds.origin.y;
topLabel.frame = CGRectMake(labelsBounds.origin.x, labelsBounds.origin.y, labelsBounds.size.width, topLabelHeight);
y += topLabelHeight + kLabelVSpacing;
audioProgressView.frame = CGRectMake(labelsBounds.origin.x, y, labelsBounds.size.width, kAudioProgressViewHeight);
y += kAudioProgressViewHeight + kLabelVSpacing;
bottomLabel.frame = CGRectMake(labelsBounds.origin.x, y, labelsBounds.size.width, bottomLabelHeight);
[labelsView addSubview:bottomLabel];
[bottomLabel autoPinWidthToSuperview];
[bottomLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:audioProgressView withOffset:kLabelVSpacing];
[bottomLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[self updateContents];
}

View File

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

View File

@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithAttachment:(TSAttachmentStream *)attachmentStream isIncoming:(BOOL)isIncoming;
- (void)createContentsForSize:(CGSize)viewSize;
- (void)createContents;
+ (CGFloat)bubbleHeight;

View File

@ -3,6 +3,7 @@
//
#import "OWSGenericAttachmentView.h"
#import "NSString+OWS.h"
#import "UIColor+JSQMessages.h"
#import "UIColor+OWS.h"
#import "UIFont+OWS.h"
@ -98,32 +99,43 @@ NS_ASSUME_NONNULL_BEGIN
return [self.textColor blendWithColor:self.bubbleBackgroundColor alpha:alpha];
}
- (void)createContentsForSize:(CGSize)viewSize
- (void)createContents
{
UIColor *textColor = (self.isIncoming ? [UIColor colorWithWhite:0.2 alpha:1.f] : [UIColor whiteColor]);
self.backgroundColor = self.bubbleBackgroundColor;
self.layoutMargins = UIEdgeInsetsZero;
// TODO: Verify that this layout works in RTL.
const CGFloat kBubbleTailWidth = 6.f;
CGRect contentFrame = CGRectMake(self.isIncoming ? kBubbleTailWidth : 0.f,
self.vMargin,
viewSize.width - kBubbleTailWidth - self.iconHMargin,
viewSize.height - self.vMargin * 2);
UIView *contentView = [UIView containerView];
[self addSubview:contentView];
[contentView autoPinLeadingToSuperviewWithMargin:self.isIncoming ? kBubbleTailWidth : 0.f];
[contentView autoPinTrailingToSuperviewWithMargin:self.isIncoming ? 0.f : kBubbleTailWidth];
[contentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.vMargin];
[contentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.vMargin];
UIImage *image = [UIImage imageNamed:@"generic-attachment-small"];
OWSAssert(image);
UIImageView *imageView = [UIImageView new];
CGRect iconFrame = CGRectMake(round(contentFrame.origin.x + self.iconHMargin),
round(contentFrame.origin.y + (contentFrame.size.height - self.iconSize) * 0.5f),
self.iconSize,
self.iconSize);
imageView.frame = iconFrame;
imageView.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
imageView.tintColor = self.bubbleBackgroundColor;
imageView.backgroundColor
= (self.isIncoming ? [UIColor colorWithRGBHex:0x9e9e9e] : [self foregroundColorWithOpacity:0.15f]);
imageView.layer.cornerRadius = MIN(imageView.bounds.size.width, imageView.bounds.size.height) * 0.5f;
[self addSubview:imageView];
[contentView addSubview:imageView];
[imageView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.iconHMargin];
[imageView autoVCenterInSuperview];
[imageView autoSetDimension:ALDimensionWidth toSize:self.iconSize];
[imageView autoSetDimension:ALDimensionHeight toSize:self.iconSize];
const CGFloat kLabelHSpacing = self.iconHSpacing;
UIView *labelsView = [UIView containerView];
[contentView addSubview:labelsView];
[labelsView autoPinLeadingToTrailingOfView:imageView margin:kLabelHSpacing];
[labelsView autoPinEdgeToSuperviewEdge:ALEdgeRight];
[labelsView autoVCenterInSuperview];
NSString *filename = self.attachmentStream.sourceFilename;
if (!filename) {
@ -145,22 +157,13 @@ NS_ASSUME_NONNULL_BEGIN
fileTypeLabel.font = [UIFont ows_mediumFontWithSize:20.f];
fileTypeLabel.adjustsFontSizeToFitWidth = YES;
fileTypeLabel.textAlignment = NSTextAlignmentCenter;
CGRect fileTypeLabelFrame = CGRectZero;
fileTypeLabelFrame.size = [fileTypeLabel sizeThatFits:CGSizeZero];
// This dimension depends on the space within the icon boundaries.
fileTypeLabelFrame.size.width = 15.f;
// Center on icon.
fileTypeLabelFrame.origin.x
= round(iconFrame.origin.x + (iconFrame.size.width - fileTypeLabelFrame.size.width) * 0.5f);
fileTypeLabelFrame.origin.y
= round(iconFrame.origin.y + (iconFrame.size.height - fileTypeLabelFrame.size.height) * 0.5f);
fileTypeLabel.frame = fileTypeLabelFrame;
[self addSubview:fileTypeLabel];
[imageView addSubview:fileTypeLabel];
[imageView autoCenterInSuperview];
[imageView autoSetDimension:ALDimensionWidth toSize:15.f];
const CGFloat kLabelHSpacing = self.iconHSpacing;
const CGFloat kLabelVSpacing = 2;
NSString *topText =
[self.attachmentStream.sourceFilename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSString *topText = [self.attachmentStream.sourceFilename ows_stripped];
if (topText.length < 1) {
topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachmentStream.contentType].uppercaseString;
}
@ -172,8 +175,9 @@ NS_ASSUME_NONNULL_BEGIN
topLabel.textColor = textColor;
topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(13.f, 15.f)];
[topLabel sizeToFit];
[self addSubview:topLabel];
[labelsView addSubview:topLabel];
[topLabel autoPinEdgeToSuperviewEdge:ALEdgeTop];
[topLabel autoPinWidthToSuperview];
NSError *error;
unsigned long long fileSize =
@ -185,21 +189,10 @@ NS_ASSUME_NONNULL_BEGIN
bottomLabel.textColor = [textColor colorWithAlphaComponent:0.85f];
bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
bottomLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)];
[bottomLabel sizeToFit];
[self addSubview:bottomLabel];
CGRect topLabelFrame = CGRectZero;
topLabelFrame.size = topLabel.bounds.size;
topLabelFrame.origin.x = round(iconFrame.origin.x + iconFrame.size.width + kLabelHSpacing);
topLabelFrame.origin.y = round(contentFrame.origin.y
+ (contentFrame.size.height - (topLabel.frame.size.height + bottomLabel.frame.size.height + kLabelVSpacing))
* 0.5f);
topLabelFrame.size.width = round((contentFrame.origin.x + contentFrame.size.width) - topLabelFrame.origin.x);
topLabel.frame = topLabelFrame;
CGRect bottomLabelFrame = topLabelFrame;
bottomLabelFrame.origin.y += topLabelFrame.size.height + kLabelVSpacing;
bottomLabel.frame = bottomLabelFrame;
[labelsView addSubview:bottomLabel];
[bottomLabel autoPinWidthToSuperview];
[bottomLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:topLabel withOffset:kLabelVSpacing];
[bottomLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom];
}
@end

View File

@ -207,7 +207,7 @@ NS_ASSUME_NONNULL_BEGIN
return (TSMessage *)self.viewItem.interaction;
}
- (void)loadForDisplay:(int)contentWidth
- (void)loadForDisplay
{
OWSAssert(self.viewItem);
OWSAssert(self.viewItem.interaction);
@ -220,7 +220,7 @@ NS_ASSUME_NONNULL_BEGIN
= isIncoming ? [self.bubbleFactory incoming] : [self.bubbleFactory outgoing];
self.bubbleImageView.image = bubbleImageData.messageBubbleImage;
[self updateDateHeader:contentWidth];
[self updateDateHeader];
[self updateFooter];
switch (self.cellType) {
@ -243,7 +243,7 @@ NS_ASSUME_NONNULL_BEGIN
case OWSMessageCellType_GenericAttachment: {
self.attachmentView =
[[OWSGenericAttachmentView alloc] initWithAttachment:self.attachmentStream isIncoming:self.isIncoming];
[self.attachmentView createContentsForSize:self.bounds.size];
[self.attachmentView createContents];
[self replaceBubbleWithView:self.attachmentView];
[self addAttachmentUploadViewIfNecessary:self.attachmentView];
break;
@ -254,6 +254,8 @@ NS_ASSUME_NONNULL_BEGIN
}
}
[self ensureViewMediaState];
// dispatch_async(dispatch_get_main_queue(), ^{
// NSLog(@"---- %@", self.viewItem.interaction.debugDescription);
// NSLog(@"cell: %@", NSStringFromCGRect(self.frame));
@ -263,8 +265,113 @@ NS_ASSUME_NONNULL_BEGIN
// });
}
- (void)updateDateHeader:(int)contentWidth
// We now eagerly create out view hierarchy (to do this exactly once per cell usage)
// but lazy-load any expensive media (photo, gif, etc.) used in those views. Note that
// this lazy-load can fail, in which case we modify the view hierarchy to use an "error"
// state. The didCellMediaFailToLoad reflects media load fails.
- (nullable id)tryToLoadCellMedia:(nullable id (^)())loadCellMediaBlock
mediaView:(UIView *)mediaView
cacheKey:(NSString *)cacheKey
{
OWSAssert(self.attachmentStream);
OWSAssert(mediaView);
OWSAssert(cacheKey);
if (self.viewItem.didCellMediaFailToLoad) {
return nil;
}
NSCache *cellMediaCache = self.delegate.cellMediaCache;
OWSAssert(cellMediaCache);
id _Nullable cellMedia = [cellMediaCache objectForKey:cacheKey];
if (cellMedia) {
DDLogVerbose(@"%@ cell media cache hit", self.logTag);
return cellMedia;
}
cellMedia = loadCellMediaBlock();
if (cellMedia) {
DDLogVerbose(@"%@ cell media cache miss", self.logTag);
[cellMediaCache setObject:cellMedia forKey:cacheKey];
} else {
DDLogError(@"%@ Failed to load cell media: %@", [self logTag], [self.attachmentStream mediaURL]);
self.viewItem.didCellMediaFailToLoad = YES;
[mediaView removeFromSuperview];
// TODO: We need to hide/remove the media view.
[self showAttachmentErrorView];
}
return cellMedia;
}
// We want to lazy-load expensive view contents and eagerly unload if the
// cell is no longer visible.
- (void)ensureViewMediaState
{
if (!self.isCellVisible) {
// Eagerly unload.
self.stillImageView.image = nil;
self.animatedImageView.image = nil;
return;
}
switch (self.cellType) {
case OWSMessageCellType_StillImage: {
if (self.stillImageView.image) {
return;
}
self.stillImageView.image = [self tryToLoadCellMedia:^{
OWSAssert([self.attachmentStream isImage]);
return self.attachmentStream.image;
}
mediaView:self.stillImageView
cacheKey:self.attachmentStream.uniqueId];
break;
}
case OWSMessageCellType_AnimatedImage: {
if (self.animatedImageView.image) {
return;
}
self.animatedImageView.image = [self tryToLoadCellMedia:^{
OWSAssert([self.attachmentStream isAnimated]);
NSString *_Nullable filePath = [self.attachmentStream filePath];
YYImage *_Nullable animatedImage = nil;
if (filePath && [NSData ows_isValidImageAtPath:filePath]) {
animatedImage = [YYImage imageWithContentsOfFile:filePath];
}
return animatedImage;
}
mediaView:self.animatedImageView
cacheKey:self.attachmentStream.uniqueId];
break;
}
case OWSMessageCellType_Video: {
if (self.stillImageView.image) {
return;
}
self.stillImageView.image = [self tryToLoadCellMedia:^{
OWSAssert([self.attachmentStream isVideo]);
return self.attachmentStream.image;
}
mediaView:self.stillImageView
cacheKey:self.attachmentStream.uniqueId];
break;
}
case OWSMessageCellType_TextMessage:
case OWSMessageCellType_OversizeTextMessage:
case OWSMessageCellType_GenericAttachment:
case OWSMessageCellType_DownloadingAttachment:
case OWSMessageCellType_Audio:
// Inexpensive cell types don't need to lazy-load or eagerly-unload.
break;
}
}
- (void)updateDateHeader
{
OWSAssert(self.contentWidth > 0);
static NSDateFormatter *dateHeaderDateFormatter = nil;
static NSDateFormatter *dateHeaderTimeFormatter = nil;
static dispatch_once_t onceToken;
@ -312,7 +419,7 @@ NS_ASSUME_NONNULL_BEGIN
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.dateHeaderLabel autoSetDimension:ALDimensionWidth toSize:self.contentWidth],
(self.isIncoming ? [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeLeading]
: [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing]),
[self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTop],
@ -452,14 +559,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(self.attachmentStream);
OWSAssert([self.attachmentStream isImage]);
UIImage *_Nullable image = self.attachmentStream.image;
if (!image) {
DDLogError(@"%@ Could not load image: %@", [self logTag], [self.attachmentStream mediaURL]);
[self showAttachmentErrorView];
return;
}
self.stillImageView = [[UIImageView alloc] initWithImage:image];
self.stillImageView = [UIImageView new];
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
self.stillImageView.contentMode = UIViewContentModeScaleAspectFill;
@ -476,19 +576,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(self.attachmentStream);
OWSAssert([self.attachmentStream isAnimated]);
NSString *_Nullable filePath = [self.attachmentStream filePath];
YYImage *_Nullable animatedImage = nil;
if (filePath && [NSData ows_isValidImageAtPath:filePath]) {
animatedImage = [YYImage imageWithContentsOfFile:filePath];
}
if (!animatedImage) {
DDLogError(@"%@ Could not load animated image: %@", [self logTag], [self.attachmentStream mediaURL]);
[self showAttachmentErrorView];
return;
}
self.animatedImageView = [[YYAnimatedImageView alloc] init];
self.animatedImageView.image = animatedImage;
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
self.animatedImageView.contentMode = UIViewContentModeScaleAspectFill;
@ -505,7 +593,7 @@ NS_ASSUME_NONNULL_BEGIN
isIncoming:self.isIncoming
viewItem:self.viewItem];
self.viewItem.lastAudioMessageView = self.audioMessageView;
[self.audioMessageView createContentsForSize:self.bounds.size];
[self.audioMessageView createContents];
[self replaceBubbleWithView:self.audioMessageView];
[self addAttachmentUploadViewIfNecessary:self.audioMessageView];
}
@ -515,16 +603,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(self.attachmentStream);
OWSAssert([self.attachmentStream isVideo]);
// CGSize size = [self mediaViewDisplaySize];
UIImage *_Nullable image = self.attachmentStream.image;
if (!image) {
DDLogError(@"%@ Could not load image: %@", [self logTag], [self.attachmentStream mediaURL]);
[self showAttachmentErrorView];
return;
}
self.stillImageView = [[UIImageView alloc] initWithImage:image];
self.stillImageView = [UIImageView new];
// We need to specify a contentMode since the size of the image
// might not match the aspect ratio of the view.
self.stillImageView.contentMode = UIViewContentModeScaleAspectFill;
@ -614,25 +693,11 @@ NS_ASSUME_NONNULL_BEGIN
[self.payloadView updateMask];
}
//// TODO:
//- (void)setFrame:(CGRect)frame {
// [super setFrame:frame];
//
// DDLogError(@"setFrame: %@ %@ %@", self.viewItem.interaction.uniqueId, self.viewItem.interaction.description,
// NSStringFromCGRect(frame));
//}
//
//// TODO:
//- (void)setBounds:(CGRect)bounds {
// [super setBounds:bounds];
//
// DDLogError(@"setBounds: %@ %@ %@", self.viewItem.interaction.uniqueId, self.viewItem.interaction.description,
// NSStringFromCGRect(bounds));
//}
- (void)showAttachmentErrorView
{
// TODO: We could do a better job of indicating that the image could not be loaded.
OWSAssert(!self.customView);
// TODO: We could do a better job of indicating that the media could not be loaded.
self.customView = [UIView new];
self.customView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f];
self.customView.userInteractionEnabled = NO;
@ -860,6 +925,8 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
[self ensureViewMediaState];
if (isCellVisible) {
if (self.message.shouldStartExpireTimer) {
[self.expirationTimerView ensureAnimations];

View File

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

View File

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

View File

@ -57,6 +57,36 @@ NS_ASSUME_NONNULL_BEGIN
[super setContentOffset:contentOffset];
}
- (void)reloadData
{
DDLogVerbose(@"%@ reloadData", self.logTag);
[super reloadData];
}
- (void)reloadSections:(NSIndexSet *)sections
{
DDLogVerbose(@"%@ reloadSections", self.logTag);
[super reloadSections:sections];
}
- (void)reloadItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths
{
DDLogVerbose(@"%@ reloadItemsAtIndexPaths", self.logTag);
[super reloadItemsAtIndexPaths:indexPaths];
}
#pragma mark - Logging
+ (NSString *)logTag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)logTag
{
return self.class.logTag;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -3,6 +3,7 @@
//
#import "ConversationInputTextView.h"
#import "NSString+OWS.h"
#import "Signal-Swift.h"
NS_ASSUME_NONNULL_BEGIN
@ -121,7 +122,7 @@ NS_ASSUME_NONNULL_BEGIN
- (NSString *)trimmedText
{
return [self.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
return [self.text ows_stripped];
}
// TODO:

View File

@ -20,6 +20,7 @@
#import "FingerprintViewController.h"
#import "FullImageViewController.h"
#import "NSAttributedString+OWS.h"
#import "NSString+OWS.h"
#import "NewGroupViewController.h"
#import "OWSAudioAttachmentPlayer.h"
#import "OWSContactOffersCell.h"
@ -165,7 +166,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
@property (nonatomic, nullable) OWSAudioAttachmentPlayer *audioAttachmentPlayer;
@property (nonatomic, nullable) NSUUID *voiceMessageUUID;
@property (nonatomic) NSTimer *readTimer;
@property (nonatomic, nullable) NSTimer *readTimer;
@property (nonatomic) NSCache *cellMediaCache;
@property (nonatomic) ConversationHeaderView *navigationBarTitleView;
@property (nonatomic) UILabel *navigationBarTitleLabel;
@property (nonatomic) UILabel *navigationBarSubtitleLabel;
@ -392,6 +394,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
_isGroupConversation = [self.thread isKindOfClass:[TSGroupThread class]];
_composeOnOpen = keyboardOnViewAppearing;
_callOnOpen = callOnViewAppearing;
_cellMediaCache = [NSCache new];
// Cache the cell media for ~24 cells.
self.cellMediaCache.countLimit = 24;
[self.uiDatabaseConnection beginLongLivedReadTransaction];
@ -526,6 +531,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
self.hasClearedUnreadMessagesIndicator = NO;
[self.dynamicInteractions clearUnreadIndicatorState];
}
[self.cellMediaCache removeAllObjects];
}
- (void)applicationWillResignActive:(NSNotification *)notification
@ -943,6 +949,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
- (void)cancelReadTimer
{
[self.readTimer invalidate];
self.readTimer = nil;
}
- (void)viewDidAppear:(BOOL)animated
@ -986,6 +993,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
[self saveDraft];
[self markVisibleMessagesAsRead];
[self cancelVoiceMemo];
[self.cellMediaCache removeAllObjects];
self.isUserScrolling = NO;
}
@ -3630,7 +3638,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
return;
}
text = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
text = [text ows_stripped];
if (text.length < 1) {
return;
@ -4025,8 +4033,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
}
cell.viewItem = viewItem;
cell.delegate = self;
cell.contentWidth = self.layout.contentWidth;
[cell loadForDisplay:self.layout.contentWidth];
[cell loadForDisplay];
return cell;
}

View File

@ -86,6 +86,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
- (nullable TSAttachmentPointer *)attachmentPointer;
- (CGSize)contentSize;
// We don't want to try to load the media for this item (if any)
// if a load has previously failed.
@property (nonatomic) BOOL didCellMediaFailToLoad;
// TODO:
//// Cells will request that this adapter clear its cached media views,
//// but the adapter should only honor requests from the last cell to

View File

@ -3,6 +3,7 @@
//
#import "ConversationViewItem.h"
#import "NSString+OWS.h"
#import "OWSAudioMessageView.h"
#import "OWSContactOffersCell.h"
#import "OWSMessageCell.h"
@ -274,14 +275,12 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if (!displayableText) {
// Only show up to 2kb of text.
const NSUInteger kMaxTextDisplayLength = 2 * 1024;
text = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
text = [text ows_stripped];
displayableText = [[DisplayableTextFilter new] displayableText:text];
if (displayableText.length > kMaxTextDisplayLength) {
// Trim whitespace before _AND_ after slicing the snipper from the string.
NSString *snippet =
[[[displayableText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]
substringWithRange:NSMakeRange(0, kMaxTextDisplayLength)]
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *snippet = [
[[displayableText ows_stripped] substringWithRange:NSMakeRange(0, kMaxTextDisplayLength)] ows_stripped];
displayableText = [NSString stringWithFormat:NSLocalizedString(@"OVERSIZE_TEXT_DISPLAY_FORMAT",
@"A display format for oversize text messages."),
snippet];

View File

@ -3,6 +3,7 @@
//
#import "CountryCodeViewController.h"
#import "NSString+OWS.h"
#import "PhoneNumberUtil.h"
#import "UIColor+OWS.h"
#import "UIFont+OWS.h"
@ -142,8 +143,7 @@
- (void)searchTextDidChange
{
NSString *searchText =
[self.searchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *searchText = [self.searchBar.text ows_stripped];
self.countryCodes = [PhoneNumberUtil countryCodesForSearchTerm:searchText];

View File

@ -9,6 +9,7 @@
#import "ContactTableViewCell.h"
#import "ContactsViewHelper.h"
#import "Environment.h"
#import "NSString+OWS.h"
#import "OWSContactsManager.h"
#import "OWSNavigationController.h"
#import "OWSTableViewController.h"
@ -515,8 +516,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
- (TSGroupModel *)makeGroup
{
NSString *groupName = [self.groupNameTextField.text
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *groupName = [self.groupNameTextField.text ows_stripped];
NSMutableArray<NSString *> *recipientIds = [self.memberRecipientIds.allObjects mutableCopy];
[recipientIds addObject:[self.contactsViewHelper localNumber]];
NSData *groupId = [SecurityUtils generateRandomBytes:16];

View File

@ -6,6 +6,7 @@
#import "AppDelegate.h"
#import "AvatarViewHelper.h"
#import "HomeViewController.h"
#import "NSString+OWS.h"
#import "OWSNavigationController.h"
#import "OWSProfileManager.h"
#import "Signal-Swift.h"
@ -410,7 +411,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
- (NSString *)normalizedProfileName
{
return [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
return [self.nameTextField.text ows_stripped];
}
- (void)updateProfileCompleted

View File

@ -6,6 +6,7 @@
#import "CodeVerificationViewController.h"
#import "CountryCodeViewController.h"
#import "Environment.h"
#import "NSString+OWS.h"
#import "PhoneNumber.h"
#import "PhoneNumberUtil.h"
#import "Signal-Swift.h"
@ -280,8 +281,7 @@ NSString *const kKeychainKey_LastRegisteredPhoneNumber = @"kKeychainKey_LastRegi
- (void)sendCodeAction
{
NSString *phoneNumberText =
[_phoneNumberTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *phoneNumberText = [_phoneNumberTextField.text ows_stripped];
if (phoneNumberText.length < 1) {
[OWSAlerts
showAlertWithTitle:NSLocalizedString(@"REGISTRATION_VIEW_NO_PHONE_NUMBER_ALERT_TITLE",

View File

@ -7,6 +7,7 @@
#import "ContactTableViewCell.h"
#import "ContactsViewHelper.h"
#import "Environment.h"
#import "NSString+OWS.h"
#import "OWSContactsManager.h"
#import "OWSContactsSearcher.h"
#import "OWSTableViewController.h"
@ -223,8 +224,7 @@ NS_ASSUME_NONNULL_BEGIN
{
NSArray<TSThread *> *threads = self.threadViewHelper.threads;
NSString *searchTerm =
[[self.searchBar text] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *searchTerm = [[self.searchBar text] ows_stripped];
if ([searchTerm isEqualToString:@""]) {
return threads;

View File

@ -4,6 +4,7 @@
#import "SendExternalFileViewController.h"
#import "Environment.h"
#import "NSString+OWS.h"
#import "Signal-Swift.h"
#import "ThreadUtil.h"
#import "UIColor+OWS.h"
@ -141,8 +142,7 @@ NS_ASSUME_NONNULL_BEGIN
// TODO: If we reuse this VC, for example to offer a "forward attachment to other thread",
// feature, this assumption would no longer apply.
OWSAssert(self.attachment);
NSString *filename =
[self.attachment.sourceFilename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSString *filename = [self.attachment.sourceFilename ows_stripped];
OWSAssert(filename.length > 0);
const NSUInteger kMaxFilenameLength = 20;
if (filename.length > kMaxFilenameLength) {

View File

@ -9,6 +9,7 @@
#import "ContactTableViewCell.h"
#import "ContactsViewHelper.h"
#import "Environment.h"
#import "NSString+OWS.h"
#import "OWSContactsManager.h"
#import "OWSNavigationController.h"
#import "OWSTableViewController.h"
@ -196,8 +197,7 @@ NS_ASSUME_NONNULL_BEGIN
UITextField *groupNameTextField = [UITextField new];
_groupNameTextField = groupNameTextField;
self.groupNameTextField.text = [self.thread.groupModel.groupName
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
self.groupNameTextField.text = [self.thread.groupModel.groupName ows_stripped];
groupNameTextField.textColor = [UIColor blackColor];
groupNameTextField.font = [UIFont ows_dynamicTypeTitle2Font];
groupNameTextField.placeholder
@ -373,8 +373,7 @@ NS_ASSUME_NONNULL_BEGIN
{
OWSAssert(self.conversationSettingsViewDelegate);
NSString *groupName = [self.groupNameTextField.text
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *groupName = [self.groupNameTextField.text ows_stripped];
TSGroupModel *groupModel = [[TSGroupModel alloc] initWithTitle:groupName
memberIds:[self.memberRecipientIds.allObjects mutableCopy]
image:self.groupAvatar

View File

@ -3,6 +3,7 @@
//
#import "OWSContactsSearcher.h"
#import "NSString+OWS.h"
#import <SignalServiceKit/PhoneNumber.h>
@interface OWSContactsSearcher ()
@ -22,7 +23,7 @@
}
- (NSArray<Contact *> *)filterWithString:(NSString *)string {
NSString *searchTerm = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *searchTerm = [string ows_stripped];
if ([searchTerm isEqualToString:@""]) {
return self.contacts;

View File

@ -4,6 +4,7 @@
#import "NotificationsManager.h"
#import "Environment.h"
#import "NSString+OWS.h"
#import "OWSContactsManager.h"
#import "OWSPreferences.h"
#import "PushManager.h"
@ -272,8 +273,7 @@ NSString *const kNotificationsManagerNewMesssageSoundName = @"NewMessage.aifc";
BOOL shouldPlaySound = [self shouldPlaySoundForNotification];
NSString *senderName = [contactsManager displayNameForPhoneIdentifier:message.authorId];
NSString *groupName =
[thread.name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *groupName = [thread.name ows_stripped];
if (groupName.length < 1) {
groupName = [MessageStrings newGroupDefaultTitle];
}