ChatMessageCell.m 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2012-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "ChatMessageCell.h"
  21. #import "ChatDefines.h"
  22. #import "TextMessage.h"
  23. #import "Conversation.h"
  24. #import "Contact.h"
  25. #import "MessageSender.h"
  26. #import "ChatViewController.h"
  27. #import "CustomResponderTextView.h"
  28. #import "Utils.h"
  29. #import "UserSettings.h"
  30. #import "EntityManager.h"
  31. #import "AvatarMaker.h"
  32. #import "ActivityUtil.h"
  33. #import "UIImage+ColoredImage.h"
  34. #import "QBPopupMenu.h"
  35. #import "RectUtil.h"
  36. #import "BaseMessage+Accessibility.h"
  37. #import "BundleUtil.h"
  38. #import "Threema-Swift.h"
  39. #import "ContactGroupPickerViewController.h"
  40. #import "FeatureMaskChecker.h"
  41. #import "ChatTextMessageCell.h"
  42. #import "FileMessageSender.h"
  43. #import "AudioMessageSender.h"
  44. #define DATE_LABEL_BG_COLOR [[Colors backgroundDark] colorWithAlphaComponent:0.9]
  45. #define REQUIRED_MENU_HEIGHT 50.0
  46. #define EMOJI_FONT_SIZE_FACTOR 3
  47. #define EMOJI_MAX_FONT_SIZE 50
  48. #define QUOTE_FONT_SIZE_FACTOR 0.8
  49. #ifdef DEBUG
  50. static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
  51. #else
  52. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  53. #endif
  54. @interface ChatMessageCell () <QBPopupMenuDelegate, ModalNavigationControllerDelegate, ContactGroupPickerDelegate, MGSwipeTableCellDelegate>
  55. @property QBPopupMenu *popupMenu;
  56. @end
  57. @implementation ChatMessageCell {
  58. UIImageView *msgBackground;
  59. UIImageView *statusImage;
  60. UILabel *dateLabel;
  61. UIImageView *typingIndicator;
  62. UIImageView *groupSenderImage;
  63. UIImageView *quoteSlideIconImage;
  64. BOOL transparent;
  65. CGSize bubbleSize;
  66. UITapGestureRecognizer *dtgr;
  67. UIPanGestureRecognizer *pan;
  68. UIImpactFeedbackGenerator *gen;
  69. BaseMessage *_messageToQuote;
  70. }
  71. @synthesize message;
  72. @synthesize typing;
  73. @synthesize chatVc;
  74. @synthesize statusImage;
  75. @synthesize msgBackground;
  76. @synthesize dtgr;
  77. + (CGFloat)heightForMessage:(BaseMessage*)message forTableWidth:(CGFloat)tableWidth {
  78. return 0;
  79. }
  80. - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier transparent:(BOOL)_transparent
  81. {
  82. self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
  83. if (self) {
  84. transparent = _transparent;
  85. self.backgroundColor = transparent ? [UIColor clearColor] : [Colors background]; // clearColor slows performance
  86. // Create message background image view
  87. msgBackground = [[UIImageView alloc] init];
  88. msgBackground.clearsContextBeforeDrawing = NO;
  89. msgBackground.backgroundColor = transparent ? [UIColor clearColor] : [Colors background]; // clearColor slows performance
  90. [self.contentView addSubview:msgBackground];
  91. // Status image
  92. statusImage = [[UIImageView alloc] init];
  93. statusImage.contentMode = UIViewContentModeScaleAspectFit;
  94. [self.contentView addSubview:statusImage];
  95. // Date label
  96. if (transparent && [UserSettings sharedUserSettings].wallpaper) {
  97. dateLabel = [[RoundedRectLabel alloc] init];
  98. ((RoundedRectLabel*)dateLabel).cornerRadius = 6;
  99. dateLabel.backgroundColor = DATE_LABEL_BG_COLOR;
  100. } else {
  101. dateLabel = [[UILabel alloc] init];
  102. dateLabel.backgroundColor = [UIColor clearColor];
  103. }
  104. dateLabel.font = [UIFont systemFontOfSize:MAX(11.0f, MIN(14.0, roundf([UserSettings sharedUserSettings].chatFontSize * 11.0 / 16.0)))];
  105. dateLabel.numberOfLines = 2;
  106. [self.contentView addSubview:dateLabel];
  107. UIImage *quoteImage = [[BundleUtil imageNamed:@"Quote"] imageWithTint:[Colors fontNormal]];
  108. quoteSlideIconImage = [[UIImageView alloc] initWithImage:quoteImage];
  109. quoteSlideIconImage.alpha = 0.0;
  110. [self.contentView addSubview:quoteSlideIconImage];
  111. gen = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
  112. // Typing indicator
  113. typingIndicator = [[UIImageView alloc] init];
  114. [self.contentView addSubview:typingIndicator];
  115. // Add gesture recognizers for copying (cannot use shouldShowMenuForRowAtIndexPath as we need
  116. // control over the horizontal position of the menu)
  117. if (@available(iOS 13.0, *)) {
  118. } else {
  119. UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
  120. [self addGestureRecognizer:lpgr];
  121. dtgr = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)];
  122. dtgr.numberOfTapsRequired = 2;
  123. [self addGestureRecognizer:dtgr];
  124. }
  125. pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureCellAction:)];
  126. pan.delegate = self;
  127. [self.contentView addGestureRecognizer:pan];
  128. [self setupColors];
  129. }
  130. return self;
  131. }
  132. - (void)setupColors {
  133. dateLabel.textColor = [Colors fontLight];
  134. typingIndicator.image = [self getStatusImageNamed:@"Typing" withCustomColor:nil];
  135. self.tintColor = [Colors main];
  136. UIView *v = [[UIView alloc] init];
  137. v.backgroundColor = [[Colors main] colorWithAlphaComponent:0.1];
  138. self.selectedBackgroundView = v;
  139. quoteSlideIconImage.image = [[BundleUtil imageNamed:@"Quote"] imageWithTint:[Colors fontNormal]];
  140. }
  141. - (void)dealloc {
  142. @try {
  143. [message removeObserver:self forKeyPath:@"read"];
  144. [message removeObserver:self forKeyPath:@"delivered"];
  145. [message removeObserver:self forKeyPath:@"sent"];
  146. [message removeObserver:self forKeyPath:@"userack"];
  147. }
  148. @catch(NSException *e) {}
  149. }
  150. - (void)setMessage:(BaseMessage *)newMessage {
  151. @try {
  152. [message removeObserver:self forKeyPath:@"read"];
  153. [message removeObserver:self forKeyPath:@"delivered"];
  154. [message removeObserver:self forKeyPath:@"sent"];
  155. [message removeObserver:self forKeyPath:@"userack"];
  156. }
  157. @catch(NSException *e) {}
  158. message = newMessage;
  159. [self setupColors];
  160. [self setBubbleHighlighted:NO];
  161. if (message.isOwn.boolValue) { // right bubble
  162. msgBackground.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
  163. statusImage.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
  164. dateLabel.textAlignment = NSTextAlignmentRight;
  165. dateLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
  166. } else { // left bubble
  167. msgBackground.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  168. statusImage.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  169. dateLabel.textAlignment = NSTextAlignmentLeft;
  170. dateLabel.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  171. }
  172. [self updateStatusImage];
  173. [self updateDateLabel];
  174. [self updateTypingIndicator];
  175. [self updateGroupSenderImage];
  176. [self setNeedsLayout];
  177. if (!self.chatVc.isOpenWithForceTouch) {
  178. [message addObserver:self forKeyPath:@"read" options:0 context:nil];
  179. [message addObserver:self forKeyPath:@"delivered" options:0 context:nil];
  180. [message addObserver:self forKeyPath:@"sent" options:0 context:nil];
  181. [message addObserver:self forKeyPath:@"userack" options:0 context:nil];
  182. }
  183. }
  184. - (void)setBubbleContentSize:(CGSize)size {
  185. CGFloat bgWidthMargin = 30.0f;
  186. CGFloat bgHeightMargin = 16.0f;
  187. bubbleSize = CGSizeMake(size.width+bgWidthMargin, size.height+bgHeightMargin);
  188. }
  189. - (void)setBubbleSize:(CGSize)size {
  190. bubbleSize = size;
  191. }
  192. - (void)layoutSubviews {
  193. [super layoutSubviews];
  194. if (_popupMenu.isVisible) {
  195. [_popupMenu dismissAnimated:YES];
  196. }
  197. CGFloat bgTopOffset = 1.0f;
  198. CGFloat bgSideMargin = 6.0f;
  199. CGSize dateLabelSize = [dateLabel sizeThatFits:CGSizeMake(60, 28)];
  200. CGFloat dateLabelWidth = ceilf(dateLabelSize.width);
  201. CGFloat dateLabelHeight = ceilf(dateLabelSize.height);
  202. if (message.isOwn.boolValue) { // right bubble
  203. msgBackground.frame = CGRectMake(self.contentView.frame.size.width-bubbleSize.width-bgSideMargin,
  204. bgTopOffset, bubbleSize.width, bubbleSize.height);
  205. CGFloat bubbleMaxY = CGRectGetMaxY(msgBackground.frame);
  206. statusImage.frame = CGRectMake(msgBackground.frame.origin.x - 8 - 20, bubbleMaxY - 27, 20, 18);
  207. dateLabel.frame = CGRectMake(statusImage.frame.origin.x - dateLabelWidth - 8, 0, dateLabelWidth, dateLabelHeight);;
  208. typingIndicator.frame = CGRectMake(4, bubbleMaxY - 22, 22, 20);
  209. if (statusImage.hidden) {
  210. dateLabel.frame = CGRectOffset(dateLabel.frame, 28, 0);
  211. }
  212. if (dateLabel.hidden == NO) {
  213. [self verticalAlignDateLabel];
  214. }
  215. } else { // left bubble
  216. msgBackground.frame = CGRectMake(bgSideMargin + self.contentLeftOffset, bgTopOffset, bubbleSize.width, bubbleSize.height);
  217. CGFloat bubbleMaxY = CGRectGetMaxY(msgBackground.frame);
  218. groupSenderImage.frame = CGRectMake(12, bubbleMaxY - 31, 27, 27);
  219. CGFloat xOffset = msgBackground.frame.origin.x + msgBackground.frame.size.width + 8;
  220. if (statusImage.hidden == NO) {
  221. statusImage.frame = CGRectMake(xOffset, bubbleMaxY - 27, 20, 18);
  222. xOffset += 28;
  223. }
  224. if (dateLabel.hidden == NO) {
  225. dateLabel.frame = CGRectMake(xOffset, bubbleMaxY - dateLabelHeight - 4, dateLabelWidth, dateLabelHeight);
  226. xOffset += 44;
  227. [self verticalAlignDateLabel];
  228. }
  229. if (typingIndicator.hidden == NO) {
  230. if (xOffset > (self.contentView.frame.size.width - 30)) {
  231. xOffset = self.contentView.frame.size.width - 30;
  232. }
  233. typingIndicator.frame = CGRectMake(xOffset, bubbleMaxY - 28, 22, 20);
  234. }
  235. }
  236. }
  237. - (void)verticalAlignDateLabel {
  238. if (statusImage.hidden) {
  239. CGFloat bubbleMaxY = CGRectGetMaxY(msgBackground.frame);
  240. dateLabel.frame = [RectUtil setYPositionOf:dateLabel.frame y:bubbleMaxY - dateLabel.frame.size.height - 11];
  241. } else {
  242. dateLabel.frame = [RectUtil rect:dateLabel.frame alignVerticalWith:statusImage.frame round:YES];
  243. }
  244. }
  245. - (void)updateDateLabel {
  246. NSDate *date = [message dateForCurrentState];
  247. if (date != nil) {
  248. if (![Utils isSameDayWithDate1:date date2:message.remoteSentDate]) {
  249. dateLabel.text = [DateFormatter shortStyleDateTime:date];
  250. } else {
  251. dateLabel.text = [DateFormatter shortStyleTimeNoDate:date];
  252. }
  253. /* set background again as it seems to be lost sometimes with RoundedRectLabel */
  254. if (transparent && [UserSettings sharedUserSettings].wallpaper)
  255. dateLabel.backgroundColor = DATE_LABEL_BG_COLOR;
  256. else
  257. dateLabel.backgroundColor = [UIColor clearColor];
  258. dateLabel.hidden = NO;
  259. } else {
  260. dateLabel.hidden = YES;
  261. }
  262. /* received message - show timestamp only if setting is enabled */
  263. if (!message.isOwn.boolValue) {
  264. dateLabel.hidden = [UserSettings sharedUserSettings].showReceivedTimestamps == NO;
  265. }
  266. }
  267. - (void)updateTypingIndicator {
  268. if (typing) {
  269. typingIndicator.hidden = NO;
  270. [self setNeedsLayout];
  271. } else {
  272. typingIndicator.hidden = YES;
  273. }
  274. }
  275. - (UIImage*)bubbleImageWithHighlight:(BOOL)bubbleHighlight {
  276. if (self.shouldHideBubbleBackground) {
  277. return nil;
  278. }
  279. if (message.isOwn.boolValue) {
  280. NSString *name = @"ChatBubbleSentMask";
  281. if (bubbleHighlight) {
  282. return [[UIImage imageNamed:name inColor:[Colors bubbleSentSelected]] stretchableImageWithLeftCapWidth:15 topCapHeight:13];
  283. } else {
  284. return [[UIImage imageNamed:name inColor:[Colors bubbleSent]] stretchableImageWithLeftCapWidth:15 topCapHeight:13];
  285. }
  286. } else {
  287. NSString *name = @"ChatBubbleReceivedMask";
  288. if (bubbleHighlight) {
  289. return [[UIImage imageNamed:name inColor:[Colors bubbleReceivedSelected]] stretchableImageWithLeftCapWidth:23 topCapHeight:15];
  290. } else {
  291. return [[UIImage imageNamed:name inColor:[Colors bubbleReceived]] stretchableImageWithLeftCapWidth:23 topCapHeight:15];
  292. }
  293. }
  294. }
  295. - (void)updateStatusImage {
  296. NSString *iconName;
  297. UIColor *color;
  298. if (message.conversation.groupId != nil || message.conversation.contact.isGatewayId) {
  299. /* group messages & gateway IDs don't have delivered/read status */
  300. if (message.isOwn.boolValue && message.sent.boolValue == NO) {
  301. iconName = @"MessageStatus_sending";
  302. }
  303. } else {
  304. if (message.isOwn.boolValue) {
  305. if (message.read.boolValue) {
  306. iconName = @"MessageStatus_read";
  307. } else if (message.delivered.boolValue) {
  308. iconName = @"MessageStatus_delivered";
  309. } else {
  310. if (message.sent.boolValue) {
  311. iconName = @"MessageStatus_sent";
  312. } else {
  313. iconName = @"MessageStatus_sending";
  314. }
  315. }
  316. }
  317. }
  318. if (message.userackDate != nil) {
  319. if (message.userack.boolValue) {
  320. iconName = @"MessageStatus_thumb_up";
  321. color = [Colors green];
  322. } else if (message.userack.boolValue == NO) {
  323. iconName = @"MessageStatus_thumb_down";
  324. color = [Colors orange];
  325. }
  326. }
  327. if (iconName) {
  328. statusImage.image = [self getStatusImageNamed:iconName withCustomColor:color];
  329. statusImage.alpha = 0.8;
  330. statusImage.hidden = NO;
  331. } else {
  332. statusImage.hidden = YES;
  333. }
  334. [self setNeedsLayout];
  335. }
  336. - (UIImage *)getStatusImageNamed:(NSString *)imageName withCustomColor:(UIColor *)color {
  337. if (color == nil && [UserSettings sharedUserSettings].wallpaper == nil) {
  338. color = [Colors fontLight];
  339. }
  340. if (color) {
  341. return [UIImage imageNamed:imageName inColor:color];
  342. } else {
  343. NSString *glowImageName = [NSString stringWithFormat:@"%@_glow", imageName];
  344. return [UIImage imageNamed:glowImageName];
  345. }
  346. }
  347. - (void)updateGroupSenderImage {
  348. if (message.sender == nil || message.isOwn.boolValue) {
  349. /* not an outgoing group message */
  350. groupSenderImage.hidden = YES;
  351. groupSenderImage.image = nil;
  352. return;
  353. }
  354. if (groupSenderImage == nil) {
  355. groupSenderImage = [[UIImageView alloc] init];
  356. [self.contentView addSubview:groupSenderImage];
  357. }
  358. groupSenderImage.image = [BundleUtil imageNamed:@"Unknown"];
  359. [[AvatarMaker sharedAvatarMaker] avatarForContact:message.sender size:27.0f masked:YES onCompletion:^(UIImage *avatarImage) {
  360. dispatch_async(dispatch_get_main_queue(), ^{
  361. groupSenderImage.image = avatarImage;
  362. });
  363. }];
  364. groupSenderImage.hidden = NO;
  365. }
  366. - (void)handleDoubleTap:(UIGestureRecognizer *)gestureRecognizer {
  367. if (gestureRecognizer.state == UIGestureRecognizerStateEnded && self.chatVc.chatContent.editing == NO) {
  368. [self showMenu];
  369. }
  370. }
  371. - (void)handleLongPress:(UIGestureRecognizer *)gestureRecognizer {
  372. if (gestureRecognizer.state == UIGestureRecognizerStateBegan && self.chatVc.chatContent.editing == NO) {
  373. [self showMenu];
  374. }
  375. }
  376. - (void)showMenu {
  377. NSMutableArray *menuItems = [NSMutableArray array];
  378. if ([self canPerformAction:@selector(userackMessage:) withSender:nil]) {
  379. UIImage *ackImage = [UIImage imageNamed:@"MessageStatus_thumb_up" inColor:[Colors green]];
  380. QBPopupMenuItem *item = [QBPopupMenuItem itemWithImage:ackImage target:self action:@selector(userackMessage:)];
  381. item.accessibilityLabel = NSLocalizedString(@"acknowledge", nil);
  382. [menuItems addObject:item];
  383. }
  384. if ([self canPerformAction:@selector(userdeclineMessage:) withSender:nil]) {
  385. UIImage *declineImage = [UIImage imageNamed:@"MessageStatus_thumb_down" inColor:[Colors orange]];
  386. QBPopupMenuItem *item = [QBPopupMenuItem itemWithImage:declineImage target:self action:@selector(userdeclineMessage:)];
  387. item.accessibilityLabel = NSLocalizedString(@"decline", nil);
  388. [menuItems addObject:item];
  389. }
  390. if ([self canPerformAction:@selector(quoteMessage:) withSender:nil]) {
  391. UIImage *quoteImage = [UIImage imageNamed:@"Quote" inColor:[UIColor whiteColor]];
  392. QBPopupMenuItem *item = [QBPopupMenuItem itemWithImage:quoteImage target:self action:@selector(quoteMessage:)];
  393. item.accessibilityLabel = NSLocalizedString(@"quote", nil);
  394. [menuItems addObject:item];
  395. }
  396. if (UIAccessibilityIsSpeakSelectionEnabled()) {
  397. if ([self canPerformAction:@selector(speakMessage:) withSender:nil]) {
  398. [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"speak", nil) target:self action:@selector(speakMessage:)]];
  399. }
  400. }
  401. if ([self canPerformAction:@selector(copyMessage:) withSender:nil]) {
  402. [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"copy", nil) target:self action:@selector(copyMessage:)]];
  403. }
  404. if ([self canPerformAction:@selector(shareMessage:) withSender:nil]) {
  405. [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"share", nil) target:self action:@selector(shareMessage:)]];
  406. }
  407. if ([self canPerformAction:@selector(resendMessage:) withSender:nil]) {
  408. [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"try_again", nil) target:self action:@selector(resendMessage:)]];
  409. }
  410. if ([self canPerformAction:@selector(detailsMessage:) withSender:nil]) {
  411. [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"details", nil) target:self action:@selector(detailsMessage:)]];
  412. }
  413. if ([self canPerformAction:@selector(deleteMessage:) withSender:nil]) {
  414. [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"delete", nil) target:self action:@selector(deleteMessage:)]];
  415. }
  416. _popupMenu = [[QBPopupMenu alloc] initWithItems:menuItems];
  417. _popupMenu.delegate = self;
  418. _popupMenu.color = [Colors popupMenuBackground];
  419. _popupMenu.highlightedColor = [Colors popupMenuHighlight];
  420. _popupMenu.nextPageAccessibilityLabel = NSLocalizedString(@"showNext", nil);
  421. _popupMenu.previousPageAccessibilityLabel = NSLocalizedString(@"showPrevious", nil);
  422. CGRect targetRect = [self targetRectForMenuPopup];
  423. [_popupMenu showInView:chatVc.view targetRect:targetRect animated:YES];
  424. UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, _popupMenu);
  425. /* add view to add a quit for voice over */
  426. UIView *quitView = [[UIView alloc] initWithFrame:chatVc.view.subviews.lastObject.frame];
  427. quitView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4];
  428. quitView.alpha = 1;
  429. quitView.isAccessibilityElement = YES;
  430. quitView.accessibilityLabel = [BundleUtil localizedStringForKey:@"quit"];
  431. quitView.accessibilityActivationPoint = CGPointMake(0.0, 0.0);
  432. quitView.backgroundColor = [UIColor clearColor];
  433. quitView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  434. quitView.userInteractionEnabled = NO;
  435. [chatVc.view.subviews.lastObject insertSubview:quitView belowSubview:_popupMenu];
  436. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapQuitPopoverMenu:)];
  437. [quitView addGestureRecognizer: tapGesture];
  438. }
  439. - (UIContextMenuConfiguration *)getContextMenu:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) {
  440. if (self.editing) {
  441. return nil;
  442. }
  443. UIContextMenuConfiguration *conf = [UIContextMenuConfiguration configurationWithIdentifier:indexPath previewProvider:^UIViewController * _Nullable{
  444. return nil;
  445. } actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
  446. NSMutableArray *menuItems = [NSMutableArray array];
  447. NSMutableArray *deleteItems = [NSMutableArray array];
  448. if ([self canPerformAction:@selector(userackMessage:) withSender:nil]) {
  449. UIImage *ackImage = [UIImage imageNamed:@"MessageStatus_thumb_up" inColor:[Colors green]];
  450. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"acknowledge", nil) image:ackImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  451. [self userackMessage:nil];
  452. }];
  453. [menuItems addObject:action];
  454. }
  455. if ([self canPerformAction:@selector(userdeclineMessage:) withSender:nil]) {
  456. UIImage *declineImage = [UIImage imageNamed:@"MessageStatus_thumb_down" inColor:[Colors orange]];
  457. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"decline", nil) image:declineImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  458. [self userdeclineMessage:nil];
  459. }];
  460. [menuItems addObject:action];
  461. }
  462. if ([self canPerformAction:@selector(quoteMessage:) withSender:nil]) {
  463. UIImage *quoteImage = [UIImage systemImageNamed:@"quote.bubble.fill" compatibleWithTraitCollection:self.traitCollection];
  464. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"quote", nil) image:quoteImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  465. [self quoteMessage:nil];
  466. }];
  467. [menuItems addObject:action];
  468. }
  469. if (UIAccessibilityIsSpeakSelectionEnabled()) {
  470. if ([self canPerformAction:@selector(speakMessage:) withSender:nil]) {
  471. UIImage *speakImage = [UIImage systemImageNamed:@"text.bubble.fill" compatibleWithTraitCollection:self.traitCollection];
  472. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"speak", nil) image:speakImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  473. [self speakMessage:nil];
  474. }];
  475. [menuItems addObject:action];
  476. }
  477. }
  478. if ([self canPerformAction:@selector(copyMessage:) withSender:nil]) {
  479. UIImage *copyImage = [UIImage systemImageNamed:@"doc.on.doc.fill" compatibleWithTraitCollection:self.traitCollection];
  480. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"copy", nil) image:copyImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  481. [self copyMessage:nil];
  482. }];
  483. [menuItems addObject:action];
  484. }
  485. if ([self canPerformAction:@selector(forwardMessage:) withSender:nil]) {
  486. UIImage *forwardImage = [UIImage systemImageNamed:@"arrowshape.turn.up.right.fill" compatibleWithTraitCollection:self.traitCollection];
  487. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"forward", nil) image:forwardImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  488. [self forwardMessage:nil];
  489. }];
  490. [menuItems addObject:action];
  491. }
  492. if ([self canPerformAction:@selector(shareMessage:) withSender:nil]) {
  493. UIImage *shareImage = [UIImage systemImageNamed:@"square.and.arrow.up.fill" compatibleWithTraitCollection:self.traitCollection];
  494. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"share", nil) image:shareImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  495. [self shareMessage:nil];
  496. }];
  497. [menuItems addObject:action];
  498. }
  499. if ([self canPerformAction:@selector(resendMessage:) withSender:nil]) {
  500. UIImage *resendImage = [UIImage systemImageNamed:@"paperplane.fill" compatibleWithTraitCollection:self.traitCollection];
  501. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"try_again", nil) image:resendImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  502. [self resendMessage:nil];
  503. }];
  504. [menuItems addObject:action];
  505. }
  506. if ([self canPerformAction:@selector(detailsMessage:) withSender:nil]) {
  507. UIImage *detailsImage = [UIImage systemImageNamed:@"info.circle.fill" compatibleWithTraitCollection:self.traitCollection];
  508. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"details", nil) image:detailsImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  509. [self detailsMessage:nil];
  510. }];
  511. [menuItems addObject:action];
  512. }
  513. if ([self canPerformAction:@selector(deleteMessage:) withSender:nil]) {
  514. UIImage *deleteImage = [UIImage systemImageNamed:@"trash.fill" compatibleWithTraitCollection:self.traitCollection];
  515. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"delete", nil) image:deleteImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  516. [self deleteMessage:nil];
  517. }];
  518. action.attributes = UIMenuElementAttributesDestructive;
  519. [deleteItems addObject:action];
  520. }
  521. UIMenu *actionsMenu = [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:menuItems];
  522. UIMenu *deleteMenu = [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:deleteItems];
  523. return [UIMenu menuWithTitle:@"" children:@[actionsMenu, deleteMenu]];
  524. }];
  525. return conf;
  526. }
  527. - (NSArray *)contextMenuItems API_AVAILABLE(ios(13.0)) {
  528. if (self.editing) {
  529. return nil;
  530. }
  531. NSMutableArray *menuItems = [NSMutableArray array];
  532. NSMutableArray *deleteItems = [NSMutableArray array];
  533. if ([self canPerformAction:@selector(userackMessage:) withSender:nil]) {
  534. UIImage *ackImage = [UIImage imageNamed:@"MessageStatus_thumb_up" inColor:[Colors green]];
  535. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"acknowledge", nil) image:ackImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  536. [self userackMessage:nil];
  537. }];
  538. [menuItems addObject:action];
  539. }
  540. if ([self canPerformAction:@selector(userdeclineMessage:) withSender:nil]) {
  541. UIImage *declineImage = [UIImage imageNamed:@"MessageStatus_thumb_down" inColor:[Colors orange]];
  542. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"decline", nil) image:declineImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  543. [self userdeclineMessage:nil];
  544. }];
  545. [menuItems addObject:action];
  546. }
  547. if ([self canPerformAction:@selector(quoteMessage:) withSender:nil]) {
  548. UIImage *quoteImage = [UIImage imageNamed:@"Quote" inColor:[Colors fontNormal]];
  549. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"quote", nil) image:quoteImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  550. [self quoteMessage:nil];
  551. }];
  552. [menuItems addObject:action];
  553. }
  554. if (UIAccessibilityIsSpeakSelectionEnabled()) {
  555. if ([self canPerformAction:@selector(speakMessage:) withSender:nil]) {
  556. UIImage *speakImage = [UIImage systemImageNamed:@"text.bubble.fill" compatibleWithTraitCollection:self.traitCollection];
  557. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"speak", nil) image:speakImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  558. [self speakMessage:nil];
  559. }];
  560. [menuItems addObject:action];
  561. }
  562. }
  563. if ([self canPerformAction:@selector(copyMessage:) withSender:nil]) {
  564. UIImage *copyImage = [UIImage systemImageNamed:@"doc.on.doc.fill" compatibleWithTraitCollection:self.traitCollection];
  565. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"copy", nil) image:copyImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  566. [self copyMessage:nil];
  567. }];
  568. [menuItems addObject:action];
  569. }
  570. if ([self canPerformAction:@selector(forwardMessage:) withSender:nil]) {
  571. UIImage *forwardImage = [UIImage systemImageNamed:@"arrowshape.turn.up.right.fill" compatibleWithTraitCollection:self.traitCollection];
  572. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"forward", nil) image:forwardImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  573. [self forwardMessage:nil];
  574. }];
  575. [menuItems addObject:action];
  576. }
  577. if ([self canPerformAction:@selector(shareMessage:) withSender:nil]) {
  578. UIImage *shareImage = [UIImage systemImageNamed:@"square.and.arrow.up.fill" compatibleWithTraitCollection:self.traitCollection];
  579. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"share", nil) image:shareImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  580. [self shareMessage:nil];
  581. }];
  582. [menuItems addObject:action];
  583. }
  584. if ([self canPerformAction:@selector(resendMessage:) withSender:nil]) {
  585. UIImage *resendImage = [UIImage systemImageNamed:@"paperplane.fill" compatibleWithTraitCollection:self.traitCollection];
  586. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"try_again", nil) image:resendImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  587. [self resendMessage:nil];
  588. }];
  589. [menuItems addObject:action];
  590. }
  591. if ([self canPerformAction:@selector(detailsMessage:) withSender:nil]) {
  592. UIImage *detailsImage = [UIImage systemImageNamed:@"info.circle.fill" compatibleWithTraitCollection:self.traitCollection];
  593. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"details", nil) image:detailsImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  594. [self detailsMessage:nil];
  595. }];
  596. [menuItems addObject:action];
  597. }
  598. if ([self canPerformAction:@selector(deleteMessage:) withSender:nil]) {
  599. UIImage *deleteImage = [UIImage systemImageNamed:@"trash.fill" compatibleWithTraitCollection:self.traitCollection];
  600. UIAction *action = [UIAction actionWithTitle:NSLocalizedString(@"delete", nil) image:deleteImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  601. [self deleteMessage:nil];
  602. }];
  603. action.attributes = UIMenuElementAttributesDestructive;
  604. [deleteItems addObject:action];
  605. }
  606. UIMenu *actionsMenu = [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:menuItems];
  607. UIMenu *deleteMenu = [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:deleteItems];
  608. return @[actionsMenu, deleteMenu];
  609. }
  610. - (void)showCallMenu {
  611. NSMutableArray *menuItems = [NSMutableArray array];
  612. if ([self canPerformAction:@selector(quoteMessage:) withSender:nil]) {
  613. UIImage *quoteImage = [UIImage imageNamed:@"Quote" inColor:[UIColor whiteColor]];
  614. QBPopupMenuItem *item = [QBPopupMenuItem itemWithImage:quoteImage target:self action:@selector(quoteMessage:)];
  615. item.accessibilityLabel = NSLocalizedString(@"quote", nil);
  616. [menuItems addObject:item];
  617. }
  618. if ([self canPerformAction:@selector(deleteMessage:) withSender:nil]) {
  619. [menuItems addObject:[QBPopupMenuItem itemWithTitle:NSLocalizedString(@"delete", nil) target:self action:@selector(deleteMessage:)]];
  620. }
  621. _popupMenu = [[QBPopupMenu alloc] initWithItems:menuItems];
  622. _popupMenu.delegate = self;
  623. _popupMenu.color = [[UIColor blackColor] colorWithAlphaComponent:0.95];
  624. _popupMenu.highlightedColor = [[UIColor darkGrayColor] colorWithAlphaComponent:0.95];
  625. _popupMenu.nextPageAccessibilityLabel = NSLocalizedString(@"showNext", nil);
  626. _popupMenu.previousPageAccessibilityLabel = NSLocalizedString(@"showPrevious", nil);
  627. CGRect targetRect = [self targetRectForMenuPopup];
  628. [_popupMenu showInView:chatVc.view targetRect:targetRect animated:YES];
  629. UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, _popupMenu);
  630. /* add view to add a quit for voice over */
  631. UIView *quitView = [[UIView alloc] initWithFrame:chatVc.view.subviews.lastObject.frame];
  632. quitView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4];
  633. quitView.alpha = 1;
  634. quitView.isAccessibilityElement = YES;
  635. quitView.accessibilityLabel = [BundleUtil localizedStringForKey:@"quit"];
  636. quitView.accessibilityActivationPoint = CGPointMake(0.0, 0.0);
  637. quitView.backgroundColor = [UIColor clearColor];
  638. quitView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  639. quitView.userInteractionEnabled = NO;
  640. [chatVc.view.subviews.lastObject insertSubview:quitView belowSubview:_popupMenu];
  641. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapQuitPopoverMenu:)];
  642. [quitView addGestureRecognizer: tapGesture];
  643. }
  644. - (CGRect)targetRectForMenuPopup {
  645. CGRect cellRect = [chatVc.view convertRect:msgBackground.frame fromView:self];
  646. CGRect containingRect = chatVc.view.frame;
  647. CGFloat minY = chatVc.topLayoutGuide.length;
  648. CGFloat maxY = chatVc.visibleChatHeight;
  649. // cell overlapping top
  650. if (CGRectGetMinY(cellRect) - REQUIRED_MENU_HEIGHT < minY) {
  651. if (CGRectGetMaxY(cellRect) + REQUIRED_MENU_HEIGHT - minY > maxY) {
  652. // cell overlapping also bottom of containing view -> show in middle of cell
  653. cellRect = [RectUtil setHeightOf:cellRect height:REQUIRED_MENU_HEIGHT];
  654. return [RectUtil rect:cellRect centerVerticalIn:containingRect];
  655. } else {
  656. // force to show on bottom by extending top cell border
  657. return [RectUtil offsetAndResizeRect:cellRect byX:0.0 byY:-100.0];
  658. }
  659. }
  660. return cellRect;
  661. }
  662. - (void)tapQuitPopoverMenu:(id)sender {
  663. [_popupMenu dismissAnimated:YES];
  664. }
  665. - (void)resendMessage:(UIMenuController*)menuController {
  666. }
  667. - (void)copyMessage:(UIMenuController *)menuController {
  668. }
  669. - (void)speakMessage:(UIMenuController *)menuController {
  670. }
  671. - (void)shareMessage:(UIMenuController *)menuController {
  672. MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:false];
  673. if ([mdmSetup disableShareMedia] == true) {
  674. ModalNavigationController *navigationController = [ContactGroupPickerViewController pickerFromStoryboardWithDelegate:self];
  675. ContactGroupPickerViewController *picker = (ContactGroupPickerViewController *)navigationController.topViewController;
  676. picker.enableMulitSelection = true;
  677. picker.enableTextInput = true;
  678. picker.submitOnSelect = false;
  679. if ([self.message isKindOfClass: [FileMessage class]]) {
  680. picker.renderType = ((FileMessage *) self.message).type;
  681. }
  682. [[AppDelegate sharedAppDelegate].window.rootViewController presentViewController:navigationController animated:YES completion:nil];
  683. } else {
  684. UIActivityViewController *activityViewController = activityViewController = [ActivityUtil activityViewControllerForMessage:self.message withView:self.chatVc.view andRect:CGRectMake(0, 0, 0, 0)];
  685. [self.chatVc presentActivityViewController:activityViewController animated:YES fromView:self];
  686. }
  687. }
  688. - (void)forwardMessage:(UIMenuController *)menuController {
  689. ModalNavigationController *navigationController = [ContactGroupPickerViewController pickerFromStoryboardWithDelegate:self];
  690. ContactGroupPickerViewController *picker = (ContactGroupPickerViewController *)navigationController.topViewController;
  691. picker.enableMulitSelection = true;
  692. picker.enableTextInput = true;
  693. picker.submitOnSelect = false;
  694. if ([self.message isKindOfClass: [FileMessage class]]) {
  695. picker.renderType = ((FileMessage *) self.message).type;
  696. }
  697. [[AppDelegate sharedAppDelegate].window.rootViewController presentViewController:navigationController animated:YES completion:nil];
  698. }
  699. - (void)setBubbleHighlighted:(BOOL)bubbleHighlighted {
  700. msgBackground.image = [self bubbleImageWithHighlight:bubbleHighlighted];
  701. }
  702. - (void)setEditing:(BOOL)editing animated:(BOOL)animated {
  703. [super setEditing:editing animated:animated];
  704. if (dtgr != nil) {
  705. dtgr.enabled = !editing;
  706. }
  707. if (editing) {
  708. self.msgBackground.userInteractionEnabled = NO;
  709. self.alpha = 0.8;
  710. } else {
  711. self.msgBackground.userInteractionEnabled = YES;
  712. self.alpha = 1.0;
  713. }
  714. }
  715. - (void)userackMessage:(UIMenuController *)menuController {
  716. [self sendUserAck:YES];
  717. }
  718. - (void)userdeclineMessage:(UIMenuController *)menuController {
  719. [self sendUserAck:NO];
  720. }
  721. - (void)sendUserAck:(BOOL)doAcknowledge {
  722. if (message.userackDate != nil && message.userack.boolValue == doAcknowledge) {
  723. return;
  724. }
  725. EntityManager *entityManager = [[EntityManager alloc] init];
  726. [entityManager performSyncBlockAndSafe:^{
  727. if (doAcknowledge) {
  728. [MessageSender sendUserAckForMessages:@[message] toIdentity:message.conversation.contact.identity async:YES quickReply:NO];
  729. message.userack = [NSNumber numberWithBool:YES];
  730. } else {
  731. [MessageSender sendUserDeclineForMessages:@[message] toIdentity:message.conversation.contact.identity async:YES quickReply:NO];
  732. message.userack = [NSNumber numberWithBool:NO];
  733. }
  734. message.userackDate = [NSDate date];
  735. [self updateStatusImage];
  736. }];
  737. }
  738. - (void)deleteMessage:(UIMenuController*)menuController {
  739. dispatch_async(dispatch_get_main_queue(), ^{
  740. [UIAlertTemplate showDestructiveAlertWithOwner:[[AppDelegate sharedAppDelegate] currentTopViewController] title:nil message:[BundleUtil localizedStringForKey:@"messages_delete_selected_confirm"] titleDestructive:[BundleUtil localizedStringForKey:@"delete"] actionDestructive:^(UIAlertAction *destructiveAction) {
  741. [chatVc cleanCellHeightCache];
  742. EntityManager *entityManager = [[EntityManager alloc] init];
  743. [entityManager performSyncBlockAndSafe:^{
  744. [[entityManager entityDestroyer] deleteObjectWithObject:message];
  745. [chatVc updateConversationLastMessage];
  746. }];
  747. [chatVc updateConversation];
  748. } titleCancel:[BundleUtil localizedStringForKey:@"cancel"] actionCancel:^(UIAlertAction *cancelAction) {
  749. }];
  750. });
  751. }
  752. - (void)detailsMessage:(UIMenuController*)menuController {
  753. [chatVc showMessageDetails:message];
  754. }
  755. - (void)quoteMessage:(UIMenuController*)menuController {
  756. if (_messageToQuote == nil || _messageToQuote == self.message) {
  757. _messageToQuote = nil;
  758. if ([[UserSettings sharedUserSettings] quoteV2Active]) {
  759. [self.chatVc.chatBar addQuotedMessage:self.message];
  760. } else {
  761. NSString *quotedText = [self textForQuote];
  762. if (quotedText.length == 0)
  763. return;
  764. Contact *sender;
  765. if (self.message.isOwn.boolValue) {
  766. sender = nil;
  767. } else if (self.message.sender != nil) {
  768. sender = self.message.sender;
  769. } else {
  770. sender = self.message.conversation.contact;
  771. }
  772. [self.chatVc.chatBar addQuotedText:quotedText quotedContact:sender];
  773. }
  774. }
  775. }
  776. - (NSString*)textForQuote {
  777. return nil;
  778. }
  779. - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
  780. if (action == @selector(copyMessage:)) {
  781. return YES;
  782. }
  783. else if (action == @selector(shareMessage:)) {
  784. if (@available(iOS 13.0, *)) {
  785. MDMSetup *mdmSetup = [[MDMSetup alloc] initWithSetup:false];
  786. if ([mdmSetup disableShareMedia] == true) {
  787. return NO;
  788. }
  789. }
  790. return YES;
  791. } else if (action == @selector(userackMessage:) && !message.isOwn.boolValue && message.conversation.groupId == nil) {
  792. return YES;
  793. } else if (action == @selector(userdeclineMessage:) && !message.isOwn.boolValue && message.conversation.groupId == nil) {
  794. return YES;
  795. } else if (action == @selector(deleteMessage:)) {
  796. return YES;
  797. } else if (action == @selector(detailsMessage:)) {
  798. return YES;
  799. } else if (action == @selector(quoteMessage:) /*&& [self textForQuote].length > 0*/) {
  800. if ([[UserSettings sharedUserSettings] quoteV2Active]) {
  801. return YES;
  802. } else {
  803. if ([self textForQuote].length > 0) {
  804. return true;
  805. }
  806. }
  807. return false;
  808. } else if (action == @selector(forwardMessage:)) {
  809. if (@available(iOS 13.0, *)) {
  810. return YES;
  811. } else {
  812. return NO;
  813. }
  814. } else {
  815. return NO;
  816. }
  817. }
  818. - (BOOL)canBecomeFirstResponder {
  819. return YES;
  820. }
  821. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  822. dispatch_async(dispatch_get_main_queue(), ^{
  823. if (object == message) {
  824. [UIView animateWithDuration:0.5 animations:^{
  825. [self updateStatusImage];
  826. [self updateDateLabel];
  827. [self updateTypingIndicator];
  828. }];
  829. }
  830. });
  831. }
  832. - (void)setTyping:(BOOL)newTyping {
  833. typing = newTyping;
  834. [self updateTypingIndicator];
  835. }
  836. - (CGFloat)contentLeftOffset {
  837. if (message.sender == nil || message.isOwn.boolValue)
  838. return 0.0f;
  839. else
  840. return 40.0f;
  841. }
  842. + (CGFloat)maxContentWidthForTableWidth:(CGFloat)tableWidth {
  843. return tableWidth - kMessageScreenMargin;
  844. }
  845. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  846. if (touches.count == 1) {
  847. if (UIAccessibilityIsVoiceOverRunning()) {
  848. /* when VoiceOver is on, double-taps on message cells will result in a touch located
  849. in the center of the cell. Since this may not be within the bubble, it will not
  850. trigger playing/showing media. Therefore, we hand this event to the cell */
  851. if ([self performPlayActionForAccessibility])
  852. return;
  853. }
  854. UITouch *touch = [touches anyObject];
  855. CGPoint point = [touch locationInView:self.contentView];
  856. if (!CGRectContainsPoint(self.msgBackground.frame, point))
  857. [self.chatVc messageBackgroundTapped:self.message];
  858. }
  859. [super touchesEnded:touches withEvent:event];
  860. }
  861. - (NSString *)accessibilityLabel {
  862. NSMutableString *text = [NSMutableString new];
  863. NSString *senderText = [message accessibilityMessageSender];
  864. if (senderText.length > 0) {
  865. [text appendFormat:@"%@. ", senderText];
  866. }
  867. [text appendFormat:@"%@\n", self.accessibilityLabelForContent];
  868. NSString *statusText = [message accessibilityMessageStatus];
  869. if (statusText.length > 0) {
  870. [text appendFormat:@"%@", statusText];
  871. }
  872. NSString *dateText = [message accessibilityMessageDate];
  873. if (dateText.length > 0) {
  874. [text appendFormat:@". %@", dateText];
  875. }
  876. return text;
  877. }
  878. - (NSString *)accessibilityLabelForContent {
  879. return @"";
  880. }
  881. - (BOOL)performPlayActionForAccessibility {
  882. return NO;
  883. }
  884. - (BOOL)shouldHideBubbleBackground {
  885. return NO;
  886. }
  887. + (UIFont *)textFont {
  888. return [UIFont systemFontOfSize: [ChatMessageCell textFontSize]];
  889. }
  890. + (CGFloat)textFontSize {
  891. return [UserSettings sharedUserSettings].chatFontSize;
  892. }
  893. + (UIFont *)quoteFont {
  894. return [UIFont systemFontOfSize: [ChatMessageCell quoteFontSize]];
  895. }
  896. + (CGFloat)quoteFontSize {
  897. return [UserSettings sharedUserSettings].chatFontSize * QUOTE_FONT_SIZE_FACTOR;
  898. }
  899. + (UIFont *)quoteIdentityFont {
  900. return [UIFont boldSystemFontOfSize: [ChatMessageCell quoteIdentityFontSize]];
  901. }
  902. + (CGFloat)quoteIdentityFontSize {
  903. return [UserSettings sharedUserSettings].chatFontSize * QUOTE_FONT_SIZE_FACTOR;
  904. }
  905. + (UIFont *)emojiFont {
  906. return [UIFont systemFontOfSize: [ChatMessageCell emojiFontSize]];
  907. }
  908. + (CGFloat)emojiFontSize {
  909. return MIN(EMOJI_MAX_FONT_SIZE, [UserSettings sharedUserSettings].chatFontSize * EMOJI_FONT_SIZE_FACTOR);
  910. }
  911. - (BOOL)highlightOccurencesOf:(NSString *)pattern {
  912. // default implementation does nothing
  913. return NO;
  914. }
  915. + (NSAttributedString *)highlightedOccurencesOf:(NSString *)pattern inString:(NSString *)text {
  916. BOOL hasMatches = NO;
  917. NSRange searchRange = NSMakeRange(0, text.length);
  918. UIFont *font = [ChatMessageCell textFont];
  919. NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text];
  920. [attributedText addAttribute:NSFontAttributeName value:font range:searchRange];
  921. [attributedText addAttribute:NSForegroundColorAttributeName value:[Colors fontNormal] range:searchRange];
  922. // fixes line height issues when text contains emojis (https://github.com/TTTAttributedLabel/TTTAttributedLabel/issues/405)
  923. NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
  924. paragraphStyle.lineHeightMultiple = 1.0;
  925. paragraphStyle.minimumLineHeight = font.lineHeight;
  926. paragraphStyle.maximumLineHeight = font.lineHeight;
  927. [attributedText addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:searchRange];
  928. if (pattern == nil) {
  929. return nil;
  930. }
  931. // options should match EntityFetcher options for fulltext search: [cd]
  932. NSStringCompareOptions options = NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch;
  933. UIColor *highlightColor = [UIColor redColor];
  934. while (true) {
  935. NSRange range = [text rangeOfString:pattern options:options range:searchRange];
  936. if (range.location == NSNotFound) {
  937. break;
  938. }
  939. hasMatches = YES;
  940. [attributedText addAttribute:NSForegroundColorAttributeName value:highlightColor range:range];
  941. NSInteger location = range.location + range.length;
  942. searchRange = NSMakeRange(location, text.length - location);
  943. }
  944. return attributedText;
  945. }
  946. - (UIViewController *)previewViewController {
  947. // default has no preview
  948. return nil;
  949. }
  950. - (UIViewController *)previewViewControllerFor:(id<UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location {
  951. // default has no preview
  952. return nil;
  953. }
  954. - (void)willDisplay {
  955. //default implementation does nothing;
  956. }
  957. - (void)didEndDisplaying {
  958. self.editing = false;
  959. //default implementation does nothing;
  960. }
  961. #pragma mark - QBPopupMenuDelegate
  962. - (void)popupMenuWillAppear:(QBPopupMenu *)popupMenu {
  963. [self setBubbleHighlighted:YES];
  964. }
  965. - (void)popupMenuWillDisappear:(QBPopupMenu *)popupMenu {
  966. [self setBubbleHighlighted:NO];
  967. UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self);
  968. }
  969. #pragma mark - Accessibility
  970. - (NSArray *)accessibilityCustomActions {
  971. NSMutableArray *actions = [NSMutableArray new];
  972. if ([self canPerformAction:@selector(userackMessage:) withSender:nil]) {
  973. UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"acknowledge", @"") target:self selector:@selector(userackMessage:)];
  974. [actions addObject:action];
  975. }
  976. if ([self canPerformAction:@selector(userdeclineMessage:) withSender:nil]) {
  977. UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"decline", @"") target:self selector:@selector(userdeclineMessage:)];
  978. [actions addObject:action];
  979. }
  980. if ([self canPerformAction:@selector(quoteMessage:) withSender:nil]) {
  981. UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"quote", @"") target:self selector:@selector(quoteMessage:)];
  982. [actions addObject:action];
  983. }
  984. if (UIAccessibilityIsSpeakSelectionEnabled()) {
  985. if ([self canPerformAction:@selector(speakMessage:) withSender:nil]) {
  986. UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"speak", @"") target:self selector:@selector(speakMessage:)];
  987. [actions addObject:action];
  988. }
  989. }
  990. if ([self canPerformAction:@selector(copyMessage:) withSender:nil]) {
  991. UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"copy", @"") target:self selector:@selector(copyMessage:)];
  992. [actions addObject:action];
  993. }
  994. if ([self canPerformAction:@selector(shareMessage:) withSender:nil]) {
  995. UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"share", @"") target:self selector:@selector(shareMessage:)];
  996. [actions addObject:action];
  997. }
  998. if ([self canPerformAction:@selector(resendMessage:) withSender:nil]) {
  999. UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"try_again", @"") target:self selector:@selector(resendMessage:)];
  1000. [actions addObject:action];
  1001. }
  1002. if ([self canPerformAction:@selector(detailsMessage:) withSender:nil]) {
  1003. UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"details", @"") target:self selector:@selector(detailsMessage:)];
  1004. [actions addObject:action];
  1005. }
  1006. if ([self canPerformAction:@selector(deleteMessage:) withSender:nil]) {
  1007. UIAccessibilityCustomAction *action = [[UIAccessibilityCustomAction alloc] initWithName:NSLocalizedString(@"delete", @"") target:self selector:@selector(deleteMessage:)];
  1008. [actions addObject:action];
  1009. }
  1010. return actions;
  1011. }
  1012. #pragma mark - Contact picker delegate
  1013. - (void)contactPicker:(ContactGroupPickerViewController*)contactPicker didPickConversations:(NSSet *)conversations renderType:(NSNumber *)renderType sendAsFile:(BOOL)sendAsFile {
  1014. if ([self.message isKindOfClass: [TextMessage class]]) {
  1015. TextMessage *textMessage = (TextMessage *)message;
  1016. for (Conversation *conversation in conversations) {
  1017. [MessageSender sendMessage:textMessage.text inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) {
  1018. ;//nop
  1019. }];
  1020. if (contactPicker.additionalTextToSend) {
  1021. [MessageSender sendMessage:contactPicker.additionalTextToSend inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) {
  1022. ;//nop
  1023. }];
  1024. }
  1025. }
  1026. [contactPicker dismissViewControllerAnimated:YES completion:nil];
  1027. }
  1028. else if ([self.message isKindOfClass: [LocationMessage class]]) {
  1029. LocationMessage *locationMessage = (LocationMessage *)message;
  1030. CLLocationCoordinate2D coordinates = CLLocationCoordinate2DMake(locationMessage.latitude.doubleValue, locationMessage.longitude.doubleValue);
  1031. double accurracy = locationMessage.accuracy.doubleValue;
  1032. for (Conversation *conversation in conversations) {
  1033. [MessageSender sendLocation:coordinates accuracy:accurracy poiName:locationMessage.poiName poiAddress:nil inConversation:conversation onCompletion:^(NSData *messageId) {
  1034. ;//nop
  1035. }];
  1036. if (contactPicker.additionalTextToSend) {
  1037. [MessageSender sendMessage:contactPicker.additionalTextToSend inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) {
  1038. ;//nop
  1039. }];
  1040. }
  1041. }
  1042. [contactPicker dismissViewControllerAnimated:YES completion:nil];
  1043. } else if ([self.message isKindOfClass: [FileMessage class]] || sendAsFile == true) {
  1044. [self handleFileMessagefromContactPicker:contactPicker didPickConversations:conversations];
  1045. } else if ([self.message isKindOfClass: [AudioMessage class]]) {
  1046. AudioMessage *audioMessage = (AudioMessage *)message;
  1047. NSData *data = [audioMessage.audio.data copy];
  1048. for (Conversation *conversation in conversations) {
  1049. URLSenderItem *item = [URLSenderItem itemWithData:data fileName:@"audio.m4a" type:UTTYPE_AUDIO renderType:@0 sendAsFile:true];
  1050. FileMessageSender *sender = [[FileMessageSender alloc] init];
  1051. [sender sendItem:item inConversation:conversation requestId:nil];
  1052. if (contactPicker.additionalTextToSend) {
  1053. item.caption = contactPicker.additionalTextToSend;
  1054. }
  1055. }
  1056. [contactPicker dismissViewControllerAnimated:YES completion:nil];
  1057. } else if ([self.message isKindOfClass: [ImageMessage class]]) {
  1058. ImageMessage *imageMessage = (ImageMessage *)message;
  1059. NSString *caption = contactPicker.additionalTextToSend;
  1060. // A ImageMessage can never be sent as file, thus the image data will always be converted
  1061. [self forwardImageMessage:imageMessage toConversations:conversations additionalTextToSend:caption];
  1062. [contactPicker dismissViewControllerAnimated:YES completion:nil];
  1063. } else if ([self.message isKindOfClass: [VideoMessage class]]) {
  1064. VideoMessage *videoMessage = (VideoMessage *)message;
  1065. NSURL *videoURL = [VideoURLSenderItemCreator writeToTemporaryDirectoryWithData:videoMessage.video.data];
  1066. if (videoURL == nil) {
  1067. DDLogError(@"VideoURL was nil.");
  1068. return;
  1069. }
  1070. VideoURLSenderItemCreator *senderCreator = [[VideoURLSenderItemCreator alloc] init];
  1071. URLSenderItem *senderItem = [senderCreator senderItemFrom:videoURL];
  1072. for (Conversation *conversation in conversations) {
  1073. if (contactPicker.additionalTextToSend) {
  1074. senderItem.caption = contactPicker.additionalTextToSend;
  1075. }
  1076. FileMessageSender *sender = [[FileMessageSender alloc] init];
  1077. [sender sendItem:senderItem inConversation:conversation requestId:nil];
  1078. }
  1079. [contactPicker dismissViewControllerAnimated:YES completion:^(){
  1080. [VideoURLSenderItemCreator cleanTemporaryDirectory];
  1081. }];
  1082. }
  1083. }
  1084. - (void) handleFileMessagefromContactPicker:(ContactGroupPickerViewController *)contactPicker didPickConversations:(NSSet *)conversations {
  1085. FeatureMaskChecker *featureMaskChecker = [[FeatureMaskChecker alloc] init];
  1086. URLSenderItem *item;
  1087. if ([self.message isKindOfClass: [FileMessage class]]) {
  1088. FileMessage *fileMessage = (FileMessage *)message;
  1089. NSNumber *type = fileMessage.type;
  1090. // Voice Messages are always forwarded with rendering type 0
  1091. if ([UTIConverter isAudioMimeType:fileMessage.mimeType]) {
  1092. type = @0;
  1093. }
  1094. item = [URLSenderItem itemWithData:fileMessage.data.data fileName:fileMessage.fileName type:fileMessage.blobGetUTI renderType:type sendAsFile:true];
  1095. }
  1096. else if ([self.message isKindOfClass: [AudioMessage class]]) {
  1097. // AudioMessage *audioMessage = (AudioMessage *)message;
  1098. // item = [URLSenderItem itemWithData:audioMessage.audio.data fileName:audioMessage.getFilename type:audioMessage.blobGetUTI renderType:@0 sendAsFile:true];
  1099. AudioMessage *audioMessage = (AudioMessage *)message;
  1100. NSData *data = [audioMessage.audio.data copy];
  1101. for (Conversation *conversation in conversations) {
  1102. AudioMessageSender *sender = [[AudioMessageSender alloc] init];
  1103. [sender startWithAudioData:data duration:audioMessage.duration inConversation:conversation requestId:nil];
  1104. if (contactPicker.additionalTextToSend) {
  1105. [MessageSender sendMessage:contactPicker.additionalTextToSend inConversation:conversation async:YES quickReply:NO requestId:nil onCompletion:^(TextMessage *message, Conversation *conv) {
  1106. ;//nop
  1107. }];
  1108. item.caption = contactPicker.additionalTextToSend;
  1109. }
  1110. }
  1111. [contactPicker dismissViewControllerAnimated:YES completion:nil];
  1112. }
  1113. else if ([self.message isKindOfClass: [ImageMessage class]]) {
  1114. ImageMessage *imageMessage = (ImageMessage *)message;
  1115. item = [URLSenderItem itemWithData:imageMessage.image.data fileName:imageMessage.getFilename type:imageMessage.blobGetUTI renderType:@0 sendAsFile:true];
  1116. }
  1117. else if ([self.message isKindOfClass: [VideoMessage class]]) {
  1118. VideoMessage *videoMessage = (VideoMessage *)message;
  1119. item = [URLSenderItem itemWithData:videoMessage.video.data fileName:videoMessage.getFilename type:videoMessage.blobGetUTI renderType:@0 sendAsFile:true];
  1120. }
  1121. if (contactPicker.additionalTextToSend) {
  1122. item.caption = contactPicker.additionalTextToSend;
  1123. }
  1124. [featureMaskChecker checkFileTransferFor:conversations presentAlarmOn:contactPicker onSuccess:^{
  1125. for (Conversation *conversation in conversations) {
  1126. FileMessageSender *urlSender = [[FileMessageSender alloc] init];
  1127. [urlSender sendItem:item inConversation:conversation];
  1128. }
  1129. [contactPicker dismissViewControllerAnimated:YES completion:nil];
  1130. } onFailure:^{
  1131. }];
  1132. }
  1133. - (void)forwardImageMessage:(ImageMessage *)imageMessage toConversations:(NSSet *)conversations additionalTextToSend:(NSString *)additionalText {
  1134. // Images in ImageMessage are always jpg
  1135. CFStringRef uti = kUTTypeJPEG;
  1136. for (Conversation *conversation in conversations) {
  1137. ImageURLSenderItemCreator *imageSender = [[ImageURLSenderItemCreator alloc] init];
  1138. URLSenderItem *item = [imageSender senderItemFrom:imageMessage.image.data uti:(__bridge NSString *)uti];
  1139. if (additionalText) {
  1140. item.caption = additionalText;
  1141. }
  1142. FileMessageSender *sender = [[FileMessageSender alloc] init];
  1143. [sender sendItem:item inConversation:conversation];
  1144. }
  1145. }
  1146. - (void)contactPickerDidCancel:(ContactGroupPickerViewController*)contactPicker {
  1147. [contactPicker dismissViewControllerAnimated:YES completion:nil];
  1148. }
  1149. #pragma mark - ModalNavigationControllerDelegate
  1150. - (void)willDismissModalNavigationController {
  1151. }
  1152. #pragma mark Gesture Recognizer
  1153. - (IBAction)panGestureCellAction:(UIPanGestureRecognizer *)recognizer {
  1154. if (UIAccessibilityIsVoiceOverRunning() || ![self canPerformAction:@selector(quoteMessage:) withSender:nil] || self.chatVc.chatContent.editing == true) {
  1155. quoteSlideIconImage.alpha = 0.0;
  1156. [recognizer.view setFrame: CGRectMake(recognizer.view.frame.origin.x, recognizer.view.frame.origin.y, recognizer.view.frame.size.width, recognizer.view.frame.size.height)];
  1157. return;
  1158. }
  1159. CGPoint translation = [recognizer translationInView:self.chatVc.view];
  1160. if (recognizer.view.frame.origin.x < 0) {
  1161. quoteSlideIconImage.alpha = 0.0;
  1162. [recognizer.view setFrame: CGRectMake(0, recognizer.view.frame.origin.y, recognizer.view.frame.size.width, recognizer.view.frame.size.height)];
  1163. return;
  1164. }
  1165. _messageToQuote = self.message;
  1166. recognizer.view.center = CGPointMake(recognizer.view.center.x+ translation.x,
  1167. recognizer.view.center.y );
  1168. [recognizer setTranslation:CGPointMake(0, 0) inView:self.chatVc.view];
  1169. if(recognizer.view.frame.origin.x > [UIScreen mainScreen].bounds.size.width * 0.9)
  1170. {
  1171. [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
  1172. quoteSlideIconImage.alpha = 0.0;
  1173. [recognizer.view setFrame: CGRectMake(0, recognizer.view.frame.origin.y, recognizer.view.frame.size.width, recognizer.view.frame.size.height)];
  1174. } completion:nil];
  1175. }
  1176. CGFloat minX = self.msgBackground.frame.size.width/2 < 75.0 && message.isOwn.boolValue ? self.msgBackground.frame.size.width / 2 : 75.0;
  1177. CGFloat newAlpha = recognizer.view.frame.origin.x / minX;
  1178. if (quoteSlideIconImage.alpha < 0.8 && newAlpha >= 0.8) {
  1179. [gen prepare];
  1180. }
  1181. if (quoteSlideIconImage.alpha < 1.0 && newAlpha >= 1.0) {
  1182. [gen impactOccurred];
  1183. }
  1184. if (message.isOwn.boolValue) {
  1185. 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);
  1186. } else {
  1187. if (message.conversation.isGroup == true) {
  1188. 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);
  1189. } else {
  1190. 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);
  1191. }
  1192. }
  1193. quoteSlideIconImage.alpha = recognizer.view.frame.origin.x / minX;
  1194. if (recognizer.state == UIGestureRecognizerStateEnded)
  1195. {
  1196. int x = recognizer.view.frame.origin.x;
  1197. [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
  1198. quoteSlideIconImage.alpha = 0.0;
  1199. [recognizer.view setFrame: CGRectMake(0, recognizer.view.frame.origin.y, recognizer.view.frame.size.width, recognizer.view.frame.size.height)];
  1200. } completion:^(BOOL finished) {
  1201. if (x > minX) {
  1202. [self quoteMessage:nil];
  1203. } else {
  1204. _messageToQuote = nil;
  1205. }
  1206. }];
  1207. }
  1208. }
  1209. - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
  1210. if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
  1211. if (self.chatVc.chatContent.editing == true) {
  1212. return true;
  1213. }
  1214. CGPoint velocity = [((UIPanGestureRecognizer *) gestureRecognizer) velocityInView:self.chatVc.chatContent];
  1215. if (fabs(velocity.x) >= fabs(velocity.y)) {
  1216. if (velocity.x < 0) {
  1217. quoteSlideIconImage.alpha = 0.0;
  1218. [gestureRecognizer.view setFrame: CGRectMake(0, gestureRecognizer.view.frame.origin.y, gestureRecognizer.view.frame.size.width, gestureRecognizer.view.frame.size.height)];
  1219. return false;
  1220. }
  1221. }
  1222. return fabs(velocity.x) >= fabs(velocity.y);
  1223. }
  1224. return true;
  1225. }
  1226. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
  1227. if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
  1228. return false;
  1229. }
  1230. return true;
  1231. }
  1232. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
  1233. return [otherGestureRecognizer isKindOfClass:[UIScreenEdgePanGestureRecognizer class]];
  1234. }
  1235. @end