/** * @copyright Copyright (c) 2020 Ivan Sein * * @author Ivan Sein * * @license GNU GPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #import "ChatMessageTableViewCell.h" #import "MaterialActivityIndicator.h" #import "SLKUIConstants.h" #import "UIImageView+AFNetworking.h" #import "UIImageView+Letters.h" #import "NextcloudTalk-Swift.h" #import "NCAPIController.h" #import "NCAppBranding.h" #import "NCChatMessage.h" #import "NCDatabaseManager.h" #import "NCUtils.h" #import "QuotedMessageView.h" @interface ChatMessageTableViewCell () @property (nonatomic, strong) UIView *quoteContainerView; @property (nonatomic, strong) ReactionsView *reactionsView; @property (nonatomic, strong) NSArray *vConstraintNormal; @property (nonatomic, strong) NSArray *vConstraintReply; @property (nonatomic, strong) ReferenceView *referenceView; @end @implementation ChatMessageTableViewCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { self.backgroundColor = [NCAppBranding backgroundColor]; [self configureSubviews]; } return self; } - (void)configureSubviews { _avatarView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, kChatCellAvatarHeight, kChatCellAvatarHeight)]; _avatarView.translatesAutoresizingMaskIntoConstraints = NO; _avatarView.userInteractionEnabled = YES; _avatarView.backgroundColor = [NCAppBranding placeholderColor]; _avatarView.layer.cornerRadius = kChatCellAvatarHeight/2.0; _avatarView.layer.masksToBounds = YES; _avatarView.contentMode = UIViewContentModeScaleToFill; UITapGestureRecognizer *avatarTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(avatarTapped:)]; [_avatarView addGestureRecognizer:avatarTap]; [self.contentView addSubview:_avatarView]; _statusView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kChatCellStatusViewHeight, kChatCellStatusViewHeight)]; _statusView.translatesAutoresizingMaskIntoConstraints = NO; [self.contentView addSubview:_statusView]; _userStatusImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 12, 12)]; _userStatusImageView.translatesAutoresizingMaskIntoConstraints = NO; _userStatusImageView.userInteractionEnabled = NO; [self.contentView addSubview:_userStatusImageView]; [self.contentView addSubview:self.titleLabel]; [self.contentView addSubview:self.dateLabel]; [self.contentView addSubview:self.bodyTextView]; if ([self.reuseIdentifier isEqualToString:ReplyMessageCellIdentifier]) { [self.contentView addSubview:self.quoteContainerView]; [_quoteContainerView addSubview:self.quotedMessageView]; UITapGestureRecognizer *quoteTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(quoteTapped:)]; [self.quoteContainerView addGestureRecognizer:quoteTap]; } if (![self.reuseIdentifier isEqualToString:AutoCompletionCellIdentifier]) { [self.contentView addSubview:self.reactionsView]; [self.contentView addSubview:self.referenceView]; } NSDictionary *views = @{@"avatarView": self.avatarView, @"userStatusImageView": self.userStatusImageView, @"statusView": self.statusView, @"titleLabel": self.titleLabel, @"dateLabel": self.dateLabel, @"bodyTextView": self.bodyTextView, @"quoteContainerView": self.quoteContainerView, @"quotedMessageView": self.quotedMessageView, @"reactionsView": self.reactionsView, @"referenceView": self.referenceView }; NSDictionary *metrics = @{@"avatarSize": @(kChatCellAvatarHeight), @"dateLabelWidth": @(kChatCellDateLabelWidth), @"statusSize": @(kChatCellStatusViewHeight), @"padding": @15, @"right": @10, @"left": @5 }; if ([self.reuseIdentifier isEqualToString:ChatMessageCellIdentifier]) { [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarView(avatarSize)]-right-[titleLabel]-[dateLabel(>=dateLabelWidth)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarView(avatarSize)]-right-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarView(avatarSize)]-right-[reactionsView(>=0)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarView(avatarSize)]-right-[referenceView(>=0)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-padding-[statusView(statusSize)]-padding-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; _vConstraintNormal = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(avatarSize)]-left-[bodyTextView(>=0@999)]-0-[referenceView(0)]-0-[reactionsView(0)]-(>=left)-|" options:0 metrics:metrics views:views]; [self.contentView addConstraints:_vConstraintNormal]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[dateLabel(avatarSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(avatarSize)]-left-[statusView(statusSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; } else if ([self.reuseIdentifier isEqualToString:ReplyMessageCellIdentifier]) { [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarView(avatarSize)]-right-[titleLabel]-[dateLabel(>=dateLabelWidth)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarView(avatarSize)]-right-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarView(avatarSize)]-right-[reactionsView(>=0)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarView(avatarSize)]-right-[referenceView(>=0)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-padding-[statusView(statusSize)]-padding-[bodyTextView(>=0)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarView(avatarSize)]-right-[quoteContainerView(bodyTextView)]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[quotedMessageView(quoteContainerView)]|" options:0 metrics:nil views:views]]; _vConstraintReply = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(avatarSize)]-left-[quoteContainerView]-left-[bodyTextView(>=0@999)]-0-[referenceView(0)]-0-[reactionsView(0)]-(>=left)-|" options:0 metrics:metrics views:views]; [self.contentView addConstraints:_vConstraintReply]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[dateLabel(avatarSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[quotedMessageView(quoteContainerView)]|" options:0 metrics:nil views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[titleLabel(avatarSize)]-left-[quoteContainerView]-left-[statusView(statusSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; } else if ([self.reuseIdentifier isEqualToString:AutoCompletionCellIdentifier]) { [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-right-[avatarView(avatarSize)]-right-[titleLabel]-right-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[titleLabel]|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-32-[userStatusImageView(12)]-(>=0)-|" options:0 metrics:metrics views:views]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-32-[userStatusImageView(12)]-(>=0)-|" options:0 metrics:metrics views:views]]; self.backgroundColor = [UIColor secondarySystemBackgroundColor]; self.titleLabel.textColor = [UIColor labelColor]; } [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-right-[avatarView(avatarSize)]-(>=0)-|" options:0 metrics:metrics views:views]]; } - (void)prepareForReuse { [super prepareForReuse]; CGFloat pointSize = [ChatMessageTableViewCell defaultFontSize]; self.titleLabel.font = [UIFont systemFontOfSize:pointSize]; self.bodyTextView.font = [UIFont systemFontOfSize:pointSize]; self.titleLabel.text = @""; self.bodyTextView.text = @""; self.dateLabel.text = @""; self.quotedMessageView.actorLabel.text = @""; self.quotedMessageView.messageLabel.text = @""; self.reactionsView.reactions = @[]; if (_vConstraintNormal) { _vConstraintNormal[4].constant = 0; _vConstraintNormal[5].constant = 0; _vConstraintNormal[7].constant = 0; } if (_vConstraintReply) { _vConstraintReply[5].constant = 0; _vConstraintReply[6].constant = 0; _vConstraintReply[8].constant = 0; } [_referenceView prepareForReuse]; [self.avatarView cancelImageDownloadTask]; self.avatarView.image = nil; self.avatarView.contentMode = UIViewContentModeScaleToFill; self.userStatusImageView.image = nil; self.userStatusImageView.backgroundColor = [UIColor clearColor]; self.message = nil; self.statusView.hidden = NO; [self.statusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)]; } #pragma mark - Gesture recognizers - (void)avatarTapped:(UIGestureRecognizer *)gestureRecognizer { if (self.delegate && self.message) { [self.delegate cellWantsToDisplayOptionsForMessageActor:self.message]; } } - (void)quoteTapped:(UIGestureRecognizer *)gestureRecognizer { if (self.delegate && self.message && self.message.parent) { [self.delegate cellWantsToScrollToMessage:self.message.parent]; } } #pragma mark - ReactionsView delegate - (void)didSelectReactionWithReaction:(NCChatReaction *)reaction { [self.delegate cellDidSelectedReaction:reaction forMessage:self.message]; } #pragma mark - Getters - (UILabel *)titleLabel { if (!_titleLabel) { _titleLabel = [UILabel new]; _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; _titleLabel.backgroundColor = [UIColor clearColor]; _titleLabel.userInteractionEnabled = NO; _titleLabel.numberOfLines = 1; _titleLabel.font = [UIFont systemFontOfSize:[ChatMessageTableViewCell defaultFontSize]]; _titleLabel.textColor = [UIColor secondaryLabelColor]; } return _titleLabel; } - (UILabel *)dateLabel { if (!_dateLabel) { _dateLabel = [UILabel new]; _dateLabel.textAlignment = NSTextAlignmentRight; _dateLabel.translatesAutoresizingMaskIntoConstraints = NO; _dateLabel.backgroundColor = [UIColor clearColor]; _dateLabel.userInteractionEnabled = NO; _dateLabel.numberOfLines = 1; _dateLabel.font = [UIFont systemFontOfSize:12.0]; _dateLabel.textColor = [UIColor secondaryLabelColor]; } return _dateLabel; } - (ReactionsView *)reactionsView { if (!_reactionsView) { UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; _reactionsView = [[ReactionsView alloc] initWithFrame:CGRectMake(0, 0, 50, 50) collectionViewLayout:flowLayout]; _reactionsView.translatesAutoresizingMaskIntoConstraints = NO; _reactionsView.reactionsDelegate = self; } return _reactionsView; } - (ReferenceView *)referenceView { if (!_referenceView) { _referenceView = [[ReferenceView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)]; _referenceView.translatesAutoresizingMaskIntoConstraints = NO; } return _referenceView; } - (UIView *)quoteContainerView { if (!_quoteContainerView) { _quoteContainerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)]; _quoteContainerView.translatesAutoresizingMaskIntoConstraints = NO; } return _quoteContainerView; } - (QuotedMessageView *)quotedMessageView { if (!_quotedMessageView) { _quotedMessageView = [[QuotedMessageView alloc] init]; _quotedMessageView.translatesAutoresizingMaskIntoConstraints = NO; } return _quotedMessageView; } - (MessageBodyTextView *)bodyTextView { if (!_bodyTextView) { _bodyTextView = [MessageBodyTextView new]; _bodyTextView.font = [UIFont systemFontOfSize:[ChatMessageTableViewCell defaultFontSize]]; } return _bodyTextView; } - (void)setupForMessage:(NCChatMessage *)message withLastCommonReadMessage:(NSInteger)lastCommonRead { self.titleLabel.text = message.actorDisplayName; self.bodyTextView.attributedText = message.parsedMessageForChat; self.messageId = message.messageId; self.message = message; NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:message.timestamp]; self.dateLabel.text = [NCUtils getTimeFromDate:date]; TalkAccount *activeAccount = [[NCDatabaseManager sharedInstance] activeAccount]; ServerCapabilities *serverCapabilities = [[NCDatabaseManager sharedInstance] serverCapabilitiesForAccountId:activeAccount.accountId]; BOOL shouldShowDeliveryStatus = [[NCDatabaseManager sharedInstance] serverHasTalkCapability:kCapabilityChatReadStatus forAccountId:activeAccount.accountId]; BOOL shouldShowReadStatus = !serverCapabilities.readStatusPrivacy; if ([message.actorType isEqualToString:@"guests"]) { self.titleLabel.text = ([message.actorDisplayName isEqualToString:@""]) ? NSLocalizedString(@"Guest", nil) : message.actorDisplayName; [self setGuestAvatar:message.actorDisplayName]; } else if ([message.actorType isEqualToString:@"bots"]) { if ([message.actorId isEqualToString:@"changelog"]) { [self setChangelogAvatar]; } else { [self setBotAvatar]; } } else { [self.avatarView setImageWithURLRequest:[[NCAPIController sharedInstance] createAvatarRequestForUser:message.actorId withStyle:self.traitCollection.userInterfaceStyle andSize:96 usingAccount:activeAccount] placeholderImage:nil success:nil failure:nil]; } // This check is just a workaround to fix the issue with the deleted parents returned by the API. NCChatMessage *parent = message.parent; if (parent.message) { self.quotedMessageView.actorLabel.text = ([parent.actorDisplayName isEqualToString:@""]) ? NSLocalizedString(@"Guest", nil) : parent.actorDisplayName; self.quotedMessageView.messageLabel.text = parent.parsedMessageForChat.string; self.quotedMessageView.highlighted = [parent isMessageFromUser:activeAccount.userId]; } if (message.isDeleting) { [self setDeliveryState:ChatMessageDeliveryStateDeleting]; } else if (message.sendingFailed) { [self setDeliveryState:ChatMessageDeliveryStateFailed]; } else if (message.isTemporary){ [self setDeliveryState:ChatMessageDeliveryStateSending]; } else if ([message isMessageFromUser:activeAccount.userId] && shouldShowDeliveryStatus) { if (lastCommonRead >= message.messageId && shouldShowReadStatus) { [self setDeliveryState:ChatMessageDeliveryStateRead]; } else { [self setDeliveryState:ChatMessageDeliveryStateSent]; } } if (message.isDeletedMessage) { self.statusView.hidden = YES; self.bodyTextView.textColor = [UIColor tertiaryLabelColor]; } [self.reactionsView updateReactionsWithReactions:message.reactionsArray]; if (message.reactionsArray.count > 0) { if (_vConstraintNormal) { _vConstraintNormal[7].constant = 40; } if (_vConstraintReply) { _vConstraintReply[8].constant = 40; } } if (message.containsURL) { if (_vConstraintNormal) { _vConstraintNormal[4].constant = 5; _vConstraintNormal[5].constant = 100; } if (_vConstraintReply) { _vConstraintReply[5].constant = 5; _vConstraintReply[6].constant = 100; } [message getReferenceDataWithCompletionBlock:^(NCChatMessage *message, NSDictionary *referenceData, NSString *url) { if (![self.message isSameMessage:message]) { return; } if (!referenceData && message.deckCard) { // In case we were unable to retrieve reference data (for example if the user has no permissions) // but the message is a shared deck card, we use the shared information to show the deck view [self.referenceView updateFor:message.deckCard]; } else { [self.referenceView updateFor:referenceData and:url]; } }]; } } - (void)setGuestAvatar:(NSString *)displayName { UIColor *guestAvatarColor = [NCAppBranding placeholderColor]; NSString *name = ([displayName isEqualToString:@""]) ? @"?" : displayName; [_avatarView setImageWithString:name color:guestAvatarColor circular:true]; } - (void)setBotAvatar { UIColor *guestAvatarColor = [UIColor colorWithRed:0.21 green:0.21 blue:0.21 alpha:1.0]; /*#363636*/ [_avatarView setImageWithString:@">" color:guestAvatarColor circular:true]; } - (void)setChangelogAvatar { [_avatarView setImage:[UIImage imageNamed:@"changelog"]]; } - (void)setDeliveryState:(ChatMessageDeliveryState)state { [self.statusView.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)]; if (state == ChatMessageDeliveryStateSending || state == ChatMessageDeliveryStateDeleting) { MDCActivityIndicator *activityIndicator = [[MDCActivityIndicator alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; activityIndicator.radius = 7.0f; activityIndicator.cycleColors = @[UIColor.lightGrayColor]; [activityIndicator startAnimating]; [self.statusView addSubview:activityIndicator]; } else if (state == ChatMessageDeliveryStateFailed) { UIImageView *errorView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; [errorView setImage:[UIImage imageNamed:@"error"]]; [self.statusView addSubview:errorView]; } else if (state == ChatMessageDeliveryStateSent) { UIImageView *checkView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; [checkView setImage:[UIImage imageNamed:@"check"]]; checkView.image = [checkView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; [checkView setTintColor:[UIColor lightGrayColor]]; [self.statusView addSubview:checkView]; } else if (state == ChatMessageDeliveryStateRead) { UIImageView *checkAllView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; [checkAllView setImage:[UIImage imageNamed:@"check-all"]]; checkAllView.image = [checkAllView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; [checkAllView setTintColor:[UIColor lightGrayColor]]; [self.statusView addSubview:checkAllView]; } } - (void)setUserStatus:(NSString *)userStatus { UIImage *statusImage = nil; if ([userStatus isEqualToString:@"online"]) { statusImage = [UIImage imageNamed:@"user-status-online-10"]; } else if ([userStatus isEqualToString:@"away"]) { statusImage = [UIImage imageNamed:@"user-status-away-10"]; } else if ([userStatus isEqualToString:@"dnd"]) { statusImage = [UIImage imageNamed:@"user-status-dnd-10"]; } if (statusImage) { [_userStatusImageView setImage:statusImage]; _userStatusImageView.contentMode = UIViewContentModeCenter; _userStatusImageView.layer.cornerRadius = 6; _userStatusImageView.clipsToBounds = YES; // When a background color is set directly to the cell it seems that there is no background configuration. // In this class, even when no background color is set, the background configuration is nil. _userStatusImageView.backgroundColor = (self.backgroundColor) ? self.backgroundColor : [[self backgroundConfiguration] backgroundColor]; } } + (CGFloat)defaultFontSize { CGFloat pointSize = 16.0; // NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; // pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory); return pointSize; } @end