ChatTextMessageCell.m 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941
  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 SafariServices;
  21. #import "ChatTextMessageCell.h"
  22. #import "TextMessage.h"
  23. #import "ChatDefines.h"
  24. #import "UserSettings.h"
  25. #import "UILabel+Markup.h"
  26. #import "NonFirstResponderActionSheet.h"
  27. #import "QRCodeActivity.h"
  28. #import "NSString+Emoji.h"
  29. #import "MyIdentityStore.h"
  30. #import "ContactStore.h"
  31. #import "Contact.h"
  32. #import "QuoteParser.h"
  33. #import "BundleUtil.h"
  34. #import "TextStyleUtils.h"
  35. #import "EntityManager.h"
  36. #import "ChatLocationMessageCell.h"
  37. #import "ActivityUtil.h"
  38. #import "Threema-Swift.h"
  39. #import "ChatCallMessageCell.h"
  40. static NSDictionary *linkAttributes = nil;
  41. static NSDictionary *activeLinkAttributes = nil;
  42. static NSDictionary *inactiveLinkAttributes = nil;
  43. static CGFloat sideMargin = 2.0f;
  44. static CGFloat quoteBarWidth = 2.0f;
  45. static CGFloat quoteBarSpacing = 8.0f;
  46. static CGFloat quoteTextSpacing = 8.0f;
  47. static CGFloat quoteRightSpacing = 3.0f;
  48. static CGFloat ZSWTappableLabelSpace = 22.0f;
  49. static CGFloat quoteImageSize = 60.0;
  50. static CGFloat quoteImageSpacing = 8.0;
  51. static CGFloat quoteIconSpacing = 8.0;
  52. static ColorTheme currentTheme;
  53. @implementation ChatTextMessageCell {
  54. EntityManager *entityManager;
  55. ZSWTappableLabel *textLabel;
  56. ZSWTappableLabel *quoteLabel;
  57. UIImageView *quoteIcon;
  58. UIImageView *quoteImagePreview;
  59. UIView *quoteBar;
  60. UIAccessibilityElement *cellElement;
  61. NSURL *actionUrl;
  62. NSString *actionPhone;
  63. NSInteger openButtonIndex, copyButtonIndex, callButtonIndex;
  64. NSString *origText;
  65. NSString *origQuotedText;
  66. NSString *origQuotedIdentity;
  67. BaseMessage *quotedMessage;
  68. }
  69. + (CGFloat)heightForMessage:(BaseMessage*)message forTableWidth:(CGFloat)tableWidth {
  70. CGSize size;
  71. CGSize maxSize = CGSizeMake([ChatMessageCell maxContentWidthForTableWidth:tableWidth], CGFLOAT_MAX);
  72. TextMessage *textMessage = (TextMessage*)message;
  73. NSString *text = [textMessage text];
  74. NSString *quotedText = nil;
  75. NSString *quotedIdentity = nil;
  76. UIImage *quotedImage = nil;
  77. UIImage *quoteIcon = nil;
  78. if (textMessage.quotedMessageId != nil) {
  79. EntityManager *entityManager = [[EntityManager alloc] init];
  80. BaseMessage *quoteMessage = [entityManager.entityFetcher messageWithId:textMessage.quotedMessageId conversation:textMessage.conversation];
  81. if (quoteMessage != nil) {
  82. if (quoteMessage.isOwn.boolValue) {
  83. quotedIdentity = [[MyIdentityStore sharedMyIdentityStore] identity];
  84. } else {
  85. if (quoteMessage.sender) {
  86. quotedIdentity = quoteMessage.sender.identity;
  87. } else {
  88. quotedIdentity = quoteMessage.conversation.contact.identity;
  89. }
  90. }
  91. quotedText = quoteMessage.quotePreviewText;
  92. if ([quoteMessage isKindOfClass:[ImageMessage class]]) {
  93. if (((ImageMessage *) quoteMessage).thumbnail != nil) {
  94. quotedImage = ((ImageMessage *) quoteMessage).thumbnail.uiImage;
  95. }
  96. }
  97. else if ([quoteMessage isKindOfClass:[VideoMessage class]]) {
  98. if (((VideoMessage *) quoteMessage).thumbnail != nil) {
  99. quotedImage = ((VideoMessage *) quoteMessage).thumbnail.uiImage;
  100. }
  101. }
  102. else if ([quoteMessage isKindOfClass:[FileMessage class]]) {
  103. if (((FileMessage *) quoteMessage).thumbnail != nil) {
  104. quotedImage = ((FileMessage *) quoteMessage).thumbnail.uiImage;
  105. }
  106. }
  107. else if ([quoteMessage isKindOfClass:[AudioMessage class]]) {
  108. quoteIcon = [BundleUtil imageNamed:@"ActionMicrophone"];
  109. }
  110. else if ([quoteMessage isKindOfClass:[BallotMessage class]]) {
  111. quoteIcon = [BundleUtil imageNamed:@"ActionBallot"];
  112. }
  113. else if ([quoteMessage isKindOfClass:[LocationMessage class]]) {
  114. quoteIcon = [BundleUtil imageNamed:@"CurrentLocation"];
  115. }
  116. } else {
  117. quotedIdentity = @"";
  118. quotedText = [BundleUtil localizedStringForKey:@"quote_not_found"];
  119. }
  120. } else {
  121. NSString *remainingBody = nil;
  122. quotedText = [QuoteParser parseQuoteFromMessage:text quotedIdentity:&quotedIdentity remainingBody:&remainingBody];
  123. if (quotedText) {
  124. text = remainingBody;
  125. }
  126. }
  127. if (![UserSettings sharedUserSettings].disableBigEmojis && [text isOnlyEmojisMaxCount:3]) {
  128. static ZSWTappableLabel *dummyLabelEmoji = nil;
  129. if (dummyLabelEmoji == nil) {
  130. dummyLabelEmoji = [ChatTextMessageCell makeAttributedLabelWithFrame:CGRectMake(0.0, 0.0, maxSize.width, maxSize.height)];
  131. }
  132. dummyLabelEmoji.font = [ChatMessageCell emojiFont];
  133. dummyLabelEmoji.attributedText = [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName: [ChatMessageCell emojiFont]}];
  134. size = [dummyLabelEmoji sizeThatFits:maxSize];
  135. } else {
  136. static ZSWTappableLabel *dummyLabel = nil;
  137. if (dummyLabel == nil) {
  138. dummyLabel = [ChatTextMessageCell makeAttributedLabelWithFrame:CGRectMake(0.0, 0.0, maxSize.width, maxSize.height)];
  139. }
  140. dummyLabel.font = [ChatMessageCell textFont];
  141. NSAttributedString *attributed = [TextStyleUtils makeAttributedStringFromString:text withFont:[ChatMessageCell textFont] textColor:[Colors fontNormal] isOwn:true application:[UIApplication sharedApplication]];
  142. NSMutableAttributedString *formattedAttributeString = [[NSMutableAttributedString alloc] initWithAttributedString:[dummyLabel applyMarkupFor:attributed]];
  143. dummyLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:formattedAttributeString textFont:[ChatMessageCell textFont] atColor:[dummyLabel.textColor colorWithAlphaComponent:0.4] messageInfo:message.isOwn.intValue application:[UIApplication sharedApplication]];
  144. size = [dummyLabel sizeThatFits:maxSize];
  145. }
  146. // Add quote?
  147. if (quotedText.length > 0 || textMessage.quotedMessageId != nil) {
  148. static ZSWTappableLabel *dummyLabelQuote = nil;
  149. CGSize maxSizeQuote;
  150. if (quotedImage != nil) {
  151. maxSizeQuote = CGSizeMake(maxSize.width - quoteBarWidth - quoteBarSpacing - quoteRightSpacing - quoteImageSize - quoteImageSpacing, CGFLOAT_MAX);
  152. } else {
  153. if (quoteIcon != nil) {
  154. maxSizeQuote = CGSizeMake(maxSize.width - quoteBarWidth - quoteBarSpacing - quoteRightSpacing - dummyLabelQuote.font.pointSize - quoteIconSpacing, CGFLOAT_MAX);
  155. } else {
  156. maxSizeQuote = CGSizeMake(maxSize.width - quoteBarWidth - quoteBarSpacing - quoteRightSpacing, CGFLOAT_MAX);
  157. }
  158. }
  159. if (dummyLabelQuote == nil) {
  160. dummyLabelQuote = [ChatTextMessageCell makeAttributedLabelWithFrame:CGRectMake(0.0, 0.0, maxSizeQuote.width, maxSizeQuote.height)];
  161. }
  162. dummyLabelQuote.font = [ChatMessageCell quoteFont];
  163. NSMutableAttributedString *quoteAttributed = [[NSMutableAttributedString alloc] initWithAttributedString:[ChatTextMessageCell makeQuoteAttributedStringForIdentity:quotedIdentity quotedText:quotedText inLabel:dummyLabelQuote]];
  164. dummyLabelQuote.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:quoteAttributed textFont:[ChatMessageCell quoteFont] atColor:[[Colors fontQuoteText] colorWithAlphaComponent:0.4] messageInfo:message.isOwn.intValue application:[UIApplication sharedApplication]];
  165. if (dummyLabelQuote.attributedText.length > 200) {
  166. NSMutableAttributedString *trimmedString = [[NSMutableAttributedString alloc] initWithAttributedString:[dummyLabelQuote.attributedText attributedSubstringFromRange:NSMakeRange(0, 200)]];
  167. NSAttributedString *ellipses = [[NSAttributedString alloc] initWithString:@"..." attributes:@{ NSForegroundColorAttributeName: [Colors fontQuoteText], NSFontAttributeName: [ChatMessageCell quoteFont]}];
  168. [trimmedString appendAttributedString:ellipses];
  169. dummyLabelQuote.attributedText = trimmedString;
  170. }
  171. CGSize quoteSize = [dummyLabelQuote sizeThatFits:maxSizeQuote];
  172. if (quotedImage != nil && quoteSize.height < quoteImageSize + quoteImageSpacing) {
  173. quoteSize.height = quoteImageSize + quoteImageSpacing;
  174. }
  175. size.height += quoteSize.height + quoteTextSpacing;
  176. size.width = MAX(size.width, quoteSize.width + quoteRightSpacing);
  177. }
  178. return size.height;
  179. }
  180. - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier transparent:(BOOL)transparent
  181. {
  182. self = [super initWithStyle:style reuseIdentifier:reuseIdentifier transparent:transparent];
  183. if (self) {
  184. // Create message text label
  185. textLabel = [ChatTextMessageCell makeAttributedLabelWithFrame:self.bounds];
  186. textLabel.tapDelegate = self;
  187. textLabel.longPressDelegate = self;
  188. [self.chatVc registerForPreviewingWithDelegate:self sourceView:textLabel];
  189. [self.contentView addSubview:textLabel];
  190. entityManager = [[EntityManager alloc] init];
  191. }
  192. return self;
  193. }
  194. - (void)setupColors {
  195. [super setupColors];
  196. textLabel.textColor = [Colors fontNormal];
  197. [self updateLinkColors];
  198. [self updateQuoteLabel];
  199. }
  200. - (void)updateLinkColors {
  201. if (currentTheme != [Colors getTheme]) {
  202. currentTheme = [Colors getTheme];
  203. textLabel.attributedText = [textLabel applyMarkupFor:[TextStyleUtils makeAttributedStringFromString:origText withFont:textLabel.font textColor:nil isOwn:self.message.isOwn.boolValue application:[UIApplication sharedApplication]]];
  204. }
  205. }
  206. - (void)layoutSubviews {
  207. CGFloat messageTextWidth;
  208. if (@available(iOS 11.0, *)) {
  209. messageTextWidth = [ChatMessageCell maxContentWidthForTableWidth:self.safeAreaLayoutGuide.layoutFrame.size.width];
  210. } else {
  211. messageTextWidth = [ChatMessageCell maxContentWidthForTableWidth:self.frame.size.width];
  212. }
  213. CGSize textSize = [textLabel sizeThatFits:CGSizeMake(messageTextWidth, CGFLOAT_MAX)];
  214. CGSize quoteSize = CGSizeMake(0, 0);
  215. CGSize bubbleSize = textSize;
  216. if (quoteLabel != nil && quoteLabel.hidden == NO) {
  217. if (quoteImagePreview.hidden == false) {
  218. quoteSize = [quoteLabel sizeThatFits:CGSizeMake(messageTextWidth - quoteBarWidth - quoteBarSpacing - quoteRightSpacing - quoteImageSize - quoteImageSpacing, CGFLOAT_MAX)];
  219. if (quoteSize.height < quoteImageSize + quoteBarSpacing) {
  220. quoteSize.height = quoteImageSize + quoteBarSpacing;
  221. }
  222. bubbleSize.height += quoteSize.height + quoteTextSpacing;
  223. bubbleSize.width = MAX(quoteSize.width + quoteImageSize + quoteImageSpacing + quoteRightSpacing, textSize.width);
  224. } else {
  225. if (quoteIcon.hidden == false) {
  226. quoteSize = [quoteLabel sizeThatFits:CGSizeMake(messageTextWidth - quoteBarWidth - quoteBarSpacing - quoteRightSpacing - [quoteLabel.font pointSize] + quoteIconSpacing, CGFLOAT_MAX)];
  227. bubbleSize.height += quoteSize.height + quoteTextSpacing;
  228. bubbleSize.width = MAX(quoteSize.width + quoteRightSpacing + [quoteLabel.font pointSize] + quoteIconSpacing, textSize.width + [quoteLabel.font pointSize] + quoteIconSpacing);
  229. } else {
  230. quoteSize = [quoteLabel sizeThatFits:CGSizeMake(messageTextWidth - quoteBarWidth - quoteBarSpacing - quoteRightSpacing, CGFLOAT_MAX)];
  231. bubbleSize.height += quoteSize.height + quoteTextSpacing;
  232. bubbleSize.width = MAX(quoteSize.width + quoteRightSpacing, textSize.width);
  233. }
  234. }
  235. }
  236. [self setBubbleContentSize:bubbleSize];
  237. [super layoutSubviews];
  238. CGFloat x;
  239. if (self.message.isOwn.boolValue) {
  240. x = self.contentView.frame.size.width-bubbleSize.width-21.0f-sideMargin;
  241. } else {
  242. x = 20.0f + self.contentLeftOffset;
  243. }
  244. CGFloat y = 7.0f;
  245. if (quoteLabel != nil && quoteLabel.hidden == NO) {
  246. if (quoteIcon.hidden == false) {
  247. quoteLabel.frame = CGRectMake(x + quoteBarWidth + quoteBarSpacing + [quoteLabel.font pointSize] + quoteIconSpacing, y, quoteSize.width, quoteSize.height);
  248. quoteIcon.frame = CGRectMake(x + quoteBarWidth + quoteBarSpacing, y + (quoteLabel.frame.size.height / 2) - ([quoteLabel.font pointSize] / 2), [quoteLabel.font pointSize], [quoteLabel.font pointSize]);
  249. } else {
  250. quoteLabel.frame = CGRectMake(x + quoteBarWidth + quoteBarSpacing, y, quoteSize.width, quoteSize.height);
  251. }
  252. quoteBar.frame = CGRectMake(x, y, quoteBarWidth, quoteSize.height);
  253. quoteImagePreview.frame = CGRectMake(quoteLabel.frame.origin.x + quoteLabel.frame.size.width + quoteImageSpacing, quoteBarSpacing, quoteImageSize, quoteImageSize);
  254. y += quoteLabel.frame.size.height;
  255. y += quoteTextSpacing;
  256. }
  257. textLabel.frame = CGRectMake(ceil(x), ceil(y - (ZSWTappableLabelSpace/2)), ceil(textSize.width), ceil(textSize.height + ZSWTappableLabelSpace));
  258. }
  259. - (NSString *)accessibilityLabelForContent {
  260. if (((NSString *)quoteLabel.text).length > 0) {
  261. NSMutableString *accessibilityText = [[NSMutableString alloc] initWithString:textLabel.text];
  262. [accessibilityText appendString:@"\n"];
  263. [accessibilityText appendString:NSLocalizedString(@"in_reply_to", nil)];
  264. [accessibilityText appendString:@"\n"];
  265. [accessibilityText appendString:quoteLabel.text];
  266. return accessibilityText;
  267. } else {
  268. return textLabel.text;
  269. }
  270. }
  271. - (void)setMessage:(BaseMessage *)newMessage {
  272. [super setMessage:newMessage];
  273. TextMessage *textMessage = (TextMessage*)self.message;
  274. origText = textMessage.text;
  275. [self updateQuoteLabel];
  276. if (![UserSettings sharedUserSettings].disableBigEmojis && [origText isOnlyEmojisMaxCount:3]) {
  277. textLabel.font = [ChatMessageCell emojiFont];
  278. textLabel.attributedText = [[NSAttributedString alloc] initWithString:origText attributes:@{NSFontAttributeName: [ChatMessageCell emojiFont]}];
  279. } else {
  280. textLabel.font = [ChatMessageCell textFont];
  281. NSAttributedString *attributed = [TextStyleUtils makeAttributedStringFromString:origText withFont:textLabel.font textColor:[Colors fontNormal] isOwn:self.message.isOwn.boolValue application:[UIApplication sharedApplication]];
  282. NSMutableAttributedString *formattedAttributeString = [[NSMutableAttributedString alloc] initWithAttributedString:[textLabel applyMarkupFor:attributed]];
  283. textLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:formattedAttributeString textFont:textLabel.font atColor:[textLabel.textColor colorWithAlphaComponent:0.4] messageInfo:textMessage.isOwn.intValue application:[UIApplication sharedApplication]];
  284. }
  285. if (self.message.isOwn.boolValue) {
  286. textLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
  287. } else {
  288. textLabel.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
  289. }
  290. cellElement = nil;
  291. [self setNeedsLayout];
  292. }
  293. - (void)updateQuoteLabel {
  294. TextMessage *textMessage = (TextMessage*)self.message;
  295. if (quoteLabel == nil) {
  296. quoteLabel = [ChatTextMessageCell makeAttributedLabelWithFrame:self.bounds];
  297. quoteLabel.font = [ChatMessageCell quoteFont];
  298. quoteLabel.tapDelegate = self;
  299. quoteLabel.longPressDelegate = self;
  300. [self.contentView addSubview:quoteLabel];
  301. quoteBar = [[UIView alloc] init];
  302. [self.contentView addSubview:quoteBar];
  303. }
  304. if (quoteImagePreview == nil) {
  305. quoteImagePreview = [[UIImageView alloc] initWithFrame:self.bounds];
  306. quoteImagePreview.contentMode = UIViewContentModeScaleAspectFill;
  307. quoteImagePreview.clipsToBounds = true;
  308. [self.contentView addSubview:quoteImagePreview];
  309. }
  310. if (quoteIcon == nil) {
  311. quoteIcon = [[UIImageView alloc] initWithFrame:self.bounds];
  312. quoteIcon.contentMode = UIViewContentModeScaleAspectFill;
  313. quoteIcon.clipsToBounds = true;
  314. [self.contentView addSubview:quoteIcon];
  315. }
  316. if (textMessage.quotedMessageId != nil) {
  317. BaseMessage *quoteMessage = [entityManager.entityFetcher messageWithId:textMessage.quotedMessageId conversation:textMessage.conversation];
  318. quoteBar.backgroundColor = [Colors quoteBar];
  319. NSString *quotedText = nil;
  320. if (quoteMessage != nil) {
  321. quotedMessage = quoteMessage;
  322. if (quoteMessage.isOwn.boolValue) {
  323. origQuotedIdentity = [[MyIdentityStore sharedMyIdentityStore] identity];
  324. } else {
  325. if (quoteMessage.sender) {
  326. origQuotedIdentity = quoteMessage.sender.identity;
  327. } else {
  328. origQuotedIdentity = quoteMessage.conversation.contact.identity;
  329. }
  330. }
  331. quotedText = quoteMessage.quotePreviewText;
  332. } else {
  333. quotedMessage = nil;
  334. origQuotedIdentity = @"";
  335. quotedText = [BundleUtil localizedStringForKey:@"quote_not_found"];
  336. origQuotedText = nil;
  337. }
  338. NSMutableAttributedString *quoteAttributed = [[NSMutableAttributedString alloc] initWithAttributedString:[ChatTextMessageCell makeQuoteAttributedStringForIdentity:origQuotedIdentity quotedText:quotedText inLabel:quoteLabel]];
  339. quoteLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:quoteAttributed textFont:quoteLabel.font atColor:[[Colors fontNormal] colorWithAlphaComponent:0.4] messageInfo:self.message.isOwn.intValue application:[UIApplication sharedApplication]];
  340. if (quoteLabel.attributedText.length > 200) {
  341. NSMutableAttributedString *trimmedString = [[NSMutableAttributedString alloc] initWithAttributedString:[quoteLabel.attributedText attributedSubstringFromRange:NSMakeRange(0, 200)]];
  342. NSAttributedString *ellipses = [[NSAttributedString alloc] initWithString:@"..." attributes:@{ NSForegroundColorAttributeName: [Colors fontQuoteText], NSFontAttributeName: [ChatMessageCell quoteFont]}];
  343. [trimmedString appendAttributedString:ellipses];
  344. quoteLabel.attributedText = trimmedString;
  345. }
  346. quoteLabel.hidden = NO;
  347. quoteBar.hidden = NO;
  348. quoteImagePreview.hidden = true;
  349. quoteImagePreview.image = nil;
  350. quoteIcon.hidden = true;
  351. quoteIcon.image = nil;
  352. if ([quotedMessage isKindOfClass:[ImageMessage class]]) {
  353. if (((ImageMessage *) quoteMessage).thumbnail != nil) {
  354. quoteImagePreview.hidden = false;
  355. quoteImagePreview.image = ((ImageMessage *)quoteMessage).thumbnail.uiImage;
  356. }
  357. }
  358. else if ([quotedMessage isKindOfClass:[VideoMessage class]]) {
  359. if (((VideoMessage *) quoteMessage).thumbnail != nil) {
  360. quoteImagePreview.hidden = false;
  361. quoteImagePreview.image = ((VideoMessage *)quoteMessage).thumbnail.uiImage;
  362. }
  363. }
  364. else if ([quotedMessage isKindOfClass:[FileMessage class]]) {
  365. if (((FileMessage *) quoteMessage).thumbnail != nil) {
  366. quoteImagePreview.hidden = false;
  367. quoteImagePreview.image = ((FileMessage *)quoteMessage).thumbnail.uiImage;
  368. }
  369. }
  370. else if ([quotedMessage isKindOfClass:[AudioMessage class]]) {
  371. quoteIcon.hidden = false;
  372. quoteIcon.image = [[BundleUtil imageNamed:@"ActionMicrophone"] imageWithTint:[Colors fontQuoteText]];
  373. }
  374. else if ([quoteMessage isKindOfClass:[BallotMessage class]]) {
  375. quoteIcon.hidden = false;
  376. quoteIcon.image = [[BundleUtil imageNamed:@"ActionBallot"] imageWithTint:[Colors fontQuoteText]];
  377. }
  378. else if ([quoteMessage isKindOfClass:[LocationMessage class]]) {
  379. quoteIcon.hidden = false;
  380. quoteIcon.image = [[BundleUtil imageNamed:@"CurrentLocation"] imageWithTint:[Colors fontQuoteText]];
  381. }
  382. return;
  383. }
  384. quoteImagePreview.image = nil;
  385. quoteIcon.image = nil;
  386. if (origText == nil) {
  387. return;
  388. }
  389. NSString *quotedIdentity = nil;
  390. NSString *remainingBody = nil;
  391. NSString *quotedText = [QuoteParser parseQuoteFromMessage:origText quotedIdentity:&quotedIdentity remainingBody:&remainingBody];
  392. if (quotedText != nil) {
  393. origQuotedText = quotedText;
  394. origText = remainingBody;
  395. origQuotedIdentity = quotedIdentity;
  396. quoteBar.backgroundColor = [Colors quoteBar];
  397. NSMutableAttributedString *quoteAttributed = [[NSMutableAttributedString alloc] initWithAttributedString:[ChatTextMessageCell makeQuoteAttributedStringForIdentity:quotedIdentity quotedText:quotedText inLabel:quoteLabel]];
  398. quoteLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:quoteAttributed textFont:quoteLabel.font atColor:[[Colors fontNormal] colorWithAlphaComponent:0.4] messageInfo:self.message.isOwn.intValue application:[UIApplication sharedApplication]];
  399. if (quoteLabel.attributedText.length > 200) {
  400. NSMutableAttributedString *trimmedString = [[NSMutableAttributedString alloc] initWithAttributedString:[quoteLabel.attributedText attributedSubstringFromRange:NSMakeRange(0, 200)]];
  401. NSAttributedString *ellipses = [[NSAttributedString alloc] initWithString:@"..." attributes:@{ NSForegroundColorAttributeName: [Colors fontQuoteText], NSFontAttributeName: [ChatMessageCell quoteFont]}];
  402. [trimmedString appendAttributedString:ellipses];
  403. quoteLabel.attributedText = trimmedString;
  404. }
  405. quoteLabel.hidden = NO;
  406. quoteBar.hidden = NO;
  407. quoteImagePreview.hidden = YES;
  408. quoteIcon.hidden = YES;
  409. quoteImagePreview.hidden = YES;
  410. } else {
  411. origQuotedText = nil;
  412. origQuotedIdentity = nil;
  413. quoteLabel.text = nil;
  414. quoteLabel.hidden = YES;
  415. quoteBar.hidden = YES;
  416. quoteImagePreview.hidden = YES;
  417. quoteIcon.hidden = YES;
  418. }
  419. }
  420. - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
  421. if (action == @selector(speakMessage:)) {
  422. return YES;
  423. } else {
  424. return [super canPerformAction:action withSender:sender];
  425. }
  426. }
  427. - (BOOL)highlightOccurencesOf:(NSString *)pattern {
  428. NSAttributedString *attributedString = [ChatMessageCell highlightedOccurencesOf:pattern inString:origText];
  429. if (![UserSettings sharedUserSettings].disableBigEmojis && [origText isOnlyEmojisMaxCount:3]) {
  430. textLabel.font = [ChatMessageCell emojiFont];
  431. NSAttributedString *emoji =[[NSAttributedString alloc] initWithString:origText attributes:@{NSFontAttributeName: [ChatMessageCell emojiFont]}];
  432. textLabel.attributedText = emoji;
  433. if (attributedString) {
  434. return YES;
  435. } else {
  436. return NO;
  437. }
  438. } else {
  439. if (attributedString) {
  440. NSMutableAttributedString *markupString = [[NSMutableAttributedString alloc] initWithAttributedString:[textLabel applyMarkupFor:attributedString]];
  441. textLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:markupString textFont:textLabel.font atColor:[textLabel.textColor colorWithAlphaComponent:0.4] messageInfo:self.message.isOwn.intValue application:[UIApplication sharedApplication]];
  442. return YES;
  443. } else {
  444. NSAttributedString *attributed = [TextStyleUtils makeAttributedStringFromString:origText withFont:textLabel.font textColor:[Colors fontNormal] isOwn:self.message.isOwn.boolValue application:[UIApplication sharedApplication]];
  445. NSMutableAttributedString *formattedAttributeString = [[NSMutableAttributedString alloc] initWithAttributedString:[textLabel applyMarkupFor:attributed]];
  446. textLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:formattedAttributeString textFont:textLabel.font atColor:[textLabel.textColor colorWithAlphaComponent:0.4] messageInfo:self.message.isOwn.intValue application:[UIApplication sharedApplication]];
  447. return NO;
  448. }
  449. }
  450. }
  451. - (void)copyMessage:(UIMenuController *)menuController {
  452. [[UIPasteboard generalPasteboard] setString:origText];
  453. }
  454. - (void)speakMessage:(UIMenuController *)menuController {
  455. AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:origText];
  456. AVSpeechSynthesizer *syn = [[AVSpeechSynthesizer alloc] init];
  457. [syn speakUtterance:utterance];
  458. }
  459. - (NSString *)textForQuote {
  460. return origText;
  461. }
  462. - (void)handleTapResult:(id)result {
  463. if ([result isKindOfClass:[Contact class]]) {
  464. [self.chatVc mentionTapped:(Contact *)result];
  465. } else {
  466. if ([result isKindOfClass:[NSString class]]) {
  467. if ([(NSString *)result isEqualToString:@"meContact"]) {
  468. [self.chatVc mentionTapped:(NSString *)result];
  469. } else {
  470. if ([(NSString *)result isEqualToString:@"searchQuote"]) {
  471. if (quotedMessage != nil) {
  472. [self.chatVc showQuotedMessage:quotedMessage];
  473. } else {
  474. if (origQuotedText != nil) {
  475. __block BaseMessage *foundMessage = nil;
  476. NSArray *messageHits = [entityManager.entityFetcher quoteMessagesContaining:origQuotedText message:self.message inConversation:self.message.conversation];
  477. [messageHits enumerateObjectsUsingBlock:^(BaseMessage *bm, NSUInteger idx, BOOL * _Nonnull stop) {
  478. if (( bm.conversation.isGroup && ( [bm.sender.identity isEqualToString:origQuotedIdentity] || ( bm.isOwn.boolValue && [[[MyIdentityStore sharedMyIdentityStore] identity] isEqualToString:origQuotedIdentity] ) ) ) || ( !bm.conversation.isGroup && ( [bm.conversation.contact.identity isEqualToString:origQuotedIdentity] || ( bm.isOwn.boolValue && [[[MyIdentityStore sharedMyIdentityStore] identity] isEqualToString:origQuotedIdentity] ) ) )) {
  479. if ([bm isKindOfClass:[TextMessage class]]) {
  480. NSString *quotedIdentity = nil;
  481. NSString *remainingBody = nil;
  482. NSString *quotedText = [QuoteParser parseQuoteFromMessage:((TextMessage *)bm).text quotedIdentity:&quotedIdentity remainingBody:&remainingBody];
  483. if (quotedText != nil) {
  484. NSString *remaining = [remainingBody stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  485. NSString *originalText = [origQuotedText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  486. if ([remaining isEqualToString:originalText]) {
  487. foundMessage = bm;
  488. *stop = YES;
  489. }
  490. } else {
  491. NSString *cellText = [((TextMessage *)bm).text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  492. NSString *originalText = [origQuotedText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  493. if ([cellText isEqualToString:originalText]) {
  494. foundMessage = bm;
  495. *stop = YES;
  496. }
  497. }
  498. }
  499. else if ([bm isKindOfClass:[ImageMessage class]]) {
  500. if ([[((ImageMessage *)bm).image getCaption] isEqualToString:origQuotedText]) {
  501. foundMessage = bm;
  502. *stop = YES;
  503. }
  504. }
  505. else if ([bm isKindOfClass:[FileMessage class]]) {
  506. if ([[((FileMessage *)bm) getCaption] isEqualToString:origQuotedText]) {
  507. foundMessage = bm;
  508. *stop = YES;
  509. }
  510. }
  511. else if ([bm isKindOfClass:[LocationMessage class]]) {
  512. NSString *locationText = [ChatLocationMessageCell displayTextForLocationMessage:(LocationMessage *)bm];
  513. if ([origQuotedText containsString:locationText]) {
  514. foundMessage = bm;
  515. *stop = YES;
  516. }
  517. }
  518. }
  519. }];
  520. if (foundMessage) {
  521. [self.chatVc showQuotedMessage:foundMessage];
  522. } else {
  523. [UIAlertTemplate showAlertWithOwner:self.chatVc title:@"" message:NSLocalizedString(@"quote_not_found", @"") actionOk:nil];
  524. }
  525. }
  526. }
  527. }
  528. }
  529. }
  530. else if ([result isKindOfClass:[NSTextCheckingResult class]]) {
  531. [self openLinkWithTextCheckingResult:(NSTextCheckingResult *)result];
  532. }
  533. }
  534. }
  535. - (void)handleLongPressResult:(NSTextCheckingResult *)result {
  536. if ([result isKindOfClass:[NSString class]]) {
  537. return;
  538. }
  539. if ([result isKindOfClass:[Contact class]]) {
  540. [self.chatVc mentionTapped:(Contact *)result];
  541. }
  542. else if (result.resultType == NSTextCheckingTypeLink) {
  543. actionUrl = result.URL;
  544. actionPhone = nil;
  545. UIAlertController *actionSheet = [NonFirstResponderActionSheet alertControllerWithTitle:[self displayStringForUrl:actionUrl] message:nil preferredStyle:UIAlertControllerStyleActionSheet];
  546. [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"open", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
  547. [IDNSafetyHelper safeOpenWithUrl:actionUrl viewController:self.chatVc];
  548. }]];
  549. [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"copy", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
  550. if (actionPhone != nil)
  551. [[UIPasteboard generalPasteboard] setString:actionPhone];
  552. else
  553. [[UIPasteboard generalPasteboard] setString:[self displayStringForUrl:actionUrl]];
  554. }]];
  555. [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleDefault handler:nil]];
  556. if (SYSTEM_IS_IPAD) {
  557. actionSheet.popoverPresentationController.sourceView = self;
  558. actionSheet.popoverPresentationController.sourceRect = self.bounds;
  559. }
  560. [self.chatVc.chatBar resignFirstResponder];
  561. [self.chatVc presentViewController:actionSheet animated:YES completion:nil];
  562. } else if (result.resultType == NSTextCheckingTypePhoneNumber) {
  563. actionPhone = result.phoneNumber;
  564. actionUrl = nil;
  565. UIAlertController *actionSheet = [NonFirstResponderActionSheet alertControllerWithTitle:actionPhone message:nil preferredStyle:UIAlertControllerStyleActionSheet];
  566. [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"call", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
  567. [self callPhoneNumber:actionPhone];
  568. }]];
  569. [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"copy", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
  570. if (actionPhone != nil)
  571. [[UIPasteboard generalPasteboard] setString:actionPhone];
  572. else
  573. [[UIPasteboard generalPasteboard] setString:[self displayStringForUrl:actionUrl]];
  574. }]];
  575. [actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleDefault handler:nil]];
  576. if (SYSTEM_IS_IPAD) {
  577. actionSheet.popoverPresentationController.sourceView = self;
  578. actionSheet.popoverPresentationController.sourceRect = self.bounds;
  579. }
  580. [self.chatVc.chatBar resignFirstResponder];
  581. [self.chatVc presentViewController:actionSheet animated:YES completion:nil];
  582. }
  583. }
  584. - (NSString*)displayStringForUrl:(NSURL*)url {
  585. NSString *urlString = [url.absoluteString stringByReplacingOccurrencesOfString:@"mailto:" withString:@""];
  586. return urlString;
  587. }
  588. + (ZSWTappableLabel*)makeAttributedLabelWithFrame:(CGRect)rect {
  589. ZSWTappableLabel *label = [[ZSWTappableLabel alloc] initWithFrame:rect];
  590. label.clearsContextBeforeDrawing = NO;
  591. label.backgroundColor = [UIColor clearColor];
  592. label.numberOfLines = 0;
  593. label.lineBreakMode = NSLineBreakByWordWrapping;
  594. label.font = [ChatMessageCell textFont];
  595. label.contentMode = UIViewContentModeScaleToFill;
  596. return label;
  597. }
  598. + (NSAttributedString*)makeQuoteAttributedStringForIdentity:(NSString*)identity quotedText:(NSString*)quotedText inLabel:(UILabel*)label {
  599. NSMutableAttributedString *quoteString = [[NSMutableAttributedString alloc] init];
  600. // Resolve identity to name
  601. Contact *contact = [[ContactStore sharedContactStore] contactForIdentity:identity];
  602. NSString *identityNewline;
  603. if ([identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) {
  604. identityNewline = [[BundleUtil localizedStringForKey:@"me"] stringByAppendingString:@"\n"];
  605. } else if (contact != nil) {
  606. identityNewline = [contact.displayName stringByAppendingString:@"\n"];
  607. } else {
  608. if ([identity length] > 0) {
  609. identityNewline = [identity stringByAppendingString:@"\n"];
  610. } else {
  611. identityNewline = @"";
  612. }
  613. }
  614. [quoteString appendAttributedString:[[NSAttributedString alloc] initWithString:identityNewline attributes:@{
  615. NSForegroundColorAttributeName: [Colors fontQuoteId],
  616. NSFontAttributeName: [ChatMessageCell quoteIdentityFont],
  617. @"ZSWTappableLabelTappableRegionAttributeName": @YES,
  618. @"NSTextCheckingResult": @"searchQuote"
  619. }]];
  620. NSMutableAttributedString *quotedTextAttr = [[NSMutableAttributedString alloc] initWithString:quotedText attributes:@{
  621. NSForegroundColorAttributeName: [Colors fontQuoteText],
  622. NSFontAttributeName: [ChatMessageCell quoteFont],
  623. @"ZSWTappableLabelTappableRegionAttributeName": @YES,
  624. @"NSTextCheckingResult": @"searchQuote"
  625. }];
  626. NSAttributedString *quotedTextAttrMarkup = [label applyMarkupFor:quotedTextAttr];
  627. [quoteString appendAttributedString:quotedTextAttrMarkup];
  628. return quoteString;
  629. }
  630. - (UIViewController *)previewViewControllerFor:(id<UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location {
  631. id<ZSWTappableLabelTappableRegionInfo> regionInfo = [textLabel tappableRegionInfoForPreviewingContext:previewingContext location:location];
  632. if (!regionInfo) {
  633. return nil;
  634. }
  635. NSTextCheckingResult *result = regionInfo.attributes[@"NSTextCheckingResult"];
  636. if ([result isKindOfClass:[NSTextCheckingResult class]]) {
  637. if (result.resultType == NSTextCheckingTypeLink && ![[result.URL absoluteString] hasPrefix:@"mailto:"]) {
  638. NSURL *url = result.URL;
  639. if ([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) {
  640. [regionInfo configurePreviewingContext:previewingContext];
  641. ThreemaSafariViewController *webController = [[ThreemaSafariViewController alloc] initWithURL:result.URL];
  642. webController.url = result.URL;
  643. return webController;
  644. }
  645. }
  646. }
  647. return nil;
  648. }
  649. - (UIContextMenuConfiguration *)getContextMenu:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) {
  650. if (!self.editing) {
  651. CGPoint convertedPoint = [textLabel convertPoint:point fromView:self.chatVc.chatContent];
  652. NSDictionary *regionInfo = [textLabel checkIsPointAction:convertedPoint];
  653. if (regionInfo != nil) {
  654. NSTextCheckingResult *result = regionInfo[@"NSTextCheckingResult"];
  655. if ([result isKindOfClass:[NSTextCheckingResult class]]) {
  656. if (result.resultType == NSTextCheckingTypeLink && ![[result.URL absoluteString] hasPrefix:@"mailto:"]) {
  657. NSURL *url = result.URL;
  658. if ([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) {
  659. ThreemaSafariViewController *webController = [[ThreemaSafariViewController alloc] initWithURL:result.URL];
  660. webController.url = result.URL;
  661. UIContextMenuConfiguration *conf = [UIContextMenuConfiguration configurationWithIdentifier:indexPath previewProvider:^UIViewController * _Nullable{
  662. return webController;
  663. } actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
  664. NSMutableArray *menuItems = [NSMutableArray array];
  665. UIImage *copyImage = [UIImage systemImageNamed:@"doc.on.doc.fill" compatibleWithTraitCollection:self.traitCollection];
  666. UIAction *action = [UIAction actionWithTitle:[BundleUtil localizedStringForKey:@"copy"] image:copyImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
  667. [[UIPasteboard generalPasteboard] setString:[self displayStringForUrl:result.URL]];
  668. }];
  669. [menuItems addObject:action];
  670. return [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:menuItems];
  671. }];
  672. return conf;
  673. }
  674. }
  675. }
  676. return nil;
  677. }
  678. }
  679. return [super getContextMenu:indexPath point:point];
  680. }
  681. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  682. if (touches.count == 1) {
  683. if (UIAccessibilityIsVoiceOverRunning()) {
  684. /* when VoiceOver is on, double-taps on message cells will result in a touch located
  685. in the center of the cell. Since this may not be within the bubble, it will not
  686. trigger playing/showing media. Therefore, we hand this event to the cell */
  687. if ([self performPlayActionForAccessibility])
  688. return;
  689. }
  690. UITouch *touch = [touches anyObject];
  691. CGPoint point = [touch locationInView:self.contentView];
  692. if (CGRectContainsPoint(quoteImagePreview.frame, point)) {
  693. [self handleTapResult:@"searchQuote"];
  694. return;
  695. }
  696. }
  697. [super touchesEnded:touches withEvent:event];
  698. }
  699. #pragma mark - ZSWTappableLabel delegate
  700. - (void)tappableLabel:(ZSWTappableLabel *)tappableLabel tappedAtIndex:(NSInteger)idx withAttributes:(NSDictionary *)attributes {
  701. [self handleTapResult:attributes[@"NSTextCheckingResult"]];
  702. }
  703. - (void)tappableLabel:(ZSWTappableLabel *)tappableLabel longPressedAtIndex:(NSInteger)idx withAttributes:(NSDictionary<NSString *,id> *)attributes {
  704. [self handleLongPressResult:attributes[@"NSTextCheckingResult"]];
  705. }
  706. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  707. if (self.editing) {
  708. // don't event forward to label
  709. return self;
  710. }
  711. return [super hitTest:point withEvent:event];
  712. }
  713. - (void)callPhoneNumber:(NSString*)phoneNumber {
  714. NSString *cleanString = [phoneNumber stringByReplacingOccurrencesOfString:@"\u00a0" withString:@""];
  715. NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"tel:%@", [cleanString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]]];
  716. [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
  717. }
  718. #pragma mark - UIAccessibilityContainer
  719. - (BOOL)isAccessibilityElement {
  720. return YES;
  721. }
  722. - (NSArray *)accessibilityCustomActions {
  723. NSMutableArray *actions = [[NSMutableArray alloc] initWithArray:[super accessibilityCustomActions]];
  724. int indexCounter = 0;
  725. NSMutableArray *tmpArray = [NSMutableArray new];
  726. NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];
  727. for (int i = 0; i < textLabel.accessibilityElementCount; i++) {
  728. UIAccessibilityElement *element = [textLabel accessibilityElementAtIndex:i];
  729. if (![element.accessibilityLabel isEqualToString:@"."] && ![element.accessibilityLabel isEqualToString:@"@"]) {
  730. NSTextCheckingResult *urlResult = [self checkTextResult:element.accessibilityLabel];
  731. if (urlResult) {
  732. UIAccessibilityCustomAction *linkAction = [[UIAccessibilityCustomAction alloc] initWithName:[NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"open", @""), element.accessibilityLabel] target:self selector:@selector(openLink:)];
  733. [tmpArray addObject:linkAction];
  734. [indexSet addIndex:indexCounter];
  735. indexCounter ++;
  736. UIAccessibilityCustomAction *shareAction = [[UIAccessibilityCustomAction alloc] initWithName:[NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"share", @""), element.accessibilityLabel] target:self selector:@selector(shareLink:)];
  737. [tmpArray addObject:shareAction];
  738. [indexSet addIndex:indexCounter];
  739. indexCounter ++;
  740. } else {
  741. UIAccessibilityCustomAction *mentionAction = [[UIAccessibilityCustomAction alloc] initWithName:[NSString stringWithFormat:@"%@ @%@", NSLocalizedString(@"details", @""), element.accessibilityLabel] target:self selector:@selector(openMention:)];
  742. [tmpArray addObject:mentionAction];
  743. [indexSet addIndex:indexCounter];
  744. indexCounter ++;
  745. }
  746. }
  747. }
  748. if (tmpArray.count > 0) {
  749. [actions insertObjects:tmpArray atIndexes:indexSet];
  750. }
  751. return actions;
  752. }
  753. - (BOOL)openLink:(UIAccessibilityCustomAction *)action {
  754. [self openLinkWithTextCheckingResult:[self checkTextResult:action.name]];
  755. return YES;
  756. }
  757. - (void)openLinkWithTextCheckingResult:(NSTextCheckingResult*)urlResult {
  758. if (urlResult.resultType == NSTextCheckingTypeLink) {
  759. [IDNSafetyHelper safeOpenWithUrl:urlResult.URL viewController:self.chatVc];
  760. } else if (urlResult.resultType == NSTextCheckingTypePhoneNumber) {
  761. [self callPhoneNumber:urlResult.phoneNumber];
  762. }
  763. }
  764. - (BOOL)shareLink:(UIAccessibilityCustomAction *)action {
  765. NSTextCheckingResult *urlResult = [self checkTextResult:action.name];
  766. if (urlResult.resultType == NSTextCheckingTypeLink) {
  767. UIActivityViewController *activityViewController = [ActivityUtil activityViewControllerWithActivityItems:@[urlResult.URL] applicationActivities:@[]];
  768. [self.chatVc presentActivityViewController:activityViewController animated:YES fromView:self];
  769. }
  770. else if (urlResult.resultType == NSTextCheckingTypePhoneNumber) {
  771. UIActivityViewController *activityViewController = [ActivityUtil activityViewControllerWithActivityItems:@[urlResult.phoneNumber] applicationActivities:@[]];
  772. [self.chatVc presentActivityViewController:activityViewController animated:YES fromView:self];
  773. }
  774. return YES;
  775. }
  776. - (BOOL)openMention:(UIAccessibilityCustomAction *)action {
  777. NSString *identity = [action.name stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"%@ @", NSLocalizedString(@"details", @"")] withString:@""];
  778. if ([identity isEqualToString:[BundleUtil localizedStringForKey:@"me"]]) {
  779. [self handleTapResult:@"meContact"];
  780. } else {
  781. Contact *contact = [[ContactStore sharedContactStore] contactForIdentity:identity];
  782. [self handleTapResult:contact];
  783. }
  784. return YES;
  785. }
  786. - (NSTextCheckingResult *)checkTextResult:(NSString *)text {
  787. NSTextCheckingTypes textCheckingTypes = NSTextCheckingTypeLink;
  788. static dispatch_once_t onceToken;
  789. static BOOL canOpenPhoneLinks;
  790. dispatch_once(&onceToken, ^{
  791. canOpenPhoneLinks = [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"tel:0"]];
  792. });
  793. if (canOpenPhoneLinks)
  794. textCheckingTypes |= NSTextCheckingTypePhoneNumber;
  795. __block NSTextCheckingResult *urlResult = nil;
  796. NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:textCheckingTypes error:NULL];
  797. [detector enumerateMatchesInString:text options:0 range:NSMakeRange(0, text.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
  798. urlResult = result;
  799. }];
  800. return urlResult;
  801. }
  802. @end