// _____ _
// |_ _| |_ _ _ ___ ___ _ __ __ _
// | | | ' \| '_/ -_) -_) ' \/ _` |_
// |_| |_||_|_| \___\___|_|_|_\__,_(_)
//
// Threema iOS Client
// Copyright (c) 2012-2020 Threema GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License, version 3,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
@import SafariServices;
#import "ChatTextMessageCell.h"
#import "TextMessage.h"
#import "ChatDefines.h"
#import "UserSettings.h"
#import "UILabel+Markup.h"
#import "NonFirstResponderActionSheet.h"
#import "QRCodeActivity.h"
#import "NSString+Emoji.h"
#import "MyIdentityStore.h"
#import "ContactStore.h"
#import "Contact.h"
#import "QuoteParser.h"
#import "BundleUtil.h"
#import "TextStyleUtils.h"
#import "EntityManager.h"
#import "ChatLocationMessageCell.h"
#import "ActivityUtil.h"
#import "Threema-Swift.h"
#import "ChatCallMessageCell.h"
static NSDictionary *linkAttributes = nil;
static NSDictionary *activeLinkAttributes = nil;
static NSDictionary *inactiveLinkAttributes = nil;
static CGFloat sideMargin = 2.0f;
static CGFloat quoteBarWidth = 2.0f;
static CGFloat quoteBarSpacing = 8.0f;
static CGFloat quoteTextSpacing = 8.0f;
static CGFloat quoteRightSpacing = 3.0f;
static CGFloat ZSWTappableLabelSpace = 22.0f;
static CGFloat quoteImageSize = 60.0;
static CGFloat quoteImageSpacing = 8.0;
static CGFloat quoteIconSpacing = 8.0;
static ColorTheme currentTheme;
@implementation ChatTextMessageCell {
EntityManager *entityManager;
ZSWTappableLabel *textLabel;
ZSWTappableLabel *quoteLabel;
UIImageView *quoteIcon;
UIImageView *quoteImagePreview;
UIView *quoteBar;
UIAccessibilityElement *cellElement;
NSURL *actionUrl;
NSString *actionPhone;
NSInteger openButtonIndex, copyButtonIndex, callButtonIndex;
NSString *origText;
NSString *origQuotedText;
NSString *origQuotedIdentity;
BaseMessage *quotedMessage;
}
+ (CGFloat)heightForMessage:(BaseMessage*)message forTableWidth:(CGFloat)tableWidth {
CGSize size;
CGSize maxSize = CGSizeMake([ChatMessageCell maxContentWidthForTableWidth:tableWidth], CGFLOAT_MAX);
TextMessage *textMessage = (TextMessage*)message;
NSString *text = [textMessage text];
NSString *quotedText = nil;
NSString *quotedIdentity = nil;
UIImage *quotedImage = nil;
UIImage *quoteIcon = nil;
if (textMessage.quotedMessageId != nil) {
EntityManager *entityManager = [[EntityManager alloc] init];
BaseMessage *quoteMessage = [entityManager.entityFetcher messageWithId:textMessage.quotedMessageId conversation:textMessage.conversation];
if (quoteMessage != nil) {
if (quoteMessage.isOwn.boolValue) {
quotedIdentity = [[MyIdentityStore sharedMyIdentityStore] identity];
} else {
if (quoteMessage.sender) {
quotedIdentity = quoteMessage.sender.identity;
} else {
quotedIdentity = quoteMessage.conversation.contact.identity;
}
}
quotedText = quoteMessage.quotePreviewText;
if ([quoteMessage isKindOfClass:[ImageMessage class]]) {
if (((ImageMessage *) quoteMessage).thumbnail != nil) {
quotedImage = ((ImageMessage *) quoteMessage).thumbnail.uiImage;
}
}
else if ([quoteMessage isKindOfClass:[VideoMessage class]]) {
if (((VideoMessage *) quoteMessage).thumbnail != nil) {
quotedImage = ((VideoMessage *) quoteMessage).thumbnail.uiImage;
}
}
else if ([quoteMessage isKindOfClass:[FileMessage class]]) {
if (((FileMessage *) quoteMessage).thumbnail != nil) {
quotedImage = ((FileMessage *) quoteMessage).thumbnail.uiImage;
}
}
else if ([quoteMessage isKindOfClass:[AudioMessage class]]) {
quoteIcon = [BundleUtil imageNamed:@"ActionMicrophone"];
}
else if ([quoteMessage isKindOfClass:[BallotMessage class]]) {
quoteIcon = [BundleUtil imageNamed:@"ActionBallot"];
}
else if ([quoteMessage isKindOfClass:[LocationMessage class]]) {
quoteIcon = [BundleUtil imageNamed:@"CurrentLocation"];
}
} else {
quotedIdentity = @"";
quotedText = [BundleUtil localizedStringForKey:@"quote_not_found"];
}
} else {
NSString *remainingBody = nil;
quotedText = [QuoteParser parseQuoteFromMessage:text quotedIdentity:"edIdentity remainingBody:&remainingBody];
if (quotedText) {
text = remainingBody;
}
}
if (![UserSettings sharedUserSettings].disableBigEmojis && [text isOnlyEmojisMaxCount:3]) {
static ZSWTappableLabel *dummyLabelEmoji = nil;
if (dummyLabelEmoji == nil) {
dummyLabelEmoji = [ChatTextMessageCell makeAttributedLabelWithFrame:CGRectMake(0.0, 0.0, maxSize.width, maxSize.height)];
}
dummyLabelEmoji.font = [ChatMessageCell emojiFont];
dummyLabelEmoji.attributedText = [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName: [ChatMessageCell emojiFont]}];
size = [dummyLabelEmoji sizeThatFits:maxSize];
} else {
static ZSWTappableLabel *dummyLabel = nil;
if (dummyLabel == nil) {
dummyLabel = [ChatTextMessageCell makeAttributedLabelWithFrame:CGRectMake(0.0, 0.0, maxSize.width, maxSize.height)];
}
dummyLabel.font = [ChatMessageCell textFont];
NSAttributedString *attributed = [TextStyleUtils makeAttributedStringFromString:text withFont:[ChatMessageCell textFont] textColor:[Colors fontNormal] isOwn:true application:[UIApplication sharedApplication]];
NSMutableAttributedString *formattedAttributeString = [[NSMutableAttributedString alloc] initWithAttributedString:[dummyLabel applyMarkupFor:attributed]];
dummyLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:formattedAttributeString textFont:[ChatMessageCell textFont] atColor:[dummyLabel.textColor colorWithAlphaComponent:0.4] messageInfo:message.isOwn.intValue application:[UIApplication sharedApplication]];
size = [dummyLabel sizeThatFits:maxSize];
}
// Add quote?
if (quotedText.length > 0 || textMessage.quotedMessageId != nil) {
static ZSWTappableLabel *dummyLabelQuote = nil;
CGSize maxSizeQuote;
if (quotedImage != nil) {
maxSizeQuote = CGSizeMake(maxSize.width - quoteBarWidth - quoteBarSpacing - quoteRightSpacing - quoteImageSize - quoteImageSpacing, CGFLOAT_MAX);
} else {
if (quoteIcon != nil) {
maxSizeQuote = CGSizeMake(maxSize.width - quoteBarWidth - quoteBarSpacing - quoteRightSpacing - dummyLabelQuote.font.pointSize - quoteIconSpacing, CGFLOAT_MAX);
} else {
maxSizeQuote = CGSizeMake(maxSize.width - quoteBarWidth - quoteBarSpacing - quoteRightSpacing, CGFLOAT_MAX);
}
}
if (dummyLabelQuote == nil) {
dummyLabelQuote = [ChatTextMessageCell makeAttributedLabelWithFrame:CGRectMake(0.0, 0.0, maxSizeQuote.width, maxSizeQuote.height)];
}
dummyLabelQuote.font = [ChatMessageCell quoteFont];
NSMutableAttributedString *quoteAttributed = [[NSMutableAttributedString alloc] initWithAttributedString:[ChatTextMessageCell makeQuoteAttributedStringForIdentity:quotedIdentity quotedText:quotedText inLabel:dummyLabelQuote]];
dummyLabelQuote.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:quoteAttributed textFont:[ChatMessageCell quoteFont] atColor:[[Colors fontQuoteText] colorWithAlphaComponent:0.4] messageInfo:message.isOwn.intValue application:[UIApplication sharedApplication]];
if (dummyLabelQuote.attributedText.length > 200) {
NSMutableAttributedString *trimmedString = [[NSMutableAttributedString alloc] initWithAttributedString:[dummyLabelQuote.attributedText attributedSubstringFromRange:NSMakeRange(0, 200)]];
NSAttributedString *ellipses = [[NSAttributedString alloc] initWithString:@"..." attributes:@{ NSForegroundColorAttributeName: [Colors fontQuoteText], NSFontAttributeName: [ChatMessageCell quoteFont]}];
[trimmedString appendAttributedString:ellipses];
dummyLabelQuote.attributedText = trimmedString;
}
CGSize quoteSize = [dummyLabelQuote sizeThatFits:maxSizeQuote];
if (quotedImage != nil && quoteSize.height < quoteImageSize + quoteImageSpacing) {
quoteSize.height = quoteImageSize + quoteImageSpacing;
}
size.height += quoteSize.height + quoteTextSpacing;
size.width = MAX(size.width, quoteSize.width + quoteRightSpacing);
}
return size.height;
}
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier transparent:(BOOL)transparent
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier transparent:transparent];
if (self) {
// Create message text label
textLabel = [ChatTextMessageCell makeAttributedLabelWithFrame:self.bounds];
textLabel.tapDelegate = self;
textLabel.longPressDelegate = self;
[self.chatVc registerForPreviewingWithDelegate:self sourceView:textLabel];
[self.contentView addSubview:textLabel];
entityManager = [[EntityManager alloc] init];
}
return self;
}
- (void)setupColors {
[super setupColors];
textLabel.textColor = [Colors fontNormal];
[self updateLinkColors];
[self updateQuoteLabel];
}
- (void)updateLinkColors {
if (currentTheme != [Colors getTheme]) {
currentTheme = [Colors getTheme];
textLabel.attributedText = [textLabel applyMarkupFor:[TextStyleUtils makeAttributedStringFromString:origText withFont:textLabel.font textColor:nil isOwn:self.message.isOwn.boolValue application:[UIApplication sharedApplication]]];
}
}
- (void)layoutSubviews {
CGFloat messageTextWidth;
if (@available(iOS 11.0, *)) {
messageTextWidth = [ChatMessageCell maxContentWidthForTableWidth:self.safeAreaLayoutGuide.layoutFrame.size.width];
} else {
messageTextWidth = [ChatMessageCell maxContentWidthForTableWidth:self.frame.size.width];
}
CGSize textSize = [textLabel sizeThatFits:CGSizeMake(messageTextWidth, CGFLOAT_MAX)];
CGSize quoteSize = CGSizeMake(0, 0);
CGSize bubbleSize = textSize;
if (quoteLabel != nil && quoteLabel.hidden == NO) {
if (quoteImagePreview.hidden == false) {
quoteSize = [quoteLabel sizeThatFits:CGSizeMake(messageTextWidth - quoteBarWidth - quoteBarSpacing - quoteRightSpacing - quoteImageSize - quoteImageSpacing, CGFLOAT_MAX)];
if (quoteSize.height < quoteImageSize + quoteBarSpacing) {
quoteSize.height = quoteImageSize + quoteBarSpacing;
}
bubbleSize.height += quoteSize.height + quoteTextSpacing;
bubbleSize.width = MAX(quoteSize.width + quoteImageSize + quoteImageSpacing + quoteRightSpacing, textSize.width);
} else {
if (quoteIcon.hidden == false) {
quoteSize = [quoteLabel sizeThatFits:CGSizeMake(messageTextWidth - quoteBarWidth - quoteBarSpacing - quoteRightSpacing - [quoteLabel.font pointSize] + quoteIconSpacing, CGFLOAT_MAX)];
bubbleSize.height += quoteSize.height + quoteTextSpacing;
bubbleSize.width = MAX(quoteSize.width + quoteRightSpacing + [quoteLabel.font pointSize] + quoteIconSpacing, textSize.width + [quoteLabel.font pointSize] + quoteIconSpacing);
} else {
quoteSize = [quoteLabel sizeThatFits:CGSizeMake(messageTextWidth - quoteBarWidth - quoteBarSpacing - quoteRightSpacing, CGFLOAT_MAX)];
bubbleSize.height += quoteSize.height + quoteTextSpacing;
bubbleSize.width = MAX(quoteSize.width + quoteRightSpacing, textSize.width);
}
}
}
[self setBubbleContentSize:bubbleSize];
[super layoutSubviews];
CGFloat x;
if (self.message.isOwn.boolValue) {
x = self.contentView.frame.size.width-bubbleSize.width-21.0f-sideMargin;
} else {
x = 20.0f + self.contentLeftOffset;
}
CGFloat y = 7.0f;
if (quoteLabel != nil && quoteLabel.hidden == NO) {
if (quoteIcon.hidden == false) {
quoteLabel.frame = CGRectMake(x + quoteBarWidth + quoteBarSpacing + [quoteLabel.font pointSize] + quoteIconSpacing, y, quoteSize.width, quoteSize.height);
quoteIcon.frame = CGRectMake(x + quoteBarWidth + quoteBarSpacing, y + (quoteLabel.frame.size.height / 2) - ([quoteLabel.font pointSize] / 2), [quoteLabel.font pointSize], [quoteLabel.font pointSize]);
} else {
quoteLabel.frame = CGRectMake(x + quoteBarWidth + quoteBarSpacing, y, quoteSize.width, quoteSize.height);
}
quoteBar.frame = CGRectMake(x, y, quoteBarWidth, quoteSize.height);
quoteImagePreview.frame = CGRectMake(quoteLabel.frame.origin.x + quoteLabel.frame.size.width + quoteImageSpacing, quoteBarSpacing, quoteImageSize, quoteImageSize);
y += quoteLabel.frame.size.height;
y += quoteTextSpacing;
}
textLabel.frame = CGRectMake(ceil(x), ceil(y - (ZSWTappableLabelSpace/2)), ceil(textSize.width), ceil(textSize.height + ZSWTappableLabelSpace));
}
- (NSString *)accessibilityLabelForContent {
if (((NSString *)quoteLabel.text).length > 0) {
NSMutableString *accessibilityText = [[NSMutableString alloc] initWithString:textLabel.text];
[accessibilityText appendString:@"\n"];
[accessibilityText appendString:NSLocalizedString(@"in_reply_to", nil)];
[accessibilityText appendString:@"\n"];
[accessibilityText appendString:quoteLabel.text];
return accessibilityText;
} else {
return textLabel.text;
}
}
- (void)setMessage:(BaseMessage *)newMessage {
[super setMessage:newMessage];
TextMessage *textMessage = (TextMessage*)self.message;
origText = textMessage.text;
[self updateQuoteLabel];
if (![UserSettings sharedUserSettings].disableBigEmojis && [origText isOnlyEmojisMaxCount:3]) {
textLabel.font = [ChatMessageCell emojiFont];
textLabel.attributedText = [[NSAttributedString alloc] initWithString:origText attributes:@{NSFontAttributeName: [ChatMessageCell emojiFont]}];
} else {
textLabel.font = [ChatMessageCell textFont];
NSAttributedString *attributed = [TextStyleUtils makeAttributedStringFromString:origText withFont:textLabel.font textColor:[Colors fontNormal] isOwn:self.message.isOwn.boolValue application:[UIApplication sharedApplication]];
NSMutableAttributedString *formattedAttributeString = [[NSMutableAttributedString alloc] initWithAttributedString:[textLabel applyMarkupFor:attributed]];
textLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:formattedAttributeString textFont:textLabel.font atColor:[textLabel.textColor colorWithAlphaComponent:0.4] messageInfo:textMessage.isOwn.intValue application:[UIApplication sharedApplication]];
}
if (self.message.isOwn.boolValue) {
textLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
} else {
textLabel.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
}
cellElement = nil;
[self setNeedsLayout];
}
- (void)updateQuoteLabel {
TextMessage *textMessage = (TextMessage*)self.message;
if (quoteLabel == nil) {
quoteLabel = [ChatTextMessageCell makeAttributedLabelWithFrame:self.bounds];
quoteLabel.font = [ChatMessageCell quoteFont];
quoteLabel.tapDelegate = self;
quoteLabel.longPressDelegate = self;
[self.contentView addSubview:quoteLabel];
quoteBar = [[UIView alloc] init];
[self.contentView addSubview:quoteBar];
}
if (quoteImagePreview == nil) {
quoteImagePreview = [[UIImageView alloc] initWithFrame:self.bounds];
quoteImagePreview.contentMode = UIViewContentModeScaleAspectFill;
quoteImagePreview.clipsToBounds = true;
[self.contentView addSubview:quoteImagePreview];
}
if (quoteIcon == nil) {
quoteIcon = [[UIImageView alloc] initWithFrame:self.bounds];
quoteIcon.contentMode = UIViewContentModeScaleAspectFill;
quoteIcon.clipsToBounds = true;
[self.contentView addSubview:quoteIcon];
}
if (textMessage.quotedMessageId != nil) {
BaseMessage *quoteMessage = [entityManager.entityFetcher messageWithId:textMessage.quotedMessageId conversation:textMessage.conversation];
quoteBar.backgroundColor = [Colors quoteBar];
NSString *quotedText = nil;
if (quoteMessage != nil) {
quotedMessage = quoteMessage;
if (quoteMessage.isOwn.boolValue) {
origQuotedIdentity = [[MyIdentityStore sharedMyIdentityStore] identity];
} else {
if (quoteMessage.sender) {
origQuotedIdentity = quoteMessage.sender.identity;
} else {
origQuotedIdentity = quoteMessage.conversation.contact.identity;
}
}
quotedText = quoteMessage.quotePreviewText;
} else {
quotedMessage = nil;
origQuotedIdentity = @"";
quotedText = [BundleUtil localizedStringForKey:@"quote_not_found"];
origQuotedText = nil;
}
NSMutableAttributedString *quoteAttributed = [[NSMutableAttributedString alloc] initWithAttributedString:[ChatTextMessageCell makeQuoteAttributedStringForIdentity:origQuotedIdentity quotedText:quotedText inLabel:quoteLabel]];
quoteLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:quoteAttributed textFont:quoteLabel.font atColor:[[Colors fontNormal] colorWithAlphaComponent:0.4] messageInfo:self.message.isOwn.intValue application:[UIApplication sharedApplication]];
if (quoteLabel.attributedText.length > 200) {
NSMutableAttributedString *trimmedString = [[NSMutableAttributedString alloc] initWithAttributedString:[quoteLabel.attributedText attributedSubstringFromRange:NSMakeRange(0, 200)]];
NSAttributedString *ellipses = [[NSAttributedString alloc] initWithString:@"..." attributes:@{ NSForegroundColorAttributeName: [Colors fontQuoteText], NSFontAttributeName: [ChatMessageCell quoteFont]}];
[trimmedString appendAttributedString:ellipses];
quoteLabel.attributedText = trimmedString;
}
quoteLabel.hidden = NO;
quoteBar.hidden = NO;
quoteImagePreview.hidden = true;
quoteImagePreview.image = nil;
quoteIcon.hidden = true;
quoteIcon.image = nil;
if ([quotedMessage isKindOfClass:[ImageMessage class]]) {
if (((ImageMessage *) quoteMessage).thumbnail != nil) {
quoteImagePreview.hidden = false;
quoteImagePreview.image = ((ImageMessage *)quoteMessage).thumbnail.uiImage;
}
}
else if ([quotedMessage isKindOfClass:[VideoMessage class]]) {
if (((VideoMessage *) quoteMessage).thumbnail != nil) {
quoteImagePreview.hidden = false;
quoteImagePreview.image = ((VideoMessage *)quoteMessage).thumbnail.uiImage;
}
}
else if ([quotedMessage isKindOfClass:[FileMessage class]]) {
if (((FileMessage *) quoteMessage).thumbnail != nil) {
quoteImagePreview.hidden = false;
quoteImagePreview.image = ((FileMessage *)quoteMessage).thumbnail.uiImage;
}
}
else if ([quotedMessage isKindOfClass:[AudioMessage class]]) {
quoteIcon.hidden = false;
quoteIcon.image = [[BundleUtil imageNamed:@"ActionMicrophone"] imageWithTint:[Colors fontQuoteText]];
}
else if ([quoteMessage isKindOfClass:[BallotMessage class]]) {
quoteIcon.hidden = false;
quoteIcon.image = [[BundleUtil imageNamed:@"ActionBallot"] imageWithTint:[Colors fontQuoteText]];
}
else if ([quoteMessage isKindOfClass:[LocationMessage class]]) {
quoteIcon.hidden = false;
quoteIcon.image = [[BundleUtil imageNamed:@"CurrentLocation"] imageWithTint:[Colors fontQuoteText]];
}
return;
}
quoteImagePreview.image = nil;
quoteIcon.image = nil;
if (origText == nil) {
return;
}
NSString *quotedIdentity = nil;
NSString *remainingBody = nil;
NSString *quotedText = [QuoteParser parseQuoteFromMessage:origText quotedIdentity:"edIdentity remainingBody:&remainingBody];
if (quotedText != nil) {
origQuotedText = quotedText;
origText = remainingBody;
origQuotedIdentity = quotedIdentity;
quoteBar.backgroundColor = [Colors quoteBar];
NSMutableAttributedString *quoteAttributed = [[NSMutableAttributedString alloc] initWithAttributedString:[ChatTextMessageCell makeQuoteAttributedStringForIdentity:quotedIdentity quotedText:quotedText inLabel:quoteLabel]];
quoteLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:quoteAttributed textFont:quoteLabel.font atColor:[[Colors fontNormal] colorWithAlphaComponent:0.4] messageInfo:self.message.isOwn.intValue application:[UIApplication sharedApplication]];
if (quoteLabel.attributedText.length > 200) {
NSMutableAttributedString *trimmedString = [[NSMutableAttributedString alloc] initWithAttributedString:[quoteLabel.attributedText attributedSubstringFromRange:NSMakeRange(0, 200)]];
NSAttributedString *ellipses = [[NSAttributedString alloc] initWithString:@"..." attributes:@{ NSForegroundColorAttributeName: [Colors fontQuoteText], NSFontAttributeName: [ChatMessageCell quoteFont]}];
[trimmedString appendAttributedString:ellipses];
quoteLabel.attributedText = trimmedString;
}
quoteLabel.hidden = NO;
quoteBar.hidden = NO;
quoteImagePreview.hidden = YES;
quoteIcon.hidden = YES;
quoteImagePreview.hidden = YES;
} else {
origQuotedText = nil;
origQuotedIdentity = nil;
quoteLabel.text = nil;
quoteLabel.hidden = YES;
quoteBar.hidden = YES;
quoteImagePreview.hidden = YES;
quoteIcon.hidden = YES;
}
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (action == @selector(speakMessage:)) {
return YES;
} else {
return [super canPerformAction:action withSender:sender];
}
}
- (BOOL)highlightOccurencesOf:(NSString *)pattern {
NSAttributedString *attributedString = [ChatMessageCell highlightedOccurencesOf:pattern inString:origText];
if (![UserSettings sharedUserSettings].disableBigEmojis && [origText isOnlyEmojisMaxCount:3]) {
textLabel.font = [ChatMessageCell emojiFont];
NSAttributedString *emoji =[[NSAttributedString alloc] initWithString:origText attributes:@{NSFontAttributeName: [ChatMessageCell emojiFont]}];
textLabel.attributedText = emoji;
if (attributedString) {
return YES;
} else {
return NO;
}
} else {
if (attributedString) {
NSMutableAttributedString *markupString = [[NSMutableAttributedString alloc] initWithAttributedString:[textLabel applyMarkupFor:attributedString]];
textLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:markupString textFont:textLabel.font atColor:[textLabel.textColor colorWithAlphaComponent:0.4] messageInfo:self.message.isOwn.intValue application:[UIApplication sharedApplication]];
return YES;
} else {
NSAttributedString *attributed = [TextStyleUtils makeAttributedStringFromString:origText withFont:textLabel.font textColor:[Colors fontNormal] isOwn:self.message.isOwn.boolValue application:[UIApplication sharedApplication]];
NSMutableAttributedString *formattedAttributeString = [[NSMutableAttributedString alloc] initWithAttributedString:[textLabel applyMarkupFor:attributed]];
textLabel.attributedText = [TextStyleUtils makeMentionsAttributedStringForAttributedString:formattedAttributeString textFont:textLabel.font atColor:[textLabel.textColor colorWithAlphaComponent:0.4] messageInfo:self.message.isOwn.intValue application:[UIApplication sharedApplication]];
return NO;
}
}
}
- (void)copyMessage:(UIMenuController *)menuController {
[[UIPasteboard generalPasteboard] setString:origText];
}
- (void)speakMessage:(UIMenuController *)menuController {
AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:origText];
AVSpeechSynthesizer *syn = [[AVSpeechSynthesizer alloc] init];
[syn speakUtterance:utterance];
}
- (NSString *)textForQuote {
return origText;
}
- (void)handleTapResult:(id)result {
if ([result isKindOfClass:[Contact class]]) {
[self.chatVc mentionTapped:(Contact *)result];
} else {
if ([result isKindOfClass:[NSString class]]) {
if ([(NSString *)result isEqualToString:@"meContact"]) {
[self.chatVc mentionTapped:(NSString *)result];
} else {
if ([(NSString *)result isEqualToString:@"searchQuote"]) {
if (quotedMessage != nil) {
[self.chatVc showQuotedMessage:quotedMessage];
} else {
if (origQuotedText != nil) {
__block BaseMessage *foundMessage = nil;
NSArray *messageHits = [entityManager.entityFetcher quoteMessagesContaining:origQuotedText message:self.message inConversation:self.message.conversation];
[messageHits enumerateObjectsUsingBlock:^(BaseMessage *bm, NSUInteger idx, BOOL * _Nonnull stop) {
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] ) ) )) {
if ([bm isKindOfClass:[TextMessage class]]) {
NSString *quotedIdentity = nil;
NSString *remainingBody = nil;
NSString *quotedText = [QuoteParser parseQuoteFromMessage:((TextMessage *)bm).text quotedIdentity:"edIdentity remainingBody:&remainingBody];
if (quotedText != nil) {
NSString *remaining = [remainingBody stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *originalText = [origQuotedText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([remaining isEqualToString:originalText]) {
foundMessage = bm;
*stop = YES;
}
} else {
NSString *cellText = [((TextMessage *)bm).text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSString *originalText = [origQuotedText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([cellText isEqualToString:originalText]) {
foundMessage = bm;
*stop = YES;
}
}
}
else if ([bm isKindOfClass:[ImageMessage class]]) {
if ([[((ImageMessage *)bm).image getCaption] isEqualToString:origQuotedText]) {
foundMessage = bm;
*stop = YES;
}
}
else if ([bm isKindOfClass:[FileMessage class]]) {
if ([[((FileMessage *)bm) getCaption] isEqualToString:origQuotedText]) {
foundMessage = bm;
*stop = YES;
}
}
else if ([bm isKindOfClass:[LocationMessage class]]) {
NSString *locationText = [ChatLocationMessageCell displayTextForLocationMessage:(LocationMessage *)bm];
if ([origQuotedText containsString:locationText]) {
foundMessage = bm;
*stop = YES;
}
}
}
}];
if (foundMessage) {
[self.chatVc showQuotedMessage:foundMessage];
} else {
[UIAlertTemplate showAlertWithOwner:self.chatVc title:@"" message:NSLocalizedString(@"quote_not_found", @"") actionOk:nil];
}
}
}
}
}
}
else if ([result isKindOfClass:[NSTextCheckingResult class]]) {
[self openLinkWithTextCheckingResult:(NSTextCheckingResult *)result];
}
}
}
- (void)handleLongPressResult:(NSTextCheckingResult *)result {
if ([result isKindOfClass:[NSString class]]) {
return;
}
if ([result isKindOfClass:[Contact class]]) {
[self.chatVc mentionTapped:(Contact *)result];
}
else if (result.resultType == NSTextCheckingTypeLink) {
actionUrl = result.URL;
actionPhone = nil;
UIAlertController *actionSheet = [NonFirstResponderActionSheet alertControllerWithTitle:[self displayStringForUrl:actionUrl] message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"open", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
[IDNSafetyHelper safeOpenWithUrl:actionUrl viewController:self.chatVc];
}]];
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"copy", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
if (actionPhone != nil)
[[UIPasteboard generalPasteboard] setString:actionPhone];
else
[[UIPasteboard generalPasteboard] setString:[self displayStringForUrl:actionUrl]];
}]];
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleDefault handler:nil]];
if (SYSTEM_IS_IPAD) {
actionSheet.popoverPresentationController.sourceView = self;
actionSheet.popoverPresentationController.sourceRect = self.bounds;
}
[self.chatVc.chatBar resignFirstResponder];
[self.chatVc presentViewController:actionSheet animated:YES completion:nil];
} else if (result.resultType == NSTextCheckingTypePhoneNumber) {
actionPhone = result.phoneNumber;
actionUrl = nil;
UIAlertController *actionSheet = [NonFirstResponderActionSheet alertControllerWithTitle:actionPhone message:nil preferredStyle:UIAlertControllerStyleActionSheet];
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"call", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
[self callPhoneNumber:actionPhone];
}]];
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"copy", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
if (actionPhone != nil)
[[UIPasteboard generalPasteboard] setString:actionPhone];
else
[[UIPasteboard generalPasteboard] setString:[self displayStringForUrl:actionUrl]];
}]];
[actionSheet addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleDefault handler:nil]];
if (SYSTEM_IS_IPAD) {
actionSheet.popoverPresentationController.sourceView = self;
actionSheet.popoverPresentationController.sourceRect = self.bounds;
}
[self.chatVc.chatBar resignFirstResponder];
[self.chatVc presentViewController:actionSheet animated:YES completion:nil];
}
}
- (NSString*)displayStringForUrl:(NSURL*)url {
NSString *urlString = [url.absoluteString stringByReplacingOccurrencesOfString:@"mailto:" withString:@""];
return urlString;
}
+ (ZSWTappableLabel*)makeAttributedLabelWithFrame:(CGRect)rect {
ZSWTappableLabel *label = [[ZSWTappableLabel alloc] initWithFrame:rect];
label.clearsContextBeforeDrawing = NO;
label.backgroundColor = [UIColor clearColor];
label.numberOfLines = 0;
label.lineBreakMode = NSLineBreakByWordWrapping;
label.font = [ChatMessageCell textFont];
label.contentMode = UIViewContentModeScaleToFill;
return label;
}
+ (NSAttributedString*)makeQuoteAttributedStringForIdentity:(NSString*)identity quotedText:(NSString*)quotedText inLabel:(UILabel*)label {
NSMutableAttributedString *quoteString = [[NSMutableAttributedString alloc] init];
// Resolve identity to name
Contact *contact = [[ContactStore sharedContactStore] contactForIdentity:identity];
NSString *identityNewline;
if ([identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) {
identityNewline = [[BundleUtil localizedStringForKey:@"me"] stringByAppendingString:@"\n"];
} else if (contact != nil) {
identityNewline = [contact.displayName stringByAppendingString:@"\n"];
} else {
if ([identity length] > 0) {
identityNewline = [identity stringByAppendingString:@"\n"];
} else {
identityNewline = @"";
}
}
[quoteString appendAttributedString:[[NSAttributedString alloc] initWithString:identityNewline attributes:@{
NSForegroundColorAttributeName: [Colors fontQuoteId],
NSFontAttributeName: [ChatMessageCell quoteIdentityFont],
@"ZSWTappableLabelTappableRegionAttributeName": @YES,
@"NSTextCheckingResult": @"searchQuote"
}]];
NSMutableAttributedString *quotedTextAttr = [[NSMutableAttributedString alloc] initWithString:quotedText attributes:@{
NSForegroundColorAttributeName: [Colors fontQuoteText],
NSFontAttributeName: [ChatMessageCell quoteFont],
@"ZSWTappableLabelTappableRegionAttributeName": @YES,
@"NSTextCheckingResult": @"searchQuote"
}];
NSAttributedString *quotedTextAttrMarkup = [label applyMarkupFor:quotedTextAttr];
[quoteString appendAttributedString:quotedTextAttrMarkup];
return quoteString;
}
- (UIViewController *)previewViewControllerFor:(id)previewingContext viewControllerForLocation:(CGPoint)location {
id regionInfo = [textLabel tappableRegionInfoForPreviewingContext:previewingContext location:location];
if (!regionInfo) {
return nil;
}
NSTextCheckingResult *result = regionInfo.attributes[@"NSTextCheckingResult"];
if ([result isKindOfClass:[NSTextCheckingResult class]]) {
if (result.resultType == NSTextCheckingTypeLink && ![[result.URL absoluteString] hasPrefix:@"mailto:"]) {
NSURL *url = result.URL;
if ([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) {
[regionInfo configurePreviewingContext:previewingContext];
ThreemaSafariViewController *webController = [[ThreemaSafariViewController alloc] initWithURL:result.URL];
webController.url = result.URL;
return webController;
}
}
}
return nil;
}
- (UIContextMenuConfiguration *)getContextMenu:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) {
if (!self.editing) {
CGPoint convertedPoint = [textLabel convertPoint:point fromView:self.chatVc.chatContent];
NSDictionary *regionInfo = [textLabel checkIsPointAction:convertedPoint];
if (regionInfo != nil) {
NSTextCheckingResult *result = regionInfo[@"NSTextCheckingResult"];
if ([result isKindOfClass:[NSTextCheckingResult class]]) {
if (result.resultType == NSTextCheckingTypeLink && ![[result.URL absoluteString] hasPrefix:@"mailto:"]) {
NSURL *url = result.URL;
if ([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) {
ThreemaSafariViewController *webController = [[ThreemaSafariViewController alloc] initWithURL:result.URL];
webController.url = result.URL;
UIContextMenuConfiguration *conf = [UIContextMenuConfiguration configurationWithIdentifier:indexPath previewProvider:^UIViewController * _Nullable{
return webController;
} actionProvider:^UIMenu * _Nullable(NSArray * _Nonnull suggestedActions) {
NSMutableArray *menuItems = [NSMutableArray array];
UIImage *copyImage = [UIImage systemImageNamed:@"doc.on.doc.fill" compatibleWithTraitCollection:self.traitCollection];
UIAction *action = [UIAction actionWithTitle:[BundleUtil localizedStringForKey:@"copy"] image:copyImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
[[UIPasteboard generalPasteboard] setString:[self displayStringForUrl:result.URL]];
}];
[menuItems addObject:action];
return [UIMenu menuWithTitle:@"" image:nil identifier:UIMenuApplication options:UIMenuOptionsDisplayInline children:menuItems];
}];
return conf;
}
}
}
return nil;
}
}
return [super getContextMenu:indexPath point:point];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (touches.count == 1) {
if (UIAccessibilityIsVoiceOverRunning()) {
/* when VoiceOver is on, double-taps on message cells will result in a touch located
in the center of the cell. Since this may not be within the bubble, it will not
trigger playing/showing media. Therefore, we hand this event to the cell */
if ([self performPlayActionForAccessibility])
return;
}
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.contentView];
if (CGRectContainsPoint(quoteImagePreview.frame, point)) {
[self handleTapResult:@"searchQuote"];
return;
}
}
[super touchesEnded:touches withEvent:event];
}
#pragma mark - ZSWTappableLabel delegate
- (void)tappableLabel:(ZSWTappableLabel *)tappableLabel tappedAtIndex:(NSInteger)idx withAttributes:(NSDictionary *)attributes {
[self handleTapResult:attributes[@"NSTextCheckingResult"]];
}
- (void)tappableLabel:(ZSWTappableLabel *)tappableLabel longPressedAtIndex:(NSInteger)idx withAttributes:(NSDictionary *)attributes {
[self handleLongPressResult:attributes[@"NSTextCheckingResult"]];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.editing) {
// don't event forward to label
return self;
}
return [super hitTest:point withEvent:event];
}
- (void)callPhoneNumber:(NSString*)phoneNumber {
NSString *cleanString = [phoneNumber stringByReplacingOccurrencesOfString:@"\u00a0" withString:@""];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"tel:%@", [cleanString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]]];
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
#pragma mark - UIAccessibilityContainer
- (BOOL)isAccessibilityElement {
return YES;
}
- (NSArray *)accessibilityCustomActions {
NSMutableArray *actions = [[NSMutableArray alloc] initWithArray:[super accessibilityCustomActions]];
int indexCounter = 0;
NSMutableArray *tmpArray = [NSMutableArray new];
NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];
for (int i = 0; i < textLabel.accessibilityElementCount; i++) {
UIAccessibilityElement *element = [textLabel accessibilityElementAtIndex:i];
if (![element.accessibilityLabel isEqualToString:@"."] && ![element.accessibilityLabel isEqualToString:@"@"]) {
NSTextCheckingResult *urlResult = [self checkTextResult:element.accessibilityLabel];
if (urlResult) {
UIAccessibilityCustomAction *linkAction = [[UIAccessibilityCustomAction alloc] initWithName:[NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"open", @""), element.accessibilityLabel] target:self selector:@selector(openLink:)];
[tmpArray addObject:linkAction];
[indexSet addIndex:indexCounter];
indexCounter ++;
UIAccessibilityCustomAction *shareAction = [[UIAccessibilityCustomAction alloc] initWithName:[NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"share", @""), element.accessibilityLabel] target:self selector:@selector(shareLink:)];
[tmpArray addObject:shareAction];
[indexSet addIndex:indexCounter];
indexCounter ++;
} else {
UIAccessibilityCustomAction *mentionAction = [[UIAccessibilityCustomAction alloc] initWithName:[NSString stringWithFormat:@"%@ @%@", NSLocalizedString(@"details", @""), element.accessibilityLabel] target:self selector:@selector(openMention:)];
[tmpArray addObject:mentionAction];
[indexSet addIndex:indexCounter];
indexCounter ++;
}
}
}
if (tmpArray.count > 0) {
[actions insertObjects:tmpArray atIndexes:indexSet];
}
return actions;
}
- (BOOL)openLink:(UIAccessibilityCustomAction *)action {
[self openLinkWithTextCheckingResult:[self checkTextResult:action.name]];
return YES;
}
- (void)openLinkWithTextCheckingResult:(NSTextCheckingResult*)urlResult {
if (urlResult.resultType == NSTextCheckingTypeLink) {
[IDNSafetyHelper safeOpenWithUrl:urlResult.URL viewController:self.chatVc];
} else if (urlResult.resultType == NSTextCheckingTypePhoneNumber) {
[self callPhoneNumber:urlResult.phoneNumber];
}
}
- (BOOL)shareLink:(UIAccessibilityCustomAction *)action {
NSTextCheckingResult *urlResult = [self checkTextResult:action.name];
if (urlResult.resultType == NSTextCheckingTypeLink) {
UIActivityViewController *activityViewController = [ActivityUtil activityViewControllerWithActivityItems:@[urlResult.URL] applicationActivities:@[]];
[self.chatVc presentActivityViewController:activityViewController animated:YES fromView:self];
}
else if (urlResult.resultType == NSTextCheckingTypePhoneNumber) {
UIActivityViewController *activityViewController = [ActivityUtil activityViewControllerWithActivityItems:@[urlResult.phoneNumber] applicationActivities:@[]];
[self.chatVc presentActivityViewController:activityViewController animated:YES fromView:self];
}
return YES;
}
- (BOOL)openMention:(UIAccessibilityCustomAction *)action {
NSString *identity = [action.name stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"%@ @", NSLocalizedString(@"details", @"")] withString:@""];
if ([identity isEqualToString:[BundleUtil localizedStringForKey:@"me"]]) {
[self handleTapResult:@"meContact"];
} else {
Contact *contact = [[ContactStore sharedContactStore] contactForIdentity:identity];
[self handleTapResult:contact];
}
return YES;
}
- (NSTextCheckingResult *)checkTextResult:(NSString *)text {
NSTextCheckingTypes textCheckingTypes = NSTextCheckingTypeLink;
static dispatch_once_t onceToken;
static BOOL canOpenPhoneLinks;
dispatch_once(&onceToken, ^{
canOpenPhoneLinks = [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"tel:0"]];
});
if (canOpenPhoneLinks)
textCheckingTypes |= NSTextCheckingTypePhoneNumber;
__block NSTextCheckingResult *urlResult = nil;
NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:textCheckingTypes error:NULL];
[detector enumerateMatchesInString:text options:0 range:NSMakeRange(0, text.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
urlResult = result;
}];
return urlResult;
}
@end