/* Copyright 2024 New Vector Ltd. Copyright 2017 Vector Creations Ltd Copyright 2015 OpenMarket Ltd Copyright (c) 2021 BWI GmbH SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ #import "ContactTableViewCell.h" #import "ThemeService.h" #import "GeneratedInterface-Swift.h" #import "AvatarGenerator.h" #import "Tools.h" #import "MXRoom+Riot.h" #import "NBPhoneNumberUtil.h" @interface ContactTableViewCell() { /** The observer of the presence for matrix user. */ id mxPresenceObserver; } @end @implementation ContactTableViewCell @synthesize mxRoom, delegate; - (void)awakeFromNib { [super awakeFromNib]; // Disable by default interactions defined in the cell // because we want [tableView didSelectRowAtIndexPath:] to be called self.thumbnailView.userInteractionEnabled = NO; // BWI: 5208 add federation icon self.federationImage = FederationIconHelper.shared.federationBadgeImage; self.federationImageView = [[UIImageView alloc] initWithImage:self.federationImage]; self.federationImageView.translatesAutoresizingMaskIntoConstraints = NO; self.federationImageView.contentMode = UIViewContentModeScaleAspectFit; [self.contentView addSubview:self.federationImageView]; // scale and new with federation icon float scaleFactor = ((self.thumbnailView.frame.size.height * 0.34) / self.federationImage.size.height); float imageWidth = scaleFactor * self.federationImage.size.width; // offset federation icon float offsetTrailing = 3 * scaleFactor; float offsetBotom = 1; [self.federationImageView.bottomAnchor constraintEqualToAnchor:self.thumbnailView.bottomAnchor constant:offsetBotom].active = YES; [self.federationImageView.trailingAnchor constraintEqualToAnchor:self.thumbnailView.trailingAnchor constant:offsetTrailing].active = YES; [self.federationImageView.heightAnchor constraintEqualToConstant:(self.thumbnailView.frame.size.height * 0.34)].active = YES; [self.federationImageView.widthAnchor constraintEqualToConstant:imageWidth].active = YES; } - (void)customizeTableViewCellRendering { [super customizeTableViewCellRendering]; // apply the vector colours self.contactDisplayNameLabel.textColor = ThemeService.shared.theme.textPrimaryColor; self.contactInformationLabel.textColor = ThemeService.shared.theme.textSecondaryColor; self.contactInformationLabel.hidden = BWIBuildSettings.shared.allowLocalContactPresence ? NO : YES; self.powerLevelLabel.textColor = ThemeService.shared.theme.textSecondaryColor; // Clear the default background color of a MXKImageView instance self.thumbnailView.defaultBackgroundColor = [UIColor clearColor]; // bwi: 5695 remove avatar borders self.thumbnailView.contentMode = UIViewContentModeScaleAspectFill; } - (void)layoutSubviews { [super layoutSubviews]; // Round image view self.thumbnailView.layer.cornerRadius = self.thumbnailView.frame.size.width / 2; self.thumbnailView.clipsToBounds = YES; // BWI: 5208 add federation icon self.federationImage = FederationIconHelper.shared.federationBadgeImage; self.federationImageView.image = self.federationImage; } - (void)prepareForReuse { [super prepareForReuse]; // Restore default values self.contentView.alpha = 1; self.userInteractionEnabled = YES; self.accessoryType = UITableViewCellAccessoryNone; self.accessoryView = nil; } - (void)setShowCustomAccessoryView:(BOOL)show { _showCustomAccessoryView = show; self.customAccessViewWidthConstraint.constant = show ? 25 : 0; } - (void)setShowMatrixIdInDisplayName:(BOOL)showMatrixIdInDisplayName { _showMatrixIdInDisplayName = showMatrixIdInDisplayName; if (contact) { [self refreshContactDisplayName]; } } #pragma mark - MXKCellRendering // returns the first matrix id of the contact // nil if there is none - (NSString*)firstMatrixId { NSString* matrixId = nil; if (contact.matrixIdentifiers.count > 0) { matrixId = contact.matrixIdentifiers.firstObject; } return matrixId; } - (void)render:(MXKCellData *)cellData { // Remove any pending observers [[NSNotificationCenter defaultCenter] removeObserver:self]; if (mxPresenceObserver) { [[NSNotificationCenter defaultCenter] removeObserver:mxPresenceObserver]; mxPresenceObserver = nil; } // Sanity check: accept only object of MXKContact classes or sub-classes NSParameterAssert([cellData isKindOfClass:[MXKContact class]]); contact = (MXKContact*)cellData; // sanity check // should never happen if (!contact) { self.thumbnailView.image = nil; self.contactDisplayNameLabel.text = nil; self.contactInformationLabel.text = nil; self.powerLevelLabel.text = nil; return; } // BWI: 5208 add federation icon if (BWIBuildSettings.shared.isFederationEnabled) { self.federationImageView.hidden = !contact.bwiIsFederated; } else { self.federationImageView.hidden = YES; } // Be warned when the thumbnail is updated [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onThumbnailUpdate:) name:kMXKContactThumbnailUpdateNotification object:nil]; [self refreshContactThumbnail]; [self refreshContactDisplayName]; if (contact.isMatrixContact) { // Observe contact presence change MXWeakify(self); mxPresenceObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXKContactManagerMatrixUserPresenceChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { MXStrongifyAndReturnIfNil(self); NSString* matrixId = self.firstMatrixId; if (matrixId && [matrixId isEqualToString:notif.object]) { [self refreshContactPresence]; } }]; [self refreshContactPresence]; [self refreshContactBadgeImage]; [self refreshLocalContactInformation]; } else { [self refreshLocalContactInformation]; } } + (CGFloat)heightForCellData:(MXKCellData*)cellData withMaximumWidth:(CGFloat)maxWidth { return 74; } - (void)didEndDisplay { // remove any pending observers [[NSNotificationCenter defaultCenter] removeObserver:self]; if (mxPresenceObserver) { [[NSNotificationCenter defaultCenter] removeObserver:mxPresenceObserver]; mxPresenceObserver = nil; } // Remove all gesture recognizer while (self.thumbnailView.gestureRecognizers.count) { [self.thumbnailView removeGestureRecognizer:self.thumbnailView.gestureRecognizers[0]]; } self.delegate = nil; contact = nil; } #pragma mark Refresh cell part - (void)refreshContactThumbnail { UIImage* image = [contact thumbnailWithPreferedSize:self.thumbnailView.frame.size]; if (!image) { NSArray *identifiers = contact.matrixIdentifiers; if (identifiers.count) { image = [AvatarGenerator generateAvatarForMatrixItem:identifiers.firstObject withDisplayName:contact.displayName]; } else if (contact.isThirdPartyInvite) { image = [AvatarGenerator generateAvatarForText:contact.displayName]; } else if ((!contact.isMatrixContact && contact.phoneNumbers.count && !contact.emailAddresses.count)) { image = [AvatarGenerator imageFromText:@"#" withBackgroundColor:ThemeService.shared.theme.tintColor]; } else { image = [AvatarGenerator imageFromText:@"@" withBackgroundColor:ThemeService.shared.theme.tintColor]; } } // bwi: 5695 remove avatar borders self.thumbnailView.contentMode = UIViewContentModeScaleAspectFill; self.thumbnailView.image = image; } - (void)refreshContactBadgeImage { NSString *matrixId = [self firstMatrixId]; // bwi: #5236 remove encryption status shield if (BWIBuildSettings.shared.showEncryptionStatusBadgeOnAvatar) { if (matrixId) { [self.mxRoom encryptionTrustLevelForUserId:matrixId onComplete:^(UserEncryptionTrustLevel userEncryptionTrustLevel) { self.avatarBadgeImageView.image = [EncryptionTrustLevelBadgeImageHelper userBadgeImageFor:userEncryptionTrustLevel]; }]; } else { self.avatarBadgeImageView.image = [EncryptionTrustLevelBadgeImageHelper userBadgeImageFor:UserEncryptionTrustLevelUnknown]; } self.avatarBadgeImageView.hidden = NO; } else { self.avatarBadgeImageView.image = nil; self.avatarBadgeImageView.hidden = YES; } } - (void)refreshContactDisplayName { self.contactDisplayNameLabel.text = contact.displayName; // Check whether the matrix identifier must be displayed. if (_showMatrixIdInDisplayName) { // Append the matrix identifier to the display name. NSArray *identifiers = contact.matrixIdentifiers; if (identifiers.count) { NSString *userId = identifiers.firstObject; // Check whether the display name is not already the matrix id if (![contact.displayName isEqualToString:userId]) { // Update the display name by adding the matrix id NSMutableAttributedString *displayNameLabelText = [[NSMutableAttributedString alloc] initWithString:contact.displayName]; NSRange strRange = NSMakeRange(0, displayNameLabelText.length); [displayNameLabelText addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:17 weight:UIFontWeightMedium] range:strRange]; NSMutableAttributedString *userIdStr = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@" (%@)", userId]]; strRange = NSMakeRange(0, userIdStr.length); [userIdStr addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:17] range:strRange]; [displayNameLabelText appendAttributedString:userIdStr]; self.contactDisplayNameLabel.attributedText = displayNameLabelText; } } } } - (void)refreshLocalContactInformation { // Display the first contact method in sub label. NSString *subLabelText = nil; if(![BWIBuildSettings.shared showContactIdentifierInDetailRow]) { if (contact.emailAddresses.count) { MXKEmail* email = contact.emailAddresses.firstObject; subLabelText = email.emailAddress; } else if (contact.phoneNumbers.count) { MXKPhoneNumber *phoneNumber = contact.phoneNumbers.firstObject; if (phoneNumber.nbPhoneNumber) { subLabelText = [[NBPhoneNumberUtil sharedInstance] format:phoneNumber.nbPhoneNumber numberFormat:NBEPhoneNumberFormatINTERNATIONAL error:nil]; } else { subLabelText = phoneNumber.textNumber; } } } else { NSArray *identifiers = contact.matrixIdentifiers; if (identifiers.count) { NSString *userId = identifiers.firstObject; subLabelText = userId; } } if (subLabelText.length) { self.contactInformationLabel.hidden = NO; } else { // Hide and fill the label with a fake string to harmonize the height of all the cells. // This is a drawback of the self-sizing cell. self.contactInformationLabel.hidden = YES; subLabelText = @"No method"; } self.contactInformationLabel.text = subLabelText; } - (void)refreshContactPresence { if (BWIBuildSettings.shared.allowLocalContactPresence) { NSString* presenceText; NSString* matrixId = self.firstMatrixId; if (matrixId) { MXUser *user = nil; // Consider here all sessions reported into contact manager NSArray* mxSessions = [MXKContactManager sharedManager].mxSessions; for (MXSession *mxSession in mxSessions) { user = [mxSession userWithUserId:matrixId]; if (user) { break; } } presenceText = [Tools presenceText:user]; } else if (contact.isThirdPartyInvite) { presenceText = [VectorL10n roomParticipantsOffline]; } self.contactInformationLabel.text = presenceText; } } #pragma mark - events - (void)onThumbnailUpdate:(NSNotification *)notif { // sanity check if ([notif.object isKindOfClass:[NSString class]]) { NSString* contactID = notif.object; if ([contactID isEqualToString:contact.contactID]) { [self refreshContactThumbnail]; } } } @end