ChatVideoMessageCell.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2012-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "ChatVideoMessageCell.h"
  21. #import "VideoMessage.h"
  22. #import "ImageData.h"
  23. #import "VideoData.h"
  24. #import "ChatDefines.h"
  25. #import <QuartzCore/QuartzCore.h>
  26. #import "Utils.h"
  27. #import "BundleUtil.h"
  28. #import "UIImage+ColoredImage.h"
  29. #import "Threema-Swift.h"
  30. #ifdef DEBUG
  31. static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
  32. #else
  33. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  34. #endif
  35. @implementation ChatVideoMessageCell {
  36. UIImageView *thumbnailView;
  37. UILabel *durationLabel;
  38. UIImageView *durationBackground;
  39. UILabel *downloadSizeLabel;
  40. UIImageView *downloadBackground;
  41. CALayer *tintLayer;
  42. UIImageView *playImageView;
  43. }
  44. + (CGFloat)heightForMessage:(BaseMessage*)message forTableWidth:(CGFloat)tableWidth {
  45. VideoMessage *videoMessage = (VideoMessage*)message;
  46. CGSize scaledSize = [ChatVideoMessageCell scaleImageSizeToCell:CGSizeMake(videoMessage.thumbnail.width.floatValue, videoMessage.thumbnail.height.floatValue) forTableWidth:tableWidth];
  47. if (scaledSize.height != scaledSize.height || scaledSize.height < 0) {
  48. scaledSize.height = 120.0;
  49. }
  50. return scaledSize.height + 6.0f - 17.0f;
  51. }
  52. - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier transparent:(BOOL)transparent
  53. {
  54. self = [super initWithStyle:style reuseIdentifier:reuseIdentifier transparent:transparent];
  55. if (self) {
  56. thumbnailView = [[UIImageView alloc] init];
  57. thumbnailView.clearsContextBeforeDrawing = NO;
  58. /* Add layer with a very slight tint so that very bright messages will still stand out against a white background */
  59. tintLayer = [CALayer layer];
  60. [self setBubbleHighlighted:NO];
  61. [thumbnailView.layer addSublayer:tintLayer];
  62. [self.contentView addSubview:thumbnailView];
  63. durationBackground = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"VideoDurationBg"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 32, 0, 0)]];
  64. durationBackground.opaque = NO;
  65. [thumbnailView addSubview:durationBackground];
  66. downloadBackground = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"VideoDownloadBg"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 32, 0, 0)]];
  67. downloadBackground.opaque = NO;
  68. [thumbnailView addSubview:downloadBackground];
  69. durationLabel = [[UILabel alloc] init];
  70. durationLabel.backgroundColor = [UIColor clearColor];
  71. durationLabel.opaque = NO;
  72. durationLabel.font = [UIFont boldSystemFontOfSize:12.0];
  73. durationLabel.textColor = [UIColor whiteColor];
  74. durationLabel.textAlignment = NSTextAlignmentRight;
  75. [durationBackground addSubview:durationLabel];
  76. downloadSizeLabel = [[UILabel alloc] init];
  77. downloadSizeLabel.backgroundColor = [UIColor clearColor];
  78. downloadSizeLabel.opaque = NO;
  79. downloadSizeLabel.font = [UIFont boldSystemFontOfSize:12.0];
  80. downloadSizeLabel.textColor = [UIColor whiteColor];
  81. downloadSizeLabel.textAlignment = NSTextAlignmentRight;
  82. downloadSizeLabel.adjustsFontSizeToFitWidth = YES;
  83. [downloadBackground addSubview:downloadSizeLabel];
  84. if (@available(iOS 11.0, *)) {
  85. thumbnailView.accessibilityIgnoresInvertColors = true;
  86. }
  87. playImageView = [[UIImageView alloc] init];
  88. playImageView.image = [[BundleUtil imageNamed:@"Play"] imageWithTint:[UIColor whiteColor]];
  89. [thumbnailView addSubview:playImageView];
  90. }
  91. return self;
  92. }
  93. - (void)dealloc {
  94. @try {
  95. [self.message removeObserver:self forKeyPath:@"video"];
  96. }
  97. @catch(NSException *e) {}
  98. }
  99. - (void)layoutSubviews {
  100. VideoMessage *videoMessage = (VideoMessage*)self.message;
  101. CGSize size = CGSizeMake(videoMessage.thumbnail.width.floatValue, videoMessage.thumbnail.height.floatValue);
  102. /* scale to fit maximum cell size */
  103. size = [ChatVideoMessageCell scaleImageSizeToCell:size forTableWidth:self.frame.size.width];
  104. if (size.height != size.height) {
  105. size.height = 120.0;
  106. }
  107. if (size.width != size.width) {
  108. size.width = 120.0;
  109. }
  110. UIEdgeInsets imageInsets = UIEdgeInsetsMake(1, 1, 5, 1);
  111. CGSize bubbleSize = CGSizeMake(size.width + imageInsets.left + imageInsets.right, size.height + imageInsets.top + imageInsets.bottom);
  112. [self setBubbleSize:bubbleSize];
  113. [super layoutSubviews];
  114. thumbnailView.frame = self.msgBackground.frame;
  115. CALayer *mask = [self bubbleMaskForImageSize:CGSizeMake(thumbnailView.frame.size.width, thumbnailView.frame.size.height)];
  116. thumbnailView.layer.mask = mask;
  117. thumbnailView.layer.masksToBounds = YES;
  118. tintLayer.bounds = thumbnailView.bounds;
  119. tintLayer.position = CGPointMake(thumbnailView.bounds.size.width/2.0, thumbnailView.bounds.size.height/2.0);
  120. if (self.message.isOwn.boolValue) {
  121. self.resendButton.frame = CGRectMake(thumbnailView.frame.origin.x - kMessageScreenMargin, thumbnailView.frame.origin.y + (thumbnailView.frame.size.height - 32) / 2, 114, 32);
  122. }
  123. /* progress bar */
  124. 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);
  125. /* duration label */
  126. durationBackground.frame = CGRectMake(0, thumbnailView.frame.size.height - 22, thumbnailView.frame.size.width + 1, 18);
  127. durationLabel.frame = CGRectMake(durationBackground.frame.size.width / 2, 0, durationBackground.frame.size.width / 2 - 12, 16);
  128. /* download size label */
  129. downloadBackground.frame = CGRectMake(0, 1, thumbnailView.frame.size.width + 1, 18);
  130. downloadSizeLabel.frame = CGRectMake(downloadBackground.frame.size.width / 2, 1, downloadBackground.frame.size.width / 2 - 12, 16);
  131. if (bubbleSize.height > 44.0 && bubbleSize.width > 44.0) {
  132. playImageView.frame = CGRectMake((bubbleSize.width / 2) - 22.0, (bubbleSize.height / 2) - 22.0 - 2.0, 44.0, 44.0);
  133. } else {
  134. CGFloat min = MIN(bubbleSize.width, bubbleSize.height);
  135. min = min - 20.0;
  136. playImageView.frame = CGRectMake((bubbleSize.width / 2) - (min/2), (bubbleSize.height / 2) - (min/2) - 2.0, min, min);
  137. }
  138. }
  139. - (NSString *)accessibilityLabelForContent {
  140. return [NSString stringWithFormat:@"%@, %d %@", NSLocalizedString(@"video", nil), ((VideoMessage*)self.message).duration.intValue, NSLocalizedString(@"seconds", nil)];
  141. }
  142. - (void)setMessage:(BaseMessage *)newMessage {
  143. if (!self.chatVc.isOpenWithForceTouch) {
  144. [self.message removeObserver:self forKeyPath:@"video"];
  145. }
  146. VideoMessage *videoMessage = (VideoMessage*)newMessage;
  147. [super setMessage:newMessage];
  148. if (!self.chatVc.isOpenWithForceTouch) {
  149. [self.message addObserver:self forKeyPath:@"video" options:0 context:nil];
  150. }
  151. thumbnailView.image = videoMessage.thumbnail.uiImage;// thumbnailWithPlayOverlay;
  152. if (videoMessage.isOwn.boolValue) {
  153. thumbnailView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
  154. durationBackground.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
  155. durationLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
  156. downloadBackground.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
  157. downloadSizeLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
  158. } else {
  159. thumbnailView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  160. durationBackground.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  161. durationLabel.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  162. downloadBackground.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  163. downloadSizeLabel.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  164. }
  165. int seconds = videoMessage.duration.intValue;
  166. int minutes = (seconds / 60);
  167. seconds -= minutes * 60;
  168. durationLabel.text = [NSString stringWithFormat:@"%d:%02d", minutes, seconds];
  169. [downloadSizeLabel setText: [Utils formatDataLength:videoMessage.videoSize.floatValue]];
  170. [self updateDownloadSize];
  171. [self setNeedsLayout];
  172. }
  173. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  174. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  175. dispatch_async(dispatch_get_main_queue(), ^{
  176. if (object == self.message && [keyPath isEqualToString:@"video"]) {
  177. [self updateDownloadSize];
  178. }
  179. });
  180. }
  181. - (void)updateDownloadSize {
  182. VideoMessage *videoMessage = (VideoMessage*)self.message;
  183. if (videoMessage.video != nil) {
  184. downloadBackground.hidden = YES;
  185. downloadSizeLabel.hidden = YES;
  186. } else {
  187. // blob ID equals nil means media was deleted
  188. downloadBackground.hidden = videoMessage.videoBlobId != nil ? NO : YES;
  189. downloadSizeLabel.hidden = NO;
  190. }
  191. }
  192. - (void)messageTapped:(id)sender {
  193. [self.chatVc videoMessageTapped:(VideoMessage*)self.message];
  194. }
  195. - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
  196. VideoMessage *videoMessage = (VideoMessage*)self.message;
  197. if (action == @selector(resendMessage:) && videoMessage.isOwn.boolValue && videoMessage.sendFailed.boolValue) {
  198. return YES;
  199. } else if (action == @selector(deleteMessage:) && videoMessage.isOwn.boolValue && videoMessage.progress != nil) {
  200. return NO; /* don't allow messages in progress to be deleted */
  201. } else if (action == @selector(copyMessage:)) {
  202. return NO; /* cannot copy videos */
  203. } else if (action == @selector(shareMessage:)) {
  204. if (@available(iOS 13.0, *)) {
  205. MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:false];
  206. if ([mdmSetup disableShareMedia] == true) {
  207. return NO;
  208. }
  209. }
  210. return (videoMessage.video != nil); /* can only save downloaded videos */
  211. } else if (action == @selector(forwardMessage:)) {
  212. if (@available(iOS 13.0, *)) {
  213. return (videoMessage.video != nil); /* can only save downloaded videos */
  214. } else {
  215. return NO;
  216. }
  217. } else {
  218. return [super canPerformAction:action withSender:sender];
  219. }
  220. }
  221. - (void)resendMessage:(UIMenuController*)menuController {
  222. DDLogError(@"VideoMessages can not be resent anymore.");
  223. }
  224. - (BOOL)performPlayActionForAccessibility {
  225. [self messageTapped:self];
  226. return YES;
  227. }
  228. - (BOOL)shouldHideBubbleBackground {
  229. VideoMessage *videoMessage = (VideoMessage*)self.message;
  230. return (videoMessage.thumbnail != nil);
  231. }
  232. - (void)setBubbleHighlighted:(BOOL)bubbleHighlighted {
  233. [super setBubbleHighlighted:bubbleHighlighted];
  234. [CATransaction begin];
  235. [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
  236. if (bubbleHighlighted) {
  237. tintLayer.backgroundColor = [[UIColor colorWithRed:0.8 green:0.8 blue:0.8 alpha:1.0] CGColor];
  238. [tintLayer setOpacity:0.25];
  239. } else {
  240. tintLayer.backgroundColor = [[UIColor colorWithRed:0 green:0 blue:0 alpha:1.0] CGColor];
  241. [tintLayer setOpacity:0.03];
  242. }
  243. [CATransaction commit];
  244. }
  245. - (UIViewController *)previewViewController {
  246. return [self.chatVc.headerView getPhotoBrowserAtMessage:self.message forPeeking:YES];
  247. }
  248. - (UIContextMenuConfiguration *)getContextMenu:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) {
  249. if (self.editing) {
  250. return nil;
  251. }
  252. VideoMessage *videoMessage = (VideoMessage*)self.message;
  253. if (videoMessage.video != nil) {
  254. if (videoMessage.video.data != nil) {
  255. UIContextMenuConfiguration *conf = [UIContextMenuConfiguration configurationWithIdentifier:indexPath previewProvider:^UIViewController * _Nullable{
  256. return [self previewViewController];
  257. } actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
  258. NSMutableArray *menuItems = [NSMutableArray arrayWithArray:[super contextMenuItems]];
  259. UIImage *copyImage = [UIImage systemImageNamed:@"square.and.arrow.down.fill" compatibleWithTraitCollection:self.traitCollection];
  260. UIAction *action = [UIAction actionWithTitle:[BundleUtil localizedStringForKey:@"save"] image:copyImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  261. NSString *filename = [NSString stringWithFormat:@"%f.%@", [[NSDate date] timeIntervalSinceReferenceDate], MEDIA_EXTENSION_VIDEO];
  262. NSURL *tmpurl = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:filename]];
  263. if (![videoMessage.video.data writeToURL:tmpurl atomically:NO]) {
  264. DDLogWarn(@"Writing movie to temporary file failed");
  265. } else {
  266. [[AlbumManager shared] saveMovieToLibraryWithMovieURL:tmpurl completionHandler:^(BOOL success) {
  267. [[NSFileManager defaultManager] removeItemAtPath:tmpurl.path error:nil];
  268. }];
  269. }
  270. }];
  271. if (self.message.isOwn.boolValue == true || self.chatVc.conversation.isGroup == true) {
  272. [menuItems insertObject:action atIndex:0];
  273. } else {
  274. [menuItems insertObject:action atIndex:1];
  275. }
  276. return [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:menuItems];
  277. }];
  278. return conf;
  279. } else {
  280. return [super getContextMenu:indexPath point:point];
  281. }
  282. } else {
  283. return [super getContextMenu:indexPath point:point];
  284. }
  285. }
  286. @end