Include bookmarks in the contact controller's contactDict, so that a -contactEnumerator also contains them. By way of updating properly, fixes #13221.
We weren't providing LO updates to observers when updating all contacts, since the enumerator was being used. This also removes a bookmark-specific iterator when using the enumerator is now sufficient.
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AIContactController.h"
19 #import "AISCLViewPlugin.h"
20 #import <Adium/AIContactHidingController.h>
22 #import <Adium/AIAccountControllerProtocol.h>
23 #import <Adium/AIInterfaceControllerProtocol.h>
24 #import <Adium/AILoginControllerProtocol.h>
25 #import <Adium/AIMenuControllerProtocol.h>
26 #import <Adium/AIToolbarControllerProtocol.h>
27 #import <Adium/AIContactAlertsControllerProtocol.h>
29 #import <AIUtilities/AIArrayAdditions.h>
30 #import <AIUtilities/AIDictionaryAdditions.h>
31 #import <AIUtilities/AIFileManagerAdditions.h>
32 #import <AIUtilities/AIMenuAdditions.h>
33 #import <AIUtilities/AIToolbarUtilities.h>
34 #import <AIUtilities/AIApplicationAdditions.h>
35 #import <AIUtilities/AIImageAdditions.h>
36 #import <AIUtilities/AIStringAdditions.h>
37 #import <Adium/AIAccount.h>
38 #import <Adium/AIChat.h>
39 #import <Adium/AIContentMessage.h>
40 #import <Adium/AIListContact.h>
41 #import <Adium/AIListGroup.h>
42 #import <Adium/AIListObject.h>
43 #import <Adium/AIMetaContact.h>
44 #import <Adium/AIService.h>
45 #import <Adium/AISortController.h>
46 #import <Adium/AIUserIcons.h>
47 #import <Adium/AIServiceIcons.h>
48 #import <Adium/AIListBookmark.h>
49 #import <Adium/AIContactList.h>
51 #define KEY_FLAT_GROUPS @"FlatGroups" //Group storage
52 #define KEY_FLAT_CONTACTS @"FlatContacts" //Contact storage
53 #define KEY_FLAT_METACONTACTS @"FlatMetaContacts" //Metacontact objectID storage
54 #define KEY_BOOKMARKS @"Bookmarks"
56 #define OBJECT_STATUS_CACHE @"Object Status Cache"
59 #define TOP_METACONTACT_ID @"TopMetaContactID"
60 #define KEY_IS_METACONTACT @"isMetaContact"
61 #define KEY_OBJECTID @"objectID"
62 #define KEY_METACONTACT_OWNERSHIP @"MetaContact Ownership"
63 #define CONTACT_DEFAULT_PREFS @"ContactPrefs"
65 #define SHOW_GROUPS_MENU_TITLE AILocalizedString(@"Show Groups",nil)
67 #define SHOW_GROUPS_IDENTIFER @"ShowGroups"
69 #define SERVICE_ID_KEY @"ServiceID"
70 #define UID_KEY @"UID"
72 @interface AIListObject ()
73 @property (readwrite, nonatomic) CGFloat orderIndex;
76 @interface AIMetaContact ()
77 - (void)removeObject:(AIListObject *)inObject;
78 - (BOOL)addObject:(AIListObject *)inObject;
79 - (AIListContact *)preferredContactForContentType:(NSString *)inType;
82 @interface AIListGroup ()
83 - (void)removeObject:(AIListObject *)inObject;
84 - (BOOL)addObject:(AIListObject *)inObject;
87 @interface AIContactList ()
88 - (void)removeObject:(AIListObject *)inObject;
89 - (BOOL)addObject:(AIListObject *)inObject;
92 @interface AIListBookmark ()
93 //Freshly minted bookmarks don't know where to restore to, since they have no serverside counterpart. This tells them.
94 - (void)setInitialGroup:(AIListGroup *)inGroup;
97 @interface AIContactController ()
98 @property (readwrite, nonatomic) BOOL useOfflineGroup;
99 - (void)saveContactList;
100 - (void)_loadBookmarks;
101 - (void)_didChangeContainer:(AIListObject<AIContainingObject> *)inContainingObject object:(AIListObject *)object;
102 - (void)prepareShowHideGroups;
103 - (void)_performChangeOfUseContactListGroups;
106 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact;
107 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray;
108 - (void)addContact:(AIListContact *)inContact toMetaContact:(AIMetaContact *)metaContact;
109 - (BOOL)_performAddContact:(AIListContact *)inContact toMetaContact:(AIMetaContact *)metaContact;
110 - (void)removeContact:(AIListContact *)inContact fromMetaContact:(AIMetaContact *)metaContact;
111 - (void)_loadMetaContactsFromArray:(NSArray *)array;
112 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict;
113 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact;
116 @implementation AIContactController
120 if ((self = [super init])) {
122 contactDict = [[NSMutableDictionary alloc] init];
123 groupDict = [[NSMutableDictionary alloc] init];
124 metaContactDict = [[NSMutableDictionary alloc] init];
125 bookmarkDict = [[NSMutableDictionary alloc] init];
126 contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
127 contactLists = [[NSMutableArray alloc] init];
129 contactPropertiesObserverManager = [AIContactObserverManager sharedManager];
135 - (void)controllerDidLoad
137 //Default contact preferences
138 [adium.preferenceController registerDefaults:[NSDictionary dictionaryNamed:CONTACT_DEFAULT_PREFS
139 forClass:[self class]]
140 forGroup:PREF_GROUP_CONTACT_LIST];
142 contactList = [[AIContactList alloc] initWithUID:ADIUM_ROOT_GROUP_NAME];
143 [contactLists addObject:contactList];
144 //Root is always "expanded"
145 [contactList setExpanded:YES];
147 //Show Groups menu item
148 [self prepareShowHideGroups];
150 //Observe content (for preferredContactForContentType:forListContact:)
151 [[NSNotificationCenter defaultCenter] addObserver:self
152 selector:@selector(didSendContent:)
153 name:CONTENT_MESSAGE_SENT
156 [self loadContactList];
157 [self sortContactList];
159 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST_DISPLAY];
162 - (void)controllerWillClose
164 [self saveContactList];
169 [adium.preferenceController unregisterPreferenceObserver:self];
171 [contactDict release];
173 [metaContactDict release];
174 [contactToMetaContactLookupDict release];
175 [contactLists release];
176 [bookmarkDict release];
178 [contactPropertiesObserverManager release];
183 - (void)clearAllMetaContactData
185 if (metaContactDict.count) {
186 [contactPropertiesObserverManager delayListObjectNotifications];
188 //Remove all the metaContacts to get any existing objects out of them
189 for (AIMetaContact *metaContact in [[[metaContactDict copy] autorelease] objectEnumerator]) {
190 [self explodeMetaContact:metaContact];
193 [contactPropertiesObserverManager endListObjectNotificationsDelay];
196 [metaContactDict release]; metaContactDict = [[NSMutableDictionary alloc] init];
197 [contactToMetaContactLookupDict release]; contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
199 //Clear the preferences for good measure
200 [adium.preferenceController setPreference:nil
201 forKey:KEY_FLAT_METACONTACTS
202 group:PREF_GROUP_CONTACT_LIST];
203 [adium.preferenceController setPreference:nil
204 forKey:KEY_METACONTACT_OWNERSHIP
205 group:PREF_GROUP_CONTACT_LIST];
207 //Clear out old metacontact files
208 [[NSFileManager defaultManager] removeFilesInDirectory:[[adium.loginController userDirectory] stringByAppendingPathComponent:OBJECT_PREFS_PATH]
209 withPrefix:@"MetaContact"
211 [[NSFileManager defaultManager] removeFilesInDirectory:[adium cachesPath]
212 withPrefix:@"MetaContact"
216 #pragma mark Local Contact List Storage
217 //Load the contact list
218 - (void)loadContactList
220 //We must load all the groups before loading contacts for the ordering system to work correctly.
221 [self _loadMetaContactsFromArray:[adium.preferenceController preferenceForKey:KEY_FLAT_METACONTACTS
222 group:PREF_GROUP_CONTACT_LIST]];
223 [self _loadBookmarks];
226 //Save the contact list
227 - (void)saveContactList
229 for (AIListGroup *listGroup in [groupDict objectEnumerator]) {
230 [listGroup setPreference:[NSNumber numberWithBool:[listGroup isExpanded]]
232 group:PREF_GROUP_CONTACT_LIST];
235 NSMutableArray *bookmarks = [NSMutableArray array];
236 for (AIListBookmark *bookmark in self.allBookmarks) {
237 [bookmarks addObject:[NSKeyedArchiver archivedDataWithRootObject:bookmark]];
240 [adium.preferenceController setPreference:bookmarks
242 group:PREF_GROUP_CONTACT_LIST];
245 - (void)_loadBookmarks
247 for (NSData *data in [adium.preferenceController preferenceForKey:KEY_BOOKMARKS group:PREF_GROUP_CONTACT_LIST]) {
248 //As a bookmark is initialized, it will add itself to the contact list in the right place
249 AIListBookmark *bookmark = [NSKeyedUnarchiver unarchiveObjectWithData:data];
252 if ([bookmarkDict objectForKey:bookmark.internalObjectID]) {
253 // In case we end up with two bookmarks with the same internalObjectID; this should be almost impossible.
254 [self removeBookmark:[bookmarkDict objectForKey:bookmark.internalObjectID]];
257 [bookmarkDict setObject:bookmark forKey:bookmark.internalObjectID];
258 [contactDict setObject:bookmark forKey:bookmark.internalObjectID];
260 //It's a newly created object, so set its initial attributes
261 [contactPropertiesObserverManager _updateAllAttributesOfObject:bookmark];
266 - (void)_loadMetaContactsFromArray:(NSArray *)array
268 for (NSString *identifier in array) {
269 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
270 NSNumber *objectID = [NSNumber numberWithInteger:[[[identifier componentsSeparatedByString:@"-"] objectAtIndex:1] integerValue]];
271 [self metaContactWithObjectID:objectID];
276 #pragma mark Contact Grouping
278 - (void)_addContactLocally:(AIListContact *)listContact toGroup:(AIListGroup *)localGroup
280 BOOL performedGrouping = NO;
282 //Protect with a retain while we are removing and adding the contact to our arrays
283 [listContact retain];
285 if (listContact.canJoinMetaContacts) {
286 AIListObject *existingObject = [localGroup objectWithService:listContact.service UID:listContact.UID];
287 if (existingObject) {
288 //If an object exists in this group with the same UID and serviceID, create a MetaContact
290 [self groupContacts:[NSArray arrayWithObjects:listContact,existingObject,nil]];
291 performedGrouping = YES;
294 AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:listContact.internalObjectID];
296 //If no object exists in this group which matches, we should check if there is already
297 //a MetaContact holding a matching ListContact, since we should include this contact in it
298 //If we found a metaContact to which we should add, do it.
300 [self addContact:listContact toMetaContact:metaContact];
301 performedGrouping = YES;
306 if (!performedGrouping) {
307 //If no similar objects exist, we add this contact directly to the list
308 [localGroup addObject:listContact];
311 [self _didChangeContainer:localGroup object:listContact];
315 [listContact release];
318 - (void)_moveContactLocally:(AIListContact *)listContact fromGroups:(NSSet *)oldGroups toGroups:(NSSet *)groups
320 //Protect with a retain while we are removing and adding the contact to our arrays
321 [listContact retain];
323 [contactPropertiesObserverManager delayListObjectNotifications];
325 //Remove this object from any local groups we have it in currently
326 for (AIListGroup *group in oldGroups) {
327 [group removeObject:listContact];
328 [self _didChangeContainer:group object:listContact];
331 for (AIListGroup *group in groups)
332 [self _addContactLocally:listContact toGroup:group];
334 [contactPropertiesObserverManager endListObjectNotificationsDelay];
336 [listContact release];
339 //Post a list grouping changed notification for the object and containing object
340 - (void)_didChangeContainer:(AIListObject<AIContainingObject> *)inContainingObject object:(AIListObject *)object
342 if ([contactPropertiesObserverManager updatesAreDelayed]) {
343 [contactPropertiesObserverManager noteContactChanged:object];
346 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_ListChanged
347 object:inContainingObject
352 - (BOOL)useContactListGroups
354 return useContactListGroups;
357 - (void)setUseContactListGroups:(BOOL)inFlag
359 if (inFlag != useContactListGroups) {
360 useContactListGroups = inFlag;
362 [self _performChangeOfUseContactListGroups];
366 - (void)_performChangeOfUseContactListGroups
368 [contactPropertiesObserverManager delayListObjectNotifications];
370 //Store the preference
371 [adium.preferenceController setPreference:[NSNumber numberWithBool:!useContactListGroups]
372 forKey:KEY_HIDE_CONTACT_LIST_GROUPS
373 group:PREF_GROUP_CONTACT_LIST_DISPLAY];
375 //Configure the sort controller to force ignoring of groups as appropriate
376 [[AISortController activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
378 //Restore the grouping of all root-level contacts
379 for (AIListContact *contact in [self contactEnumerator]) {
380 [contact restoreGrouping];
383 //Stop delaying object notifications; this will automatically resort the contact list, so we're done.
384 [contactPropertiesObserverManager endListObjectNotificationsDelay];
387 - (void)prepareShowHideGroups
389 //Load the preference
390 useContactListGroups = ![[adium.preferenceController preferenceForKey:KEY_HIDE_CONTACT_LIST_GROUPS
391 group:PREF_GROUP_CONTACT_LIST_DISPLAY] boolValue];
393 //Show offline contacts menu item
394 menuItem_showGroups = [[NSMenuItem alloc] initWithTitle:SHOW_GROUPS_MENU_TITLE
396 action:@selector(toggleShowGroups:)
399 [menuItem_showGroups setState:useContactListGroups];
401 [adium.menuController addMenuItem:menuItem_showGroups toLocation:LOC_View_Toggles];
404 NSToolbarItem *toolbarItem;
405 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SHOW_GROUPS_IDENTIFER
406 label:AILocalizedString(@"Show Groups",nil)
407 paletteLabel:AILocalizedString(@"Toggle Groups Display",nil)
408 toolTip:AILocalizedString(@"Toggle display of groups",nil)
410 settingSelector:@selector(setImage:)
411 itemContent:[NSImage imageNamed:(useContactListGroups ?
412 @"togglegroups_transparent" :
414 forClass:[self class]
416 action:@selector(toggleShowGroupsToolbar:)
418 [adium.toolbarController registerToolbarItem:toolbarItem forToolbarType:@"ContactList"];
421 - (IBAction)toggleShowGroups:(id)sender
424 useContactListGroups = !useContactListGroups;
425 [menuItem_showGroups setState:useContactListGroups];
427 //Update the contact list. Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure.
428 [self performSelector:@selector(_performChangeOfUseContactListGroups)
433 - (IBAction)toggleShowGroupsToolbar:(id)sender
435 [self toggleShowGroups:sender];
437 [sender setImage:[NSImage imageNamed:(useContactListGroups ?
438 @"togglegroups_transparent" :
440 forClass:[self class]]];
443 @synthesize useOfflineGroup;
445 - (AIListGroup *)offlineGroup
447 return [self groupWithUID:AILocalizedString(@"Offline", "Name of offline group")];
450 #pragma mark Meta Contacts
453 * @brief Create or load a metaContact
455 * @param inObjectID The objectID of an existing but unloaded metaContact, or nil to create and save a new metaContact
457 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID
459 BOOL shouldRestoreContacts = YES;
461 //If no object ID is provided, use the next available object ID
462 //(MetaContacts should always have an individually unique object id)
464 NSInteger topID = [[adium.preferenceController preferenceForKey:TOP_METACONTACT_ID
465 group:PREF_GROUP_CONTACT_LIST] integerValue];
466 inObjectID = [NSNumber numberWithInteger:topID];
467 [adium.preferenceController setPreference:[NSNumber numberWithInteger:([inObjectID integerValue] + 1)]
468 forKey:TOP_METACONTACT_ID
469 group:PREF_GROUP_CONTACT_LIST];
471 //No reason to waste time restoring contacts when none are in the meta contact yet.
472 shouldRestoreContacts = NO;
475 //Look for a metacontact with this object ID. If none is found, create one
476 //and add its contained contacts to it.
477 NSString *metaContactDictKey = [AIMetaContact internalObjectIDFromObjectID:inObjectID];
479 AIMetaContact *metaContact = [metaContactDict objectForKey:metaContactDictKey];
481 metaContact = [(AIMetaContact *)[AIMetaContact alloc] initWithObjectID:inObjectID];
483 //Keep track of it in our metaContactDict for retrieval by objectID
484 [metaContactDict setObject:metaContact forKey:metaContactDictKey];
486 //Add it to our more general contactDict, as well
487 [contactDict setObject:metaContact forKey:[metaContact internalUniqueObjectID]];
489 /* We restore contacts (actually, internalIDs for contacts, to be added as necessary later) if the metaContact
490 * existed before this call to metaContactWithObjectID:
492 if (shouldRestoreContacts)
493 [self _restoreContactsToMetaContact:metaContact];
495 /* As with contactWithService:account:UID, update all attributes so observers are initially informed of
496 * this object's existence.
498 [contactPropertiesObserverManager _updateAllAttributesOfObject:metaContact];
500 [metaContact release];
507 * @brief Associate the appropriate internal IDs for contained contacts with a metaContact
509 * @result YES if one or more contacts was associated with the metaContact; NO if none were.
511 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact
513 NSDictionary *allMetaContactsDict = [adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
514 group:PREF_GROUP_CONTACT_LIST];
515 NSArray *containedContactsArray = [allMetaContactsDict objectForKey:metaContact.internalObjectID];
517 if (containedContactsArray.count) {
518 [self _restoreContactsToMetaContact:metaContact
519 fromContainedContactsArray:containedContactsArray];
529 * @brief Associate the internal IDs for an array of contacts with a specific metaContact
531 * This does not actually place any AIListContacts within the metaContact. Instead, it updates the contactToMetaContactLookupDict
532 * dictionary to have metaContact associated with the list contacts specified by containedContactsArray. This
533 * allows us to add them lazily to the metaContact (in contactWithService:account:UID:) as necessary.
535 * @param metaContact The metaContact to which contact referneces are added
536 * @param containedContactsArray An array of NSDictionary objects, each of which has SERVICE_ID_KEY and UID_KEY which together specify an internalObjectID of an AIListContact
538 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray
540 for (NSDictionary *containedContactDict in containedContactsArray) {
541 /* Before Adium 0.80, metaContacts could be created within metaContacts. Simply ignore any attempt to restore
542 * such erroneous data, which will have a YES boolValue for KEY_IS_METACONTACT. */
543 if (![[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) {
544 /* Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject.
546 * As listObjects are loaded/created/requested which match this internalObjectID,
547 * they will be inserted into the metaContact.
549 NSString *internalObjectID = [AIListObject internalObjectIDForServiceID:[containedContactDict objectForKey:SERVICE_ID_KEY]
550 UID:[containedContactDict objectForKey:UID_KEY]];
551 [contactToMetaContactLookupDict setObject:metaContact
552 forKey:internalObjectID];
557 //Add a list object to a meta contact, setting preferences and such
558 //so the association is lasting across program launches.
559 - (void)addContact:(AIListContact *)inContact toMetaContact:(AIMetaContact *)metaContact
562 //I can't think of why one would want to add an entire group to a metacontact. Let's say you can't.
563 NSLog(@"Warning: addContact:toMetaContact: Attempted to add %@ to %@",inContact,metaContact);
567 if (inContact == metaContact) return;
569 //If listObject contains other contacts, perform addContact:toMetaContact: recursively
570 if ([inContact conformsToProtocol:@protocol(AIContainingObject)]) {
571 for (AIListContact *someObject in ((AIListObject<AIContainingObject> *)inContact).containedObjects) {
572 [self addContact:someObject toMetaContact:metaContact];
576 //Obtain any metaContact this listObject is currently within, so we can remove it later
577 AIMetaContact *oldMetaContact = [contactToMetaContactLookupDict objectForKey:[inContact internalObjectID]];
579 if ([self _performAddContact:inContact toMetaContact:metaContact] && metaContact != oldMetaContact) {
580 //If this listObject was not in this metaContact in any form before, store the change
581 //Remove the list object from any other metaContact it is in at present
583 [self removeContact:inContact fromMetaContact:oldMetaContact];
585 [self _storeListObject:inContact inMetaContact:metaContact];
587 //Do the update thing
588 [contactPropertiesObserverManager _updateAllAttributesOfObject:metaContact];
593 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact
595 //we only allow group->meta->contact, not group->meta->meta->contact
596 NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
598 // AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact);
599 NSDictionary *containedContactDict;
600 NSMutableDictionary *allMetaContactsDict;
601 NSMutableArray *containedContactsArray;
603 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
605 //Get the dictionary of all metaContacts
606 allMetaContactsDict = [[adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
607 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
608 if (!allMetaContactsDict) {
609 allMetaContactsDict = [[NSMutableDictionary alloc] init];
612 //Load the array for the new metaContact
613 containedContactsArray = [[allMetaContactsDict objectForKey:metaContactInternalObjectID] mutableCopy];
614 if (!containedContactsArray) containedContactsArray = [[NSMutableArray alloc] init];
615 containedContactDict = nil;
617 //Create the dictionary describing this list object
618 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
619 listObject.service.serviceID, SERVICE_ID_KEY,
620 listObject.UID, UID_KEY, nil];
622 //Only add if this dict isn't already in the array
623 if (containedContactDict && ([containedContactsArray indexOfObject:containedContactDict] == NSNotFound)) {
624 [containedContactsArray addObject:containedContactDict];
625 [allMetaContactsDict setObject:containedContactsArray forKey:metaContactInternalObjectID];
628 [self _saveMetaContacts:allMetaContactsDict];
630 [adium.contactAlertsController mergeAndMoveContactAlertsFromListObject:listObject
631 intoListObject:metaContact];
634 [allMetaContactsDict release];
635 [containedContactsArray release];
638 //Actually adds a list contact to a meta contact. No preferences are changed.
639 //Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict
640 //for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful.
641 - (BOOL)_performAddContact:(AIListContact *)inContact toMetaContact:(AIMetaContact *)metaContact
643 //we only allow group->meta->contact, not group->meta->meta->contact
644 NSParameterAssert([metaContact canContainObject:inContact]);
648 //Remove the object from its previous containing groups
649 [self _moveContactLocally:inContact fromGroups:inContact.groups toGroups:[NSSet set]];
651 //AIMetaContact will handle reassigning the list object's grouping to being itself
652 if ((success = [metaContact addObject:inContact])) {
653 [contactToMetaContactLookupDict setObject:metaContact forKey:[inContact internalObjectID]];
655 [self _didChangeContainer:metaContact object:inContact];
657 //Ensure the metacontact ends up in the appropriate groups
658 [metaContact restoreGrouping];
664 - (void)removeContact:(AIListContact *)inContact fromMetaContact:(AIMetaContact *)metaContact
666 //we only allow group->meta->contact, not group->meta->meta->contact
667 NSParameterAssert(![inContact conformsToProtocol:@protocol(AIContainingObject)]);
669 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
671 //Get the dictionary of all metaContacts
672 NSMutableDictionary *allMetaContactsDict = [adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
673 group:PREF_GROUP_CONTACT_LIST];
675 //Load the array for the metaContact
676 NSArray *containedContactsArray = [allMetaContactsDict objectForKey:metaContactInternalObjectID];
678 //Enumerate it, looking only for the appropriate type of containedContactDict
680 NSString *listObjectUID = inContact.UID;
681 NSString *listObjectServiceID = inContact.service.serviceID;
683 NSDictionary *containedContactDict = nil;
684 for (containedContactDict in containedContactsArray) {
685 if ([[containedContactDict objectForKey:UID_KEY] isEqualToString:listObjectUID] &&
686 [[containedContactDict objectForKey:SERVICE_ID_KEY] isEqualToString:listObjectServiceID]) {
691 //If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result
692 if (containedContactDict) {
693 NSMutableArray *newContainedContactsArray;
694 NSMutableDictionary *newAllMetaContactsDict;
696 newContainedContactsArray = [containedContactsArray mutableCopy];
697 [newContainedContactsArray removeObjectIdenticalTo:containedContactDict];
699 newAllMetaContactsDict = [allMetaContactsDict mutableCopy];
700 [newAllMetaContactsDict setObject:newContainedContactsArray
701 forKey:metaContactInternalObjectID];
703 [self _saveMetaContacts:newAllMetaContactsDict];
705 [newContainedContactsArray release];
706 [newAllMetaContactsDict release];
709 //The listObject can be within the metaContact without us finding a containedContactDict if we are removing multiple
710 //listContacts referring to the same UID & serviceID combination - that is, on multiple accounts on the same service.
711 //We therefore request removal of the object regardless of the if (containedContactDict) check above.
712 [metaContact removeObject:inContact];
714 [self _didChangeContainer:metaContact object:inContact];
718 * @brief Determine the existing metacontact into which a grouping of UIDs and services would be placed
720 * @param UIDsArray NSArray of UIDs
721 * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
723 * @result Either the existing AIMetaContact -[self groupUIDs:forServices:usingMetaContactHint:] would return if passed a nil metaContactHint,
724 * or nil (if no existing metacontact would be used).
726 - (AIMetaContact *)knownMetaContactForGroupingUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray
728 AIMetaContact *metaContact = nil;
729 NSInteger count = [UIDsArray count];
731 for (NSInteger i = 0; i < count; i++) {
732 if ((metaContact = [contactToMetaContactLookupDict objectForKey:[AIListObject internalObjectIDForServiceID:[servicesArray objectAtIndex:i]
733 UID:[UIDsArray objectAtIndex:i]]])) {
742 * @brief Groups UIDs for services into a single metacontact
744 * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to
745 * a UID and a service, respectively, which together define a contact which should be included in the grouping.
747 * Assumption: This is only called after the contact list is finished loading, which occurs via
748 * -(void)controllerDidLoad above.
750 * @param UIDsArray NSArray of UIDs
751 * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
752 * @param metaContactHint If passed, an AIMetaContact to use for the grouping if an existing one isn't found. If nil, a new metacontact will be craeted in that case.
754 - (AIMetaContact *)groupUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray usingMetaContactHint:(AIMetaContact *)metaContactHint
756 NSMutableSet *internalObjectIDs = [[NSMutableSet alloc] init];
757 AIMetaContact *metaContact = nil;
758 NSString *internalObjectID;
759 NSInteger count = [UIDsArray count];
761 /* Build an array of all contacts matching this description (multiple accounts on the same service listing
762 * the same UID mean that we can have multiple AIListContact objects with a UID/service combination)
764 for (NSUInteger i = 0; i < count; i++) {
765 NSString *serviceID = [servicesArray objectAtIndex:i];
766 NSString *UID = [UIDsArray objectAtIndex:i];
768 internalObjectID = [AIListObject internalObjectIDForServiceID:serviceID
771 metaContact = [contactToMetaContactLookupDict objectForKey:internalObjectID];
774 [internalObjectIDs addObject:internalObjectID];
777 if ([internalObjectIDs count] > 1) {
778 //Create a new metaContact is we didn't find one and weren't supplied a hint
779 if (!metaContact && !(metaContact = metaContactHint)) {
780 AILogWithSignature(@"New metacontact to group %@ on %@", UIDsArray, servicesArray);
781 metaContact = [self metaContactWithObjectID:nil];
784 for (internalObjectID in internalObjectIDs) {
785 AIListObject *existingObject;
786 if ((existingObject = [self existingListObjectWithUniqueID:internalObjectID])) {
787 /* If there is currently an object (or multiple objects) matching this internalObjectID
788 * we should add immediately.
790 NSAssert([metaContact canContainObject:existingObject], @"Attempting to add something metacontacts can't hold to a metacontact");
791 [self addContact:(id)existingObject
792 toMetaContact:metaContact];
794 /* If no objects matching this internalObjectID exist, we can simply add to the
795 * contactToMetaContactLookupDict for use if such an object is created later.
797 [contactToMetaContactLookupDict setObject:metaContact
798 forKey:internalObjectID];
803 [internalObjectIDs release];
808 /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added.
810 * This will reuse an existing metacontact (for one of the contacts in the array) if possible.
811 * @param contactsToGroupArray Contacts to group together
813 - (AIMetaContact *)groupContacts:(NSArray *)contactsToGroupArray
815 AIMetaContact *metaContact = nil;
817 //Look for a metacontact we were passed directly
818 for (AIListContact *listContact in contactsToGroupArray) {
819 if ([listContact isKindOfClass:[AIMetaContact class]]) {
820 metaContact = (AIMetaContact *)listContact;
825 //If we weren't passed a metacontact, look for an existing metacontact associated with a passed contact
827 for (AIListContact *listContact in contactsToGroupArray) {
828 if (![listContact isKindOfClass:[AIMetaContact class]] &&
829 (metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]])) {
835 //Create a new metaContact is we didn't find one.
837 AILogWithSignature(@"New metacontact to group %@", contactsToGroupArray);
838 metaContact = [self metaContactWithObjectID:nil];
841 /* Add all these contacts to our MetaContact.
842 * Some may already be present, but that's fine, as nothing will happen.
844 for (AIListContact *listContact in contactsToGroupArray) {
845 [self addContact:listContact toMetaContact:metaContact];
851 - (void)explodeMetaContact:(AIMetaContact *)metaContact
853 //Remove the objects within it from being inside it
854 [contactPropertiesObserverManager delayListObjectNotifications];
855 NSArray *containedObjects = metaContact.containedObjects;
857 NSMutableDictionary *allMetaContactsDict = [[adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
858 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
860 for (AIListContact *object in containedObjects) {
862 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
863 [contactToMetaContactLookupDict removeObjectForKey:[object internalObjectID]];
865 [self removeContact:object fromMetaContact:metaContact];
868 //Then, procede to remove the metaContact
871 [metaContact retain];
873 //Remove it from its containing groups (no contained contacts == present in no groups)
874 [metaContact restoreGrouping];
876 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
878 //Remove our reference to it internally
879 [metaContactDict removeObjectForKey:metaContactInternalObjectID];
881 //Remove it from the preferences dictionary
882 [allMetaContactsDict removeObjectForKey:metaContactInternalObjectID];
884 //Save the updated allMetaContactsDict which no longer lists the metaContact
885 [self _saveMetaContacts:allMetaContactsDict];
887 [contactPropertiesObserverManager endListObjectNotificationsDelay];
889 //Protection is overrated.
890 [metaContact release];
891 [allMetaContactsDict release];
894 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict
896 AILog(@"MetaContacts: Saving!");
897 [adium.preferenceController setPreference:allMetaContactsDict
898 forKey:KEY_METACONTACT_OWNERSHIP
899 group:PREF_GROUP_CONTACT_LIST];
900 [adium.preferenceController setPreference:[allMetaContactsDict allKeys]
901 forKey:KEY_FLAT_METACONTACTS
902 group:PREF_GROUP_CONTACT_LIST];
905 //Sort list objects alphabetically by their display name
906 NSInteger contactDisplayNameSort(AIListObject *objectA, AIListObject *objectB, void *context)
908 return [objectA.displayName caseInsensitiveCompare:objectB.displayName];
911 #pragma mark Preference observing
913 * @brief Preferences changed
915 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
916 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
919 ![key isEqualToString:KEY_HIDE_CONTACTS] &&
920 ![key isEqualToString:KEY_SHOW_OFFLINE_CONTACTS] &&
921 ![key isEqualToString:KEY_USE_OFFLINE_GROUP] &&
922 ![key isEqualToString:KEY_HIDE_CONTACT_LIST_GROUPS]) {
926 BOOL shouldUseOfflineGroup = ((![[prefDict objectForKey:KEY_HIDE_CONTACTS] boolValue] ||
927 [[prefDict objectForKey:KEY_SHOW_OFFLINE_CONTACTS] boolValue]) &&
928 [[prefDict objectForKey:KEY_USE_OFFLINE_GROUP] boolValue]);
930 if (shouldUseOfflineGroup != self.useOfflineGroup || !key) {
931 self.useOfflineGroup = shouldUseOfflineGroup;
933 if (self.useOfflineGroup)
934 [contactPropertiesObserverManager registerListObjectObserver:self];
936 [contactPropertiesObserverManager updateAllListObjectsForObserver:self];
938 if (!self.useOfflineGroup)
939 [contactPropertiesObserverManager unregisterListObjectObserver:self];
944 * @brief Move contacts to and from the offline group as necessary as their online state changes.
946 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
948 if ((!inModifiedKeys || [inModifiedKeys containsObject:@"Online"]) && [inObject isKindOfClass:[AIListContact class]]) {
949 [((AIListContact *)inObject).parentContact restoreGrouping];
955 #pragma mark Contact Sorting
957 //Sort the entire contact list
958 - (void)sortContactList
960 [self sortContactLists:contactLists];
963 - (void)sortContactLists:(NSArray *)lists
965 for(AIContactList *list in lists) {
968 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_OrderChanged object:nil];
971 //Sort an individual object
972 - (void)sortListObject:(AIListObject *)inObject
974 if ([contactPropertiesObserverManager updatesAreDelayed]) {
975 [contactPropertiesObserverManager noteContactChanged:inObject];
978 for (AIListGroup *group in inObject.groups) {
979 //Sort the groups containing this object
980 [group sortListObject:inObject];
981 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_OrderChanged object:group];
986 #pragma mark Contact List Access
988 @synthesize contactList;
991 * @brief Return an array of all contact list groups
993 - (NSArray *)allGroups
995 return [groupDict allValues];
999 * @brief Returns a flat array of all contacts
1001 * This does not include metacontacts or bookmarks.
1003 - (NSArray *)allContacts
1005 NSMutableArray *result = [[[NSMutableArray alloc] init] autorelease];
1007 for (AIListContact *contact in self.contactEnumerator) {
1008 /* We want only contacts, not metacontacts. For a given contact, -[contact parentContact] could be used to access the meta. */
1009 if (![contact conformsToProtocol:@protocol(AIContainingObject)] || [contact isKindOfClass:[AIListBookmark class]])
1010 [result addObject:contact];
1017 * @brief Returns a flat array of all bookmarks.
1019 - (NSArray *)allBookmarks
1021 return [[[bookmarkDict allValues] copy] autorelease];
1025 * @brief Returns a flat array of all metacontacts
1027 - (NSArray *)allMetaContacts
1029 return [metaContactDict allValues];
1032 //Return a flat array of all the objects in a group on an account (and all subgroups, if desired)
1033 - (NSArray *)allContactsInObject:(AIListObject<AIContainingObject> *)inGroup onAccount:(AIAccount *)inAccount
1035 NSParameterAssert(inGroup != nil);
1037 NSMutableArray *contactArray = [NSMutableArray array];
1039 for (AIListObject *object in inGroup) {
1040 if ([object conformsToProtocol:@protocol(AIContainingObject)]) {
1041 [contactArray addObjectsFromArray:[self allContactsInObject:(AIListObject<AIContainingObject> *)object
1042 onAccount:inAccount]];
1043 } else if ([object isMemberOfClass:[AIListContact class]] && (!inAccount || ([(AIListContact *)object account] == inAccount)))
1044 [contactArray addObject:object];
1047 return contactArray;
1050 #pragma mark Contact List Menus
1052 //Returns a menu containing all the groups within a group
1053 //- Selector called on group selection is selectGroup:
1054 //- The menu items represented object is the group it represents
1055 - (NSMenu *)groupMenuWithTarget:(id)target
1057 NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
1058 [menu setAutoenablesItems:NO];
1060 // Separate groups by the contact list they're on.
1061 NSMutableDictionary *groupsByList = [NSMutableDictionary dictionaryWithCapacity:contactLists.count];
1062 for (AIListGroup *group in self.allGroups) {
1063 if (group != self.offlineGroup) {
1064 NSMutableArray *groups = [groupsByList objectForKey:group.contactList.UID];
1066 groups = [NSMutableArray array];
1067 [groupsByList setObject:groups forKey:group.contactList.UID];
1069 [groups addObject:group];
1073 // Now traverse the contactLists array in order and build a menu showing the groups in each list with separators in between.
1075 for (AIContactList *list in contactLists) {
1076 NSMutableArray *groups = [groupsByList objectForKey:list.UID];
1077 [groups sortUsingActiveSortControllerInContainer:list];
1078 for (AIListGroup *group in groups) {
1079 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:group.displayName
1081 action:@selector(selectGroup:)
1083 [menuItem setRepresentedObject:group];
1084 [menu addItem:menuItem];
1090 // Add the separator unless this is the last list.
1091 if (i < contactLists.count) {
1092 [menu addItem:[NSMenuItem separatorItem]];
1098 return [menu autorelease];
1101 #pragma mark Retrieving Specific Contacts
1104 * @brief Change the UID for a contact
1106 * @param UID The new UID to set
1107 * @param contact The contact whose UID is going to be changed
1109 * Preserves our reference to the contact as the UID changes, allowing us to continue
1110 * returning it when asked about the new UID in the future.
1112 - (void)setUID:(NSString *)UID forContact:(AIListContact *)contact
1116 // Remove the old value, its internal ID is going to change.
1117 [contactDict removeObjectForKey:contact.internalUniqueObjectID];
1120 [contact setUID:UID];
1122 // Add it back int othe dict.
1123 [contactDict setObject:contact forKey:contact.internalUniqueObjectID];
1128 - (AIListContact *)contactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1130 if (!(inUID && [inUID length] && inService)) return nil; //Ignore invalid requests
1132 AIListContact *contact = nil;
1133 NSString *key = [AIListContact internalUniqueObjectIDForService:inService
1136 contact = [contactDict objectForKey:key];
1139 contact = [[AIListContact alloc] initWithUID:inUID account:inAccount service:inService];
1141 //Check to see if we should add to a metaContact
1142 AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:[contact internalObjectID]];
1144 /* We already know to add this object to the metaContact, since we did it before with another object,
1145 but this particular listContact is new and needs to be added directly to the metaContact
1146 (on future launches, the metaContact will obtain it automatically since all contacts matching this UID
1147 and serviceID should be included). */
1148 [self _performAddContact:contact toMetaContact:metaContact];
1151 //Set the contact as mobile if it is a phone number
1152 if ([inUID hasPrefix:@"+"]) {
1153 [contact setIsMobile:YES notify:NotifyNever];
1157 [contactDict setObject:contact forKey:key];
1159 //Do the update thing
1160 [contactPropertiesObserverManager _updateAllAttributesOfObject:contact];
1168 - (void)accountDidStopTrackingContact:(AIListContact *)inContact
1170 [[inContact retain] autorelease];
1172 /* Remove after a short delay. Otherwise, the removal may be visible as the object remains in the contact
1173 * list until a display delay is over, which would show up as the name going blank on metacontacts and other
1176 * Of course, this really means that the object delay code is somehow failing to actually delay all updates.
1177 * I can't figure out where or why, so this is a hack around it. Ugh. -evands 10/08
1179 for (AIListObject<AIContainingObject> *container in inContact.containingObjects) {
1180 [container performSelector:@selector(removeObjectAfterAccountStopsTracking:)
1181 withObject:inContact
1185 [contactDict removeObjectForKey:inContact.internalUniqueObjectID];
1189 * @brief Find an existing bookmark
1191 * Finds an existing bookmark for a given AIChat
1193 - (AIListBookmark *)existingBookmarkForChat:(AIChat *)inChat
1195 return [self existingBookmarkForChatName:inChat.name
1196 onAccount:inChat.account
1197 chatCreationInfo:inChat.chatCreationDictionary];
1201 * @brief Find an existing bookmark
1203 * Finds an existing bookmark for given information.
1205 - (AIListBookmark *)existingBookmarkForChatName:(NSString *)inName
1206 onAccount:(AIAccount *)inAccount
1207 chatCreationInfo:(NSDictionary *)inCreationInfo
1209 AIListBookmark *existingBookmark = nil;
1211 for(AIListBookmark *listBookmark in self.allBookmarks) {
1212 if([listBookmark.name isEqualToString:[inAccount.service normalizeChatName:inName]] &&
1213 listBookmark.account == inAccount &&
1214 ((!listBookmark.chatCreationDictionary && !inCreationInfo) ||
1215 ([listBookmark.chatCreationDictionary isEqualToDictionary:inCreationInfo]))) {
1216 existingBookmark = listBookmark;
1221 return existingBookmark;
1226 * @brief Find or create a bookmark for a chat
1228 - (AIListBookmark *)bookmarkForChat:(AIChat *)inChat inGroup:(AIListGroup *)group
1230 AIListBookmark *bookmark = [self existingBookmarkForChat:inChat];
1233 bookmark = [[[AIListBookmark alloc] initWithChat:inChat] autorelease];
1235 if ([bookmarkDict objectForKey:bookmark.internalObjectID]) {
1236 // In case we end up with two bookmarks with the same internalObjectID; this should be almost impossible.
1237 [self removeBookmark:[bookmarkDict objectForKey:bookmark.internalObjectID]];
1240 [bookmarkDict setObject:bookmark forKey:bookmark.internalObjectID];
1242 [bookmark setInitialGroup:group];
1244 [self saveContactList];
1247 [bookmark restoreGrouping];
1249 //Do the update thing
1250 [contactPropertiesObserverManager _updateAllAttributesOfObject:bookmark];
1256 * @brief Remove a bookmark
1258 - (void)removeBookmark:(AIListBookmark *)listBookmark
1260 [self moveContact:listBookmark fromGroups:listBookmark.groups intoGroups:[NSSet set]];
1261 [bookmarkDict removeObjectForKey:listBookmark.internalObjectID];
1262 [contactDict removeObjectForKey:listBookmark.internalObjectID];
1264 [self saveContactList];
1267 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1269 if (inService && [inUID length]) {
1270 return [contactDict objectForKey:[AIListContact internalUniqueObjectIDForService:inService
1278 * @brief Return a set of all contacts with a specified UID and service
1280 * @param service The AIService in question
1281 * @param inUID The UID, which should be normalized (lower case, no spaces, etc.) as appropriate for the service
1283 - (NSSet *)allContactsWithService:(AIService *)service UID:(NSString *)inUID
1285 NSMutableSet *returnContactSet = [NSMutableSet set];
1287 for (AIAccount *account in [adium.accountController accountsCompatibleWithService:service]) {
1288 AIListContact *listContact = [self existingContactWithService:service
1293 [returnContactSet addObject:listContact];
1297 return returnContactSet;
1300 - (AIListObject *)existingListObjectWithUniqueID:(NSString *)uniqueID
1303 for (AIListObject *listObject in contactDict.objectEnumerator) {
1304 if ([listObject.internalObjectID isEqualToString:uniqueID]) return listObject;
1308 for (AIListGroup *listObject in groupDict.objectEnumerator) {
1309 if ([listObject.internalObjectID isEqualToString:uniqueID]) return listObject;
1313 for (AIMetaContact *listObject in metaContactDict.objectEnumerator) {
1314 if ([listObject.internalObjectID isEqualToString:uniqueID]) return listObject;
1321 * @brief Get the best AIListContact to send a given content type to a contat
1323 * The resulting AIListContact will be the most available individual contact (not metacontact) on the best account to
1324 * receive the specified content type.
1326 * @result The contact, or nil if it is impossible to send inType to inContact
1328 - (AIListContact *)preferredContactForContentType:(NSString *)inType forListContact:(AIListContact *)inContact
1330 if ([inContact isKindOfClass:[AIMetaContact class]])
1331 inContact = [(AIMetaContact *)inContact preferredContactForContentType:inType];
1333 /* Find the best account for talking to this contact, and return an AIListContact on that account.
1334 * We'll get nil if no account can send inType to inContact.
1336 AIAccount *account = [adium.accountController preferredAccountForSendingContentType:inType toContact:inContact];
1339 return [self contactWithService:inContact.service account:account UID:inContact.UID];
1344 //XXX - This is ridiculous.
1345 - (AIListContact *)preferredContactWithUID:(NSString *)inUID andServiceID:(NSString *)inService forSendingContentType:(NSString *)inType
1347 AIService *theService = [adium.accountController firstServiceWithServiceID:inService];
1348 AIListContact *tempListContact = [[AIListContact alloc] initWithUID:inUID
1349 service:theService];
1350 AIAccount *account = [adium.accountController preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
1351 toContact:tempListContact];
1352 [tempListContact release];
1354 return [self contactWithService:theService account:account UID:inUID];
1359 * @brief Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts
1361 * If the destination contact's parent contact differs from the destination contact itself, the chat is with a metaContact.
1362 * If that metaContact's preferred destination for messaging isn't the same as the contact which was just messaged,
1363 * update the preference so that a new chat with this metaContact would default to the proper contact.
1365 - (void)didSendContent:(NSNotification *)notification
1367 AIChat *chat = [[notification userInfo] objectForKey:@"AIChat"];
1368 AIListContact *destContact = chat.listObject;
1369 AIListContact *metaContact = destContact.metaContact;
1374 NSString *destinationInternalObjectID = destContact.internalObjectID;
1375 NSString *currentPreferredDestination = [metaContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT group:OBJECT_STATUS_CACHE];
1377 if (![destinationInternalObjectID isEqualToString:currentPreferredDestination]) {
1378 [metaContact setPreference:destinationInternalObjectID
1379 forKey:KEY_PREFERRED_DESTINATION_CONTACT
1380 group:OBJECT_STATUS_CACHE];
1384 #pragma mark Retrieving Groups
1386 //Retrieve a group from the contact list (Creating if necessary)
1387 - (AIListGroup *)groupWithUID:(NSString *)groupUID
1389 NSParameterAssert(groupUID != nil);
1391 //Return our root group if it is requested.
1392 if ([groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME])
1393 return [self contactList];
1395 AIListGroup *group = nil;
1396 if (!(group = [groupDict objectForKey:[groupUID lowercaseString]])) {
1398 group = [[AIListGroup alloc] initWithUID:groupUID];
1401 //Update afterwards, in case it's being called inside updateListObject already.
1402 [contactPropertiesObserverManager performSelector:@selector(_updateAllAttributesOfObject:) withObject:group afterDelay:0.0];
1403 [groupDict setObject:group forKey:[groupUID lowercaseString]];
1405 //Add to the contact list
1406 [contactList addObject:group];
1407 [self _didChangeContainer:contactList object:group];
1414 #pragma mark Contact list editing
1415 - (void)removeListGroup:(AIListGroup *)group
1417 AIContactList *containingObject = group.contactList;
1419 //Remove all the contacts from this group
1420 for (AIListContact *contact in group.containedObjects) {
1421 [contact removeFromGroup:group];
1424 //Delete the group from all active accounts
1425 for (AIAccount *account in adium.accountController.accounts) {
1426 if (account.online) {
1427 [account deleteGroup:group];
1431 //Then, procede to delete the group
1433 [containingObject removeObject:group];
1434 [groupDict removeObjectForKey:[group.UID lowercaseString]];
1435 [self _didChangeContainer:containingObject object:group];
1439 - (void)requestAddContactWithUID:(NSString *)contactUID service:(AIService *)inService account:(AIAccount *)inAccount
1441 NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:contactUID
1443 if (inService) [userInfo setObject:inService forKey:@"AIService"];
1444 if (inAccount) [userInfo setObject:inAccount forKey:@"AIAccount"];
1446 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_AddNewContact
1451 - (void)moveContact:(AIListContact *)contact fromGroups:(NSSet *)oldGroups intoGroups:(NSSet *)groups
1453 [contactPropertiesObserverManager delayListObjectNotifications];
1454 if (contact.metaContact) {
1455 AIMetaContact *meta = contact.metaContact;
1456 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1457 [contactToMetaContactLookupDict removeObjectForKey:contact.internalObjectID];
1459 for (AIListContact *matchingContact in [self allContactsWithService:contact.service UID:contact.UID]) {
1460 [self removeContact:matchingContact fromMetaContact:meta];
1464 if (contact.existsServerside) {
1465 if (contact.account.online)
1466 [contact.account moveListObjects:[NSArray arrayWithObject:contact] fromGroups:oldGroups toGroups:groups];
1468 [self _moveContactLocally:contact fromGroups:oldGroups toGroups:groups];
1470 if ([contact conformsToProtocol:@protocol(AIContainingObject)]) {
1471 id<AIContainingObject> container = (id<AIContainingObject>)contact;
1473 //This is a meta contact, move the objects within it.
1474 for (AIListContact *child in container) {
1475 //Only move the contact if it is actually listed on the account in question
1476 if (child.account.online && !child.isStranger)
1477 [child.account moveListObjects:[NSArray arrayWithObject:child] fromGroups:oldGroups toGroups:groups];
1481 [contactPropertiesObserverManager endListObjectNotificationsDelay];
1484 #pragma mark Detached Contact Lists
1485 - (void)moveGroup:(AIListGroup *)group fromContactList:(AIContactList *)oldContactList toContactList:(AIContactList *)newContactList
1487 if (![oldContactList containsObject:group] || [newContactList containsObject:group]) {
1491 [oldContactList removeObject:group];
1492 [newContactList addObject:group];
1494 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_ListChanged
1495 object:newContactList
1498 if (!oldContactList.containedObjects.count) {
1499 [[NSNotificationCenter defaultCenter] postNotificationName:DetachedContactListIsEmpty
1500 object:oldContactList
1503 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_ListChanged
1504 object:oldContactList
1510 * @returns Empty contact list
1512 - (AIContactList *)createDetachedContactList
1514 static NSInteger count = 0;
1515 AIContactList *list = [[AIContactList alloc] initWithUID:[NSString stringWithFormat:@"Detached%ld",count++]];
1516 [contactLists addObject:list];
1522 * @brief Removes detached contact list
1524 - (void)removeDetachedContactList:(AIContactList *)detachedList
1526 [contactLists removeObject:detachedList];
1531 @implementation AIContactController (ContactControllerHelperAccess)
1532 - (NSEnumerator *)contactEnumerator
1534 return [contactDict objectEnumerator];
1536 - (NSEnumerator *)groupEnumerator
1538 return [groupDict objectEnumerator];