12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // 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 <https://www.gnu.org/licenses/>.
- #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 () <QBPopupMenuDelegate, ModalNavigationControllerDelegate, ContactGroupPickerDelegate, MGSwipeTableCellDelegate>
- @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<UIMenuElement *> * _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<UIViewControllerPreviewing>)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
|