another.im-ios/Monal/Classes/MLContact.m
2024-11-18 15:53:52 +01:00

869 lines
31 KiB
Objective-C

//
// MLContact.m
// Monal
//
// Created by Anurodh Pokharel on 11/27/19.
// Copyright © 2019 Monal.im. All rights reserved.
//
#import "MLContact.h"
#import "MLMessage.h"
#import "HelperTools.h"
#import "DataLayer.h"
#import "xmpp.h"
#import "MLXMPPManager.h"
#import "MLOMEMO.h"
#import "MLNotificationQueue.h"
#import "MLImageManager.h"
#import "MLVoIPProcessor.h"
#import "MonalAppDelegate.h"
#import "MLMucProcessor.h"
@import Intents;
NSString* const kSubBoth = @"both";
NSString* const kSubNone = @"none";
NSString* const kSubTo = @"to";
NSString* const kSubFrom = @"from";
NSString* const kSubRemove = @"remove";
NSString* const kAskSubscribe = @"subscribe";
static NSMutableDictionary* _singletonCache;
@interface MLContact ()
{
NSInteger _unreadCount;
monal_void_block_t _cancelNickChange;
monal_void_block_t _cancelFullNameChange;
UIImage* _avatar;
}
@property (nonatomic, assign) BOOL isSelfChat;
@property (nonatomic, assign) BOOL isInRoster;
@property (nonatomic, assign) BOOL isSubscribedTo;
@property (nonatomic, assign) BOOL isSubscribedFrom;
@property (nonatomic, assign) BOOL hasIncomingContactRequest;
@property (nonatomic, strong) NSNumber* accountID;
@property (nonatomic, strong) NSString* contactJid;
@property (nonatomic, strong) NSString* fullName;
@property (nonatomic, strong) NSString* nickName;
@property (nonatomic, strong) xmpp* account;
@property (nonatomic, strong) NSSet<NSString*>* rosterGroups;
@property (nonatomic, strong) NSDate* _Nullable lastInteractionTime;
@property (nonatomic, assign) BOOL isTyping;
@property (nonatomic, assign) NSInteger unreadCount;
@property (nonatomic, assign) BOOL isPinned;
@property (nonatomic, assign) BOOL isBlocked;
@property (nonatomic, assign) BOOL isMuted;
@property (nonatomic, assign) BOOL isActiveChat;
@property (nonatomic, assign) BOOL isMuc;
@property (nonatomic, strong) NSString* groupSubject;
@property (nonatomic, strong) NSString* mucType;
@property (nonatomic, strong) NSString* accountNickInGroup;
@property (nonatomic, assign) BOOL isMentionOnly;
@property (nonatomic, strong) NSString* subscription;
@property (nonatomic, strong) NSString* ask;
@property (nonatomic, strong) NSString* contactDisplayName;
@end
@implementation MLContact
+(void) initialize
{
_singletonCache = [NSMutableDictionary new];
}
+(MLContact*) makeDummyContact:(int) type
{
if(type == 1)
{
return [self contactFromDictionary:@{
@"buddy_name": @"user@example.org",
@"nick_name": @"",
@"full_name": @"",
@"subscription": kSubBoth,
@"ask": @"",
@"account_id": @1,
//@"muc_subject": nil,
//@"muc_nick": nil,
@"Muc": @NO,
@"pinned": @NO,
@"encrypt": @NO,
@"muted": @NO,
@"status": @"",
@"state": @"online",
@"count": @1,
@"isActiveChat": @YES,
@"lastInteraction": [[NSDate date] initWithTimeIntervalSince1970:0],
@"rosterGroups": [NSSet new],
}];
}
else if(type == 2)
{
return [self contactFromDictionary:@{
@"buddy_name": @"group@example.org",
@"nick_name": @"",
@"full_name": @"Die coole Gruppe",
@"subscription": kSubBoth,
@"ask": @"",
@"account_id": @1,
//@"muc_subject": nil,
@"muc_nick": @"my_group_nick",
@"muc_type": kMucTypeGroup,
@"Muc": @YES,
@"pinned": @NO,
@"encrypt": @NO,
@"muted": @NO,
@"status": @"",
@"state": @"online",
@"count": @2,
@"isActiveChat": @YES,
@"lastInteraction": [[NSDate date] initWithTimeIntervalSince1970:1640153174],
@"rosterGroups": [NSSet new],
}];
}
else if(type == 3)
{
return [self contactFromDictionary:@{
@"buddy_name": @"channel@example.org",
@"nick_name": @"",
@"full_name": @"Der coolste Channel überhaupt",
@"subscription": kSubBoth,
@"ask": @"",
@"account_id": @1,
//@"muc_subject": nil,
@"muc_nick": @"my_channel_nick",
@"muc_type": kMucTypeChannel,
@"Muc": @YES,
@"pinned": @NO,
@"encrypt": @NO,
@"muted": @NO,
@"status": @"",
@"state": @"online",
@"count": @3,
@"isActiveChat": @YES,
@"lastInteraction": [[NSDate date] initWithTimeIntervalSince1970:1640157074],
@"rosterGroups": [NSSet new],
}];
}
else
{
return [self contactFromDictionary:@{
@"buddy_name": @"user2@example.org",
@"nick_name": @"",
@"full_name": @"Zweiter User mit Roster Name",
@"subscription": kSubBoth,
@"ask": @"",
@"account_id": @1,
//@"muc_subject": nil,
//@"muc_nick": nil,
@"Muc": @NO,
@"pinned": @NO,
@"encrypt": @NO,
@"muted": @NO,
@"status": @"",
@"state": @"online",
@"count": @4,
@"isActiveChat": @YES,
@"lastInteraction": [[NSDate date] initWithTimeIntervalSince1970:1640157174],
@"rosterGroups": [NSSet new],
}];
}
}
+(BOOL) supportsSecureCoding
{
return YES;
}
+(NSString*) ownDisplayNameForAccount:(xmpp*) account
{
NSDictionary* accountDic = [[DataLayer sharedInstance] detailsForAccount:account.accountID];
NSString* displayName = accountDic[kRosterName];
DDLogVerbose(@"Own nickname in accounts table %@: '%@'", account.accountID, displayName);
if(!displayName || !displayName.length)
{
// default is local part, see https://docs.modernxmpp.org/client/design/#contexts
NSDictionary* jidParts = [HelperTools splitJid:account.connectionProperties.identity.jid];
displayName = jidParts[@"node"];
}
DDLogVerbose(@"Calculated ownDisplayName for '%@': %@", account.connectionProperties.identity.jid, displayName);
return nilDefault(displayName, @"");
}
+(MLContact*) createContactFromDatabaseWithJid:(NSString*) jid andAccountID:(NSNumber*) accountID
{
NSDictionary* contactDict = [[DataLayer sharedInstance] contactDictionaryForUsername:jid forAccount:accountID];
MLContact* retval;
// check if we know this contact and return a dummy one if not
if(contactDict == nil)
{
DDLogInfo(@"Returning dummy MLContact for %@ on accountID %@", jid, accountID);
retval = [self contactFromDictionary:@{
@"buddy_name": jid.lowercaseString,
@"nick_name": @"",
@"full_name": @"",
@"subscription": kSubNone,
@"ask": @"",
@"account_id": accountID,
//@"muc_subject": nil,
//@"muc_nick": nil,
@"Muc": @NO,
@"mentionOnly": @NO,
@"pinned": @NO,
@"encrypt": @NO,
@"muted": @NO,
@"status": @"",
@"state": @"offline",
@"count": @0,
@"isActiveChat": @NO,
@"lastInteraction": nilWrapper(nil),
@"rosterGroups": [NSSet set],
}];
}
else
{
retval = [self contactFromDictionary:contactDict];
}
//initialize the blocking state, which is not stored in the buddylist table
retval.isBlocked = [[DataLayer sharedInstance] isBlockedContact:retval];
return retval;
}
+(MLContact*) createContactFromJid:(NSString*) jid andAccountID:(NSNumber*) accountID
{
MLAssert(jid != nil, @"jid must not be nil");
MLAssert(accountID != nil && accountID.intValue >= 0, @"accountID must not be nil and > 0");
NSString* cacheKey = [NSString stringWithFormat:@"%@|%@", accountID, jid];
@synchronized(_singletonCache) {
if(_singletonCache[cacheKey] != nil)
{
MLContact* obj = ((WeakContainer*)_singletonCache[cacheKey]).obj;
if(obj != nil)
return obj;
else
[_singletonCache removeObjectForKey:cacheKey];
}
MLContact* retval = [self createContactFromDatabaseWithJid:jid andAccountID:accountID];
_singletonCache[cacheKey] = [[WeakContainer alloc] initWithObj:retval];
return retval;
}
}
-(instancetype) init
{
self = [super init];
//watch for all sorts of changes and update our singleton dynamically
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleLastInteractionTimeUpdate:) name:kMonalLastInteractionUpdatedNotice object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleBlockListRefresh:) name:kMonalBlockListRefresh object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refresh) name:kMonalRefresh object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRefresh object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContactRefresh:) name:kMonalContactRemoved object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMucSubjectChange:) name:kMonalMucSubjectChanged object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUnreadCount) name:kMonalNewMessageNotice object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUnreadCount) name:kMonalDeletedMessageNotice object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateUnreadCount) name:kMLMessageSentToContact object:nil];
return self;
}
-(void) dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(void) handleLastInteractionTimeUpdate:(NSNotification*) notification
{
NSDictionary* data = notification.userInfo;
NSNumber* notificationAccountID = data[@"accountID"];
if(![self.contactJid isEqualToString:data[@"jid"]] || self.accountID.intValue != notificationAccountID.intValue)
return; // ignore other accounts or contacts
self.isTyping = [data[@"isTyping"] boolValue];
if(data[@"lastInteraction"] == nil)
return;
//this will be nil if "urn:xmpp:idle:1" is not supported by any of the contact's devices
DDLogVerbose(@"Updating lastInteractionTime=%@ of %@", data[@"lastInteraction"], self);
self.lastInteractionTime = nilExtractor(data[@"lastInteraction"]);
}
-(void) handleBlockListRefresh:(NSNotification*) notification
{
NSDictionary* data = notification.userInfo;
NSNumber* notificationAccountID = data[@"accountID"];
if(self.accountID.intValue != notificationAccountID.intValue)
return; // ignore other accounts
self.isBlocked = [[DataLayer sharedInstance] isBlockedContact:self];
DDLogInfo(@"Updated the blocking state of contact %@ => isBlocked=%@", self, bool2str(self.isBlocked));
}
-(void) handleContactRefresh:(NSNotification*) notification
{
NSDictionary* data = notification.userInfo;
MLContact* contact = data[@"contact"];
if(![self.contactJid isEqualToString:contact.contactJid] || self.accountID.intValue != contact.accountID.intValue)
return; // ignore other accounts or contacts
[self refresh];
[self updateUnreadCount];
//only handle avatar updates if the property was already used and the old avatar is cached in this contact
if(_avatar != nil)
{
UIImage* newAvatar = [[MLImageManager sharedInstance] getIconForContact:self];
if(newAvatar != self->_avatar)
{
DDLogDebug(@"Setting new avatar for %@", self);
self.avatar = newAvatar; //use self.avatar instead of _avatar to make sure KVO works properly
}
}
}
-(void) handleMucSubjectChange:(NSNotification*) notification
{
xmpp* account = notification.object;
NSString* room = notification.userInfo[@"room"];
NSString* subject = notification.userInfo[@"subject"];
if(![self.contactJid isEqualToString:room] || self.accountID.intValue != account.accountID.intValue)
return; // ignore other accounts or contacts
self.groupSubject = nilDefault(subject, @"");
}
-(void) refresh
{
[self updateWithContact:[[self class] createContactFromDatabaseWithJid:self.contactJid andAccountID:self.accountID]];
}
-(void) updateUnreadCount
{
_unreadCount = -1; // mark it as "uncached" --> will be recalculated on next access
}
-(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName;
{
return [self contactDisplayNameWithFallback:fallbackName andSelfnotesPrefix:YES];
}
-(NSString*) contactDisplayNameWithFallback:(NSString* _Nullable) fallbackName andSelfnotesPrefix:(BOOL) hasSelfnotesPrefix
{
//DDLogVerbose(@"Calculating contact display name...");
NSString* displayName;
if(!self.isSelfChat)
{
if(fallbackName == nil)
{
//default is local part, see https://docs.modernxmpp.org/client/design/#contexts
NSDictionary* jidParts = [HelperTools splitJid:self.contactJid];
fallbackName = jidParts[@"host"];
if(jidParts[@"node"] != nil)
fallbackName = jidParts[@"node"];
}
if(self.nickName && self.nickName.length > 0)
{
//DDLogVerbose(@"Using nickName: %@", self.nickName);
displayName = self.nickName;
}
else if(self.fullName && self.fullName.length > 0)
{
//DDLogVerbose(@"Using fullName: %@", self.fullName);
displayName = self.fullName;
}
else
{
//DDLogVerbose(@"Using fallback: %@", fallbackName);
displayName = fallbackName;
}
}
else
{
xmpp* account = self.account;
if(hasSelfnotesPrefix)
{
//add "Note to self: " prefix for selfchats
if([[DataLayer sharedInstance] enabledAccountCnts].intValue > 1)
displayName = [NSString stringWithFormat:NSLocalizedString(@"Notes to self: %@", @""), [[self class] ownDisplayNameForAccount:account]];
else
displayName = NSLocalizedString(@"Notes to self", @"");
}
else
displayName = [[self class] ownDisplayNameForAccount:account];
}
DDLogVerbose(@"Calculated contactDisplayName for '%@': %@", self.contactJid, displayName);
MLAssert(displayName != nil, @"Display name should never be nil!", (@{
@"jid": nilWrapper(self.contactJid),
@"nickName": nilWrapper(self.nickName),
@"fullName": nilWrapper(self.fullName),
@"fallbackName": nilWrapper(fallbackName)
}));
return displayName;
}
-(NSString*) contactDisplayName
{
return [self contactDisplayNameWithFallback:nil];
}
+(NSSet*) keyPathsForValuesAffectingContactDisplayName
{
return [NSSet setWithObjects:@"nickName", @"fullName", @"contactJid", nil];
}
-(NSString*) contactDisplayNameWithoutSelfnotesPrefix
{
return [self contactDisplayNameWithFallback:nil andSelfnotesPrefix:NO];
}
+(NSSet*) keyPathsForValuesAffectingContactDisplayNameWithoutSelfnotesPrefix
{
return [NSSet setWithObjects:@"nickName", @"fullName", @"contactJid", nil];
}
-(NSString*) nickNameView
{
return nilDefault(self.nickName, @"");
}
-(void) setNickNameView:(NSString*) name
{
MLAssert(!self.isMuc, @"Using nickNameView only allowed for 1:1 contacts!", (@{@"contact": self}));
if([self.nickName isEqualToString:name] || name == nil)
return; //no change at all
self.nickName = name;
// abort old change timer and start a new one
if(_cancelNickChange)
_cancelNickChange();
// delay changes because we don't want to update the roster on our server too often while typing
_cancelNickChange = createTimer(2.0, (^{
xmpp* account = self.account;
[account updateRosterItem:self withName:self.nickName];
}));
}
+(NSSet*) keyPathsForValuesAffectingNickNameView
{
return [NSSet setWithObjects:@"nickName", nil];
}
-(NSString*) fullNameView
{
return nilDefault(self.fullName, @"");
}
-(void) setFullNameView:(NSString*) name
{
MLAssert(self.isMuc, @"Using fullNameView only allowed for mucs!", (@{@"contact": self}));
if([self.fullName isEqualToString:name] || name == nil)
return; //no change at all
self.fullName = name;
xmpp* account = self.account;
[[DataLayer sharedInstance] setFullName:self.fullName forContact:self.contactJid andAccount:account.accountID];
// abort old change timer and start a new one
if(_cancelFullNameChange)
_cancelFullNameChange();
// delay changes because we don't want to update the roster on our server too often while typing
_cancelFullNameChange = createTimer(2.0, (^{
[account.mucProcessor changeNameOfMuc:self.contactJid to:self.fullName];
}));
}
+(NSSet*) keyPathsForValuesAffectingFullNameView
{
return [NSSet setWithObjects:@"fullName", nil];
}
-(UIImage*) avatar
{
// return already cached image
if(_avatar != nil)
return _avatar;
// load avatar from MLImageManager (use self.avatar instead of _avatar to make sure KVO works properly)
self.avatar = [[MLImageManager sharedInstance] getIconForContact:self];
return _avatar;
}
-(void) setAvatar:(UIImage*) avatar
{
if(avatar != nil)
_avatar = avatar;
else
_avatar = [UIImage new]; //empty dummy image, to not save nil (should never happen, MLImageManager has default images)
}
-(BOOL) hasAvatar
{
return [[MLImageManager sharedInstance] hasIconForContact:self];
}
-(BOOL) isSelfChat
{
xmpp* account = self.account;
return [self.contactJid isEqualToString:account.connectionProperties.identity.jid];
}
+(NSSet*) keyPathsForValuesAffectingIsSelfChat
{
return [NSSet setWithObjects:@"contactJid", @"accountID", nil];
}
-(BOOL) isInRoster
{
//either we already allowed each other or we allow this contact and asked them to allow us
//--> if isInRoster is true this is displayed as "remove contact" in contact details, otherwise it will be displayed as "add contact"
//(mucs have a subscription of 'both', ensured by the datalayer)
return [self.subscription isEqualToString:kSubBoth] || ([self.subscription isEqualToString:kSubFrom] && [self.ask isEqualToString:kAskSubscribe]);
}
+(NSSet*) keyPathsForValuesAffectingIsInRoster
{
return [NSSet setWithObjects:@"subscription", @"ask", nil];
}
-(BOOL) isSubscribedTo
{
return [self.subscription isEqualToString:kSubBoth]
|| [self.subscription isEqualToString:kSubTo];
}
+(NSSet*) keyPathsForValuesAffectingIsSubscribedTo
{
return [NSSet setWithObjects:@"subscription", nil];
}
-(BOOL) isSubscribedFrom
{
return [self.subscription isEqualToString:kSubBoth]
|| [self.subscription isEqualToString:kSubFrom];
}
+(NSSet*) keyPathsForValuesAffectingIsSubscribedFrom
{
return [NSSet setWithObjects:@"subscription", nil];
}
-(BOOL) isSubscribedBoth
{
return [self.subscription isEqualToString:kSubBoth];
}
+(NSSet*) keyPathsForValuesAffectingIsSubscribedBoth
{
return [NSSet setWithObjects:@"subscription", nil];
}
-(BOOL) hasIncomingContactRequest
{
return self.isMuc == NO && [[DataLayer sharedInstance] hasContactRequestForContact:self];
}
+(NSSet*) keyPathsForValuesAffectingHasIncomingContactRequest
{
return [NSSet setWithObjects:@"isMuc", nil];
}
-(BOOL) hasOutgoingContactRequest
{
return self.isMuc == NO && [self.ask isEqualToString:kAskSubscribe];
}
+(NSSet*) keyPathsForValuesAffectingHasOutgoingContactRequest
{
return [NSSet setWithObjects:@"isMuc", @"ask", nil];
}
-(xmpp* _Nullable) account
{
return [[MLXMPPManager sharedInstance] getEnabledAccountForID:self.accountID];
}
+(NSSet*) keyPathsForValuesAffectingAccount
{
return [NSSet setWithObject:@"accountID"];
}
// this will cache the unread count on first access
-(NSInteger) unreadCount
{
if(_unreadCount == -1)
_unreadCount = [[[DataLayer sharedInstance] countUserUnreadMessages:self.contactJid forAccount:self.accountID] integerValue];
return _unreadCount;
}
-(void) removeShareInteractions
{
[INInteraction deleteInteractionsWithIdentifiers:@[[NSString stringWithFormat:@"%@|%@", self.accountID, self.contactJid]] completion:^(NSError* error) {
if(error != nil)
DDLogError(@"Could not delete all SiriKit interactions: %@", error);
}];
}
-(void) toggleMute:(BOOL) mute
{
if(self.isMuted == mute)
return;
if(mute)
[[DataLayer sharedInstance] muteContact:self];
else
[[DataLayer sharedInstance] unMuteContact:self];
self.isMuted = mute;
}
-(void) toggleMentionOnly:(BOOL) mentionOnly
{
if(!self.isMuc || self.isMentionOnly == mentionOnly)
return;
if(mentionOnly)
[[DataLayer sharedInstance] setMucAlertOnMentionOnly:self.contactJid onAccount:self.accountID];
else
[[DataLayer sharedInstance] setMucAlertOnAll:self.contactJid onAccount:self.accountID];
self.isMentionOnly = mentionOnly;
}
-(BOOL) toggleEncryption:(BOOL) encrypt
{
#ifdef DISABLE_OMEMO
return NO;
#else
xmpp* account = self.account;
if(account == nil || account.omemo == nil)
return NO;
if(self.isMuc == NO)
{
NSSet* knownDevices = [account.omemo knownDevicesForAddressName:self.contactJid];
DDLogVerbose(@"Current isEncrypted=%@, encrypt=%@, knownDevices=%@", bool2str(self.isEncrypted), bool2str(encrypt), knownDevices);
if(!self.isEncrypted && encrypt && knownDevices.count == 0)
{
// request devicelist again
[account.omemo subscribeAndFetchDevicelistIfNoSessionExistsForJid:self.contactJid];
return NO;
}
}
else if([self.mucType isEqualToString:kMucTypeGroup] == NO)
{
return NO;
}
if(self.isEncrypted == encrypt)
return YES;
if(encrypt)
[[DataLayer sharedInstance] encryptForJid:self.contactJid andAccountID:self.accountID];
else
[[DataLayer sharedInstance] disableEncryptForJid:self.contactJid andAccountID:self.accountID];
self.isEncrypted = encrypt;
return YES;
#endif
}
-(void) togglePinnedChat:(BOOL) pinned
{
if(self.isPinned == pinned)
return;
if(pinned)
[[DataLayer sharedInstance] pinChat:self.accountID andBuddyJid:self.contactJid];
else
[[DataLayer sharedInstance] unPinChat:self.accountID andBuddyJid:self.contactJid];
self.isPinned = pinned;
// update active chats
xmpp* account = self.account;
if(account == nil)
return;
[[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:account userInfo:@{@"contact":self, @"pinningChanged": @YES}];
}
-(BOOL) toggleBlocked:(BOOL) block
{
if(self.isBlocked == block)
return YES;
xmpp* account = self.account;
if(account == nil)
return NO;
if(![account.connectionProperties.serverDiscoFeatures containsObject:@"urn:xmpp:blocking"])
return NO;
[[MLXMPPManager sharedInstance] block:block contact:self];
return YES;
}
-(void) removeFromRoster
{
[[MLXMPPManager sharedInstance] removeContact:self];
[self removeShareInteractions];
}
-(void) addToRoster
{
[[MLXMPPManager sharedInstance] addContact:self];
}
-(void) clearHistory
{
[[DataLayer sharedInstance] clearMessagesWithBuddy:self.contactJid onAccount:self.accountID];
[[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil];
}
#pragma mark - NSCoding
-(void) encodeWithCoder:(NSCoder*) coder
{
[coder encodeObject:self.contactJid forKey:@"contactJid"];
[coder encodeObject:self.nickName forKey:@"nickName"];
[coder encodeObject:self.fullName forKey:@"fullName"];
[coder encodeObject:self.subscription forKey:@"subscription"];
[coder encodeObject:self.ask forKey:@"ask"];
[coder encodeObject:self.accountID forKey:@"accountID"];
[coder encodeObject:self.groupSubject forKey:@"groupSubject"];
[coder encodeObject:self.accountNickInGroup forKey:@"accountNickInGroup"];
[coder encodeObject:self.mucType forKey:@"mucType"];
[coder encodeBool:self.isMuc forKey:@"isMuc"];
[coder encodeBool:self.isMentionOnly forKey:@"isMentionOnly"];
[coder encodeBool:self.isPinned forKey:@"isPinned"];
[coder encodeBool:self.isBlocked forKey:@"isBlocked"];
[coder encodeObject:self.statusMessage forKey:@"statusMessage"];
[coder encodeObject:self.state forKey:@"state"];
[coder encodeInteger:self->_unreadCount forKey:@"unreadCount"];
[coder encodeBool:self.isActiveChat forKey:@"isActiveChat"];
[coder encodeBool:self.isEncrypted forKey:@"isEncrypted"];
[coder encodeBool:self.isMuted forKey:@"isMuted"];
[coder encodeObject:self.lastInteractionTime forKey:@"lastInteractionTime"];
[coder encodeObject:self.rosterGroups forKey:@"rosterGroups"];
}
-(instancetype) initWithCoder:(NSCoder*) coder
{
self = [self init];
self.contactJid = [coder decodeObjectForKey:@"contactJid"];
self.nickName = [coder decodeObjectForKey:@"nickName"];
self.fullName = [coder decodeObjectForKey:@"fullName"];
self.subscription = [coder decodeObjectForKey:@"subscription"];
self.ask = [coder decodeObjectForKey:@"ask"];
self.accountID = [coder decodeObjectForKey:@"accountID"];
self.groupSubject = [coder decodeObjectForKey:@"groupSubject"];
self.accountNickInGroup = [coder decodeObjectForKey:@"accountNickInGroup"];
self.mucType = [coder decodeObjectForKey:@"mucType"];
self.isMuc = [coder decodeBoolForKey:@"isMuc"];
self.isMentionOnly = [coder decodeBoolForKey:@"isMentionOnly"];
self.isPinned = [coder decodeBoolForKey:@"isPinned"];
self.isBlocked = [coder decodeBoolForKey:@"isBlocked"];
self.statusMessage = [coder decodeObjectForKey:@"statusMessage"];
self.state = [coder decodeObjectForKey:@"state"];
self->_unreadCount = [coder decodeIntegerForKey:@"unreadCount"];
self.isActiveChat = [coder decodeBoolForKey:@"isActiveChat"];
self.isEncrypted = [coder decodeBoolForKey:@"isEncrypted"];
self.isMuted = [coder decodeBoolForKey:@"isMuted"];
self.lastInteractionTime = [coder decodeObjectForKey:@"lastInteractionTime"];
self.rosterGroups = [coder decodeObjectForKey:@"rosterGroups"];
return self;
}
-(void) updateWithContact:(MLContact*) contact
{
updateIfIdNotEqual(self.contactJid, contact.contactJid);
updateIfIdNotEqual(self.nickName, contact.nickName);
updateIfIdNotEqual(self.fullName, contact.fullName);
updateIfIdNotEqual(self.subscription, contact.subscription);
updateIfIdNotEqual(self.ask, contact.ask);
updateIfIdNotEqual(self.accountID, contact.accountID);
updateIfIdNotEqual(self.groupSubject, contact.groupSubject);
updateIfIdNotEqual(self.accountNickInGroup, contact.accountNickInGroup);
updateIfPrimitiveNotEqual(self.isMuc, contact.isMuc);
if(self.isMuc)
updateIfIdNotEqual(self.mucType, nilDefault(contact.mucType, kMucTypeChannel));
updateIfPrimitiveNotEqual(self.isMentionOnly, contact.isMentionOnly);
updateIfPrimitiveNotEqual(self.isPinned, contact.isPinned);
updateIfPrimitiveNotEqual(self.isBlocked, contact.isBlocked);
updateIfIdNotEqual(self.statusMessage, contact.statusMessage);
updateIfIdNotEqual(self.state, contact.state);
updateIfPrimitiveNotEqual(self->_unreadCount, contact->_unreadCount);
updateIfPrimitiveNotEqual(self.isActiveChat, contact.isActiveChat);
updateIfPrimitiveNotEqual(self.isEncrypted, contact.isEncrypted);
updateIfPrimitiveNotEqual(self.isMuted, contact.isMuted);
//don't update lastInteractionTime from contact, we dynamically update ourselves by handling kMonalLastInteractionUpdatedNotice
//updateIfIdNotEqual(self.lastInteractionTime, contact.lastInteractionTime);
updateIfIdNotEqual(self.rosterGroups, contact.rosterGroups);
}
-(BOOL) isEqualToMessage:(MLMessage*) message
{
return message != nil &&
[self.contactJid isEqualToString:message.buddyName] &&
self.accountID.intValue == message.accountID.intValue;
}
-(BOOL) isEqualToContact:(MLContact*) contact
{
return contact != nil &&
[self.contactJid isEqualToString:contact.contactJid] &&
self.accountID.intValue == contact.accountID.intValue;
}
-(BOOL) isEqual:(id _Nullable) object
{
if(object == nil || self == object)
return YES;
else if([object isKindOfClass:[MLContact class]])
return [self isEqualToContact:(MLContact*)object];
else if([object isKindOfClass:[MLMessage class]])
return [self isEqualToMessage:(MLMessage*)object];
else
return NO;
}
-(NSUInteger) hash
{
return [self.contactJid hash] ^ [self.accountID hash];
}
-(NSString*) id
{
return [NSString stringWithFormat:@"%@|%@", self.accountID, self.contactJid];
}
-(NSString*) description
{
return [NSString stringWithFormat:@"%@: %@ (%@) %@%@%@, kSub=%@", self.accountID, self.contactJid, self.isMuc ? self.mucType : @"1:1", self.isInRoster ? @"inRoster" : @"not(inRoster)", self.hasIncomingContactRequest ? @"[incomingContactRequest]" : @"", self.hasOutgoingContactRequest ? @"[outgoingContactRequest]" : @"", self.subscription];
}
+(MLContact*) contactFromDictionary:(NSDictionary*) dic
{
MLContact* contact = [MLContact new];
contact.contactJid = [dic objectForKey:@"buddy_name"];
contact.nickName = nilDefault([dic objectForKey:@"nick_name"], @"");
contact.fullName = nilDefault([dic objectForKey:@"full_name"], @"");
contact.subscription = nilDefault([dic objectForKey:@"subscription"], kSubNone);
contact.ask = nilDefault([dic objectForKey:@"ask"], @"");
contact.accountID = [dic objectForKey:@"account_id"];
contact.groupSubject = nilDefault([dic objectForKey:@"muc_subject"], @"");
contact.accountNickInGroup = nilDefault([dic objectForKey:@"muc_nick"], @"");
contact.mucType = [dic objectForKey:@"muc_type"];
contact.isMuc = [[dic objectForKey:@"Muc"] boolValue];
if(contact.isMuc && !contact.mucType)
contact.mucType = kMucTypeChannel; //default value
contact.mucType = nilDefault(contact.mucType, @"");
contact.isMentionOnly = [[dic objectForKey:@"mentionOnly"] boolValue];
contact.isPinned = [[dic objectForKey:@"pinned"] boolValue];
contact.statusMessage = nilDefault([dic objectForKey:@"status"], @"");
contact.state = nilDefault([dic objectForKey:@"state"], @"online");
contact->_unreadCount = -1;
contact.isActiveChat = [[dic objectForKey:@"isActiveChat"] boolValue];
contact.isEncrypted = [[dic objectForKey:@"encrypt"] boolValue];
contact.isMuted = [[dic objectForKey:@"muted"] boolValue];
// initial value comes from db, all other values get updated by our kMonalLastInteractionUpdatedNotice handler
contact.lastInteractionTime = nilExtractor([dic objectForKey:@"lastInteraction"]); //no default needed, already done in DataLayer
contact.rosterGroups = [dic objectForKey:@"rosterGroups"];
contact->_avatar = nil;
MLAssert(contact.rosterGroups != nil, @"rosterGroups must be non-nil (if a user is in no groups, it should be empty set)");
return contact;
}
@end