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];
259 //It's a newly created object, so set its initial attributes
260 [contactPropertiesObserverManager _updateAllAttributesOfObject:bookmark];
265 - (void)_loadMetaContactsFromArray:(NSArray *)array
267 for (NSString *identifier in array) {
268 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
269 NSNumber *objectID = [NSNumber numberWithInteger:[[[identifier componentsSeparatedByString:@"-"] objectAtIndex:1] integerValue]];
270 [self metaContactWithObjectID:objectID];
275 #pragma mark Contact Grouping
277 - (void)_addContactLocally:(AIListContact *)listContact toGroup:(AIListGroup *)localGroup
279 BOOL performedGrouping = NO;
281 //Protect with a retain while we are removing and adding the contact to our arrays
282 [listContact retain];
284 if (listContact.canJoinMetaContacts) {
285 AIListObject *existingObject = [localGroup objectWithService:listContact.service UID:listContact.UID];
286 if (existingObject) {
287 //If an object exists in this group with the same UID and serviceID, create a MetaContact
289 [self groupContacts:[NSArray arrayWithObjects:listContact,existingObject,nil]];
290 performedGrouping = YES;
293 AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:listContact.internalObjectID];
295 //If no object exists in this group which matches, we should check if there is already
296 //a MetaContact holding a matching ListContact, since we should include this contact in it
297 //If we found a metaContact to which we should add, do it.
299 [self addContact:listContact toMetaContact:metaContact];
300 performedGrouping = YES;
305 if (!performedGrouping) {
306 //If no similar objects exist, we add this contact directly to the list
307 [localGroup addObject:listContact];
310 [self _didChangeContainer:localGroup object:listContact];
314 [listContact release];
317 - (void)_moveContactLocally:(AIListContact *)listContact fromGroups:(NSSet *)oldGroups toGroups:(NSSet *)groups
319 //Protect with a retain while we are removing and adding the contact to our arrays
320 [listContact retain];
322 [contactPropertiesObserverManager delayListObjectNotifications];
324 //Remove this object from any local groups we have it in currently
325 for (AIListGroup *group in oldGroups) {
326 [group removeObject:listContact];
327 [self _didChangeContainer:group object:listContact];
330 for (AIListGroup *group in groups)
331 [self _addContactLocally:listContact toGroup:group];
333 [contactPropertiesObserverManager endListObjectNotificationsDelay];
335 [listContact release];
338 //Post a list grouping changed notification for the object and containing object
339 - (void)_didChangeContainer:(AIListObject<AIContainingObject> *)inContainingObject object:(AIListObject *)object
341 if ([contactPropertiesObserverManager updatesAreDelayed]) {
342 [contactPropertiesObserverManager noteContactChanged:object];
345 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_ListChanged
346 object:inContainingObject
351 - (BOOL)useContactListGroups
353 return useContactListGroups;
356 - (void)setUseContactListGroups:(BOOL)inFlag
358 if (inFlag != useContactListGroups) {
359 useContactListGroups = inFlag;
361 [self _performChangeOfUseContactListGroups];
365 - (void)_performChangeOfUseContactListGroups
367 [contactPropertiesObserverManager delayListObjectNotifications];
369 //Store the preference
370 [adium.preferenceController setPreference:[NSNumber numberWithBool:!useContactListGroups]
371 forKey:KEY_HIDE_CONTACT_LIST_GROUPS
372 group:PREF_GROUP_CONTACT_LIST_DISPLAY];
374 //Configure the sort controller to force ignoring of groups as appropriate
375 [[AISortController activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
377 //Restore the grouping of all root-level contacts
378 for (AIListContact *contact in [self contactEnumerator]) {
379 [contact restoreGrouping];
382 //Restore the grouping of all list bookmarks
383 for (AIListBookmark *bookmark in [self allBookmarks]) {
384 [bookmark restoreGrouping];
387 //Stop delaying object notifications; this will automatically resort the contact list, so we're done.
388 [contactPropertiesObserverManager endListObjectNotificationsDelay];
391 - (void)prepareShowHideGroups
393 //Load the preference
394 useContactListGroups = ![[adium.preferenceController preferenceForKey:KEY_HIDE_CONTACT_LIST_GROUPS
395 group:PREF_GROUP_CONTACT_LIST_DISPLAY] boolValue];
397 //Show offline contacts menu item
398 menuItem_showGroups = [[NSMenuItem alloc] initWithTitle:SHOW_GROUPS_MENU_TITLE
400 action:@selector(toggleShowGroups:)
403 [menuItem_showGroups setState:useContactListGroups];
405 [adium.menuController addMenuItem:menuItem_showGroups toLocation:LOC_View_Toggles];
408 NSToolbarItem *toolbarItem;
409 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SHOW_GROUPS_IDENTIFER
410 label:AILocalizedString(@"Show Groups",nil)
411 paletteLabel:AILocalizedString(@"Toggle Groups Display",nil)
412 toolTip:AILocalizedString(@"Toggle display of groups",nil)
414 settingSelector:@selector(setImage:)
415 itemContent:[NSImage imageNamed:(useContactListGroups ?
416 @"togglegroups_transparent" :
418 forClass:[self class]
420 action:@selector(toggleShowGroupsToolbar:)
422 [adium.toolbarController registerToolbarItem:toolbarItem forToolbarType:@"ContactList"];
425 - (IBAction)toggleShowGroups:(id)sender
428 useContactListGroups = !useContactListGroups;
429 [menuItem_showGroups setState:useContactListGroups];
431 //Update the contact list. Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure.
432 [self performSelector:@selector(_performChangeOfUseContactListGroups)
437 - (IBAction)toggleShowGroupsToolbar:(id)sender
439 [self toggleShowGroups:sender];
441 [sender setImage:[NSImage imageNamed:(useContactListGroups ?
442 @"togglegroups_transparent" :
444 forClass:[self class]]];
447 @synthesize useOfflineGroup;
449 - (AIListGroup *)offlineGroup
451 return [self groupWithUID:AILocalizedString(@"Offline", "Name of offline group")];
454 #pragma mark Meta Contacts
457 * @brief Create or load a metaContact
459 * @param inObjectID The objectID of an existing but unloaded metaContact, or nil to create and save a new metaContact
461 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID
463 BOOL shouldRestoreContacts = YES;
465 //If no object ID is provided, use the next available object ID
466 //(MetaContacts should always have an individually unique object id)
468 NSInteger topID = [[adium.preferenceController preferenceForKey:TOP_METACONTACT_ID
469 group:PREF_GROUP_CONTACT_LIST] integerValue];
470 inObjectID = [NSNumber numberWithInteger:topID];
471 [adium.preferenceController setPreference:[NSNumber numberWithInteger:([inObjectID integerValue] + 1)]
472 forKey:TOP_METACONTACT_ID
473 group:PREF_GROUP_CONTACT_LIST];
475 //No reason to waste time restoring contacts when none are in the meta contact yet.
476 shouldRestoreContacts = NO;
479 //Look for a metacontact with this object ID. If none is found, create one
480 //and add its contained contacts to it.
481 NSString *metaContactDictKey = [AIMetaContact internalObjectIDFromObjectID:inObjectID];
483 AIMetaContact *metaContact = [metaContactDict objectForKey:metaContactDictKey];
485 metaContact = [(AIMetaContact *)[AIMetaContact alloc] initWithObjectID:inObjectID];
487 //Keep track of it in our metaContactDict for retrieval by objectID
488 [metaContactDict setObject:metaContact forKey:metaContactDictKey];
490 //Add it to our more general contactDict, as well
491 [contactDict setObject:metaContact forKey:[metaContact internalUniqueObjectID]];
493 /* We restore contacts (actually, internalIDs for contacts, to be added as necessary later) if the metaContact
494 * existed before this call to metaContactWithObjectID:
496 if (shouldRestoreContacts)
497 [self _restoreContactsToMetaContact:metaContact];
499 /* As with contactWithService:account:UID, update all attributes so observers are initially informed of
500 * this object's existence.
502 [contactPropertiesObserverManager _updateAllAttributesOfObject:metaContact];
504 [metaContact release];
511 * @brief Associate the appropriate internal IDs for contained contacts with a metaContact
513 * @result YES if one or more contacts was associated with the metaContact; NO if none were.
515 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact
517 NSDictionary *allMetaContactsDict = [adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
518 group:PREF_GROUP_CONTACT_LIST];
519 NSArray *containedContactsArray = [allMetaContactsDict objectForKey:metaContact.internalObjectID];
521 if (containedContactsArray.count) {
522 [self _restoreContactsToMetaContact:metaContact
523 fromContainedContactsArray:containedContactsArray];
533 * @brief Associate the internal IDs for an array of contacts with a specific metaContact
535 * This does not actually place any AIListContacts within the metaContact. Instead, it updates the contactToMetaContactLookupDict
536 * dictionary to have metaContact associated with the list contacts specified by containedContactsArray. This
537 * allows us to add them lazily to the metaContact (in contactWithService:account:UID:) as necessary.
539 * @param metaContact The metaContact to which contact referneces are added
540 * @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
542 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray
544 for (NSDictionary *containedContactDict in containedContactsArray) {
545 /* Before Adium 0.80, metaContacts could be created within metaContacts. Simply ignore any attempt to restore
546 * such erroneous data, which will have a YES boolValue for KEY_IS_METACONTACT. */
547 if (![[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) {
548 /* Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject.
550 * As listObjects are loaded/created/requested which match this internalObjectID,
551 * they will be inserted into the metaContact.
553 NSString *internalObjectID = [AIListObject internalObjectIDForServiceID:[containedContactDict objectForKey:SERVICE_ID_KEY]
554 UID:[containedContactDict objectForKey:UID_KEY]];
555 [contactToMetaContactLookupDict setObject:metaContact
556 forKey:internalObjectID];
561 //Add a list object to a meta contact, setting preferences and such
562 //so the association is lasting across program launches.
563 - (void)addContact:(AIListContact *)inContact toMetaContact:(AIMetaContact *)metaContact
566 //I can't think of why one would want to add an entire group to a metacontact. Let's say you can't.
567 NSLog(@"Warning: addContact:toMetaContact: Attempted to add %@ to %@",inContact,metaContact);
571 if (inContact == metaContact) return;
573 //If listObject contains other contacts, perform addContact:toMetaContact: recursively
574 if ([inContact conformsToProtocol:@protocol(AIContainingObject)]) {
575 for (AIListContact *someObject in ((AIListObject<AIContainingObject> *)inContact).containedObjects) {
576 [self addContact:someObject toMetaContact:metaContact];
580 //Obtain any metaContact this listObject is currently within, so we can remove it later
581 AIMetaContact *oldMetaContact = [contactToMetaContactLookupDict objectForKey:[inContact internalObjectID]];
583 if ([self _performAddContact:inContact toMetaContact:metaContact] && metaContact != oldMetaContact) {
584 //If this listObject was not in this metaContact in any form before, store the change
585 //Remove the list object from any other metaContact it is in at present
587 [self removeContact:inContact fromMetaContact:oldMetaContact];
589 [self _storeListObject:inContact inMetaContact:metaContact];
591 //Do the update thing
592 [contactPropertiesObserverManager _updateAllAttributesOfObject:metaContact];
597 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact
599 //we only allow group->meta->contact, not group->meta->meta->contact
600 NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
602 // AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact);
603 NSDictionary *containedContactDict;
604 NSMutableDictionary *allMetaContactsDict;
605 NSMutableArray *containedContactsArray;
607 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
609 //Get the dictionary of all metaContacts
610 allMetaContactsDict = [[adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
611 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
612 if (!allMetaContactsDict) {
613 allMetaContactsDict = [[NSMutableDictionary alloc] init];
616 //Load the array for the new metaContact
617 containedContactsArray = [[allMetaContactsDict objectForKey:metaContactInternalObjectID] mutableCopy];
618 if (!containedContactsArray) containedContactsArray = [[NSMutableArray alloc] init];
619 containedContactDict = nil;
621 //Create the dictionary describing this list object
622 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
623 listObject.service.serviceID, SERVICE_ID_KEY,
624 listObject.UID, UID_KEY, nil];
626 //Only add if this dict isn't already in the array
627 if (containedContactDict && ([containedContactsArray indexOfObject:containedContactDict] == NSNotFound)) {
628 [containedContactsArray addObject:containedContactDict];
629 [allMetaContactsDict setObject:containedContactsArray forKey:metaContactInternalObjectID];
632 [self _saveMetaContacts:allMetaContactsDict];
634 [adium.contactAlertsController mergeAndMoveContactAlertsFromListObject:listObject
635 intoListObject:metaContact];
638 [allMetaContactsDict release];
639 [containedContactsArray release];
642 //Actually adds a list contact to a meta contact. No preferences are changed.
643 //Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict
644 //for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful.
645 - (BOOL)_performAddContact:(AIListContact *)inContact toMetaContact:(AIMetaContact *)metaContact
647 //we only allow group->meta->contact, not group->meta->meta->contact
648 NSParameterAssert([metaContact canContainObject:inContact]);
652 //Remove the object from its previous containing groups
653 [self _moveContactLocally:inContact fromGroups:inContact.groups toGroups:[NSSet set]];
655 //AIMetaContact will handle reassigning the list object's grouping to being itself
656 if ((success = [metaContact addObject:inContact])) {
657 [contactToMetaContactLookupDict setObject:metaContact forKey:[inContact internalObjectID]];
659 [self _didChangeContainer:metaContact object:inContact];
661 //Ensure the metacontact ends up in the appropriate groups
662 [metaContact restoreGrouping];
668 - (void)removeContact:(AIListContact *)inContact fromMetaContact:(AIMetaContact *)metaContact
670 //we only allow group->meta->contact, not group->meta->meta->contact
671 NSParameterAssert(![inContact conformsToProtocol:@protocol(AIContainingObject)]);
673 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
675 //Get the dictionary of all metaContacts
676 NSMutableDictionary *allMetaContactsDict = [adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
677 group:PREF_GROUP_CONTACT_LIST];
679 //Load the array for the metaContact
680 NSArray *containedContactsArray = [allMetaContactsDict objectForKey:metaContactInternalObjectID];
682 //Enumerate it, looking only for the appropriate type of containedContactDict
684 NSString *listObjectUID = inContact.UID;
685 NSString *listObjectServiceID = inContact.service.serviceID;
687 NSDictionary *containedContactDict = nil;
688 for (containedContactDict in containedContactsArray) {
689 if ([[containedContactDict objectForKey:UID_KEY] isEqualToString:listObjectUID] &&
690 [[containedContactDict objectForKey:SERVICE_ID_KEY] isEqualToString:listObjectServiceID]) {
695 //If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result
696 if (containedContactDict) {
697 NSMutableArray *newContainedContactsArray;
698 NSMutableDictionary *newAllMetaContactsDict;
700 newContainedContactsArray = [containedContactsArray mutableCopy];
701 [newContainedContactsArray removeObjectIdenticalTo:containedContactDict];
703 newAllMetaContactsDict = [allMetaContactsDict mutableCopy];
704 [newAllMetaContactsDict setObject:newContainedContactsArray
705 forKey:metaContactInternalObjectID];
707 [self _saveMetaContacts:newAllMetaContactsDict];
709 [newContainedContactsArray release];
710 [newAllMetaContactsDict release];
713 //The listObject can be within the metaContact without us finding a containedContactDict if we are removing multiple
714 //listContacts referring to the same UID & serviceID combination - that is, on multiple accounts on the same service.
715 //We therefore request removal of the object regardless of the if (containedContactDict) check above.
716 [metaContact removeObject:inContact];
718 [self _didChangeContainer:metaContact object:inContact];
722 * @brief Determine the existing metacontact into which a grouping of UIDs and services would be placed
724 * @param UIDsArray NSArray of UIDs
725 * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
727 * @result Either the existing AIMetaContact -[self groupUIDs:forServices:usingMetaContactHint:] would return if passed a nil metaContactHint,
728 * or nil (if no existing metacontact would be used).
730 - (AIMetaContact *)knownMetaContactForGroupingUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray
732 AIMetaContact *metaContact = nil;
733 NSInteger count = [UIDsArray count];
735 for (NSInteger i = 0; i < count; i++) {
736 if ((metaContact = [contactToMetaContactLookupDict objectForKey:[AIListObject internalObjectIDForServiceID:[servicesArray objectAtIndex:i]
737 UID:[UIDsArray objectAtIndex:i]]])) {
746 * @brief Groups UIDs for services into a single metacontact
748 * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to
749 * a UID and a service, respectively, which together define a contact which should be included in the grouping.
751 * Assumption: This is only called after the contact list is finished loading, which occurs via
752 * -(void)controllerDidLoad above.
754 * @param UIDsArray NSArray of UIDs
755 * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
756 * @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.
758 - (AIMetaContact *)groupUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray usingMetaContactHint:(AIMetaContact *)metaContactHint
760 NSMutableSet *internalObjectIDs = [[NSMutableSet alloc] init];
761 AIMetaContact *metaContact = nil;
762 NSString *internalObjectID;
763 NSInteger count = [UIDsArray count];
765 /* Build an array of all contacts matching this description (multiple accounts on the same service listing
766 * the same UID mean that we can have multiple AIListContact objects with a UID/service combination)
768 for (NSUInteger i = 0; i < count; i++) {
769 NSString *serviceID = [servicesArray objectAtIndex:i];
770 NSString *UID = [UIDsArray objectAtIndex:i];
772 internalObjectID = [AIListObject internalObjectIDForServiceID:serviceID
775 metaContact = [contactToMetaContactLookupDict objectForKey:internalObjectID];
778 [internalObjectIDs addObject:internalObjectID];
781 if ([internalObjectIDs count] > 1) {
782 //Create a new metaContact is we didn't find one and weren't supplied a hint
783 if (!metaContact && !(metaContact = metaContactHint)) {
784 AILogWithSignature(@"New metacontact to group %@ on %@", UIDsArray, servicesArray);
785 metaContact = [self metaContactWithObjectID:nil];
788 for (internalObjectID in internalObjectIDs) {
789 AIListObject *existingObject;
790 if ((existingObject = [self existingListObjectWithUniqueID:internalObjectID])) {
791 /* If there is currently an object (or multiple objects) matching this internalObjectID
792 * we should add immediately.
794 NSAssert([metaContact canContainObject:existingObject], @"Attempting to add something metacontacts can't hold to a metacontact");
795 [self addContact:(id)existingObject
796 toMetaContact:metaContact];
798 /* If no objects matching this internalObjectID exist, we can simply add to the
799 * contactToMetaContactLookupDict for use if such an object is created later.
801 [contactToMetaContactLookupDict setObject:metaContact
802 forKey:internalObjectID];
807 [internalObjectIDs release];
812 /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added.
814 * This will reuse an existing metacontact (for one of the contacts in the array) if possible.
815 * @param contactsToGroupArray Contacts to group together
817 - (AIMetaContact *)groupContacts:(NSArray *)contactsToGroupArray
819 AIMetaContact *metaContact = nil;
821 //Look for a metacontact we were passed directly
822 for (AIListContact *listContact in contactsToGroupArray) {
823 if ([listContact isKindOfClass:[AIMetaContact class]]) {
824 metaContact = (AIMetaContact *)listContact;
829 //If we weren't passed a metacontact, look for an existing metacontact associated with a passed contact
831 for (AIListContact *listContact in contactsToGroupArray) {
832 if (![listContact isKindOfClass:[AIMetaContact class]] &&
833 (metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]])) {
839 //Create a new metaContact is we didn't find one.
841 AILogWithSignature(@"New metacontact to group %@", contactsToGroupArray);
842 metaContact = [self metaContactWithObjectID:nil];
845 /* Add all these contacts to our MetaContact.
846 * Some may already be present, but that's fine, as nothing will happen.
848 for (AIListContact *listContact in contactsToGroupArray) {
849 [self addContact:listContact toMetaContact:metaContact];
855 - (void)explodeMetaContact:(AIMetaContact *)metaContact
857 //Remove the objects within it from being inside it
858 [contactPropertiesObserverManager delayListObjectNotifications];
859 NSArray *containedObjects = metaContact.containedObjects;
861 NSMutableDictionary *allMetaContactsDict = [[adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
862 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
864 for (AIListContact *object in containedObjects) {
866 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
867 [contactToMetaContactLookupDict removeObjectForKey:[object internalObjectID]];
869 [self removeContact:object fromMetaContact:metaContact];
872 //Then, procede to remove the metaContact
875 [metaContact retain];
877 //Remove it from its containing groups (no contained contacts == present in no groups)
878 [metaContact restoreGrouping];
880 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
882 //Remove our reference to it internally
883 [metaContactDict removeObjectForKey:metaContactInternalObjectID];
885 //Remove it from the preferences dictionary
886 [allMetaContactsDict removeObjectForKey:metaContactInternalObjectID];
888 //Save the updated allMetaContactsDict which no longer lists the metaContact
889 [self _saveMetaContacts:allMetaContactsDict];
891 [contactPropertiesObserverManager endListObjectNotificationsDelay];
893 //Protection is overrated.
894 [metaContact release];
895 [allMetaContactsDict release];
898 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict
900 AILog(@"MetaContacts: Saving!");
901 [adium.preferenceController setPreference:allMetaContactsDict
902 forKey:KEY_METACONTACT_OWNERSHIP
903 group:PREF_GROUP_CONTACT_LIST];
904 [adium.preferenceController setPreference:[allMetaContactsDict allKeys]
905 forKey:KEY_FLAT_METACONTACTS
906 group:PREF_GROUP_CONTACT_LIST];
909 //Sort list objects alphabetically by their display name
910 NSInteger contactDisplayNameSort(AIListObject *objectA, AIListObject *objectB, void *context)
912 return [objectA.displayName caseInsensitiveCompare:objectB.displayName];
915 #pragma mark Preference observing
917 * @brief Preferences changed
919 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
920 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
923 ![key isEqualToString:KEY_HIDE_CONTACTS] &&
924 ![key isEqualToString:KEY_SHOW_OFFLINE_CONTACTS] &&
925 ![key isEqualToString:KEY_USE_OFFLINE_GROUP] &&
926 ![key isEqualToString:KEY_HIDE_CONTACT_LIST_GROUPS]) {
930 BOOL shouldUseOfflineGroup = ((![[prefDict objectForKey:KEY_HIDE_CONTACTS] boolValue] ||
931 [[prefDict objectForKey:KEY_SHOW_OFFLINE_CONTACTS] boolValue]) &&
932 [[prefDict objectForKey:KEY_USE_OFFLINE_GROUP] boolValue]);
934 if (shouldUseOfflineGroup != self.useOfflineGroup || !key) {
935 self.useOfflineGroup = shouldUseOfflineGroup;
937 if (self.useOfflineGroup)
938 [contactPropertiesObserverManager registerListObjectObserver:self];
940 [contactPropertiesObserverManager updateAllListObjectsForObserver:self];
942 if (!self.useOfflineGroup)
943 [contactPropertiesObserverManager unregisterListObjectObserver:self];
948 * @brief Move contacts to and from the offline group as necessary as their online state changes.
950 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
952 if ((!inModifiedKeys || [inModifiedKeys containsObject:@"Online"]) && [inObject isKindOfClass:[AIListContact class]]) {
953 [((AIListContact *)inObject).parentContact restoreGrouping];
959 #pragma mark Contact Sorting
961 //Sort the entire contact list
962 - (void)sortContactList
964 [self sortContactLists:contactLists];
967 - (void)sortContactLists:(NSArray *)lists
969 for(AIContactList *list in lists) {
972 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_OrderChanged object:nil];
975 //Sort an individual object
976 - (void)sortListObject:(AIListObject *)inObject
978 if ([contactPropertiesObserverManager updatesAreDelayed]) {
979 [contactPropertiesObserverManager noteContactChanged:inObject];
982 for (AIListGroup *group in inObject.groups) {
983 //Sort the groups containing this object
984 [group sortListObject:inObject];
985 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_OrderChanged object:group];
990 #pragma mark Contact List Access
992 @synthesize contactList;
995 * @brief Return an array of all contact list groups
997 - (NSArray *)allGroups
999 return [groupDict allValues];
1003 * @brief Returns a flat array of all contacts
1005 * This does not include metacontacts
1007 - (NSArray *)allContacts
1009 NSMutableArray *result = [[[NSMutableArray alloc] init] autorelease];
1011 for (AIListContact *contact in self.contactEnumerator) {
1012 /* We want only contacts, not metacontacts. For a given contact, -[contact parentContact] could be used to access the meta. */
1013 if (![contact conformsToProtocol:@protocol(AIContainingObject)])
1014 [result addObject:contact];
1021 * @brief Returns a flat array of all bookmarks.
1023 - (NSArray *)allBookmarks
1025 return [[[bookmarkDict allValues] copy] autorelease];
1029 * @brief Returns a flat array of all metacontacts
1031 - (NSArray *)allMetaContacts
1033 return [metaContactDict allValues];
1036 //Return a flat array of all the objects in a group on an account (and all subgroups, if desired)
1037 - (NSArray *)allContactsInObject:(AIListObject<AIContainingObject> *)inGroup onAccount:(AIAccount *)inAccount
1039 NSParameterAssert(inGroup != nil);
1041 NSMutableArray *contactArray = [NSMutableArray array];
1043 for (AIListObject *object in inGroup) {
1044 if ([object conformsToProtocol:@protocol(AIContainingObject)]) {
1045 [contactArray addObjectsFromArray:[self allContactsInObject:(AIListObject<AIContainingObject> *)object
1046 onAccount:inAccount]];
1047 } else if ([object isMemberOfClass:[AIListContact class]] && (!inAccount || ([(AIListContact *)object account] == inAccount)))
1048 [contactArray addObject:object];
1051 return contactArray;
1054 #pragma mark Contact List Menus
1056 //Returns a menu containing all the groups within a group
1057 //- Selector called on group selection is selectGroup:
1058 //- The menu items represented object is the group it represents
1059 - (NSMenu *)groupMenuWithTarget:(id)target
1061 NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
1062 [menu setAutoenablesItems:NO];
1064 // Separate groups by the contact list they're on.
1065 NSMutableDictionary *groupsByList = [NSMutableDictionary dictionaryWithCapacity:contactLists.count];
1066 for (AIListGroup *group in self.allGroups) {
1067 if (group != self.offlineGroup) {
1068 NSMutableArray *groups = [groupsByList objectForKey:group.contactList.UID];
1070 groups = [NSMutableArray array];
1071 [groupsByList setObject:groups forKey:group.contactList.UID];
1073 [groups addObject:group];
1077 // Now traverse the contactLists array in order and build a menu showing the groups in each list with separators in between.
1079 for (AIContactList *list in contactLists) {
1080 NSMutableArray *groups = [groupsByList objectForKey:list.UID];
1081 [groups sortUsingActiveSortControllerInContainer:list];
1082 for (AIListGroup *group in groups) {
1083 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:group.displayName
1085 action:@selector(selectGroup:)
1087 [menuItem setRepresentedObject:group];
1088 [menu addItem:menuItem];
1094 // Add the separator unless this is the last list.
1095 if (i < contactLists.count) {
1096 [menu addItem:[NSMenuItem separatorItem]];
1102 return [menu autorelease];
1105 #pragma mark Retrieving Specific Contacts
1108 * @brief Change the UID for a contact
1110 * @param UID The new UID to set
1111 * @param contact The contact whose UID is going to be changed
1113 * Preserves our reference to the contact as the UID changes, allowing us to continue
1114 * returning it when asked about the new UID in the future.
1116 - (void)setUID:(NSString *)UID forContact:(AIListContact *)contact
1120 // Remove the old value, its internal ID is going to change.
1121 [contactDict removeObjectForKey:contact.internalUniqueObjectID];
1124 [contact setUID:UID];
1126 // Add it back int othe dict.
1127 [contactDict setObject:contact forKey:contact.internalUniqueObjectID];
1132 - (AIListContact *)contactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1134 if (!(inUID && [inUID length] && inService)) return nil; //Ignore invalid requests
1136 AIListContact *contact = nil;
1137 NSString *key = [AIListContact internalUniqueObjectIDForService:inService
1140 contact = [contactDict objectForKey:key];
1143 contact = [[AIListContact alloc] initWithUID:inUID account:inAccount service:inService];
1145 //Check to see if we should add to a metaContact
1146 AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:[contact internalObjectID]];
1148 /* We already know to add this object to the metaContact, since we did it before with another object,
1149 but this particular listContact is new and needs to be added directly to the metaContact
1150 (on future launches, the metaContact will obtain it automatically since all contacts matching this UID
1151 and serviceID should be included). */
1152 [self _performAddContact:contact toMetaContact:metaContact];
1155 //Set the contact as mobile if it is a phone number
1156 if ([inUID hasPrefix:@"+"]) {
1157 [contact setIsMobile:YES notify:NotifyNever];
1161 [contactDict setObject:contact forKey:key];
1163 //Do the update thing
1164 [contactPropertiesObserverManager _updateAllAttributesOfObject:contact];
1172 - (void)accountDidStopTrackingContact:(AIListContact *)inContact
1174 [[inContact retain] autorelease];
1176 /* Remove after a short delay. Otherwise, the removal may be visible as the object remains in the contact
1177 * list until a display delay is over, which would show up as the name going blank on metacontacts and other
1180 * Of course, this really means that the object delay code is somehow failing to actually delay all updates.
1181 * I can't figure out where or why, so this is a hack around it. Ugh. -evands 10/08
1183 for (AIListObject<AIContainingObject> *container in inContact.containingObjects) {
1184 [container performSelector:@selector(removeObjectAfterAccountStopsTracking:)
1185 withObject:inContact
1189 [contactDict removeObjectForKey:inContact.internalUniqueObjectID];
1193 * @brief Find an existing bookmark
1195 * Finds an existing bookmark for a given AIChat
1197 - (AIListBookmark *)existingBookmarkForChat:(AIChat *)inChat
1199 return [self existingBookmarkForChatName:inChat.name
1200 onAccount:inChat.account
1201 chatCreationInfo:inChat.chatCreationDictionary];
1205 * @brief Find an existing bookmark
1207 * Finds an existing bookmark for given information.
1209 - (AIListBookmark *)existingBookmarkForChatName:(NSString *)inName
1210 onAccount:(AIAccount *)inAccount
1211 chatCreationInfo:(NSDictionary *)inCreationInfo
1213 AIListBookmark *existingBookmark = nil;
1215 for(AIListBookmark *listBookmark in self.allBookmarks) {
1216 if([listBookmark.name isEqualToString:[inAccount.service normalizeChatName:inName]] &&
1217 listBookmark.account == inAccount &&
1218 ((!listBookmark.chatCreationDictionary && !inCreationInfo) ||
1219 ([listBookmark.chatCreationDictionary isEqualToDictionary:inCreationInfo]))) {
1220 existingBookmark = listBookmark;
1225 return existingBookmark;
1230 * @brief Find or create a bookmark for a chat
1232 - (AIListBookmark *)bookmarkForChat:(AIChat *)inChat inGroup:(AIListGroup *)group
1234 AIListBookmark *bookmark = [self existingBookmarkForChat:inChat];
1237 bookmark = [[[AIListBookmark alloc] initWithChat:inChat] autorelease];
1239 if ([bookmarkDict objectForKey:bookmark.internalObjectID]) {
1240 // In case we end up with two bookmarks with the same internalObjectID; this should be almost impossible.
1241 [self removeBookmark:[bookmarkDict objectForKey:bookmark.internalObjectID]];
1244 [bookmarkDict setObject:bookmark forKey:bookmark.internalObjectID];
1246 [bookmark setInitialGroup:group];
1248 [self saveContactList];
1251 [bookmark restoreGrouping];
1253 //Do the update thing
1254 [contactPropertiesObserverManager _updateAllAttributesOfObject:bookmark];
1260 * @brief Remove a bookmark
1262 - (void)removeBookmark:(AIListBookmark *)listBookmark
1264 [self moveContact:listBookmark fromGroups:listBookmark.groups intoGroups:[NSSet set]];
1265 [bookmarkDict removeObjectForKey:listBookmark.internalObjectID];
1267 [self saveContactList];
1270 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1272 if (inService && [inUID length]) {
1273 return [contactDict objectForKey:[AIListContact internalUniqueObjectIDForService:inService
1281 * @brief Return a set of all contacts with a specified UID and service
1283 * @param service The AIService in question
1284 * @param inUID The UID, which should be normalized (lower case, no spaces, etc.) as appropriate for the service
1286 - (NSSet *)allContactsWithService:(AIService *)service UID:(NSString *)inUID
1288 NSMutableSet *returnContactSet = [NSMutableSet set];
1290 for (AIAccount *account in [adium.accountController accountsCompatibleWithService:service]) {
1291 AIListContact *listContact = [self existingContactWithService:service
1296 [returnContactSet addObject:listContact];
1300 return returnContactSet;
1303 - (AIListObject *)existingListObjectWithUniqueID:(NSString *)uniqueID
1306 for (AIListObject *listObject in contactDict.objectEnumerator) {
1307 if ([listObject.internalObjectID isEqualToString:uniqueID]) return listObject;
1311 for (AIListGroup *listObject in groupDict.objectEnumerator) {
1312 if ([listObject.internalObjectID isEqualToString:uniqueID]) return listObject;
1316 for (AIMetaContact *listObject in metaContactDict.objectEnumerator) {
1317 if ([listObject.internalObjectID isEqualToString:uniqueID]) return listObject;
1324 * @brief Get the best AIListContact to send a given content type to a contat
1326 * The resulting AIListContact will be the most available individual contact (not metacontact) on the best account to
1327 * receive the specified content type.
1329 * @result The contact, or nil if it is impossible to send inType to inContact
1331 - (AIListContact *)preferredContactForContentType:(NSString *)inType forListContact:(AIListContact *)inContact
1333 if ([inContact isKindOfClass:[AIMetaContact class]])
1334 inContact = [(AIMetaContact *)inContact preferredContactForContentType:inType];
1336 /* Find the best account for talking to this contact, and return an AIListContact on that account.
1337 * We'll get nil if no account can send inType to inContact.
1339 AIAccount *account = [adium.accountController preferredAccountForSendingContentType:inType toContact:inContact];
1342 return [self contactWithService:inContact.service account:account UID:inContact.UID];
1347 //XXX - This is ridiculous.
1348 - (AIListContact *)preferredContactWithUID:(NSString *)inUID andServiceID:(NSString *)inService forSendingContentType:(NSString *)inType
1350 AIService *theService = [adium.accountController firstServiceWithServiceID:inService];
1351 AIListContact *tempListContact = [[AIListContact alloc] initWithUID:inUID
1352 service:theService];
1353 AIAccount *account = [adium.accountController preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
1354 toContact:tempListContact];
1355 [tempListContact release];
1357 return [self contactWithService:theService account:account UID:inUID];
1362 * @brief Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts
1364 * If the destination contact's parent contact differs from the destination contact itself, the chat is with a metaContact.
1365 * If that metaContact's preferred destination for messaging isn't the same as the contact which was just messaged,
1366 * update the preference so that a new chat with this metaContact would default to the proper contact.
1368 - (void)didSendContent:(NSNotification *)notification
1370 AIChat *chat = [[notification userInfo] objectForKey:@"AIChat"];
1371 AIListContact *destContact = chat.listObject;
1372 AIListContact *metaContact = destContact.metaContact;
1377 NSString *destinationInternalObjectID = destContact.internalObjectID;
1378 NSString *currentPreferredDestination = [metaContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT group:OBJECT_STATUS_CACHE];
1380 if (![destinationInternalObjectID isEqualToString:currentPreferredDestination]) {
1381 [metaContact setPreference:destinationInternalObjectID
1382 forKey:KEY_PREFERRED_DESTINATION_CONTACT
1383 group:OBJECT_STATUS_CACHE];
1387 #pragma mark Retrieving Groups
1389 //Retrieve a group from the contact list (Creating if necessary)
1390 - (AIListGroup *)groupWithUID:(NSString *)groupUID
1392 NSParameterAssert(groupUID != nil);
1394 //Return our root group if it is requested.
1395 if ([groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME])
1396 return [self contactList];
1398 AIListGroup *group = nil;
1399 if (!(group = [groupDict objectForKey:[groupUID lowercaseString]])) {
1401 group = [[AIListGroup alloc] initWithUID:groupUID];
1404 //Update afterwards, in case it's being called inside updateListObject already.
1405 [contactPropertiesObserverManager performSelector:@selector(_updateAllAttributesOfObject:) withObject:group afterDelay:0.0];
1406 [groupDict setObject:group forKey:[groupUID lowercaseString]];
1408 //Add to the contact list
1409 [contactList addObject:group];
1410 [self _didChangeContainer:contactList object:group];
1417 #pragma mark Contact list editing
1418 - (void)removeListGroup:(AIListGroup *)group
1420 AIContactList *containingObject = group.contactList;
1422 //Remove all the contacts from this group
1423 for (AIListContact *contact in group.containedObjects) {
1424 [contact removeFromGroup:group];
1427 //Delete the group from all active accounts
1428 for (AIAccount *account in adium.accountController.accounts) {
1429 if (account.online) {
1430 [account deleteGroup:group];
1434 //Then, procede to delete the group
1436 [containingObject removeObject:group];
1437 [groupDict removeObjectForKey:[group.UID lowercaseString]];
1438 [self _didChangeContainer:containingObject object:group];
1442 - (void)requestAddContactWithUID:(NSString *)contactUID service:(AIService *)inService account:(AIAccount *)inAccount
1444 NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:contactUID
1446 if (inService) [userInfo setObject:inService forKey:@"AIService"];
1447 if (inAccount) [userInfo setObject:inAccount forKey:@"AIAccount"];
1449 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_AddNewContact
1454 - (void)moveContact:(AIListContact *)contact fromGroups:(NSSet *)oldGroups intoGroups:(NSSet *)groups
1456 [contactPropertiesObserverManager delayListObjectNotifications];
1457 if (contact.metaContact) {
1458 AIMetaContact *meta = contact.metaContact;
1459 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1460 [contactToMetaContactLookupDict removeObjectForKey:contact.internalObjectID];
1462 for (AIListContact *matchingContact in [self allContactsWithService:contact.service UID:contact.UID]) {
1463 [self removeContact:matchingContact fromMetaContact:meta];
1467 if (contact.existsServerside) {
1468 if (contact.account.online)
1469 [contact.account moveListObjects:[NSArray arrayWithObject:contact] fromGroups:oldGroups toGroups:groups];
1471 [self _moveContactLocally:contact fromGroups:oldGroups toGroups:groups];
1473 if ([contact conformsToProtocol:@protocol(AIContainingObject)]) {
1474 id<AIContainingObject> container = (id<AIContainingObject>)contact;
1476 //This is a meta contact, move the objects within it.
1477 for (AIListContact *child in container) {
1478 //Only move the contact if it is actually listed on the account in question
1479 if (child.account.online && !child.isStranger)
1480 [child.account moveListObjects:[NSArray arrayWithObject:child] fromGroups:oldGroups toGroups:groups];
1484 [contactPropertiesObserverManager endListObjectNotificationsDelay];
1487 #pragma mark Detached Contact Lists
1488 - (void)moveGroup:(AIListGroup *)group fromContactList:(AIContactList *)oldContactList toContactList:(AIContactList *)newContactList
1490 if (![oldContactList containsObject:group] || [newContactList containsObject:group]) {
1494 [oldContactList removeObject:group];
1495 [newContactList addObject:group];
1497 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_ListChanged
1498 object:newContactList
1501 if (!oldContactList.containedObjects.count) {
1502 [[NSNotificationCenter defaultCenter] postNotificationName:DetachedContactListIsEmpty
1503 object:oldContactList
1506 [[NSNotificationCenter defaultCenter] postNotificationName:Contact_ListChanged
1507 object:oldContactList
1513 * @returns Empty contact list
1515 - (AIContactList *)createDetachedContactList
1517 static NSInteger count = 0;
1518 AIContactList *list = [[AIContactList alloc] initWithUID:[NSString stringWithFormat:@"Detached%ld",count++]];
1519 [contactLists addObject:list];
1525 * @brief Removes detached contact list
1527 - (void)removeDetachedContactList:(AIContactList *)detachedList
1529 [contactLists removeObject:detachedList];
1534 @implementation AIContactController (ContactControllerHelperAccess)
1535 - (NSEnumerator *)contactEnumerator
1537 return [contactDict objectEnumerator];
1539 - (NSEnumerator *)groupEnumerator
1541 return [groupDict objectEnumerator];