// _____ _
// |_ _| |_ _ _ ___ ___ _ __ __ _
// | | | ' \| '_/ -_) -_) ' \/ _` |_
// |_| |_||_|_| \___\___|_|_|_\__,_(_)
//
// Threema iOS Client
// Copyright (c) 2017-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 "ChatCallMessageCell.h"
#import "SystemMessage.h"
#import "ZSWTappableLabel.h"
#import "UILabel+Markup.h"
#import "ChatDefines.h"
#import "UserSettings.h"
#import "UIImage+ColoredImage.h"
#import "ImageUtils.h"
#import "QBPopupMenuItem.h"
#import "QBPopupMenu.h"
#import "Contact.h"
#import "UIDefines.h"
#import "TextStyleUtils.h"
#import "BundleUtil.h"
#import "Utils.h"
#import "ServerConnector.h"
#import "ChatTableDataSource.h"
static CGFloat sideMargin = 2.0f;
static CGFloat ZSWTappableLabelSpace = 16.0f;
static ColorTheme currentTheme;
@implementation ChatCallMessageCell {
ZSWTappableLabel *titleLabel;
ZSWTappableLabel *descriptionLabel;
UIImageView *imageView;
UIImageView *callIcon;
UIAccessibilityElement *cellElement;
NSString *titleText;
NSString *descriptionText;
}
+ (CGFloat)heightForMessage:(BaseMessage*)message forTableWidth:(CGFloat)tableWidth {
CGSize titleSize;
CGSize descriptionSize;
CGSize maxSize = CGSizeMake([ChatMessageCell maxContentWidthForTableWidth:tableWidth] - ZSWTappableLabelSpace - sideMargin, CGFLOAT_MAX);
NSString *text = [(SystemMessage *)message format];
NSString *description = [(SystemMessage *)message callDetail];
static ZSWTappableLabel *dummyTitleLabel = nil;
if (dummyTitleLabel == nil) {
dummyTitleLabel = [ChatCallMessageCell makeAttributedLabelWithFrame:CGRectMake(0.0, 0.0, maxSize.width, maxSize.height)];
}
dummyTitleLabel.font = [UIFont boldSystemFontOfSize:[ChatMessageCell textFontSize]];
static dispatch_once_t onceToken;
static BOOL canOpenPhoneLinks;
dispatch_once(&onceToken, ^{
canOpenPhoneLinks = [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"tel:0"]];
});
NSString *spaces = @"";
for (int i = 0; i < (dummyTitleLabel.font.pointSize / 2.2); i++) {
spaces = [NSString stringWithFormat:@"%@ ", spaces];
}
text = [NSString stringWithFormat:@"%@%@",spaces, [(SystemMessage *)message format]];
NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc] initWithAttributedString:[dummyTitleLabel applyMarkupFor:[TextStyleUtils makeAttributedStringFromString:text withFont:dummyTitleLabel.font textColor:nil isOwn:true application:[UIApplication sharedApplication]]]];
dummyTitleLabel.attributedText = titleString;
titleSize = [dummyTitleLabel sizeThatFits:maxSize];
static ZSWTappableLabel *dummyDescriptionLabel = nil;
if (dummyDescriptionLabel == nil) {
dummyDescriptionLabel = [ChatCallMessageCell makeAttributedLabelWithFrame:CGRectMake(0.0, 0.0, maxSize.width, maxSize.height)];
}
dummyDescriptionLabel.font = [UIFont systemFontOfSize:[ChatMessageCell textFontSize] - 2.0];
if (description == nil || description.length == 0) {
description = @" ";
}
dummyDescriptionLabel.attributedText = [dummyDescriptionLabel applyMarkupFor:[TextStyleUtils makeAttributedStringFromString:description withFont:dummyDescriptionLabel.font textColor:nil isOwn:true application:[UIApplication sharedApplication]]];
descriptionSize = [dummyDescriptionLabel sizeThatFits:maxSize];
return titleSize.height + descriptionSize.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
titleLabel = [ChatCallMessageCell makeAttributedLabelWithFrame:self.bounds];
descriptionLabel = [ChatCallMessageCell makeAttributedLabelWithFrame:self.bounds];
descriptionLabel.textAlignment = NSTextAlignmentRight;
imageView = [UIImageView new];
imageView.contentMode = UIViewContentModeCenter;
imageView.image = [ImageUtils imageWithImage:[UIImage imageNamed:@"ThreemaPhone" inColor:[UIColor whiteColor]] scaledToSize:CGSizeMake(25, 25)];
imageView.clipsToBounds = YES;
callIcon = [UIImageView new];
callIcon.contentMode = UIViewContentModeScaleAspectFit;
[self.contentView addSubview:titleLabel];
[self.contentView addSubview:descriptionLabel];
[self.contentView addSubview:imageView];
[self.contentView addSubview:callIcon];
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
tgr.numberOfTapsRequired = 1;
[self addGestureRecognizer:tgr];
if (self.dtgr != nil) {
[tgr requireGestureRecognizerToFail:self.dtgr];
}
self.statusImage.hidden = YES;
}
return self;
}
- (void)setupColors {
[super setupColors];
titleLabel.textColor = [Colors fontNormal];
descriptionLabel.textColor = [Colors fontNormal];
[self updateLinkColors];
}
- (void)updateLinkColors {
if (currentTheme != [Colors getTheme]) {
currentTheme = [Colors getTheme];
titleLabel.attributedText = [titleLabel applyMarkupFor:[TextStyleUtils makeAttributedStringFromString:titleText withFont:titleLabel.font textColor:nil isOwn:self.message.isOwn.boolValue application:[UIApplication sharedApplication]]];
descriptionLabel.attributedText = [descriptionLabel applyMarkupFor:[TextStyleUtils makeAttributedStringFromString:descriptionText withFont:descriptionLabel.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 titleSize = [titleLabel sizeThatFits:CGSizeMake(messageTextWidth, CGFLOAT_MAX)];
CGSize descriptionSize = [descriptionLabel sizeThatFits:CGSizeMake(messageTextWidth, CGFLOAT_MAX)];
CGFloat callImageWidth = 40.0f;
CGSize bubbleSize = CGSizeMake(MAX(titleSize.width, descriptionSize.width), titleSize.height + descriptionSize.height);
[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 = 14.0f;
if (descriptionSize.height == 0 || [descriptionLabel.text isEqualToString:@" "]) {
titleLabel.frame = CGRectMake(x, (y/2) + (bubbleSize.height/2) - (titleSize.height/2), bubbleSize.width, titleSize.height);
descriptionLabel.hidden = YES;
} else {
titleLabel.frame = CGRectMake(x, y - (ZSWTappableLabelSpace/2), bubbleSize.width, titleSize.height + (ZSWTappableLabelSpace/3));
descriptionLabel.frame = CGRectMake(x, titleLabel.frame.origin.y + titleLabel.frame.size.height - (ZSWTappableLabelSpace/1.5), bubbleSize.width, descriptionSize.height + ZSWTappableLabelSpace);
descriptionLabel.hidden = NO;
}
CAShapeLayer * maskLayer = [CAShapeLayer layer];
if (self.message.isOwn.boolValue) {
imageView.frame = CGRectMake(self.msgBackground.frame.origin.x, self.msgBackground.frame.origin.y + 1.0, callImageWidth, self.msgBackground.frame.size.height - 7.0);
maskLayer.path = [UIBezierPath bezierPathWithRoundedRect: imageView.bounds byRoundingCorners: UIRectCornerTopLeft | UIRectCornerBottomLeft cornerRadii: (CGSize){10.0, 10.}].CGPath;
} else {
imageView.frame = CGRectMake(self.msgBackground.frame.origin.x + self.msgBackground.frame.size.width - callImageWidth, self.msgBackground.frame.origin.y + 1, callImageWidth, self.msgBackground.frame.size.height - 7.0);
maskLayer.path = [UIBezierPath bezierPathWithRoundedRect: imageView.bounds byRoundingCorners: UIRectCornerTopRight | UIRectCornerBottomRight cornerRadii: (CGSize){10.0, 10.}].CGPath;
}
imageView.layer.mask = maskLayer;
imageView.backgroundColor = [Colors bubbleCallButton];
CGFloat lineHeight = titleLabel.font.lineHeight;
if (descriptionSize.height == 0 || [descriptionLabel.text isEqualToString:@" "]) {
callIcon.frame = CGRectMake(titleLabel.frame.origin.x, titleLabel.frame.origin.y + lineHeight - titleLabel.font.pointSize - 2.0, (titleLabel.font.pointSize/2)*3, titleLabel.font.pointSize);
} else {
callIcon.frame = CGRectMake(titleLabel.frame.origin.x, titleLabel.frame.origin.y + lineHeight - titleLabel.font.pointSize, (titleLabel.font.pointSize/2)*3, titleLabel.font.pointSize);
}
}
- (NSString *)accessibilityLabelForContent {
return [NSString stringWithFormat:@"%@, %@", titleLabel.text, descriptionLabel.text];
}
- (void)setMessage:(BaseMessage *)newMessage {
[super setMessage:newMessage];
NSError *error;
SystemMessage *systemMessage = (SystemMessage *)self.message;
if (systemMessage.arg) {
NSDictionary *argDict = [NSJSONSerialization JSONObjectWithData:systemMessage.arg options:NSJSONReadingAllowFragments error:&error];
if (error) {
self.message.isOwn = @0;
} else {
self.message.isOwn = argDict[@"CallInitiator"];
}
} else {
self.message.isOwn = @1;
}
NSString *spaces = @"";
for (int i = 0; i < (titleLabel.font.pointSize / 2.2); i++) {
spaces = [NSString stringWithFormat:@"%@ ", spaces];
}
titleText = [NSString stringWithFormat:@"%@%@",spaces, [systemMessage format]];
descriptionText = systemMessage.callDetail;
if (descriptionText == nil || descriptionText.length == 0) {
descriptionText = @" ";
}
titleLabel.font = [UIFont boldSystemFontOfSize:[ChatMessageCell textFontSize]];
descriptionLabel.font = [UIFont systemFontOfSize:[ChatMessageCell textFontSize] - 2.0];
NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc] initWithAttributedString:[titleLabel applyMarkupFor:[TextStyleUtils makeAttributedStringFromString:titleText withFont:titleLabel.font textColor:nil isOwn:self.message.isOwn.boolValue application:[UIApplication sharedApplication]]]];
switch ([systemMessage.type integerValue]) {
case kSystemMessageCallEnded:
if (systemMessage.haveCallTime) {
if (!self.message.isOwn.boolValue) {
callIcon.image = [UIImage imageNamed:@"CallDownGreen" inColor:[Colors green]];
} else {
callIcon.image = [UIImage imageNamed:@"CallUpGreen" inColor:[Colors green]];
}
} else {
if (!self.message.isOwn.boolValue) {
callIcon.image = [UIImage imageNamed:@"CallLeftRed"];
} else {
callIcon.image = [UIImage imageNamed:@"CallUpRed"];
}
}
break;
case kSystemMessageCallRejected:
if (!self.message.isOwn.boolValue) {
callIcon.image = [UIImage imageNamed:@"CallLeftOrange"];
} else {
callIcon.image = [UIImage imageNamed:@"CallRightRed"];
}
break;
case kSystemMessageCallRejectedBusy:
if (!self.message.isOwn.boolValue) {
callIcon.image = [UIImage imageNamed:@"CallLeftRed"];
} else {
callIcon.image = [UIImage imageNamed:@"CallRightRed"];
}
break;
case kSystemMessageCallRejectedTimeout:
if (!self.message.isOwn.boolValue) {
callIcon.image = [UIImage imageNamed:@"CallLeftRed"];
} else {
callIcon.image = [UIImage imageNamed:@"CallRightRed"];
}
break;
case kSystemMessageCallRejectedDisabled:
callIcon.image = [UIImage imageNamed:@"CallRightRed"];
break;
case kSystemMessageCallMissed:
callIcon.image = [UIImage imageNamed:@"CallLeftRed"];
break;
default:
callIcon.image = [UIImage imageNamed:@"CallUpGreen" inColor:[Colors green]];
break;
}
titleLabel.attributedText = titleString;
descriptionLabel.attributedText = [descriptionLabel applyMarkupFor:[TextStyleUtils makeAttributedStringFromString:descriptionText withFont:descriptionLabel.font textColor:nil isOwn:self.message.isOwn.boolValue application:[UIApplication sharedApplication]]];
if (self.message.isOwn.boolValue) {
titleLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
descriptionLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
} else {
titleLabel.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
descriptionLabel.autoresizingMask = UIViewAutoresizingFlexibleRightMargin;
}
cellElement = nil;
[self setNeedsLayout];
}
+ (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;
}
#pragma mark - Private functions
+ (NSAttributedString*)attachmentWithImage:(UIImage*)attachment label:(UILabel *)label boundsImage:(BOOL)boundsImage {
NSTextAttachment* textAttachment = [[NSTextAttachment alloc] initWithData:nil ofType:nil];
textAttachment.image = attachment;
if (boundsImage) {
textAttachment.bounds = CGRectMake(0.0, -label.font.pointSize/6, label.font.pointSize, label.font.pointSize);
}
NSAttributedString* string = [NSAttributedString attributedStringWithAttachment:textAttachment];
return string;
}
- (void)handleTap:(UIGestureRecognizer *)gestureRecognizer {
CGPoint p = [gestureRecognizer locationInView:self];
if (CGRectContainsPoint(self.msgBackground.frame, p)) {
if ([UserSettings sharedUserSettings].enableThreemaCall && is64Bit == 1) {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded && self.editing == NO) {
if ([ServerConnector sharedServerConnector].connectionState == ConnectionStateLoggedIn) {
NSString *message = [NSString stringWithFormat:NSLocalizedString(@"call_contact_alert", nil), self.chatVc.conversation.contact.displayName];
UIAlertController *errAlert = [UIAlertController alertControllerWithTitle:nil message:message preferredStyle:UIAlertControllerStyleAlert];
[errAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"call", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
[self.chatVc startVoipCall:false];
}]];
[errAlert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
}]];
[self.chatVc presentViewController:errAlert animated:YES completion:nil];
} else {
// Alert no internet connection
NSString *title = NSLocalizedString(@"cannot_connect_title", nil);
NSString *message = NSLocalizedString(@"cannot_connect_message", nil);
[UIAlertTemplate showAlertWithOwner:self.chatVc title:title message:message actionOk:nil];
}
} else {
UITableView *tableView = (UITableView *)self.superview;
CGPoint p2 = [gestureRecognizer locationInView:tableView];
NSIndexPath *indexPath = [tableView indexPathForRowAtPoint:p2];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell.selected) {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
[((ChatTableDataSource *) tableView.dataSource) tableView:tableView didDeselectRowAtIndexPath:indexPath];
} else {
[tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[((ChatTableDataSource *) tableView.dataSource) tableView:tableView didSelectRowAtIndexPath:indexPath];
}
}
}
} else {
if (self.editing) {
UITableView *tableView = (UITableView *)self.superview;
CGPoint p2 = [gestureRecognizer locationInView:tableView];
NSIndexPath *indexPath = [tableView indexPathForRowAtPoint:p2];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell.selected) {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
[((ChatTableDataSource *) tableView.dataSource) tableView:tableView didDeselectRowAtIndexPath:indexPath];
} else {
[tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
[((ChatTableDataSource *) tableView.dataSource) tableView:tableView didSelectRowAtIndexPath:indexPath];
}
}
}
}
#pragma mark - Override functions
- (void)updateStatusImage {
self.statusImage.hidden = YES;
[self setNeedsLayout];
}
- (UIImage*)bubbleImageWithHighlight:(BOOL)bubbleHighlight {
if (self.shouldHideBubbleBackground) {
return nil;
}
if (self.message.isOwn.boolValue) {
NSString *name = @"ChatBubbleSentMask";
if (bubbleHighlight) {
return [[UIImage imageNamed:name inColor:[Colors bubbleSent]] stretchableImageWithLeftCapWidth:15 topCapHeight:13];
} else {
return [[UIImage imageNamed:name inColor:[Colors bubbleSent]] stretchableImageWithLeftCapWidth:15 topCapHeight:13];
}
} else {
NSString *name = @"ChatBubbleReceivedMask";
if (bubbleHighlight) {
return [[UIImage imageNamed:name inColor:[Colors bubbleReceived]] stretchableImageWithLeftCapWidth:23 topCapHeight:15];
} else {
return [[UIImage imageNamed:name inColor:[Colors bubbleReceived]] stretchableImageWithLeftCapWidth:23 topCapHeight:15];
}
}
}
- (void)setBubbleContentSize:(CGSize)size {
CGFloat bgWidthMargin = 34.0f;
CGFloat bgHeightMargin = 16.0f;
CGFloat callImageWidth = 40.0f;
self.bubbleSize = CGSizeMake(size.width+bgWidthMargin+callImageWidth, size.height+bgHeightMargin);
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (action == @selector(deleteMessage:)) {
return YES;
} else {
return NO;
}
}
- (NSString *)textForQuote {
return titleText;
}
#pragma mark - ZSWTappableLabel delegate
- (void)tappableLabel:(ZSWTappableLabel *)tappableLabel tappedAtIndex:(NSInteger)idx withAttributes:(NSDictionary *)attributes {
}
- (void)tappableLabel:(ZSWTappableLabel *)tappableLabel longPressedAtIndex:(NSInteger)idx withAttributes:(NSDictionary *)attributes {
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.editing) {
// don't event forward to label
return self;
}
return [super hitTest:point withEvent:event];
}
#pragma mark - UIAccessibilityContainer
- (BOOL)isAccessibilityElement {
return NO;
}
- (NSInteger)accessibilityElementCount {
return [titleLabel accessibilityElementCount]+1;
}
- (id)accessibilityElementAtIndex:(NSInteger)index {
// Fake an additional last element that encompasses the entire cell
// and adds additional information about the message.
if (index == ([titleLabel accessibilityElementCount])) {
return [self cellElement];
} else {
return [titleLabel accessibilityElementAtIndex:index];
}
}
- (NSInteger)indexOfAccessibilityElement:(id)element {
if (element == [self cellElement])
return [titleLabel accessibilityElementCount];
else
return [titleLabel indexOfAccessibilityElement:element];
}
- (UIAccessibilityElement*)cellElement {
if (cellElement == nil) {
cellElement = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:self];
cellElement.accessibilityLabel = [self accessibilityLabel];
cellElement.accessibilityTraits = UIAccessibilityTraitStaticText;
}
cellElement.accessibilityFrame = [self convertRect:self.bounds toView:nil];
return cellElement;
}
@end