// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2012-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 "ChatVideoMessageCell.h" #import "VideoMessage.h" #import "ImageData.h" #import "VideoData.h" #import "ChatDefines.h" #import #import "Utils.h" #import "BundleUtil.h" #import "UIImage+ColoredImage.h" #import "Threema-Swift.h" #ifdef DEBUG static const DDLogLevel ddLogLevel = DDLogLevelVerbose; #else static const DDLogLevel ddLogLevel = DDLogLevelWarning; #endif @implementation ChatVideoMessageCell { UIImageView *thumbnailView; UILabel *durationLabel; UIImageView *durationBackground; UILabel *downloadSizeLabel; UIImageView *downloadBackground; CALayer *tintLayer; UIImageView *playImageView; } + (CGFloat)heightForMessage:(BaseMessage*)message forTableWidth:(CGFloat)tableWidth { VideoMessage *videoMessage = (VideoMessage*)message; CGSize scaledSize = [ChatVideoMessageCell scaleImageSizeToCell:CGSizeMake(videoMessage.thumbnail.width.floatValue, videoMessage.thumbnail.height.floatValue) forTableWidth:tableWidth]; if (scaledSize.height != scaledSize.height || scaledSize.height < 0) { scaledSize.height = 120.0; } return scaledSize.height + 6.0f - 17.0f; } - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier transparent:(BOOL)transparent { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier transparent:transparent]; if (self) { thumbnailView = [[UIImageView alloc] init]; thumbnailView.clearsContextBeforeDrawing = NO; /* Add layer with a very slight tint so that very bright messages will still stand out against a white background */ tintLayer = [CALayer layer]; [self setBubbleHighlighted:NO]; [thumbnailView.layer addSublayer:tintLayer]; [self.contentView addSubview:thumbnailView]; durationBackground = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"VideoDurationBg"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 32, 0, 0)]]; durationBackground.opaque = NO; [thumbnailView addSubview:durationBackground]; downloadBackground = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"VideoDownloadBg"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 32, 0, 0)]]; downloadBackground.opaque = NO; [thumbnailView addSubview:downloadBackground]; durationLabel = [[UILabel alloc] init]; durationLabel.backgroundColor = [UIColor clearColor]; durationLabel.opaque = NO; durationLabel.font = [UIFont boldSystemFontOfSize:12.0]; durationLabel.textColor = [UIColor whiteColor]; durationLabel.textAlignment = NSTextAlignmentRight; [durationBackground addSubview:durationLabel]; 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; [downloadBackground addSubview:downloadSizeLabel]; if (@available(iOS 11.0, *)) { thumbnailView.accessibilityIgnoresInvertColors = true; } playImageView = [[UIImageView alloc] init]; playImageView.image = [[BundleUtil imageNamed:@"Play"] imageWithTint:[UIColor whiteColor]]; [thumbnailView addSubview:playImageView]; } return self; } - (void)dealloc { @try { [self.message removeObserver:self forKeyPath:@"video"]; } @catch(NSException *e) {} } - (void)layoutSubviews { VideoMessage *videoMessage = (VideoMessage*)self.message; CGSize size = CGSizeMake(videoMessage.thumbnail.width.floatValue, videoMessage.thumbnail.height.floatValue); /* scale to fit maximum cell size */ size = [ChatVideoMessageCell scaleImageSizeToCell:size forTableWidth:self.frame.size.width]; if (size.height != size.height) { size.height = 120.0; } if (size.width != size.width) { size.width = 120.0; } UIEdgeInsets imageInsets = UIEdgeInsetsMake(1, 1, 5, 1); CGSize bubbleSize = CGSizeMake(size.width + imageInsets.left + imageInsets.right, size.height + imageInsets.top + imageInsets.bottom); [self setBubbleSize:bubbleSize]; [super layoutSubviews]; thumbnailView.frame = self.msgBackground.frame; CALayer *mask = [self bubbleMaskForImageSize:CGSizeMake(thumbnailView.frame.size.width, thumbnailView.frame.size.height)]; thumbnailView.layer.mask = mask; thumbnailView.layer.masksToBounds = YES; tintLayer.bounds = thumbnailView.bounds; tintLayer.position = CGPointMake(thumbnailView.bounds.size.width/2.0, thumbnailView.bounds.size.height/2.0); if (self.message.isOwn.boolValue) { self.resendButton.frame = CGRectMake(thumbnailView.frame.origin.x - kMessageScreenMargin, thumbnailView.frame.origin.y + (thumbnailView.frame.size.height - 32) / 2, 114, 32); } /* progress bar */ self.progressBar.frame = CGRectMake(thumbnailView.frame.origin.x + 16.0f, thumbnailView.frame.origin.y + thumbnailView.frame.size.height - 40.0f, size.width - 32.0f, 16); /* duration label */ durationBackground.frame = CGRectMake(0, thumbnailView.frame.size.height - 22, thumbnailView.frame.size.width + 1, 18); durationLabel.frame = CGRectMake(durationBackground.frame.size.width / 2, 0, durationBackground.frame.size.width / 2 - 12, 16); /* download size label */ downloadBackground.frame = CGRectMake(0, 1, thumbnailView.frame.size.width + 1, 18); downloadSizeLabel.frame = CGRectMake(downloadBackground.frame.size.width / 2, 1, downloadBackground.frame.size.width / 2 - 12, 16); if (bubbleSize.height > 44.0 && bubbleSize.width > 44.0) { playImageView.frame = CGRectMake((bubbleSize.width / 2) - 22.0, (bubbleSize.height / 2) - 22.0 - 2.0, 44.0, 44.0); } else { CGFloat min = MIN(bubbleSize.width, bubbleSize.height); min = min - 20.0; playImageView.frame = CGRectMake((bubbleSize.width / 2) - (min/2), (bubbleSize.height / 2) - (min/2) - 2.0, min, min); } } - (NSString *)accessibilityLabelForContent { return [NSString stringWithFormat:@"%@, %d %@", NSLocalizedString(@"video", nil), ((VideoMessage*)self.message).duration.intValue, NSLocalizedString(@"seconds", nil)]; } - (void)setMessage:(BaseMessage *)newMessage { if (!self.chatVc.isOpenWithForceTouch) { [self.message removeObserver:self forKeyPath:@"video"]; } VideoMessage *videoMessage = (VideoMessage*)newMessage; [super setMessage:newMessage]; if (!self.chatVc.isOpenWithForceTouch) { [self.message addObserver:self forKeyPath:@"video" options:0 context:nil]; } thumbnailView.image = videoMessage.thumbnail.uiImage;// thumbnailWithPlayOverlay; if (videoMessage.isOwn.boolValue) { thumbnailView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; durationBackground.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; durationLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; downloadBackground.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; downloadSizeLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; } else { thumbnailView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; durationBackground.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; durationLabel.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; downloadBackground.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; downloadSizeLabel.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; } int seconds = videoMessage.duration.intValue; int minutes = (seconds / 60); seconds -= minutes * 60; durationLabel.text = [NSString stringWithFormat:@"%d:%02d", minutes, seconds]; [downloadSizeLabel setText: [Utils formatDataLength:videoMessage.videoSize.floatValue]]; [self updateDownloadSize]; [self setNeedsLayout]; } - (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:@"video"]) { [self updateDownloadSize]; } }); } - (void)updateDownloadSize { VideoMessage *videoMessage = (VideoMessage*)self.message; if (videoMessage.video != nil) { downloadBackground.hidden = YES; downloadSizeLabel.hidden = YES; } else { // blob ID equals nil means media was deleted downloadBackground.hidden = videoMessage.videoBlobId != nil ? NO : YES; downloadSizeLabel.hidden = NO; } } - (void)messageTapped:(id)sender { [self.chatVc videoMessageTapped:(VideoMessage*)self.message]; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { VideoMessage *videoMessage = (VideoMessage*)self.message; if (action == @selector(resendMessage:) && videoMessage.isOwn.boolValue && videoMessage.sendFailed.boolValue) { return YES; } else if (action == @selector(deleteMessage:) && videoMessage.isOwn.boolValue && videoMessage.progress != nil) { return NO; /* don't allow messages in progress to be deleted */ } else if (action == @selector(copyMessage:)) { return NO; /* cannot copy videos */ } else if (action == @selector(shareMessage:)) { if (@available(iOS 13.0, *)) { MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:false]; if ([mdmSetup disableShareMedia] == true) { return NO; } } return (videoMessage.video != nil); /* can only save downloaded videos */ } else if (action == @selector(forwardMessage:)) { if (@available(iOS 13.0, *)) { return (videoMessage.video != nil); /* can only save downloaded videos */ } else { return NO; } } else { return [super canPerformAction:action withSender:sender]; } } - (void)resendMessage:(UIMenuController*)menuController { DDLogError(@"VideoMessages can not be resent anymore."); } - (BOOL)performPlayActionForAccessibility { [self messageTapped:self]; return YES; } - (BOOL)shouldHideBubbleBackground { VideoMessage *videoMessage = (VideoMessage*)self.message; return (videoMessage.thumbnail != nil); } - (void)setBubbleHighlighted:(BOOL)bubbleHighlighted { [super setBubbleHighlighted:bubbleHighlighted]; [CATransaction begin]; [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; if (bubbleHighlighted) { tintLayer.backgroundColor = [[UIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:1.0] CGColor]; [tintLayer setOpacity:0.25]; } else { tintLayer.backgroundColor = [[UIColor colorWithRed:0 green:0 blue:0 alpha:1.0] CGColor]; [tintLayer setOpacity:0.03]; } [CATransaction commit]; } - (UIViewController *)previewViewController { return [self.chatVc.headerView getPhotoBrowserAtMessage:self.message forPeeking:YES]; } - (UIContextMenuConfiguration *)getContextMenu:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) { if (self.editing) { return nil; } VideoMessage *videoMessage = (VideoMessage*)self.message; if (videoMessage.video != nil) { if (videoMessage.video.data != nil) { UIContextMenuConfiguration *conf = [UIContextMenuConfiguration configurationWithIdentifier:indexPath previewProvider:^UIViewController * _Nullable{ return [self previewViewController]; } actionProvider:^UIMenu * _Nullable(NSArray * _Nonnull suggestedActions) { NSMutableArray *menuItems = [NSMutableArray arrayWithArray:[super contextMenuItems]]; UIImage *copyImage = [UIImage systemImageNamed:@"square.and.arrow.down.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:[BundleUtil localizedStringForKey:@"save"] image:copyImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { NSString *filename = [NSString stringWithFormat:@"%f.%@", [[NSDate date] timeIntervalSinceReferenceDate], MEDIA_EXTENSION_VIDEO]; NSURL *tmpurl = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:filename]]; if (![videoMessage.video.data writeToURL:tmpurl atomically:NO]) { DDLogWarn(@"Writing movie to temporary file failed"); } else { [[AlbumManager shared] saveMovieToLibraryWithMovieURL:tmpurl completionHandler:^(BOOL success) { [[NSFileManager defaultManager] removeItemAtPath:tmpurl.path error:nil]; }]; } }]; if (self.message.isOwn.boolValue == true || self.chatVc.conversation.isGroup == true) { [menuItems insertObject:action atIndex:0]; } else { [menuItems insertObject:action atIndex:1]; } return [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:menuItems]; }]; return conf; } else { return [super getContextMenu:indexPath point:point]; } } else { return [super getContextMenu:indexPath point:point]; } } @end