// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2015-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import "ChatFileMessageCell.h" #import "FileMessage.h" #import "ImageData.h" #import "FileMessageSender.h" #import "RectUtil.h" #import "Utils.h" #import "BlobMessageLoader.h" #import "ProtocolDefines.h" #import "UTIConverter.h" #import "BundleUtil.h" #import "FileMessagePreview.h" #import "UIImage+ColoredImage.h" #import "MDMSetup.h" #import "BlobUtil.h" #import "PinnedHTTPSURLLoader.h" #import "NaClCrypto.h" #import "EntityManager.h" #define DOWNLOAD_VIEW_HEIGHT 18.0f #define THUMBNAIL_SIZE 64.0 #define THUMBNAIL_SMALL_SIZE 36.0 #define MIN_HEIGHT 34.0f #define NAME_LABEL_PADDING 16.0f #define PROGRESSBAR_PADDING 40.0f #define THUMBNAIL_PADDING 8.0f #define RESEND_BUTTON_WIDTH 114.0f @interface ChatFileMessageCell () @property UIImageView *thumbnailView; @property UILabel *downloadSizeLabel; @property UILabel *nameLabel; @property UIImageView *downloadBackground; @property FileMessagePreview *fileMessagPreview; @end @implementation ChatFileMessageCell + (CGFloat)heightForMessage:(BaseMessage*)message forTableWidth:(CGFloat)tableWidth { CGSize maxSize = CGSizeMake([ChatMessageCell maxContentWidthForTableWidth:tableWidth] - NAME_LABEL_PADDING, CGFLOAT_MAX); static UILabel *dummyLabel = nil; if (dummyLabel == nil) { dummyLabel = [[UILabel alloc] init]; dummyLabel.clearsContextBeforeDrawing = NO; dummyLabel.numberOfLines = 0; dummyLabel.lineBreakMode = NSLineBreakByWordWrapping; dummyLabel.textAlignment = NSTextAlignmentCenter; } dummyLabel.font = [ChatMessageCell textFont]; dummyLabel.attributedText = [self displayTextForMessage:message]; CGSize textSize = [dummyLabel sizeThatFits:maxSize]; textSize.height = ceilf(textSize.height); CGFloat thumbnailSize = [self thumbnailSizeForMessage:(FileMessage*)message]; return MAX(textSize.height + DOWNLOAD_VIEW_HEIGHT + thumbnailSize + 2*THUMBNAIL_PADDING, MIN_HEIGHT); } + (CGFloat)thumbnailSizeForMessage:(FileMessage *)message { CGFloat thumbnailSize = THUMBNAIL_SIZE; if (((FileMessage*)message).thumbnail == nil) { thumbnailSize = THUMBNAIL_SMALL_SIZE; } return thumbnailSize; } - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier transparent:(BOOL)transparent { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier transparent:transparent]; if (self) { CGRect rect = CGRectMake(0.0, 0.0, THUMBNAIL_SIZE, THUMBNAIL_SIZE); _thumbnailView = [[UIImageView alloc] initWithFrame:rect]; _thumbnailView.clearsContextBeforeDrawing = NO; _thumbnailView.contentMode = UIViewContentModeScaleAspectFit; [self setBubbleHighlighted:NO]; [self.contentView addSubview:self.thumbnailView]; UIImage *downloadBackgroundImage = [[UIImage imageNamed:@"VideoDownloadBg"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 32, 0, 0)]; _downloadBackground = [[UIImageView alloc] initWithImage:downloadBackgroundImage]; _downloadBackground.opaque = NO; [self.contentView addSubview:self.downloadBackground]; _downloadSizeLabel = [[UILabel alloc] init]; _downloadSizeLabel.backgroundColor = [UIColor clearColor]; _downloadSizeLabel.opaque = NO; _downloadSizeLabel.font = [UIFont boldSystemFontOfSize:12.0]; _downloadSizeLabel.textColor = [UIColor whiteColor]; _downloadSizeLabel.textAlignment = NSTextAlignmentRight; _downloadSizeLabel.adjustsFontSizeToFitWidth = YES; [self.contentView addSubview:_downloadSizeLabel]; _nameLabel = [[UILabel alloc] init]; _nameLabel.clearsContextBeforeDrawing = NO; _nameLabel.backgroundColor = [UIColor clearColor]; _nameLabel.numberOfLines = 0; _nameLabel.lineBreakMode = NSLineBreakByWordWrapping; _nameLabel.font = [ChatMessageCell textFont]; _nameLabel.textAlignment = NSTextAlignmentCenter; [self.contentView addSubview:_nameLabel]; } return self; } - (void)setupColors { [super setupColors]; _nameLabel.textColor = [Colors fontNormal]; } - (void)dealloc { @try { [self.message removeObserver:self forKeyPath:@"data"]; } @catch(NSException *e) {} } - (void)setMessage:(BaseMessage *)newMessage { @try { [self.message removeObserver:self forKeyPath:@"data"]; } @catch(NSException *e) {} FileMessage *fileMessage = (FileMessage*)newMessage; [super setMessage:fileMessage]; if (!self.chatVc.isOpenWithForceTouch) { [self.message addObserver:self forKeyPath:@"data" options:0 context:nil]; } [_nameLabel setAttributedText:[ChatFileMessageCell displayTextForMessage:self.message]]; [_downloadSizeLabel setText: [Utils formatDataLength:fileMessage.fileSize.floatValue]]; [self updateThumbnailImage]; UIViewAutoresizing resizing = UIViewAutoresizingFlexibleRightMargin; if (fileMessage.isOwn.boolValue) { resizing = UIViewAutoresizingFlexibleLeftMargin; } _thumbnailView.autoresizingMask = resizing; _downloadBackground.autoresizingMask = resizing; _downloadSizeLabel.autoresizingMask = resizing; [self updateDownloadSize]; [self setNeedsLayout]; } - (void)layoutSubviews { CGFloat messageTextWidth; if (@available(iOS 11.0, *)) { messageTextWidth = [ChatMessageCell maxContentWidthForTableWidth:self.safeAreaLayoutGuide.layoutFrame.size.width]; } else { messageTextWidth = [ChatMessageCell maxContentWidthForTableWidth:self.frame.size.width]; } CGSize textSize = [_nameLabel sizeThatFits:CGSizeMake(messageTextWidth - NAME_LABEL_PADDING, CGFLOAT_MAX)]; CGFloat thumbnailSize = [ChatFileMessageCell thumbnailSizeForMessage:(FileMessage*)self.message]; CGFloat height = ceilf(textSize.height + DOWNLOAD_VIEW_HEIGHT + thumbnailSize + 2*THUMBNAIL_PADDING); CGSize size = CGSizeMake(textSize.width + NAME_LABEL_PADDING, height); [self setBubbleContentSize:size]; [super layoutSubviews]; CGRect backgroundRect = self.msgBackground.frame; CALayer *mask = [self bubbleMaskForImageSize:backgroundRect.size]; _downloadBackground.layer.mask = mask; _downloadBackground.layer.masksToBounds = YES; _downloadBackground.frame = CGRectMake(backgroundRect.origin.x, backgroundRect.origin.y, backgroundRect.size.width, DOWNLOAD_VIEW_HEIGHT); _downloadSizeLabel.frame = CGRectMake(backgroundRect.origin.x + NAME_LABEL_PADDING/2.0, backgroundRect.origin.y, backgroundRect.size.width - NAME_LABEL_PADDING - 10.0, DOWNLOAD_VIEW_HEIGHT); CGFloat yOffset = CGRectGetMaxY(_downloadBackground.frame) + THUMBNAIL_PADDING; _thumbnailView.frame = [RectUtil setYPositionOf:_thumbnailView.frame y: yOffset]; _thumbnailView.frame = [RectUtil rect:_thumbnailView.frame centerHorizontalIn:backgroundRect round:YES]; _thumbnailView.frame = [RectUtil offsetRect:_thumbnailView.frame byX:backgroundRect.origin.x byY:0.0]; yOffset += _thumbnailView.frame.size.height + THUMBNAIL_PADDING/2.0; self.progressBar.frame = CGRectMake(backgroundRect.origin.x + PROGRESSBAR_PADDING/2.0, yOffset, backgroundRect.size.width - PROGRESSBAR_PADDING, self.progressBar.frame.size.height); yOffset += THUMBNAIL_PADDING; if (self.message.isOwn.boolValue) { CGFloat resendButtonX = backgroundRect.origin.x - RESEND_BUTTON_WIDTH; CGFloat resendButtonHeight; CGFloat resendButtonWidth; if (resendButtonX < 8.0) { self.resendButton.titleLabel.numberOfLines = 2; resendButtonWidth = backgroundRect.origin.x - 16.0; resendButtonX = 8.0; resendButtonHeight = 64.0; } else { self.resendButton.titleLabel.numberOfLines = 1; resendButtonHeight = 32.0; resendButtonWidth = RESEND_BUTTON_WIDTH; } self.resendButton.frame = CGRectMake(resendButtonX, _thumbnailView.frame.origin.y + (_thumbnailView.frame.size.height - resendButtonHeight) / 2.0, resendButtonWidth, resendButtonHeight); } _nameLabel.frame = CGRectMake(0, yOffset, textSize.width, textSize.height); _nameLabel.frame = [RectUtil rect:_nameLabel.frame centerHorizontalIn:backgroundRect round:YES]; _nameLabel.frame = [RectUtil offsetRect:_nameLabel.frame byX:backgroundRect.origin.x byY:0.0]; } - (void)updateThumbnailImage { FileMessage *fileMessage = (FileMessage*) self.message; if (fileMessage.blobThumbnailId != nil && fileMessage.thumbnail == nil) { // load thumbnail [self loadThumbnail: fileMessage]; } UIImage *thumbnailImage = [FileMessagePreview thumbnailForFileMessage:fileMessage]; CGFloat thumbnailSize = [ChatFileMessageCell thumbnailSizeForMessage:(FileMessage*)self.message]; _thumbnailView.frame = [RectUtil setSizeOf:_thumbnailView.frame width:thumbnailSize height:thumbnailSize]; _thumbnailView.frame = [RectUtil rect:_thumbnailView.frame centerIn:self.msgBackground.frame]; _thumbnailView.image = thumbnailImage; } - (void)loadThumbnail:(FileMessage *)fileMessage { NSURLRequest *request = [BlobUtil urlRequestForBlobId:fileMessage.blobThumbnailId]; PinnedHTTPSURLLoader *thumbnailLoader = [[PinnedHTTPSURLLoader alloc] init]; [thumbnailLoader startWithURLRequest:request onCompletion:^(NSData *data) { /* Decrypt the box */ NSData *thumbnailData = [[NaClCrypto sharedCrypto] symmetricDecryptData:data withKey:[fileMessage encryptionKey] nonce:[NSData dataWithBytesNoCopy:kNonce_2 length:sizeof(kNonce_2) freeWhenDone:NO]]; if (thumbnailData != nil) { EntityManager *entityManager = [[EntityManager alloc] init]; [entityManager performSyncBlockAndSafe:^{ ImageData *thumbnail = [entityManager.entityCreator imageData]; thumbnail.data = thumbnailData; // load image to determine size UIImage *thumbnailImage = [UIImage imageWithData:thumbnailData]; thumbnail.width = [NSNumber numberWithInt:thumbnailImage.size.width]; thumbnail.height = [NSNumber numberWithInt:thumbnailImage.size.height]; fileMessage.thumbnail = thumbnail; }]; [self setNeedsLayout]; } } onError:^(NSError *error) { // do nothing }]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; dispatch_async(dispatch_get_main_queue(), ^{ if (object == self.message && [keyPath isEqualToString:@"data"]) { [self updateDownloadSize]; } }); } - (void)updateDownloadSize { FileMessage *fileMessage = (FileMessage*)self.message; if (fileMessage.data != nil) { _downloadBackground.hidden = YES; _downloadSizeLabel.textColor = [Colors fontNormal]; } else { // blob ID equals nil means media was deleted _downloadBackground.hidden = fileMessage.blobId != nil ? NO : YES; _downloadSizeLabel.textColor = [UIColor whiteColor]; } } - (void)messageTapped:(id)sender { FileMessage *fileMessage = (FileMessage*)self.message; if (fileMessage.data == nil) { /* need to download this file first */ BlobMessageLoader *loader = [[BlobMessageLoader alloc] init]; [loader startWithMessage:fileMessage onCompletion:^(BaseMessage *message) { [self showDetails]; } onError:^(NSError *error) { if (error.code != kErrorCodeUserCancelled) { [UIAlertTemplate showAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:error.localizedDescription message:error.localizedFailureReason actionOk:nil]; } }]; } else { [self showDetails]; } } - (void)showDetails { if (self.chatVc.visible == NO) { return; } // to prevent keyboard issue when playing audio using UIDocumentInteractionController (IOS-163) [self.chatVc.chatBar resignFirstResponder]; FileMessage *fileMessage = (FileMessage*)self.message; _fileMessagPreview = [FileMessagePreview fileMessagePreviewFor:fileMessage]; [_fileMessagPreview showOn:self.chatVc]; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (action == @selector(resendMessage:) && self.message.isOwn.boolValue && self.message.sendFailed.boolValue) return YES; else if (action == @selector(deleteMessage:) && self.message.isOwn.boolValue && !self.message.sent.boolValue && !self.message.sendFailed.boolValue) return NO; /* don't allow messages in progress to be deleted */ else if (action == @selector(copyMessage:)) return NO; /* cannot copy files */ else if (action == @selector(shareMessage:)) { if (@available(iOS 13.0, *)) { MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:false]; if ([mdmSetup disableShareMedia] == true) { return NO; } } return (((FileMessage*)self.message).data != nil); /* can only save downloaded files */ } else if (action == @selector(forwardMessage:)) if (@available(iOS 13.0, *)) { return (((FileMessage*)self.message).data != nil); /* can only save downloaded files */ } else { return NO; } else return [super canPerformAction:action withSender:sender]; } - (NSString *)textForQuote { FileMessage *fileMessage = (FileMessage*)self.message; return [fileMessage getCaption]; } - (void)resendMessage:(UIMenuController*)menuController { FileMessage *fileMessage = (FileMessage*)self.message; FileMessageSender *sender = [[FileMessageSender alloc] init]; [sender retryMessage:fileMessage]; } - (BOOL)performPlayActionForAccessibility { [self messageTapped:self]; return YES; } - (BOOL)highlightOccurencesOf:(NSString *)pattern { NSAttributedString *attributedString = [ChatMessageCell highlightedOccurencesOf:pattern inString:_nameLabel.text]; if (attributedString) { _nameLabel.attributedText = attributedString; return YES; } return NO; } + (NSAttributedString*)displayTextForMessage:(BaseMessage*)message { FileMessage *fileMessage = (FileMessage*)message; NSString *caption = [fileMessage getCaption]; NSMutableAttributedString *labelText; UIFont *font = [ChatMessageCell textFont]; if (caption.length > 0) { UIFont *captionFont = [font fontWithSize:font.pointSize*0.85]; labelText = [[NSMutableAttributedString alloc] initWithString:fileMessage.fileName attributes:@{ NSFontAttributeName : font }]; [labelText appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\n" attributes:@{ NSFontAttributeName : captionFont }]]; [labelText appendAttributedString:[[NSAttributedString alloc] initWithString:caption attributes:@{ NSFontAttributeName : captionFont }]]; } else { NSString *name = fileMessage.fileName; if (name == nil || name.length == 0) { name = @"Unknown"; } labelText = [[NSMutableAttributedString alloc] initWithString:name]; } return labelText; } - (NSString *)accessibilityLabelForContent { FileMessage *fileMessage = (FileMessage*)self.message; NSString *type = [UTIConverter localizedDescriptionForMimeType:fileMessage.mimeType]; NSString *name = fileMessage.fileName; NSString *size = [Utils formatDataLength:fileMessage.fileSize.floatValue]; NSString *fileInfo = [NSString stringWithFormat:@"%@. %@. %@", type, name, size]; NSString *caption = [fileMessage getCaption]; if (caption.length > 0) { return [NSString stringWithFormat:@"%@. %@", fileInfo, caption]; } else { return fileInfo; } } - (UIContextMenuConfiguration *)getContextMenu:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) { if (!self.editing) { CGPoint convertedPoint = [_thumbnailView convertPoint:point fromView:self.chatVc.chatContent]; FileMessage *fileMessage = (FileMessage*)self.message; if ([_thumbnailView pointInside:convertedPoint withEvent:nil] && fileMessage.data != nil) { if (fileMessage.data.data != nil) { UIContextMenuConfiguration *conf = [UIContextMenuConfiguration configurationWithIdentifier:indexPath previewProvider:^UIViewController * _Nullable{ return [self.chatVc.headerView getPhotoBrowserAtMessage:self.message forPeeking:YES]; } actionProvider:^UIMenu * _Nullable(NSArray * _Nonnull suggestedActions) { return nil; }]; return conf; } else { return [super getContextMenu:indexPath point:point]; } } } return [super getContextMenu:indexPath point:point]; } @end