// // 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* 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