// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // 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 "ChatMessageCell.h" #import "ChatDefines.h" #import "TextMessage.h" #import "Conversation.h" #import "Contact.h" #import "MessageSender.h" #import "ChatViewController.h" #import "CustomResponderTextView.h" #import "Utils.h" #import "UserSettings.h" #import "EntityManager.h" #import "AvatarMaker.h" #import "ActivityUtil.h" #import "UIImage+ColoredImage.h" #import "QBPopupMenu.h" #import "RectUtil.h" #import "BaseMessage+Accessibility.h" #import "BundleUtil.h" #import "Threema-Swift.h" #import "ContactGroupPickerViewController.h" #import "FeatureMaskChecker.h" #import "ChatTextMessageCell.h" #import "FileMessageSender.h" #import "AudioMessageSender.h" #define DATE_LABEL_BG_COLOR [[Colors backgroundDark] colorWithAlphaComponent:0.9] #define REQUIRED_MENU_HEIGHT 50.0 #define EMOJI_FONT_SIZE_FACTOR 3 #define EMOJI_MAX_FONT_SIZE 50 #define QUOTE_FONT_SIZE_FACTOR 0.8 #ifdef DEBUG static const DDLogLevel ddLogLevel = DDLogLevelVerbose; #else static const DDLogLevel ddLogLevel = DDLogLevelWarning; #endif @interface ChatMessageCell () @property QBPopupMenu *popupMenu; @end @implementation ChatMessageCell { UIImageView *msgBackground; UIImageView *statusImage; UILabel *dateLabel; UIImageView *typingIndicator; UIImageView *groupSenderImage; UIImageView *quoteSlideIconImage; BOOL transparent; CGSize bubbleSize; UITapGestureRecognizer *dtgr; UIPanGestureRecognizer *pan; UIImpactFeedbackGenerator *gen; BaseMessage *_messageToQuote; } @synthesize message; @synthesize typing; @synthesize chatVc; @synthesize statusImage; @synthesize msgBackground; @synthesize dtgr; + (CGFloat)heightForMessage:(BaseMessage*)message forTableWidth:(CGFloat)tableWidth { return 0; } - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier transparent:(BOOL)_transparent { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { transparent = _transparent; self.backgroundColor = transparent ? [UIColor clearColor] : [Colors background]; // clearColor slows performance // Create message background image view msgBackground = [[UIImageView alloc] init]; msgBackground.clearsContextBeforeDrawing = NO; msgBackground.backgroundColor = transparent ? [UIColor clearColor] : [Colors background]; // clearColor slows performance [self.contentView addSubview:msgBackground]; // Status image statusImage = [[UIImageView alloc] init]; statusImage.contentMode = UIViewContentModeScaleAspectFit; [self.contentView addSubview:statusImage]; // Date label if (transparent && [UserSettings sharedUserSettings].wallpaper) { dateLabel = [[RoundedRectLabel alloc] init]; ((RoundedRectLabel*)dateLabel).cornerRadius = 6; dateLabel.backgroundColor = DATE_LABEL_BG_COLOR; } else { dateLabel = [[UILabel alloc] init]; dateLabel.backgroundColor = [UIColor clearColor]; } dateLabel.font = [UIFont systemFontOfSize:MAX(11.0f, MIN(14.0, roundf([UserSettings sharedUserSettings].chatFontSize * 11.0 / 16.0)))]; dateLabel.numberOfLines = 2; [self.contentView addSubview:dateLabel]; UIImage *quoteImage = [[BundleUtil imageNamed:@"Quote"] imageWithTint:[Colors fontNormal]]; quoteSlideIconImage = [[UIImageView alloc] initWithImage:quoteImage]; quoteSlideIconImage.alpha = 0.0; [self.contentView addSubview:quoteSlideIconImage]; gen = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; // Typing indicator typingIndicator = [[UIImageView alloc] init]; [self.contentView addSubview:typingIndicator]; // Add gesture recognizers for copying (cannot use shouldShowMenuForRowAtIndexPath as we need // control over the horizontal position of the menu) if (@available(iOS 13.0, *)) { } else { UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; [self addGestureRecognizer:lpgr]; dtgr = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)]; dtgr.numberOfTapsRequired = 2; [self addGestureRecognizer:dtgr]; } pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureCellAction:)]; pan.delegate = self; [self.contentView addGestureRecognizer:pan]; [self setupColors]; } return self; } - (void)setupColors { dateLabel.textColor = [Colors fontLight]; typingIndicator.image = [self getStatusImageNamed:@"Typing" withCustomColor:nil]; self.tintColor = [Colors main]; UIView *v = [[UIView alloc] init]; v.backgroundColor = [[Colors main] colorWithAlphaComponent:0.1]; self.selectedBackgroundView = v; quoteSlideIconImage.image = [[BundleUtil imageNamed:@"Quote"] imageWithTint:[Colors fontNormal]]; } - (void)dealloc { @try { [message removeObserver:self forKeyPath:@"read"]; [message removeObserver:self forKeyPath:@"delivered"]; [message removeObserver:self forKeyPath:@"sent"]; [message removeObserver:self forKeyPath:@"userack"]; } @catch(NSException *e) {} } - (void)setMessage:(BaseMessage *)newMessage { @try { [message removeObserver:self forKeyPath:@"read"]; [message removeObserver:self forKeyPath:@"delivered"]; [message removeObserver:self forKeyPath:@"sent"]; [message removeObserver:self forKeyPath:@"userack"]; } @catch(NSException *e) {} message = newMessage; [self setupColors]; [self setBubbleHighlighted:NO]; if (message.isOwn.boolValue) { // right bubble msgBackground.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; statusImage.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; dateLabel.textAlignment = NSTextAlignmentRight; dateLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; } else { // left bubble msgBackground.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; statusImage.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; dateLabel.textAlignment = NSTextAlignmentLeft; dateLabel.autoresizingMask = UIViewAutoresizingFlexibleRightMargin; } [self updateStatusImage]; [self updateDateLabel]; [self updateTypingIndicator]; [self updateGroupSenderImage]; [self setNeedsLayout]; if (!self.chatVc.isOpenWithForceTouch) { [message addObserver:self forKeyPath:@"read" options:0 context:nil]; [message addObserver:self forKeyPath:@"delivered" options:0 context:nil]; [message addObserver:self forKeyPath:@"sent" options:0 context:nil]; [message addObserver:self forKeyPath:@"userack" options:0 context:nil]; } } - (void)setBubbleContentSize:(CGSize)size { CGFloat bgWidthMargin = 30.0f; CGFloat bgHeightMargin = 16.0f; bubbleSize = CGSizeMake(size.width+bgWidthMargin, size.height+bgHeightMargin); } - (void)setBubbleSize:(CGSize)size { bubbleSize = size; } - (void)layoutSubviews { [super layoutSubviews]; if (_popupMenu.isVisible) { [_popupMenu dismissAnimated:YES]; } CGFloat bgTopOffset = 1.0f; CGFloat bgSideMargin = 6.0f; CGSize dateLabelSize = [dateLabel sizeThatFits:CGSizeMake(60, 28)]; CGFloat dateLabelWidth = ceilf(dateLabelSize.width); CGFloat dateLabelHeight = ceilf(dateLabelSize.height); if (message.isOwn.boolValue) { // right bubble msgBackground.frame = CGRectMake(self.contentView.frame.size.width-bubbleSize.width-bgSideMargin, bgTopOffset, bubbleSize.width, bubbleSize.height); CGFloat bubbleMaxY = CGRectGetMaxY(msgBackground.frame); statusImage.frame = CGRectMake(msgBackground.frame.origin.x - 8 - 20, bubbleMaxY - 27, 20, 18); dateLabel.frame = CGRectMake(statusImage.frame.origin.x - dateLabelWidth - 8, 0, dateLabelWidth, dateLabelHeight);; typingIndicator.frame = CGRectMake(4, bubbleMaxY - 22, 22, 20); if (statusImage.hidden) { dateLabel.frame = CGRectOffset(dateLabel.frame, 28, 0); } if (dateLabel.hidden == NO) { [self verticalAlignDateLabel]; } } else { // left bubble msgBackground.frame = CGRectMake(bgSideMargin + self.contentLeftOffset, bgTopOffset, bubbleSize.width, bubbleSize.height); CGFloat bubbleMaxY = CGRectGetMaxY(msgBackground.frame); groupSenderImage.frame = CGRectMake(12, bubbleMaxY - 31, 27, 27); CGFloat xOffset = msgBackground.frame.origin.x + msgBackground.frame.size.width + 8; if (statusImage.hidden == NO) { statusImage.frame = CGRectMake(xOffset, bubbleMaxY - 27, 20, 18); xOffset += 28; } if (dateLabel.hidden == NO) { dateLabel.frame = CGRectMake(xOffset, bubbleMaxY - dateLabelHeight - 4, dateLabelWidth, dateLabelHeight); xOffset += 44; [self verticalAlignDateLabel]; } if (typingIndicator.hidden == NO) { if (xOffset > (self.contentView.frame.size.width - 30)) { xOffset = self.contentView.frame.size.width - 30; } typingIndicator.frame = CGRectMake(xOffset, bubbleMaxY - 28, 22, 20); } } } - (void)verticalAlignDateLabel { if (statusImage.hidden) { CGFloat bubbleMaxY = CGRectGetMaxY(msgBackground.frame); dateLabel.frame = [RectUtil setYPositionOf:dateLabel.frame y:bubbleMaxY - dateLabel.frame.size.height - 11]; } else { dateLabel.frame = [RectUtil rect:dateLabel.frame alignVerticalWith:statusImage.frame round:YES]; } } - (void)updateDateLabel { NSDate *date = [message dateForCurrentState]; if (date != nil) { if (![Utils isSameDayWithDate1:date date2:message.remoteSentDate]) { dateLabel.text = [DateFormatter shortStyleDateTime:date]; } else { dateLabel.text = [DateFormatter shortStyleTimeNoDate:date]; } /* set background again as it seems to be lost sometimes with RoundedRectLabel */ if (transparent && [UserSettings sharedUserSettings].wallpaper) dateLabel.backgroundColor = DATE_LABEL_BG_COLOR; else dateLabel.backgroundColor = [UIColor clearColor]; dateLabel.hidden = NO; } else { dateLabel.hidden = YES; } /* received message - show timestamp only if setting is enabled */ if (!message.isOwn.boolValue) { dateLabel.hidden = [UserSettings sharedUserSettings].showReceivedTimestamps == NO; } } - (void)updateTypingIndicator { if (typing) { typingIndicator.hidden = NO; [self setNeedsLayout]; } else { typingIndicator.hidden = YES; } } - (UIImage*)bubbleImageWithHighlight:(BOOL)bubbleHighlight { if (self.shouldHideBubbleBackground) { return nil; } if (message.isOwn.boolValue) { NSString *name = @"ChatBubbleSentMask"; if (bubbleHighlight) { return [[UIImage imageNamed:name inColor:[Colors bubbleSentSelected]] stretchableImageWithLeftCapWidth:15 topCapHeight:13]; } else { return [[UIImage imageNamed:name inColor:[Colors bubbleSent]] stretchableImageWithLeftCapWidth:15 topCapHeight:13]; } } else { NSString *name = @"ChatBubbleReceivedMask"; if (bubbleHighlight) { return [[UIImage imageNamed:name inColor:[Colors bubbleReceivedSelected]] stretchableImageWithLeftCapWidth:23 topCapHeight:15]; } else { return [[UIImage imageNamed:name inColor:[Colors bubbleReceived]] stretchableImageWithLeftCapWidth:23 topCapHeight:15]; } } } - (void)updateStatusImage { NSString *iconName; UIColor *color; if (message.conversation.groupId != nil || message.conversation.contact.isGatewayId) { /* group messages & gateway IDs don't have delivered/read status */ if (message.isOwn.boolValue && message.sent.boolValue == NO) { iconName = @"MessageStatus_sending"; } } else { if (message.isOwn.boolValue) { if (message.read.boolValue) { iconName = @"MessageStatus_read"; } else if (message.delivered.boolValue) { iconName = @"MessageStatus_delivered"; } else { if (message.sent.boolValue) { iconName = @"MessageStatus_sent"; } else { iconName = @"MessageStatus_sending"; } } } } if (message.userackDate != nil) { if (message.userack.boolValue) { iconName = @"MessageStatus_thumb_up"; color = [Colors green]; } else if (message.userack.boolValue == NO) { iconName = @"MessageStatus_thumb_down"; color = [Colors orange]; } } if (iconName) { statusImage.image = [self getStatusImageNamed:iconName withCustomColor:color]; statusImage.alpha = 0.8; statusImage.hidden = NO; } else { statusImage.hidden = YES; } [self setNeedsLayout]; } - (UIImage *)getStatusImageNamed:(NSString *)imageName withCustomColor:(UIColor *)color { if (color == nil && [UserSettings sharedUserSettings].wallpaper == nil) { color = [Colors fontLight]; } if (color) { return [UIImage imageNamed:imageName inColor:color]; } else { NSString *glowImageName = [NSString stringWithFormat:@"%@_glow", imageName]; return [UIImage imageNamed:glowImageName]; } } - (void)updateGroupSenderImage { if (message.sender == nil || message.isOwn.boolValue) { /* not an outgoing group message */ groupSenderImage.hidden = YES; groupSenderImage.image = nil; return; } if (groupSenderImage == nil) { groupSenderImage = [[UIImageView alloc] init]; [self.contentView addSubview:groupSenderImage]; } groupSenderImage.image = [BundleUtil imageNamed:@"Unknown"]; [[AvatarMaker sharedAvatarMaker] avatarForContact:message.sender size:27.0f masked:YES onCompletion:^(UIImage *avatarImage) { dispatch_async(dispatch_get_main_queue(), ^{ groupSenderImage.image = avatarImage; }); }]; groupSenderImage.hidden = NO; } - (void)handleDoubleTap:(UIGestureRecognizer *)gestureRecognizer { if (gestureRecognizer.state == UIGestureRecognizerStateEnded && self.chatVc.chatContent.editing == NO) { [self showMenu]; } } - (void)handleLongPress:(UIGestureRecognizer *)gestureRecognizer { if (gestureRecognizer.state == UIGestureRecognizerStateBegan && self.chatVc.chatContent.editing == NO) { [self showMenu]; } } - (void)showMenu { NSMutableArray *menuItems = [NSMutableArray array]; if ([self canPerformAction:@selector(userackMessage:) withSender:nil]) { UIImage *ackImage = [UIImage imageNamed:@"MessageStatus_thumb_up" inColor:[Colors green]]; QBPopupMenuItem *item = [QBPopupMenuItem itemWithImage:ackImage target:self action:@selector(userackMessage:)]; item.accessibilityLabel = NSLocalizedString(@"acknowledge", nil); [menuItems addObject:item]; } if ([self canPerformAction:@selector(userdeclineMessage:) withSender:nil]) { UIImage *declineImage = [UIImage imageNamed:@"MessageStatus_thumb_down" inColor:[Colors orange]]; QBPopupMenuItem *item = [QBPopupMenuItem itemWithImage:declineImage target:self action:@selector(userdeclineMessage:)]; item.accessibilityLabel = NSLocalizedString(@"decline", nil); [menuItems addObject:item]; } if ([self canPerformAction:@selector(quoteMessage:) withSender:nil]) { UIImage *quoteImage = [UIImage imageNamed:@"Quote" inColor:[UIColor whiteColor]]; QBPopupMenuItem *item = [QBPopupMenuItem itemWithImage:quoteImage target:self action:@selector(quoteMessage:)]; item.accessibilityLabel = NSLocalizedString(@"quote", nil); [menuItems addObject:item]; } if (UIAccessibilityIsSpeakSelectionEnabled()) { if ([self canPerformAction:@selector(speakMessage:) withSender:nil]) { [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"speak", nil) target:self action:@selector(speakMessage:)]]; } } if ([self canPerformAction:@selector(copyMessage:) withSender:nil]) { [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"copy", nil) target:self action:@selector(copyMessage:)]]; } if ([self canPerformAction:@selector(shareMessage:) withSender:nil]) { [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"share", nil) target:self action:@selector(shareMessage:)]]; } if ([self canPerformAction:@selector(resendMessage:) withSender:nil]) { [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"try_again", nil) target:self action:@selector(resendMessage:)]]; } if ([self canPerformAction:@selector(detailsMessage:) withSender:nil]) { [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"details", nil) target:self action:@selector(detailsMessage:)]]; } if ([self canPerformAction:@selector(deleteMessage:) withSender:nil]) { [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"delete", nil) target:self action:@selector(deleteMessage:)]]; } _popupMenu = [[QBPopupMenu alloc] initWithItems:menuItems]; _popupMenu.delegate = self; _popupMenu.color = [Colors popupMenuBackground]; _popupMenu.highlightedColor = [Colors popupMenuHighlight]; _popupMenu.nextPageAccessibilityLabel = NSLocalizedString(@"showNext", nil); _popupMenu.previousPageAccessibilityLabel = NSLocalizedString(@"showPrevious", nil); CGRect targetRect = [self targetRectForMenuPopup]; [_popupMenu showInView:chatVc.view targetRect:targetRect animated:YES]; UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, _popupMenu); /* add view to add a quit for voice over */ UIView *quitView = [[UIView alloc] initWithFrame:chatVc.view.subviews.lastObject.frame]; quitView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4]; quitView.alpha = 1; quitView.isAccessibilityElement = YES; quitView.accessibilityLabel = [BundleUtil localizedStringForKey:@"quit"]; quitView.accessibilityActivationPoint = CGPointMake(0.0, 0.0); quitView.backgroundColor = [UIColor clearColor]; quitView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; quitView.userInteractionEnabled = NO; [chatVc.view.subviews.lastObject insertSubview:quitView belowSubview:_popupMenu]; UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapQuitPopoverMenu:)]; [quitView addGestureRecognizer: tapGesture]; } - (UIContextMenuConfiguration *)getContextMenu:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) { if (self.editing) { return nil; } UIContextMenuConfiguration *conf = [UIContextMenuConfiguration configurationWithIdentifier:indexPath previewProvider:^UIViewController * _Nullable{ return nil; } actionProvider:^UIMenu * _Nullable(NSArray * _Nonnull suggestedActions) { NSMutableArray *menuItems = [NSMutableArray array]; NSMutableArray *deleteItems = [NSMutableArray array]; if ([self canPerformAction:@selector(userackMessage:) withSender:nil]) { UIImage *ackImage = [UIImage imageNamed:@"MessageStatus_thumb_up" inColor:[Colors green]]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"acknowledge", nil) image:ackImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self userackMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(userdeclineMessage:) withSender:nil]) { UIImage *declineImage = [UIImage imageNamed:@"MessageStatus_thumb_down" inColor:[Colors orange]]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"decline", nil) image:declineImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self userdeclineMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(quoteMessage:) withSender:nil]) { UIImage *quoteImage = [UIImage systemImageNamed:@"quote.bubble.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"quote", nil) image:quoteImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self quoteMessage:nil]; }]; [menuItems addObject:action]; } if (UIAccessibilityIsSpeakSelectionEnabled()) { if ([self canPerformAction:@selector(speakMessage:) withSender:nil]) { UIImage *speakImage = [UIImage systemImageNamed:@"text.bubble.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"speak", nil) image:speakImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self speakMessage:nil]; }]; [menuItems addObject:action]; } } if ([self canPerformAction:@selector(copyMessage:) withSender:nil]) { UIImage *copyImage = [UIImage systemImageNamed:@"doc.on.doc.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"copy", nil) image:copyImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self copyMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(forwardMessage:) withSender:nil]) { UIImage *forwardImage = [UIImage systemImageNamed:@"arrowshape.turn.up.right.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"forward", nil) image:forwardImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self forwardMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(shareMessage:) withSender:nil]) { UIImage *shareImage = [UIImage systemImageNamed:@"square.and.arrow.up.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"share", nil) image:shareImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self shareMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(resendMessage:) withSender:nil]) { UIImage *resendImage = [UIImage systemImageNamed:@"paperplane.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"try_again", nil) image:resendImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self resendMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(detailsMessage:) withSender:nil]) { UIImage *detailsImage = [UIImage systemImageNamed:@"info.circle.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"details", nil) image:detailsImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self detailsMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(deleteMessage:) withSender:nil]) { UIImage *deleteImage = [UIImage systemImageNamed:@"trash.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"delete", nil) image:deleteImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self deleteMessage:nil]; }]; action.attributes = UIMenuElementAttributesDestructive; [deleteItems addObject:action]; } UIMenu *actionsMenu = [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:menuItems]; UIMenu *deleteMenu = [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:deleteItems]; return [UIMenu menuWithTitle:@"" children:@[actionsMenu, deleteMenu]]; }]; return conf; } - (NSArray *)contextMenuItems API_AVAILABLE(ios(13.0)) { if (self.editing) { return nil; } NSMutableArray *menuItems = [NSMutableArray array]; NSMutableArray *deleteItems = [NSMutableArray array]; if ([self canPerformAction:@selector(userackMessage:) withSender:nil]) { UIImage *ackImage = [UIImage imageNamed:@"MessageStatus_thumb_up" inColor:[Colors green]]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"acknowledge", nil) image:ackImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self userackMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(userdeclineMessage:) withSender:nil]) { UIImage *declineImage = [UIImage imageNamed:@"MessageStatus_thumb_down" inColor:[Colors orange]]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"decline", nil) image:declineImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self userdeclineMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(quoteMessage:) withSender:nil]) { UIImage *quoteImage = [UIImage imageNamed:@"Quote" inColor:[Colors fontNormal]]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"quote", nil) image:quoteImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self quoteMessage:nil]; }]; [menuItems addObject:action]; } if (UIAccessibilityIsSpeakSelectionEnabled()) { if ([self canPerformAction:@selector(speakMessage:) withSender:nil]) { UIImage *speakImage = [UIImage systemImageNamed:@"text.bubble.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"speak", nil) image:speakImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self speakMessage:nil]; }]; [menuItems addObject:action]; } } if ([self canPerformAction:@selector(copyMessage:) withSender:nil]) { UIImage *copyImage = [UIImage systemImageNamed:@"doc.on.doc.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"copy", nil) image:copyImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self copyMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(forwardMessage:) withSender:nil]) { UIImage *forwardImage = [UIImage systemImageNamed:@"arrowshape.turn.up.right.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"forward", nil) image:forwardImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self forwardMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(shareMessage:) withSender:nil]) { UIImage *shareImage = [UIImage systemImageNamed:@"square.and.arrow.up.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"share", nil) image:shareImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self shareMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(resendMessage:) withSender:nil]) { UIImage *resendImage = [UIImage systemImageNamed:@"paperplane.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"try_again", nil) image:resendImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self resendMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(detailsMessage:) withSender:nil]) { UIImage *detailsImage = [UIImage systemImageNamed:@"info.circle.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"details", nil) image:detailsImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self detailsMessage:nil]; }]; [menuItems addObject:action]; } if ([self canPerformAction:@selector(deleteMessage:) withSender:nil]) { UIImage *deleteImage = [UIImage systemImageNamed:@"trash.fill" compatibleWithTraitCollection:self.traitCollection]; UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"delete", nil) image:deleteImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) { [self deleteMessage:nil]; }]; action.attributes = UIMenuElementAttributesDestructive; [deleteItems addObject:action]; } UIMenu *actionsMenu = [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:menuItems]; UIMenu *deleteMenu = [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:deleteItems]; return @[actionsMenu, deleteMenu]; } - (void)showCallMenu { NSMutableArray *menuItems = [NSMutableArray array]; if ([self canPerformAction:@selector(quoteMessage:) withSender:nil]) { UIImage *quoteImage = [UIImage imageNamed:@"Quote" inColor:[UIColor whiteColor]]; QBPopupMenuItem *item = [QBPopupMenuItem itemWithImage:quoteImage target:self action:@selector(quoteMessage:)]; item.accessibilityLabel = NSLocalizedString(@"quote", nil); [menuItems addObject:item]; } if ([self canPerformAction:@selector(deleteMessage:) withSender:nil]) { [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"delete", nil) target:self action:@selector(deleteMessage:)]]; } _popupMenu = [[QBPopupMenu alloc] initWithItems:menuItems]; _popupMenu.delegate = self; _popupMenu.color = [[UIColor blackColor] colorWithAlphaComponent:0.95]; _popupMenu.highlightedColor = [[UIColor darkGrayColor] colorWithAlphaComponent:0.95]; _popupMenu.nextPageAccessibilityLabel = NSLocalizedString(@"showNext", nil); _popupMenu.previousPageAccessibilityLabel = NSLocalizedString(@"showPrevious", nil); CGRect targetRect = [self targetRectForMenuPopup]; [_popupMenu showInView:chatVc.view targetRect:targetRect animated:YES]; UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, _popupMenu); /* add view to add a quit for voice over */ UIView *quitView = [[UIView alloc] initWithFrame:chatVc.view.subviews.lastObject.frame]; quitView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4]; quitView.alpha = 1; quitView.isAccessibilityElement = YES; quitView.accessibilityLabel = [BundleUtil localizedStringForKey:@"quit"]; quitView.accessibilityActivationPoint = CGPointMake(0.0, 0.0); quitView.backgroundColor = [UIColor clearColor]; quitView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; quitView.userInteractionEnabled = NO; [chatVc.view.subviews.lastObject insertSubview:quitView belowSubview:_popupMenu]; UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapQuitPopoverMenu:)]; [quitView addGestureRecognizer: tapGesture]; } - (CGRect)targetRectForMenuPopup { CGRect cellRect = [chatVc.view convertRect:msgBackground.frame fromView:self]; CGRect containingRect = chatVc.view.frame; CGFloat minY = chatVc.topLayoutGuide.length; CGFloat maxY = chatVc.visibleChatHeight; // cell overlapping top if (CGRectGetMinY(cellRect) - REQUIRED_MENU_HEIGHT < minY) { if (CGRectGetMaxY(cellRect) + REQUIRED_MENU_HEIGHT - minY > maxY) { // cell overlapping also bottom of containing view -> show in middle of cell cellRect = [RectUtil setHeightOf:cellRect height:REQUIRED_MENU_HEIGHT]; return [RectUtil rect:cellRect centerVerticalIn:containingRect]; } else { // force to show on bottom by extending top cell border return [RectUtil offsetAndResizeRect:cellRect byX:0.0 byY:-100.0]; } } return cellRect; } - (void)tapQuitPopoverMenu:(id)sender { [_popupMenu dismissAnimated:YES]; } - (void)resendMessage:(UIMenuController*)menuController { } - (void)copyMessage:(UIMenuController *)menuController { } - (void)speakMessage:(UIMenuController *)menuController { } - (void)shareMessage:(UIMenuController *)menuController { MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:false]; if ([mdmSetup disableShareMedia] == true) { ModalNavigationController *navigationController = [ContactGroupPickerViewController pickerFromStoryboardWithDelegate:self]; ContactGroupPickerViewController *picker = (ContactGroupPickerViewController *)navigationController.topViewController; picker.enableMulitSelection = true; picker.enableTextInput = true; picker.submitOnSelect = false; if ([self.message isKindOfClass: [FileMessage class]]) { picker.renderType = ((FileMessage *) self.message).type; } [[AppDelegate sharedAppDelegate].window.rootViewController presentViewController:navigationController animated:YES completion:nil]; } else { UIActivityViewController *activityViewController = activityViewController = [ActivityUtil activityViewControllerForMessage:self.message withView:self.chatVc.view andRect:CGRectMake(0, 0, 0, 0)]; [self.chatVc presentActivityViewController:activityViewController animated:YES fromView:self]; } } - (void)forwardMessage:(UIMenuController *)menuController { ModalNavigationController *navigationController = [ContactGroupPickerViewController pickerFromStoryboardWithDelegate:self]; ContactGroupPickerViewController *picker = (ContactGroupPickerViewController *)navigationController.topViewController; picker.enableMulitSelection = true; picker.enableTextInput = true; picker.submitOnSelect = false; if ([self.message isKindOfClass: [FileMessage class]]) { picker.renderType = ((FileMessage *) self.message).type; } [[AppDelegate sharedAppDelegate].window.rootViewController presentViewController:navigationController animated:YES completion:nil]; } - (void)setBubbleHighlighted:(BOOL)bubbleHighlighted { msgBackground.image = [self bubbleImageWithHighlight:bubbleHighlighted]; } - (void)setEditing:(BOOL)editing animated:(BOOL)animated { [super setEditing:editing animated:animated]; if (dtgr != nil) { dtgr.enabled = !editing; } if (editing) { self.msgBackground.userInteractionEnabled = NO; self.alpha = 0.8; } else { self.msgBackground.userInteractionEnabled = YES; self.alpha = 1.0; } } - (void)userackMessage:(UIMenuController *)menuController { [self sendUserAck:YES]; } - (void)userdeclineMessage:(UIMenuController *)menuController { [self sendUserAck:NO]; } - (void)sendUserAck:(BOOL)doAcknowledge { if (message.userackDate != nil && message.userack.boolValue == doAcknowledge) { return; } EntityManager *entityManager = [[EntityManager alloc] init]; [entityManager performSyncBlockAndSafe:^{ if (doAcknowledge) { [MessageSender sendUserAckForMessages:@[message] toIdentity:message.conversation.contact.identity async:YES quickReply:NO]; message.userack = [NSNumber numberWithBool:YES]; } else { [MessageSender sendUserDeclineForMessages:@[message] toIdentity:message.conversation.contact.identity async:YES quickReply:NO]; message.userack = [NSNumber numberWithBool:NO]; } message.userackDate = [NSDate date]; [self updateStatusImage]; }]; } - (void)deleteMessage:(UIMenuController*)menuController { dispatch_async(dispatch_get_main_queue(), ^{ [UIAlertTemplate showDestructiveAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:nil message:[BundleUtil localizedStringForKey:@"messages_delete_selected_confirm"] titleDestructive:[BundleUtil localizedStringForKey:@"delete"] actionDestructive:^(UIAlertAction *destructiveAction) { [chatVc cleanCellHeightCache]; EntityManager *entityManager = [[EntityManager alloc] init]; [entityManager performSyncBlockAndSafe:^{ [[entityManager entityDestroyer] deleteObjectWithObject:message]; [chatVc updateConversationLastMessage]; }]; [chatVc updateConversation]; } titleCancel:[BundleUtil localizedStringForKey:@"cancel"] actionCancel:^(UIAlertAction *cancelAction) { }]; }); } - (void)detailsMessage:(UIMenuController*)menuController { [chatVc showMessageDetails:message]; } - (void)quoteMessage:(UIMenuController*)menuController { if (_messageToQuote == nil || _messageToQuote == self.message) { _messageToQuote = nil; if ([[UserSettings sharedUserSettings] quoteV2Active]) { [self.chatVc.chatBar addQuotedMessage:self.message]; } else { NSString *quotedText = [self textForQuote]; if (quotedText.length == 0) return; Contact *sender; if (self.message.isOwn.boolValue) { sender = nil; } else if (self.message.sender != nil) { sender = self.message.sender; } else { sender = self.message.conversation.contact; } [self.chatVc.chatBar addQuotedText:quotedText quotedContact:sender]; } } } - (NSString*)textForQuote { return nil; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (action == @selector(copyMessage:)) { return YES; } else if (action == @selector(shareMessage:)) { if (@available(iOS 13.0, *)) { MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:false]; if ([mdmSetup disableShareMedia] == true) { return NO; } } return YES; } else if (action == @selector(userackMessage:) && !message.isOwn.boolValue && message.conversation.groupId == nil) { return YES; } else if (action == @selector(userdeclineMessage:) && !message.isOwn.boolValue && message.conversation.groupId == nil) { return YES; } else if (action == @selector(deleteMessage:)) { return YES; } else if (action == @selector(detailsMessage:)) { return YES; } else if (action == @selector(quoteMessage:) /*&& [self textForQuote].length > 0*/) { if ([[UserSettings sharedUserSettings] quoteV2Active]) { return YES; } else { if ([self textForQuote].length > 0) { return true; } } return false; } else if (action == @selector(forwardMessage:)) { if (@available(iOS 13.0, *)) { return YES; } else { return NO; } } else { return NO; } } - (BOOL)canBecomeFirstResponder { return YES; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { dispatch_async(dispatch_get_main_queue(), ^{ if (object == message) { [UIView animateWithDuration:0.5 animations:^{ [self updateStatusImage]; [self updateDateLabel]; [self updateTypingIndicator]; }]; } }); } - (void)setTyping:(BOOL)newTyping { typing = newTyping; [self updateTypingIndicator]; } - (CGFloat)contentLeftOffset { if (message.sender == nil || message.isOwn.boolValue) return 0.0f; else return 40.0f; } + (CGFloat)maxContentWidthForTableWidth:(CGFloat)tableWidth { return tableWidth - kMessageScreenMargin; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (touches.count == 1) { if (UIAccessibilityIsVoiceOverRunning()) { /* when VoiceOver is on, double-taps on message cells will result in a touch located in the center of the cell. Since this may not be within the bubble, it will not trigger playing/showing media. Therefore, we hand this event to the cell */ if ([self performPlayActionForAccessibility]) return; } UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView:self.contentView]; if (!CGRectContainsPoint(self.msgBackground.frame, point)) [self.chatVc messageBackgroundTapped:self.message]; } [super touchesEnded:touches withEvent:event]; } - (NSString *)accessibilityLabel { NSMutableString *text = [NSMutableString new]; NSString *senderText = [message accessibilityMessageSender]; if (senderText.length > 0) { [text appendFormat:@"%@. ", senderText]; } [text appendFormat:@"%@\n", self.accessibilityLabelForContent]; NSString *statusText = [message accessibilityMessageStatus]; if (statusText.length > 0) { [text appendFormat:@"%@", statusText]; } NSString *dateText = [message accessibilityMessageDate]; if (dateText.length > 0) { [text appendFormat:@". %@", dateText]; } return text; } - (NSString *)accessibilityLabelForContent { return @""; } - (BOOL)performPlayActionForAccessibility { return NO; } - (BOOL)shouldHideBubbleBackground { return NO; } + (UIFont *)textFont { return [UIFont systemFontOfSize: [ChatMessageCell textFontSize]]; } + (CGFloat)textFontSize { return [UserSettings sharedUserSettings].chatFontSize; } + (UIFont *)quoteFont { return [UIFont systemFontOfSize: [ChatMessageCell quoteFontSize]]; } + (CGFloat)quoteFontSize { return [UserSettings sharedUserSettings].chatFontSize * QUOTE_FONT_SIZE_FACTOR; } + (UIFont *)quoteIdentityFont { return [UIFont boldSystemFontOfSize: [ChatMessageCell quoteIdentityFontSize]]; } + (CGFloat)quoteIdentityFontSize { return [UserSettings sharedUserSettings].chatFontSize * QUOTE_FONT_SIZE_FACTOR; } + (UIFont *)emojiFont { return [UIFont systemFontOfSize: [ChatMessageCell emojiFontSize]]; } + (CGFloat)emojiFontSize { return MIN(EMOJI_MAX_FONT_SIZE, [UserSettings sharedUserSettings].chatFontSize * EMOJI_FONT_SIZE_FACTOR); } - (BOOL)highlightOccurencesOf:(NSString *)pattern { // default implementation does nothing return NO; } + (NSAttributedString *)highlightedOccurencesOf:(NSString *)pattern inString:(NSString *)text { BOOL hasMatches = NO; NSRange searchRange = NSMakeRange(0, text.length); UIFont *font = [ChatMessageCell textFont]; NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text]; [attributedText addAttribute:NSFontAttributeName value:font range:searchRange]; [attributedText addAttribute:NSForegroundColorAttributeName value:[Colors fontNormal] range:searchRange]; // fixes line height issues when text contains emojis (https://github.com/TTTAttributedLabel/TTTAttributedLabel/issues/405) NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; paragraphStyle.lineHeightMultiple = 1.0; paragraphStyle.minimumLineHeight = font.lineHeight; paragraphStyle.maximumLineHeight = font.lineHeight; [attributedText addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:searchRange]; if (pattern == nil) { return nil; } // options should match EntityFetcher options for fulltext search: [cd] NSStringCompareOptions options = NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch; UIColor *highlightColor = [UIColor redColor]; while (true) { NSRange range = [text rangeOfString:pattern options:options range:searchRange]; if (range.location == NSNotFound) { break; } hasMatches = YES; [attributedText addAttribute:NSForegroundColorAttributeName value:highlightColor range:range]; NSInteger location = range.location + range.length; searchRange = NSMakeRange(location, text.length - location); } return attributedText; } - (UIViewController *)previewViewController { // default has no preview return nil; } - (UIViewController *)previewViewControllerFor:(id)previewingContext viewControllerForLocation:(CGPoint)location { // default has no preview return nil; } - (void)willDisplay { //default implementation does nothing; } - (void)didEndDisplaying { self.editing = false; //default implementation does nothing; } #pragma mark - QBPopupMenuDelegate - (void)popupMenuWillAppear:(QBPopupMenu *)popupMenu { [self setBubbleHighlighted:YES]; } - (void)popupMenuWillDisappear:(QBPopupMenu *)popupMenu { [self setBubbleHighlighted:NO]; UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self); } #pragma mark - Accessibility - (NSArray *)accessibilityCustomActions { NSMutableArray *actions = [NSMutableArray new]; if ([self canPerformAction:@selector(userackMessage:) withSender:nil]) { UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"acknowledge", @"") target:self selector:@selector(userackMessage:)]; [actions addObject:action]; } if ([self canPerformAction:@selector(userdeclineMessage:) withSender:nil]) { UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"decline", @"") target:self selector:@selector(userdeclineMessage:)]; [actions addObject:action]; } if ([self canPerformAction:@selector(quoteMessage:) withSender:nil]) { UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"quote", @"") target:self selector:@selector(quoteMessage:)]; [actions addObject:action]; } if (UIAccessibilityIsSpeakSelectionEnabled()) { if ([self canPerformAction:@selector(speakMessage:) withSender:nil]) { UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"speak", @"") target:self selector:@selector(speakMessage:)]; [actions addObject:action]; } } if ([self canPerformAction:@selector(copyMessage:) withSender:nil]) { UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"copy", @"") target:self selector:@selector(copyMessage:)]; [actions addObject:action]; } if ([self canPerformAction:@selector(shareMessage:) withSender:nil]) { UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"share", @"") target:self selector:@selector(shareMessage:)]; [actions addObject:action]; } if ([self canPerformAction:@selector(resendMessage:) withSender:nil]) { UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"try_again", @"") target:self selector:@selector(resendMessage:)]; [actions addObject:action]; } if ([self canPerformAction:@selector(detailsMessage:) withSender:nil]) { UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"details", @"") target:self selector:@selector(detailsMessage:)]; [actions addObject:action]; } if ([self canPerformAction:@selector(deleteMessage:) withSender:nil]) { UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"delete", @"") target:self selector:@selector(deleteMessage:)]; [actions addObject:action]; } return actions; } #pragma mark - Contact picker delegate - (void)contactPicker:(ContactGroupPickerViewController*)contactPicker didPickConversations:(NSSet *)conversations renderType:(NSNumber *)renderType sendAsFile:(BOOL)sendAsFile { if ([self.message isKindOfClass: [TextMessage class]]) { TextMessage *textMessage = (TextMessage *)message; for (Conversation *conversation in conversations) { [MessageSender sendMessage:textMessage.text inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) { ;//nop }]; if (contactPicker.additionalTextToSend) { [MessageSender sendMessage:contactPicker.additionalTextToSend inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) { ;//nop }]; } } [contactPicker dismissViewControllerAnimated:YES completion:nil]; } else if ([self.message isKindOfClass: [LocationMessage class]]) { LocationMessage *locationMessage = (LocationMessage *)message; CLLocationCoordinate2D coordinates = CLLocationCoordinate2DMake(locationMessage.latitude.doubleValue, locationMessage.longitude.doubleValue); double accurracy = locationMessage.accuracy.doubleValue; for (Conversation *conversation in conversations) { [MessageSender sendLocation:coordinates accuracy:accurracy poiName:locationMessage.poiName poiAddress:nil inConversation:conversation onCompletion:^(NSData *messageId) { ;//nop }]; if (contactPicker.additionalTextToSend) { [MessageSender sendMessage:contactPicker.additionalTextToSend inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) { ;//nop }]; } } [contactPicker dismissViewControllerAnimated:YES completion:nil]; } else if ([self.message isKindOfClass: [FileMessage class]] || sendAsFile == true) { [self handleFileMessagefromContactPicker:contactPicker didPickConversations:conversations]; } else if ([self.message isKindOfClass: [AudioMessage class]]) { AudioMessage *audioMessage = (AudioMessage *)message; NSData *data = [audioMessage.audio.data copy]; for (Conversation *conversation in conversations) { URLSenderItem *item = [URLSenderItem itemWithData:data fileName:@"audio.m4a" type:UTTYPE_AUDIO renderType:@0 sendAsFile:true]; FileMessageSender *sender = [[FileMessageSender alloc] init]; [sender sendItem:item inConversation:conversation requestId:nil]; if (contactPicker.additionalTextToSend) { item.caption = contactPicker.additionalTextToSend; } } [contactPicker dismissViewControllerAnimated:YES completion:nil]; } else if ([self.message isKindOfClass: [ImageMessage class]]) { ImageMessage *imageMessage = (ImageMessage *)message; NSString *caption = contactPicker.additionalTextToSend; // A ImageMessage can never be sent as file, thus the image data will always be converted [self forwardImageMessage:imageMessage toConversations:conversations additionalTextToSend:caption]; [contactPicker dismissViewControllerAnimated:YES completion:nil]; } else if ([self.message isKindOfClass: [VideoMessage class]]) { VideoMessage *videoMessage = (VideoMessage *)message; NSURL *videoURL = [VideoURLSenderItemCreator writeToTemporaryDirectoryWithData:videoMessage.video.data]; if (videoURL == nil) { DDLogError(@"VideoURL was nil."); return; } VideoURLSenderItemCreator *senderCreator = [[VideoURLSenderItemCreator alloc] init]; URLSenderItem *senderItem = [senderCreator senderItemFrom:videoURL]; for (Conversation *conversation in conversations) { if (contactPicker.additionalTextToSend) { senderItem.caption = contactPicker.additionalTextToSend; } FileMessageSender *sender = [[FileMessageSender alloc] init]; [sender sendItem:senderItem inConversation:conversation requestId:nil]; } [contactPicker dismissViewControllerAnimated:YES completion:^(){ [VideoURLSenderItemCreator cleanTemporaryDirectory]; }]; } } - (void) handleFileMessagefromContactPicker:(ContactGroupPickerViewController *)contactPicker didPickConversations:(NSSet *)conversations { FeatureMaskChecker *featureMaskChecker = [[FeatureMaskChecker alloc] init]; URLSenderItem *item; if ([self.message isKindOfClass: [FileMessage class]]) { FileMessage *fileMessage = (FileMessage *)message; NSNumber *type = fileMessage.type; // Voice Messages are always forwarded with rendering type 0 if ([UTIConverter isAudioMimeType:fileMessage.mimeType]) { type = @0; } item = [URLSenderItem itemWithData:fileMessage.data.data fileName:fileMessage.fileName type:fileMessage.blobGetUTI renderType:type sendAsFile:true]; } else if ([self.message isKindOfClass: [AudioMessage class]]) { // AudioMessage *audioMessage = (AudioMessage *)message; // item = [URLSenderItem itemWithData:audioMessage.audio.data fileName:audioMessage.getFilename type:audioMessage.blobGetUTI renderType:@0 sendAsFile:true]; AudioMessage *audioMessage = (AudioMessage *)message; NSData *data = [audioMessage.audio.data copy]; for (Conversation *conversation in conversations) { AudioMessageSender *sender = [[AudioMessageSender alloc] init]; [sender startWithAudioData:data duration:audioMessage.duration inConversation:conversation requestId:nil]; if (contactPicker.additionalTextToSend) { [MessageSender sendMessage:contactPicker.additionalTextToSend inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) { ;//nop }]; item.caption = contactPicker.additionalTextToSend; } } [contactPicker dismissViewControllerAnimated:YES completion:nil]; } else if ([self.message isKindOfClass: [ImageMessage class]]) { ImageMessage *imageMessage = (ImageMessage *)message; item = [URLSenderItem itemWithData:imageMessage.image.data fileName:imageMessage.getFilename type:imageMessage.blobGetUTI renderType:@0 sendAsFile:true]; } else if ([self.message isKindOfClass: [VideoMessage class]]) { VideoMessage *videoMessage = (VideoMessage *)message; item = [URLSenderItem itemWithData:videoMessage.video.data fileName:videoMessage.getFilename type:videoMessage.blobGetUTI renderType:@0 sendAsFile:true]; } if (contactPicker.additionalTextToSend) { item.caption = contactPicker.additionalTextToSend; } [featureMaskChecker checkFileTransferFor:conversations presentAlarmOn:contactPicker onSuccess:^{ for (Conversation *conversation in conversations) { FileMessageSender *urlSender = [[FileMessageSender alloc] init]; [urlSender sendItem:item inConversation:conversation]; } [contactPicker dismissViewControllerAnimated:YES completion:nil]; } onFailure:^{ }]; } - (void)forwardImageMessage:(ImageMessage *)imageMessage toConversations:(NSSet *)conversations additionalTextToSend:(NSString *)additionalText { // Images in ImageMessage are always jpg CFStringRef uti = kUTTypeJPEG; for (Conversation *conversation in conversations) { ImageURLSenderItemCreator *imageSender = [[ImageURLSenderItemCreator alloc] init]; URLSenderItem *item = [imageSender senderItemFrom:imageMessage.image.data uti:(__bridge NSString *)uti]; if (additionalText) { item.caption = additionalText; } FileMessageSender *sender = [[FileMessageSender alloc] init]; [sender sendItem:item inConversation:conversation]; } } - (void)contactPickerDidCancel:(ContactGroupPickerViewController*)contactPicker { [contactPicker dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - ModalNavigationControllerDelegate - (void)willDismissModalNavigationController { } #pragma mark Gesture Recognizer - (IBAction)panGestureCellAction:(UIPanGestureRecognizer *)recognizer { if (UIAccessibilityIsVoiceOverRunning() || ![self canPerformAction:@selector(quoteMessage:) withSender:nil] || self.chatVc.chatContent.editing == true) { quoteSlideIconImage.alpha = 0.0; [recognizer.view setFrame: CGRectMake(recognizer.view.frame.origin.x, recognizer.view.frame.origin.y, recognizer.view.frame.size.width, recognizer.view.frame.size.height)]; return; } CGPoint translation = [recognizer translationInView:self.chatVc.view]; if (recognizer.view.frame.origin.x < 0) { quoteSlideIconImage.alpha = 0.0; [recognizer.view setFrame: CGRectMake(0, recognizer.view.frame.origin.y, recognizer.view.frame.size.width, recognizer.view.frame.size.height)]; return; } _messageToQuote = self.message; recognizer.view.center = CGPointMake(recognizer.view.center.x+ translation.x, recognizer.view.center.y ); [recognizer setTranslation:CGPointMake(0, 0) inView:self.chatVc.view]; if(recognizer.view.frame.origin.x > [UIScreen mainScreen].bounds.size.width * 0.9) { [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ quoteSlideIconImage.alpha = 0.0; [recognizer.view setFrame: CGRectMake(0, recognizer.view.frame.origin.y, recognizer.view.frame.size.width, recognizer.view.frame.size.height)]; } completion:nil]; } CGFloat minX = self.msgBackground.frame.size.width/2 < 75.0 && message.isOwn.boolValue ? self.msgBackground.frame.size.width / 2 : 75.0; CGFloat newAlpha = recognizer.view.frame.origin.x / minX; if (quoteSlideIconImage.alpha < 0.8 && newAlpha >= 0.8) { [gen prepare]; } if (quoteSlideIconImage.alpha < 1.0 && newAlpha >= 1.0) { [gen impactOccurred]; } if (message.isOwn.boolValue) { quoteSlideIconImage.frame = CGRectMake(dateLabel.frame.origin.x - 30.0, recognizer.view.frame.origin.y + (recognizer.view.frame.size.height / 2) - 10.0, 20.0, 20.0); } else { if (message.conversation.isGroup == true) { quoteSlideIconImage.frame = CGRectMake(groupSenderImage.frame.origin.x - 30.0, recognizer.view.frame.origin.y + (recognizer.view.frame.size.height / 2) - 10.0, 20.0, 20.0); } else { quoteSlideIconImage.frame = CGRectMake(self.msgBackground.frame.origin.x - 30.0, recognizer.view.frame.origin.y + (recognizer.view.frame.size.height / 2) - 10.0, 20.0, 20.0); } } quoteSlideIconImage.alpha = recognizer.view.frame.origin.x / minX; if (recognizer.state == UIGestureRecognizerStateEnded) { int x = recognizer.view.frame.origin.x; [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ quoteSlideIconImage.alpha = 0.0; [recognizer.view setFrame: CGRectMake(0, recognizer.view.frame.origin.y, recognizer.view.frame.size.width, recognizer.view.frame.size.height)]; } completion:^(BOOL finished) { if (x > minX) { [self quoteMessage:nil]; } else { _messageToQuote = nil; } }]; } } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) { if (self.chatVc.chatContent.editing == true) { return true; } CGPoint velocity = [((UIPanGestureRecognizer *) gestureRecognizer) velocityInView:self.chatVc.chatContent]; if (fabs(velocity.x) >= fabs(velocity.y)) { if (velocity.x < 0) { quoteSlideIconImage.alpha = 0.0; [gestureRecognizer.view setFrame: CGRectMake(0, gestureRecognizer.view.frame.origin.y, gestureRecognizer.view.frame.size.width, gestureRecognizer.view.frame.size.height)]; return false; } } return fabs(velocity.x) >= fabs(velocity.y); } return true; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) { return false; } return true; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return [otherGestureRecognizer isKindOfClass:[UIScreenEdgePanGestureRecognizer class]]; } @end