element-ios/Riot/Modules/MatrixKit/Models/Contact/MXKContact.m

655 lines
18 KiB
Objective-C

/*
Copyright 2024 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C
Copyright 2017 Vector Creations Ltd
Copyright 2015 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
#import "MXKContact.h"
#import "MXKEmail.h"
#import "MXKPhoneNumber.h"
NSString *const kMXKContactThumbnailUpdateNotification = @"kMXKContactThumbnailUpdateNotification";
NSString *const kMXKContactLocalContactPrefixId = @"Local_";
NSString *const kMXKContactMatrixContactPrefixId = @"Matrix_";
NSString *const kMXKContactDefaultContactPrefixId = @"Default_";
@interface MXKContact()
{
UIImage* contactThumbnail;
UIImage* matrixThumbnail;
// The matrix id of the contact (used when the contact is not defined in the contacts book)
MXKContactField *matrixIdField;
}
@end
@implementation MXKContact
@synthesize isMatrixContact, isThirdPartyInvite;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ (NSString*)contactID:(ABRecordRef)record
{
return [NSString stringWithFormat:@"%@%d", kMXKContactLocalContactPrefixId, ABRecordGetRecordID(record)];
}
- (id)init
{
self = [super init];
if (self)
{
matrixIdField = nil;
isMatrixContact = NO;
_matrixAvatarURL = nil;
isThirdPartyInvite = NO;
}
return self;
}
- (id)initLocalContactWithABRecord:(ABRecordRef)record
{
self = [self init];
if (self)
{
// compute a contact ID
_contactID = [MXKContact contactID:record];
// use the contact book display name
_displayName = (__bridge NSString*) ABRecordCopyCompositeName(record);
// avoid nil display name
// the display name is used to sort contacts
if (!_displayName)
{
_displayName = @"";
}
// extract the phone numbers and their related label
ABMultiValueRef multi = ABRecordCopyValue(record, kABPersonPhoneProperty);
CFIndex nCount = ABMultiValueGetCount(multi);
NSMutableArray* pns = [[NSMutableArray alloc] initWithCapacity:nCount];
for (int i = 0; i < nCount; i++)
{
CFTypeRef phoneRef = ABMultiValueCopyValueAtIndex(multi, i);
NSString *phoneVal = (__bridge NSString*)phoneRef;
// sanity check
if (0 != [phoneVal length])
{
CFStringRef lblRef = ABMultiValueCopyLabelAtIndex(multi, i);
CFStringRef localizedLblRef = nil;
NSString *lbl = @"";
if (lblRef != nil)
{
localizedLblRef = ABAddressBookCopyLocalizedLabel(lblRef);
if (localizedLblRef)
{
lbl = (__bridge NSString*)localizedLblRef;
}
else
{
lbl = (__bridge NSString*)lblRef;
}
}
else
{
localizedLblRef = ABAddressBookCopyLocalizedLabel(kABOtherLabel);
if (localizedLblRef)
{
lbl = (__bridge NSString*)localizedLblRef;
}
}
[pns addObject:[[MXKPhoneNumber alloc] initWithTextNumber:phoneVal type:lbl contactID:_contactID matrixID:nil]];
if (lblRef)
{
CFRelease(lblRef);
}
if (localizedLblRef)
{
CFRelease(localizedLblRef);
}
}
// release meory
if (phoneRef)
{
CFRelease(phoneRef);
}
}
CFRelease(multi);
_phoneNumbers = pns;
// extract the emails
multi = ABRecordCopyValue(record, kABPersonEmailProperty);
nCount = ABMultiValueGetCount(multi);
NSMutableArray *emails = [[NSMutableArray alloc] initWithCapacity:nCount];
for (int i = 0; i < nCount; i++)
{
CFTypeRef emailValRef = ABMultiValueCopyValueAtIndex(multi, i);
NSString *emailVal = (__bridge NSString*)emailValRef;
// sanity check
if ((nil != emailVal) && (0 != [emailVal length]))
{
CFStringRef lblRef = ABMultiValueCopyLabelAtIndex(multi, i);
CFStringRef localizedLblRef = nil;
NSString *lbl = @"";
if (lblRef != nil)
{
localizedLblRef = ABAddressBookCopyLocalizedLabel(lblRef);
if (localizedLblRef)
{
lbl = (__bridge NSString*)localizedLblRef;
}
else
{
lbl = (__bridge NSString*)lblRef;
}
}
else
{
localizedLblRef = ABAddressBookCopyLocalizedLabel(kABOtherLabel);
if (localizedLblRef)
{
lbl = (__bridge NSString*)localizedLblRef;
}
}
[emails addObject: [[MXKEmail alloc] initWithEmailAddress:emailVal type:lbl contactID:_contactID matrixID:nil]];
if (lblRef)
{
CFRelease(lblRef);
}
if (localizedLblRef)
{
CFRelease(localizedLblRef);
}
}
if (emailValRef)
{
CFRelease(emailValRef);
}
}
CFRelease(multi);
_emailAddresses = emails;
// thumbnail/picture
// check whether the contact has a picture
if (ABPersonHasImageData(record))
{
CFDataRef dataRef;
dataRef = ABPersonCopyImageDataWithFormat(record, kABPersonImageFormatThumbnail);
if (dataRef)
{
contactThumbnail = [UIImage imageWithData:(__bridge NSData*)dataRef];
CFRelease(dataRef);
}
}
}
return self;
}
#pragma clang diagnostic pop
- (id)initMatrixContactWithDisplayName:(NSString*)displayName andMatrixID:(NSString*)matrixID
{
self = [self init];
if (self)
{
_contactID = [NSString stringWithFormat:@"%@%@", kMXKContactMatrixContactPrefixId, [[NSUUID UUID] UUIDString]];
// Sanity check
if (matrixID.length)
{
// used when the contact is not defined in the contacts book
matrixIdField = [[MXKContactField alloc] initWithContactID:_contactID matrixID:matrixID];
isMatrixContact = YES;
}
// _displayName must not be nil
// it is used to sort the contacts
if (displayName)
{
_displayName = displayName;
}
else
{
_displayName = @"";
}
}
return self;
}
- (id)initMatrixContactWithDisplayName:(NSString*)displayName matrixID:(NSString*)matrixID andMatrixAvatarURL:(NSString*)matrixAvatarURL
{
self = [self initMatrixContactWithDisplayName:displayName andMatrixID:matrixID];
if (self)
{
matrixIdField.matrixAvatarURL = matrixAvatarURL;
}
return self;
}
- (id)initContactWithDisplayName:(NSString*)displayName
emails:(NSArray<MXKEmail*> *)emails
phoneNumbers:(NSArray<MXKPhoneNumber*> *)phones
andThumbnail:(UIImage *)thumbnail
{
self = [self init];
if (self)
{
_contactID = [NSString stringWithFormat:@"%@%@", kMXKContactDefaultContactPrefixId, [[NSUUID UUID] UUIDString]];
// _displayName must not be nil
// it is used to sort the contacts
if (displayName)
{
_displayName = displayName;
}
else
{
_displayName = @"";
}
_emailAddresses = emails;
_phoneNumbers = phones;
contactThumbnail = thumbnail;
}
return self;
}
#pragma mark -
- (NSString*)sortingDisplayName
{
if (!_sortingDisplayName)
{
// Sanity check - display name should not be nil here
if (self.displayName)
{
NSCharacterSet *specialCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"_!~`@#$%^&*-+();:={}[],.<>?\\/\"\'"];
_sortingDisplayName = [self.displayName stringByTrimmingCharactersInSet:specialCharacterSet];
}
else
{
return @"";
}
}
return _sortingDisplayName;
}
- (BOOL)hasPrefix:(NSString*)prefix
{
prefix = [prefix lowercaseString];
// Check first display name
if (_displayName.length)
{
NSString *lowercaseString = [_displayName lowercaseString];
if ([lowercaseString hasPrefix:prefix])
{
return YES;
}
NSArray *components = [lowercaseString componentsSeparatedByString:@" "];
for (NSString *component in components)
{
NSString *theComponent = [component stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if ([theComponent hasPrefix:prefix])
{
return YES;
}
}
}
// Check matrix identifiers
NSArray *identifiers = self.matrixIdentifiers;
NSString *idPrefix = prefix;
if (![prefix hasPrefix:@"@"])
{
idPrefix = [NSString stringWithFormat:@"@%@", prefix];
}
for (NSString* mxId in identifiers)
{
if ([[mxId lowercaseString] hasPrefix:idPrefix])
{
return YES;
}
}
// Check email
for (MXKEmail* email in _emailAddresses)
{
if ([email.emailAddress hasPrefix:prefix])
{
return YES;
}
}
// Check phones
for (MXKPhoneNumber* phone in _phoneNumbers)
{
if ([phone hasPrefix:prefix])
{
return YES;
}
}
return NO;
}
- (BOOL)matchedWithPatterns:(NSArray*)patterns
{
BOOL matched = NO;
if (patterns.count > 0)
{
matched = YES;
// test first display name
for (NSString* pattern in patterns)
{
if ([_displayName rangeOfString:pattern options:NSCaseInsensitiveSearch].location == NSNotFound)
{
matched = NO;
break;
}
}
NSArray *identifiers = self.matrixIdentifiers;
if (!matched && identifiers.count > 0)
{
for (NSString* mxId in identifiers)
{
// Consider only the first part of the matrix id (ignore homeserver name)
NSRange range = [mxId rangeOfString:@":"];
if (range.location != NSNotFound)
{
NSString *mxIdName = [mxId substringToIndex:range.location];
for (NSString* pattern in patterns)
{
if ([mxIdName rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound)
{
matched = YES;
break;
}
}
if (matched)
{
break;
}
}
}
}
if (!matched && _phoneNumbers.count > 0)
{
for (MXKPhoneNumber* phonenumber in _phoneNumbers)
{
if ([phonenumber matchedWithPatterns:patterns])
{
matched = YES;
break;
}
}
}
if (!matched && _emailAddresses.count > 0)
{
for (MXKEmail* email in _emailAddresses)
{
if ([email matchedWithPatterns:patterns])
{
matched = YES;
break;
}
}
}
}
else
{
// if there is no pattern to search, it should always matched
matched = YES;
}
return matched;
}
- (void)setDefaultCountryCode:(NSString *)defaultCountryCode
{
for (MXKPhoneNumber* phonenumber in _phoneNumbers)
{
phonenumber.defaultCountryCode = defaultCountryCode;
}
_defaultCountryCode = defaultCountryCode;
}
#pragma mark - getter/setter
- (NSArray*)matrixIdentifiers
{
NSMutableArray* identifiers = [[NSMutableArray alloc] init];
if (matrixIdField)
{
[identifiers addObject:matrixIdField.matrixID];
}
for (MXKEmail* email in _emailAddresses)
{
if (email.matrixID && ([identifiers indexOfObject:email.matrixID] == NSNotFound))
{
[identifiers addObject:email.matrixID];
}
}
for (MXKPhoneNumber* pn in _phoneNumbers)
{
if (pn.matrixID && ([identifiers indexOfObject:pn.matrixID] == NSNotFound))
{
[identifiers addObject:pn.matrixID];
}
}
return identifiers;
}
- (void)setDisplayName:(NSString *)displayName
{
// a display name must not be emptied
// it is used to sort the contacts
if (displayName.length == 0)
{
_displayName = _contactID;
}
else
{
_displayName = displayName;
}
}
- (void)resetMatrixThumbnail
{
matrixThumbnail = nil;
_matrixAvatarURL = nil;
// Reset the avatar in the contact fields too.
[matrixIdField resetMatrixAvatar];
for (MXKEmail* email in _emailAddresses)
{
[email resetMatrixAvatar];
}
}
- (UIImage*)thumbnailWithPreferedSize:(CGSize)size
{
// Consider first the local thumbnail if any.
if (contactThumbnail)
{
return contactThumbnail;
}
// Check whether a matrix thumbnail is already found.
if (matrixThumbnail)
{
return matrixThumbnail;
}
// Look for a thumbnail from the matrix identifiers
MXKContactField* firstField = matrixIdField;
if (firstField)
{
if (firstField.avatarImage)
{
matrixThumbnail = firstField.avatarImage;
_matrixAvatarURL = firstField.matrixAvatarURL;
return matrixThumbnail;
}
}
// try to replace the thumbnail by the matrix one
if (_emailAddresses.count > 0)
{
// list the linked email
// search if one email field has a dedicated thumbnail
for (MXKEmail* email in _emailAddresses)
{
if (email.avatarImage)
{
matrixThumbnail = email.avatarImage;
_matrixAvatarURL = email.matrixAvatarURL;
return matrixThumbnail;
}
else if (!firstField && email.matrixID)
{
firstField = email;
}
}
}
if (_phoneNumbers.count > 0)
{
// list the linked phones
// search if one phone field has a dedicated thumbnail
for (MXKPhoneNumber* phoneNb in _phoneNumbers)
{
if (phoneNb.avatarImage)
{
matrixThumbnail = phoneNb.avatarImage;
_matrixAvatarURL = phoneNb.matrixAvatarURL;
return matrixThumbnail;
}
else if (!firstField && phoneNb.matrixID)
{
firstField = phoneNb;
}
}
}
// if no thumbnail has been found
// try to load the first field one
if (firstField)
{
// should be retrieved by the cell info
[firstField loadAvatarWithSize:size];
}
return nil;
}
- (UIImage*)thumbnail
{
return [self thumbnailWithPreferedSize:CGSizeMake(256, 256)];
}
#pragma mark NSCoding
- (id)initWithCoder:(NSCoder *)coder
{
_contactID = [coder decodeObjectForKey:@"contactID"];
_displayName = [coder decodeObjectForKey:@"displayName"];
matrixIdField = [coder decodeObjectForKey:@"matrixIdField"];
_phoneNumbers = [coder decodeObjectForKey:@"phoneNumbers"];
_emailAddresses = [coder decodeObjectForKey:@"emailAddresses"];
NSData *data = [coder decodeObjectForKey:@"contactThumbnail"];
if (!data)
{
// Check the legacy storage.
data = [coder decodeObjectForKey:@"contactBookThumbnail"];
}
if (data)
{
contactThumbnail = [UIImage imageWithData:data];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:_contactID forKey:@"contactID"];
[coder encodeObject:_displayName forKey:@"displayName"];
if (matrixIdField)
{
[coder encodeObject:matrixIdField forKey:@"matrixIdField"];
}
if (_phoneNumbers.count)
{
[coder encodeObject:_phoneNumbers forKey:@"phoneNumbers"];
}
if (_emailAddresses.count)
{
[coder encodeObject:_emailAddresses forKey:@"emailAddresses"];
}
if (contactThumbnail)
{
@autoreleasepool
{
NSData *data = UIImageJPEGRepresentation(contactThumbnail, 0.8);
[coder encodeObject:data forKey:@"contactThumbnail"];
}
}
}
@end