Signal-iOS/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageStickerView.m
2020-09-17 21:13:14 -03:00

546 lines
17 KiB
Objective-C

//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageStickerView.h"
#import "OWSLabel.h"
#import "OWSMessageBubbleView.h"
#import "OWSMessageFooterView.h"
#import "Signal-Swift.h"
#import <SignalMessaging/UIView+OWS.h>
#import <YYImage/YYImage.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageStickerView ()
@property (nonatomic) UIStackView *stackView;
@property (nonatomic) UILabel *senderNameLabel;
@property (nonatomic) UIView *senderNameContainer;
@property (nonatomic, nullable) UIView *bodyMediaView;
// Should lazy-load expensive view contents (images, etc.).
// Should do nothing if view is already loaded.
@property (nonatomic, nullable) dispatch_block_t loadCellContentBlock;
// Should unload all expensive view contents (images, etc.).
@property (nonatomic, nullable) dispatch_block_t unloadCellContentBlock;
@property (nonatomic, nullable) NSMutableArray<NSLayoutConstraint *> *viewConstraints;
@property (nonatomic) OWSMessageFooterView *footerView;
@end
#pragma mark -
@implementation OWSMessageStickerView
- (instancetype)init
{
self = [super initWithFrame:CGRectZero];
if (!self) {
return self;
}
[self commonInit];
return self;
}
- (void)commonInit
{
// Ensure only called once.
OWSAssertDebug(!self.stackView);
_viewConstraints = [NSMutableArray new];
self.layoutMargins = UIEdgeInsetsZero;
self.userInteractionEnabled = YES;
self.stackView = [UIStackView new];
self.stackView.axis = UILayoutConstraintAxisVertical;
self.stackView.alignment = UIStackViewAlignmentFill;
// NOTE: we don't use bottom margin or spacing.
CGFloat margin = OWSMessageStickerView.marginAndSpacing;
self.stackView.layoutMargins = UIEdgeInsetsMake(margin, margin, 0, margin);
self.stackView.layoutMarginsRelativeArrangement = YES;
self.senderNameLabel = [OWSLabel new];
self.senderNameContainer = [UIView new];
self.senderNameContainer.layoutMargins = UIEdgeInsetsMake(0, 0, self.senderNameBottomSpacing, 0);
[self.senderNameContainer addSubview:self.senderNameLabel];
[self.senderNameLabel autoPinEdgesToSuperviewMargins];
self.footerView = [OWSMessageFooterView new];
}
+ (CGFloat)marginAndSpacing
{
return 8;
}
#pragma mark - Convenience Accessors
- (TSMessage *)message
{
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
return (TSMessage *)self.viewItem.interaction;
}
- (BOOL)isIncoming
{
return self.viewItem.interaction.interactionType == OWSInteractionType_IncomingMessage;
}
#pragma mark - Load
- (BOOL)isBorderless
{
return YES;
}
- (void)configureViews
{
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.viewItem);
OWSAssertDebug(self.viewItem.interaction);
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
OWSAssertDebug(self.viewItem.stickerAttachment != nil || self.viewItem.isFailedSticker);
CGSize bodyMediaSize = [self bodyMediaSize];
[self addSubview:self.stackView];
[self.viewConstraints addObjectsFromArray:[self.stackView autoPinEdgesToSuperviewEdges]];
if (self.shouldShowSenderName) {
[self configureSenderNameLabel];
[self insertAnyTextViewsIntoStackView:@[
self.senderNameContainer,
]];
}
UIView *bodyMediaView = [self loadStickerView];
OWSAssertDebug(self.loadCellContentBlock);
OWSAssertDebug(self.unloadCellContentBlock);
bodyMediaView.clipsToBounds = YES;
self.bodyMediaView = bodyMediaView;
bodyMediaView.userInteractionEnabled = NO;
// Wrap the sticker view - otherwise, a long header or footer
// will horizontally stretch the sticker content.
UIStackView *bodyViewWrapper = [[UIStackView alloc] initWithArrangedSubviews:@[ bodyMediaView ]];
bodyViewWrapper.axis = UILayoutConstraintAxisVertical;
bodyViewWrapper.alignment = UIStackViewAlignmentLeading;
[self.stackView addArrangedSubview:bodyViewWrapper];
[self.footerView configureWithConversationViewItem:self.viewItem
conversationStyle:self.conversationStyle
isIncoming:self.isIncoming
isOverlayingMedia:NO
isOutsideBubble:YES];
[self insertAnyTextViewsIntoStackView:@[
self.footerView,
]];
CGSize bubbleSize = [self measureSize];
[self.viewConstraints addObjectsFromArray:@[
[self autoSetDimension:ALDimensionWidth toSize:bubbleSize.width],
[bodyMediaView autoSetDimension:ALDimensionWidth toSize:bodyMediaSize.width],
[bodyMediaView autoSetDimension:ALDimensionHeight toSize:bodyMediaSize.height],
[bodyViewWrapper autoSetDimension:ALDimensionHeight toSize:bodyMediaSize.height],
]];
}
- (CGFloat)senderNameBottomSpacing
{
return 2.f;
}
- (BOOL)insertAnyTextViewsIntoStackView:(NSArray<UIView *> *)textViews
{
if (textViews.count < 1) {
return NO;
}
UIStackView *textStackView = [[UIStackView alloc] initWithArrangedSubviews:textViews];
textStackView.axis = UILayoutConstraintAxisVertical;
textStackView.spacing = self.textViewVSpacing;
textStackView.layoutMarginsRelativeArrangement = YES;
textStackView.layoutMargins = UIEdgeInsetsMake(self.conversationStyle.textInsetTop,
OWSMessageStickerView.textInsetHorizontal,
self.conversationStyle.textInsetBottom,
OWSMessageStickerView.textInsetHorizontal);
[self.stackView addArrangedSubview:textStackView];
return YES;
}
+ (CGFloat)textInsetHorizontal
{
return 0.f;
}
- (CGFloat)textViewVSpacing
{
return 2.f;
}
#pragma mark - Load / Unload
- (void)loadContent
{
if (self.loadCellContentBlock) {
self.loadCellContentBlock();
}
}
- (void)unloadContent
{
if (self.unloadCellContentBlock) {
self.unloadCellContentBlock();
}
}
#pragma mark - Subviews
- (BOOL)shouldShowSenderName
{
return self.viewItem.senderName.length > 0;
}
- (void)configureSenderNameLabel
{
OWSAssertDebug(self.senderNameLabel);
OWSAssertDebug(self.shouldShowSenderName);
self.senderNameLabel.textColor = self.bodyTextColor;
self.senderNameLabel.font = OWSMessageView.senderNameFont;
self.senderNameLabel.attributedText = self.viewItem.senderName;
self.senderNameLabel.lineBreakMode = NSLineBreakByTruncatingTail;
}
- (UIView *)loadStickerView
{
if (self.viewItem.isFailedSticker) {
return [self loadFailedStickerView];
}
OWSAssertDebug(self.viewItem.stickerAttachment != nil);
TSAttachmentStream *stickerAttachment = self.viewItem.stickerAttachment;
if (stickerAttachment.shouldBeRenderedByYY) {
YYAnimatedImageView *stickerView = [YYAnimatedImageView new];
stickerView.contentMode = UIViewContentModeScaleAspectFit;
stickerView.accessibilityLabel =
[OWSMessageView accessibilityLabelWithDescription:NSLocalizedString(@"ACCESSIBILITY_LABEL_STICKER",
@"Accessibility label for stickers.")
authorName:self.viewItem.accessibilityAuthorName];
self.loadCellContentBlock = ^{
NSString *_Nullable filePath = stickerAttachment.originalFilePath;
OWSCAssertDebug(filePath);
YYImage *_Nullable image = [[YYImage alloc] initWithContentsOfFile:filePath];
OWSCAssertDebug(image);
stickerView.image = image;
};
self.unloadCellContentBlock = ^{ stickerView.image = nil; };
return stickerView;
} else {
UIImageView *stickerView = [UIImageView new];
stickerView.contentMode = UIViewContentModeScaleAspectFit;
stickerView.accessibilityLabel =
[OWSMessageView accessibilityLabelWithDescription:NSLocalizedString(@"ACCESSIBILITY_LABEL_STICKER",
@"Accessibility label for stickers.")
authorName:self.viewItem.accessibilityAuthorName];
self.loadCellContentBlock = ^{
NSString *_Nullable filePath = stickerAttachment.originalFilePath;
OWSCAssertDebug(filePath);
UIImage *_Nullable image = [UIImage imageWithContentsOfFile:filePath];
OWSCAssertDebug(image);
stickerView.image = image;
};
self.unloadCellContentBlock = ^{ stickerView.image = nil; };
return stickerView;
}
}
- (UIView *)loadFailedStickerView
{
OWSAssertDebug(self.viewItem.isFailedSticker);
UIView *roundedRectView = [UIView new];
roundedRectView.backgroundColor = UIColor.ows_gray45Color;
roundedRectView.layer.cornerRadius = 8;
UIView *pillboxView = [[OWSLayerView alloc] initWithFrame:CGRectZero
layoutCallback:^(UIView *view) {
view.layer.cornerRadius = MIN(view.width, view.height) * 0.5f;
}];
pillboxView.backgroundColor = Theme.washColor;
[roundedRectView addSubview:pillboxView];
[pillboxView autoCenterInSuperview];
UIImageView *iconView = [UIImageView withTemplateImageName:@"download-filled-2-24" tintColor:Theme.washColor];
UIView *circleView = [UIView new];
circleView.backgroundColor = UIColor.ows_gray45Color;
circleView.layer.cornerRadius = 12.f;
[circleView addSubview:iconView];
[iconView autoPinEdgesToSuperviewEdges];
UILabel *label = [UILabel new];
label.text = NSLocalizedString(@"STICKERS_FAILED_DOWNLOAD", @"Label for a sticker that failed to download.");
label.font = UIFont.ows_dynamicTypeCaption1Font.ows_semibold;
label.textColor = UIColor.ows_gray45Color;
UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
circleView,
label,
]];
stackView.axis = UILayoutConstraintAxisHorizontal;
stackView.spacing = 4;
stackView.alignment = UIStackViewAlignmentCenter;
stackView.layoutMargins = UIEdgeInsetsMake(4, 4, 4, 4);
stackView.layoutMarginsRelativeArrangement = YES;
[pillboxView addSubview:stackView];
[stackView autoPinEdgesToSuperviewEdges];
CGFloat minMargin = 10;
CGFloat maxWidth = self.stickerSize
- (minMargin * 2 + stackView.spacing + stackView.layoutMargins.left + stackView.layoutMargins.right);
[stackView autoSetDimension:ALDimensionWidth toSize:maxWidth relation:NSLayoutRelationLessThanOrEqual];
self.loadCellContentBlock = ^{
// Do nothing.
};
self.unloadCellContentBlock = ^{
// Do nothing.
};
return roundedRectView;
}
#pragma mark - Measurement
- (CGFloat)stickerSize
{
return 175;
}
- (CGSize)bodyMediaSize
{
OWSAssertDebug(self.viewItem.stickerAttachment != nil || self.viewItem.isFailedSticker);
return CGSizeMake(self.stickerSize, self.stickerSize);
}
- (nullable NSValue *)senderNameSize:(CGFloat)maxContentWidth
{
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
if (!self.shouldShowSenderName) {
return nil;
}
CGFloat hMargins = OWSMessageStickerView.textInsetHorizontal * 2;
const int maxTextWidth = (int)floor(maxContentWidth - hMargins);
[self configureSenderNameLabel];
CGSize result = CGSizeCeil([self.senderNameLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]);
result.width = MIN(result.width, maxTextWidth);
result.height += self.senderNameBottomSpacing;
return [NSValue valueWithCGSize:result];
}
- (CGSize)measureSize
{
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.conversationStyle.viewWidth > 0);
OWSAssertDebug(self.viewItem);
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
OWSAssertDebug(self.viewItem.stickerAttachment != nil || self.viewItem.isFailedSticker);
CGSize cellSize = CGSizeZero;
CGFloat margin = OWSMessageStickerView.marginAndSpacing;
CGFloat maxContentWidth = self.conversationStyle.maxMessageWidth - margin * 2;
NSMutableArray<NSValue *> *textViewSizes = [NSMutableArray new];
NSValue *_Nullable senderNameSize = [self senderNameSize:maxContentWidth];
if (senderNameSize) {
[textViewSizes addObject:senderNameSize];
}
CGSize bodyMediaSize = [self bodyMediaSize];
cellSize.width = MAX(cellSize.width, bodyMediaSize.width);
cellSize.height += bodyMediaSize.height;
if (textViewSizes.count > 0) {
CGSize groupSize = [self sizeForTextViewGroup:textViewSizes];
cellSize.width = MAX(cellSize.width, groupSize.width);
cellSize.height += groupSize.height;
[textViewSizes removeAllObjects];
}
CGSize footerSize = [self.footerView measureWithConversationViewItem:self.viewItem];
footerSize.width = MIN(footerSize.width, maxContentWidth);
[textViewSizes addObject:[NSValue valueWithCGSize:footerSize]];
if (textViewSizes.count > 0) {
CGSize groupSize = [self sizeForTextViewGroup:textViewSizes];
cellSize.width = MAX(cellSize.width, groupSize.width);
cellSize.height += groupSize.height;
}
OWSAssertDebug(cellSize.width > 0 && cellSize.height > 0);
cellSize.width += margin * 2;
cellSize.height += margin;
cellSize = CGSizeCeil(cellSize);
OWSAssertDebug(cellSize.width <= self.conversationStyle.maxMessageWidth);
cellSize.width = MIN(cellSize.width, self.conversationStyle.maxMessageWidth);
return cellSize;
}
- (CGSize)sizeForTextViewGroup:(NSArray<NSValue *> *)textViewSizes
{
OWSAssertDebug(textViewSizes);
OWSAssertDebug(textViewSizes.count > 0);
OWSAssertDebug(self.conversationStyle);
OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
CGSize result = CGSizeZero;
for (NSValue *size in textViewSizes) {
result.width = MAX(result.width, size.CGSizeValue.width);
result.height += size.CGSizeValue.height;
}
result.height += self.textViewVSpacing * (textViewSizes.count - 1);
result.height += (self.conversationStyle.textInsetTop + self.conversationStyle.textInsetBottom);
result.width += OWSMessageStickerView.textInsetHorizontal * 2;
return result;
}
#pragma mark -
- (UIColor *)bodyTextColor
{
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[TSMessage class]]);
TSMessage *message = (TSMessage *)self.viewItem.interaction;
return [self.conversationStyle bubbleTextColorWithMessage:message];
}
- (void)prepareForReuse
{
[NSLayoutConstraint deactivateConstraints:self.viewConstraints];
self.viewConstraints = [NSMutableArray new];
self.delegate = nil;
for (UIView *subview in self.subviews) {
[subview removeFromSuperview];
}
if (self.unloadCellContentBlock) {
self.unloadCellContentBlock();
}
self.loadCellContentBlock = nil;
self.unloadCellContentBlock = nil;
for (UIView *subview in self.bodyMediaView.subviews) {
[subview removeFromSuperview];
}
[self.bodyMediaView removeFromSuperview];
self.bodyMediaView = nil;
[self.footerView removeFromSuperview];
[self.footerView prepareForReuse];
for (UIView *subview in self.stackView.subviews) {
[subview removeFromSuperview];
}
}
#pragma mark - Gestures
- (BOOL)willHandleTapGesture:(UITapGestureRecognizer *)sender
{
return YES;
}
- (void)handleTapGesture:(UITapGestureRecognizer *)sender
{
if (sender.state != UIGestureRecognizerStateRecognized) {
OWSLogVerbose(@"Ignoring tap on message: %@", self.viewItem.interaction.debugDescription);
return;
}
if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
if (outgoingMessage.messageState == TSOutgoingMessageStateFailed) {
return;
} else if (outgoingMessage.messageState == TSOutgoingMessageStateSending) {
// Ignore taps on outgoing messages being sent.
return;
}
}
if (self.viewItem.isFailedSticker) {
TSMessage *message = (TSMessage *)self.viewItem.interaction;
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
NSArray<TSAttachment *> *attachments = [message allAttachmentsWithTransaction:transaction.unwrapGrdbRead];
[self.attachmentDownloads
downloadAttachmentsForMessage:message
bypassPendingMessageRequest:NO
attachments:attachments
success:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
// Do nothing.
}
failure:^(NSError *error) {
// Do nothing.
}];
}];
return;
}
StickerPackInfo *_Nullable stickerPackInfo = self.viewItem.stickerInfo.packInfo;
if (!stickerPackInfo) {
OWSFailDebug(@"Missing stickerPackInfo.");
return;
}
[self.delegate showStickerPack:stickerPackInfo];
}
- (BOOL)handlePanGesture:(UIPanGestureRecognizer *)sender
{
return NO;
}
- (void)handleMediaTapGesture:(CGPoint)locationInMessageBubble
{
OWSFailDebug(@"This method should never be called.");
}
- (OWSMessageGestureLocation)gestureLocationForLocation:(CGPoint)locationInMessageBubble
{
return OWSMessageGestureLocation_Sticker;
}
@end
NS_ASSUME_NONNULL_END