{{{
[adiumx.git] / Source / AIContactController.m
blobee52e5c3733ee6d61d0deddbb9597c491b50df29
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
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.
8  * 
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.
12  * 
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.
15  */
17 // $Id$
19 #import "AIContactController.h"
21 #import <Adium/AIAccountControllerProtocol.h>
22 #import <Adium/AIInterfaceControllerProtocol.h>
23 #import <Adium/AILoginController.h>
24 #import <Adium/AIMenuControllerProtocol.h>
25 #import <Adium/AIPreferenceControllerProtocol.h>
26 #import <Adium/AIToolbarControllerProtocol.h>
27 #import <Adium/AIContactAlertsControllerProtocol.h>
29 #import <AIUtilities/AIDictionaryAdditions.h>
30 #import <AIUtilities/AIFileManagerAdditions.h>
31 #import <AIUtilities/AIMenuAdditions.h>
32 #import <AIUtilities/AIToolbarUtilities.h>
33 #import <AIUtilities/AIApplicationAdditions.h>
34 #import <AIUtilities/AIImageAdditions.h>
35 #import <AIUtilities/AIStringAdditions.h>
36 #import <Adium/AIAccount.h>
37 #import <Adium/AIChat.h>
38 #import <Adium/AIContentMessage.h>
39 #import <Adium/AIListContact.h>
40 #import <Adium/AIListGroup.h>
41 #import <Adium/AIListObject.h>
42 #import <Adium/AIMetaContact.h>
43 #import <Adium/AIService.h>
44 #import <Adium/AISortController.h>
45 #import <Adium/AIUserIcons.h>
46 #import <Adium/AIServiceIcons.h>
48 #import "AdiumAuthorization.h"
51 #define PREF_GROUP_CONTACT_LIST                 @"Contact List"                 //Contact list preference group
52 #define KEY_FLAT_GROUPS                                 @"FlatGroups"                   //Group storage
53 #define KEY_FLAT_CONTACTS                               @"FlatContacts"                 //Contact storage
54 #define KEY_FLAT_METACONTACTS                   @"FlatMetaContacts"             //Metacontact objectID storage
56 #define OBJECT_STATUS_CACHE                             @"Object Status Cache"
58 #define UPDATE_CLUMP_INTERVAL                   1.0
60 #define TOP_METACONTACT_ID                              @"TopMetaContactID"
61 #define KEY_IS_METACONTACT                              @"isMetaContact"
62 #define KEY_OBJECTID                                    @"objectID"
63 #define KEY_METACONTACT_OWNERSHIP               @"MetaContact Ownership"
64 #define CONTACT_DEFAULT_PREFS                   @"ContactPrefs"
66 #define SHOW_GROUPS_MENU_TITLE                  AILocalizedString(@"Show Groups",nil)
67 #define SHOW_GROUPS_IDENTIFER                   @"ShowGroups"
69 #define KEY_HIDE_CONTACT_LIST_GROUPS    @"Hide Contact List Groups"
70 #define KEY_USE_OFFLINE_GROUP                   @"Use Offline Group"
71 #define KEY_SHOW_OFFLINE_CONTACTS               @"Show Offline Contacts"
73 #define PREF_GROUP_CONTACT_LIST_DISPLAY @"Contact List Display"
75 #define SERVICE_ID_KEY                                  @"ServiceID"
76 #define UID_KEY                                                 @"UID"
78 @interface AIContactController (PRIVATE)
79 - (AIListGroup *)processGetGroupNamed:(NSString *)serverGroup;
80 - (void)_performDelayedUpdates:(NSTimer *)timer;
81 - (void)loadContactList;
82 - (void)saveContactList;
83 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
84 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject;
85 - (void)prepareContactInfo;
87 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inGroup withTarget:(id)target firstLevel:(BOOL)firstLevel;
88 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level;
90 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects;
91 - (void)_loadGroupsFromArray:(NSArray *)array;
93 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object;
94 - (void)prepareShowHideGroups;
95 - (void)_performChangeOfUseContactListGroups;
97 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inGroup:(AIListObject<AIContainingObject> *)group;
98 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group;
99 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName;
101 //MetaContacts
102 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID;
103 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact;
104 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray;
105 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
106 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
107 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact;
108 - (void)_loadMetaContactsFromArray:(NSArray *)array;
109 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict;
110 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact;
111 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact;
113 - (void)_addMenuItemsFromArray:(NSArray *)contactArray toMenu:(NSMenu *)contactMenu target:(id)target offlineContacts:(BOOL)offlineContacts;
115 - (void)_performChangeOfUseOfflineGroup;
117 @end
119 @implementation AIContactController
121 //init
122 - (id)init
124         if ((self = [super init])) {
125                 //
126                 contactObservers = [[NSMutableSet alloc] init];
127                 sortControllerArray = [[NSMutableArray alloc] init];
128                 activeSortController = nil;
129                 delayedStatusChanges = 0;
130                 delayedModifiedStatusKeys = [[NSMutableSet alloc] init];
131                 delayedAttributeChanges = 0;
132                 delayedModifiedAttributeKeys = [[NSMutableSet alloc] init];
133                 delayedContactChanges = 0;
134                 delayedUpdateRequests = 0;
135                 updatesAreDelayed = NO;
136                 
137                 //
138                 contactDict = [[NSMutableDictionary alloc] init];
139                 groupDict = [[NSMutableDictionary alloc] init];
140                 metaContactDict = [[NSMutableDictionary alloc] init];
141                 contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
142         }
143         
144         return self;
147 //finish initing
148 - (void)controllerDidLoad
149 {       
150         //Default contact preferences
151         [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:CONTACT_DEFAULT_PREFS
152                                                                                                                                                 forClass:[self class]]
153                                                                                   forGroup:PREF_GROUP_CONTACT_LIST];
154         
155         contactList = [[AIListGroup alloc] initWithUID:ADIUM_ROOT_GROUP_NAME];
156         
157         //Show Groups menu item
158         [self prepareShowHideGroups];
159         
160         //Observe content (for preferredContactForContentType:forListContact:)
161     [[adium notificationCenter] addObserver:self
162                                    selector:@selector(didSendContent:)
163                                        name:CONTENT_MESSAGE_SENT
164                                      object:nil];
166         [self loadContactList];
167         [self sortContactList];
169         adiumAuthorization = [[AdiumAuthorization alloc] init];
171         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST_DISPLAY];
174 //close
175 - (void)controllerWillClose
177         [self saveContactList];
180 //dealloc
181 - (void)dealloc
183         [[adium preferenceController] unregisterPreferenceObserver:self];
185     [contactList release];
186     [contactObservers release]; contactObservers = nil;
188     [super dealloc];
191 - (void)clearAllMetaContactData
193         NSString                *path;
194         NSDictionary    *metaContactDictCopy = [metaContactDict copy];
195         NSEnumerator    *enumerator;
196         AIMetaContact   *metaContact;
198         if ([metaContactDictCopy count]) {
199                 [self delayListObjectNotifications];
201                 //Remove all the metaContacts to get any existing objects out of them
202                 enumerator = [metaContactDictCopy objectEnumerator];
203                 while ((metaContact = [enumerator nextObject])) {
204                         [self breakdownAndRemoveMetaContact:metaContact];
205                 }
207                 [self endListObjectNotificationsDelay];
208         }
210         [metaContactDict release]; metaContactDict = [[NSMutableDictionary alloc] init];
211         [contactToMetaContactLookupDict release]; contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
213         //Clear the preferences for good measure
214         [[adium preferenceController] setPreference:nil
215                                                                                  forKey:KEY_FLAT_METACONTACTS
216                                                                                   group:PREF_GROUP_CONTACT_LIST];
217         [[adium preferenceController] setPreference:nil
218                                                                                  forKey:KEY_METACONTACT_OWNERSHIP
219                                                                                   group:PREF_GROUP_CONTACT_LIST];
221         //Clear out old metacontact files
222         path = [[[adium loginController] userDirectory] stringByAppendingPathComponent:OBJECT_PREFS_PATH];
223         [[NSFileManager defaultManager] removeFilesInDirectory:path
224                                                                                                 withPrefix:@"MetaContact"
225                                                                                          movingToTrash:NO];
226         [[NSFileManager defaultManager] removeFilesInDirectory:[adium cachesPath]
227                                                                                                 withPrefix:@"MetaContact"
228                                                                                          movingToTrash:NO];
230         [metaContactDictCopy release];
233 //Local Contact List Storage -------------------------------------------------------------------------------------------
234 #pragma mark Local Contact List Storage
235 //Load the contact list
236 - (void)loadContactList
238         //We must load all the groups before loading contacts for the ordering system to work correctly.
239         [self _loadMetaContactsFromArray:[[adium preferenceController] preferenceForKey:KEY_FLAT_METACONTACTS
240                                                                                                                                                           group:PREF_GROUP_CONTACT_LIST]];
243 //Save the contact list
244 - (void)saveContactList
246         NSEnumerator *enumerator = [groupDict objectEnumerator];
247         AIListGroup      *listGroup;
248         
249         while ((listGroup = [enumerator nextObject])) {
250                 [listGroup setPreference:[NSNumber numberWithBool:[listGroup isExpanded]]
251                                                   forKey:@"IsExpanded"
252                                                    group:PREF_GROUP_CONTACT_LIST];
253         }
256 - (void)_loadMetaContactsFromArray:(NSArray *)array
258         NSEnumerator    *enumerator = [array objectEnumerator];
259         NSString                *identifier;
261         while ((identifier = [enumerator nextObject])) {
262                 NSNumber *objectID = [NSNumber numberWithInt:[[[identifier componentsSeparatedByString:@"-"] objectAtIndex:1] intValue]];
263                 [self metaContactWithObjectID:objectID];
264         }
267 //Flattened array of the contact list content
268 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects
270         NSMutableArray  *array = [NSMutableArray array];
271         NSEnumerator    *enumerator = [listObjects objectEnumerator];;
272         AIListObject    *object;
274         //Create temporary strings outside the loop
275         NSString        *Group = @"Group";
276         NSString        *Type = @"Type";
277         NSString        *Expanded = @"Expanded";
279         while ((object = [enumerator nextObject])) {
280                         [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
281                                 Group, Type,
282                                 [object UID], UID_KEY,
283                                 [NSNumber numberWithBool:[(AIListGroup *)object isExpanded]], Expanded,
284                                 nil]];
285         }
287         return array;
291 //Status and Display updates -------------------------------------------------------------------------------------------
292 #pragma mark Status and Display updates
293 //These delay Contact_ListChanged, ListObject_AttributesChanged, Contact_OrderChanged notificationsDelays,
294 //sorting and redrawing to prevent redundancy when making a large number of changes
295 //Explicit delay.  Call endListObjectNotificationsDelay to end
296 - (void)delayListObjectNotifications
298         delayedUpdateRequests++;
299         updatesAreDelayed = YES;
302 //End an explicit delay
303 - (void)endListObjectNotificationsDelay
305         delayedUpdateRequests--;
306         if (delayedUpdateRequests == 0 && !delayedUpdateTimer) {
307                 [self _performDelayedUpdates:nil];
308         }
311 //Delay all list object notifications until a period of inactivity occurs.  This is useful for accounts that do not
312 //know when they have finished connecting but still want to mute events.
313 - (void)delayListObjectNotificationsUntilInactivity
315     if (!delayedUpdateTimer) {
316                 updatesAreDelayed = YES;
317                 delayedUpdateTimer = [[NSTimer scheduledTimerWithTimeInterval:UPDATE_CLUMP_INTERVAL
318                                                                                                                            target:self
319                                                                                                                          selector:@selector(_performDelayedUpdates:)
320                                                                                                                          userInfo:nil
321                                                                                                                           repeats:YES] retain];
322     } else {
323                 //Reset the timer
324                 [delayedUpdateTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:UPDATE_CLUMP_INTERVAL]];
325         }
328 //Update the status of a list object.  This will update any information that is otherwise too expensive to update
329 //automatically, such as their profile.
330 - (void)updateListContactStatus:(AIListContact *)inContact
332         //If we're dealing with a meta contact, update the status of the contacts contained within it
333         if ([inContact isKindOfClass:[AIMetaContact class]]) {
334                 NSEnumerator    *enumerator = [[(AIMetaContact *)inContact listContacts] objectEnumerator];
335                 AIListContact   *contact;
337                 while ((contact = [enumerator nextObject])) {
338                         [self updateListContactStatus:contact];
339                 }
341         } else {
342                 AIAccount *account = [inContact account];
343                 if (![account online]) {
344                         account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
345                                                                                                                                                          toContact:inContact];
346                 }
348                 [account updateContactStatus:inContact];
349         }
352 //Called after modifying a contact's status
353 // Silent: Silences all events, notifications, sounds, overlays, etc. that would have been associated with this status change
354 - (void)listObjectStatusChanged:(AIListObject *)inObject modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
356     NSSet                       *modifiedAttributeKeys;
358     //Let all observers know the contact's status has changed before performing any sorting or further notifications
359         modifiedAttributeKeys = [self _informObserversOfObjectStatusChange:inObject withKeys:inModifiedKeys silent:silent];
361     //Resort the contact list
362         if (updatesAreDelayed) {
363                 delayedStatusChanges++;
364                 [delayedModifiedStatusKeys unionSet:inModifiedKeys];
365         } else {
366                 //We can safely skip sorting if we know the modified attributes will invoke a resort later
367                 if (![[self activeSortController] shouldSortForModifiedAttributeKeys:modifiedAttributeKeys] &&
368                    [[self activeSortController] shouldSortForModifiedStatusKeys:inModifiedKeys]) {
369                         [self sortListObject:inObject];
370                 }
371         }
373     //Post an attributes changed message (if necessary)
374     if ([modifiedAttributeKeys count]) {
375                 [self listObjectAttributesChanged:inObject modifiedKeys:modifiedAttributeKeys];
376     }
379 //Call after modifying an object's display attributes
380 //(When modifying display attributes in response to a status change, this is not necessary)
381 - (void)listObjectAttributesChanged:(AIListObject *)inObject modifiedKeys:(NSSet *)inModifiedKeys
383         if (updatesAreDelayed) {
384                 delayedAttributeChanges++;
385                 [delayedModifiedAttributeKeys unionSet:inModifiedKeys];
386         } else {
387         //Resort the contact list if necessary
388         if ([[self activeSortController] shouldSortForModifiedAttributeKeys:inModifiedKeys]) {
389                         [self sortListObject:inObject];
390         }
392         //Post an attributes changed message
393                 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
394                                                                                                   object:inObject
395                                                                                                 userInfo:(inModifiedKeys ?
396                                                                                                                   [NSDictionary dictionaryWithObject:inModifiedKeys
397                                                                                                                                                                           forKey:@"Keys"] :
398                                                                                                                   nil)];
399         }
402 //Performs any delayed list object/handle updates
403 - (void)_performDelayedUpdates:(NSTimer *)timer
405         BOOL    updatesOccured = (delayedStatusChanges || delayedAttributeChanges || delayedContactChanges);
407         //Send out global attribute & status changed notifications (to cover any delayed updates)
408         if (updatesOccured) {
409                 BOOL shouldSort = NO;
411                 //Inform observers of any changes
412                 if (delayedContactChanges) {
413 //                      [[adium notificationCenter] postNotificationName:Contact_ListChanged object:nil];
414                         delayedContactChanges = 0;
415                         shouldSort = YES;
416                 }
417                 if (delayedStatusChanges) {
418                         if (!shouldSort &&
419                            [[self activeSortController] shouldSortForModifiedStatusKeys:delayedModifiedStatusKeys]) {
420                                 shouldSort = YES;
421                         }
422                         [delayedModifiedStatusKeys removeAllObjects];
423                         delayedStatusChanges = 0;
424                 }
425                 if (delayedAttributeChanges) {
426                         if (!shouldSort &&
427                            [[self activeSortController] shouldSortForModifiedAttributeKeys:delayedModifiedAttributeKeys]) {
428                                 shouldSort = YES;
429                         }
430                         [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
431                                                                                                           object:nil
432                                                                                                         userInfo:(delayedModifiedAttributeKeys ?
433                                                                                                                           [NSDictionary dictionaryWithObject:delayedModifiedAttributeKeys
434                                                                                                                                                                                   forKey:@"Keys"] :
435                                                                                                                           nil)];
436                         [delayedModifiedAttributeKeys removeAllObjects];
437                         delayedAttributeChanges = 0;
438                 }
440                 //Sort only if necessary
441                 if (shouldSort) {
442                         [self sortContactList];
443                 }
444         }
446     //If no more updates are left to process, disable the update timer
447         //If there are no delayed update requests, remove the hold
448         if (!delayedUpdateTimer || !updatesOccured) {
449                 if (delayedUpdateTimer) {
450                         [delayedUpdateTimer invalidate];
451                         [delayedUpdateTimer release];
452                         delayedUpdateTimer = nil;
453                 }
454                 if (delayedUpdateRequests == 0) {
455                         updatesAreDelayed = NO;
456                 }
457     }
460 #pragma mark Contact Grouping
461 //Contact Grouping -----------------------------------------------------------------------------------------------------
463 //Redetermine the local grouping of a contact in response to server grouping information or an external change
464 - (void)listObjectRemoteGroupingChanged:(AIListContact *)inContact
466         AIListObject<AIContainingObject>        *containingObject;
467         NSString                                                        *remoteGroupName = [inContact remoteGroupName];
469         [inContact retain];
471         containingObject = [inContact containingObject];
473         if ([containingObject isKindOfClass:[AIMetaContact class]]) {
475                 /* If inContact's containingObject is a metaContact, and that metaContact has no containingObject,
476                  * use inContact's remote grouping as the metaContact's grouping.
477                  */
478                 if (![containingObject containingObject] && [remoteGroupName length]) {
479                         //If no similar objects exist, we add this contact directly to the list
480                         //Create a group for the contact even if contact list groups aren't on,
481                         //otherwise requests for all the contact list groups will return nothing
482                         AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
484                         localGroup = (useContactListGroups ?
485                                                   ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
486                                                   contactList);
488                         [localGroup addObject:containingObject];
489                         [self _listChangedGroup:localGroup object:containingObject];
490                         AILog(@"listObjectRemoteGroupingChanged: %@ is in %@, which was moved to %@",inContact,containingObject,localGroup);
491                 }
493         } else {
494                 //If we have a remoteGroupName, add the contact locally to the list
495                 if (remoteGroupName) {
496                         //Create a group for the contact even if contact list groups aren't on,
497                         //otherwise requests for all the contact list groups will return nothing
498                         AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
500                         localGroup = (useContactListGroups ?
501                                                   ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
502                                                   contactList);
504                         AILog(@"listObjectRemoteGroupingChanged: %@: remoteGroupName %@ --> %@",inContact,remoteGroupName,localGroup);
506                         [self _moveContactLocally:inContact
507                                                           toGroup:localGroup];
509                 } else {
510                         //If !remoteGroupName, remove the contact from any local groups
511                         if (containingObject) {
512                                 //Remove the object
513                                 [(AIListGroup *)containingObject removeObject:inContact];
515                                 [self _listChangedGroup:(AIListGroup *)containingObject object:inContact];
516                                 
517                                 AILog(@"listObjectRemoteGroupingChanged: %@: -- !remoteGroupName so removed from %@",inContact,containingObject);
518                         }
519                 }
520         }
522         BOOL    isCurrentlyAStranger = [inContact isStranger];
523         if ((isCurrentlyAStranger && (remoteGroupName != nil)) ||
524            (!isCurrentlyAStranger && (remoteGroupName == nil))) {
525                 [inContact setStatusObject:(remoteGroupName ? [NSNumber numberWithBool:YES] : nil)
526                                                         forKey:@"NotAStranger"
527                                                         notify:NotifyLater];
528                 [inContact notifyOfChangedStatusSilently:YES];
529         }
531         [inContact release];
534 - (void)_moveContactLocally:(AIListContact *)listContact toGroup:(AIListGroup *)localGroup
536         AIListObject    *containingObject;
537         AIListObject    *existingObject;
538         BOOL                    performedGrouping = NO;
540         //Protect with a retain while we are removing and adding the contact to our arrays
541         [listContact retain];
543         //XXX
544         AILog(@"Moving %@ to %@",listContact,localGroup);
545         
546         //Remove this object from any local groups we have it in currently
547         if ((containingObject = [listContact containingObject]) &&
548            ([containingObject isKindOfClass:[AIListGroup class]])) {
549                 //Remove the object
550                 [(AIListGroup *)containingObject removeObject:listContact];
551                 [self _listChangedGroup:(AIListGroup *)containingObject object:listContact];
552         }
554         if ((existingObject = [localGroup objectWithService:[listContact service] UID:[listContact UID]])) {
555                 //If an object exists in this group with the same UID and serviceID, create a MetaContact
556                 //for the two.
557                 [self groupListContacts:[NSArray arrayWithObjects:listContact,existingObject,nil]];
558                 performedGrouping = YES;
560         } else {
561                 AIMetaContact   *metaContact;
563                 //If no object exists in this group which matches, we should check if there is already
564                 //a MetaContact holding a matching ListContact, since we should include this contact in it
565                 //If we found a metaContact to which we should add, do it.
566                 if ((metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]])) {
567                         //XXX
568                         AILog(@"Found an existing metacontact; adding %@ to %@",listContact,metaContact);
570                         [self addListObject:listContact toMetaContact:metaContact];
571                         performedGrouping = YES;
572                 }
573         }
575         if (!performedGrouping) {
576                 //If no similar objects exist, we add this contact directly to the list
577                 [localGroup addObject:listContact];
579                 //Add
580                 [self _listChangedGroup:localGroup object:listContact];
581         }
583         //Cleanup
584         [listContact release];
587 - (AIListGroup *)remoteGroupForContact:(AIListContact *)inContact
589         AIListGroup             *group;
591         if ([inContact isKindOfClass:[AIMetaContact class]]) {
592                 //For a metaContact, the closest we have to a remote group is the group it is within locally
593                 group = [(AIMetaContact *)inContact parentGroup];
595         } else {
596                 NSString        *remoteGroup = [inContact remoteGroupName];
597                 group = (remoteGroup ? [self groupWithUID:remoteGroup] : nil);
598         }
600         return group;
603 //Post a list grouping changed notification for the object and group
604 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object
606         if (updatesAreDelayed) {
607                 delayedContactChanges++;
608         } else {
609                 [[adium notificationCenter] postNotificationName:Contact_ListChanged
610                                                                                                   object:object
611                                                                                                 userInfo:(group ? [NSDictionary dictionaryWithObject:group forKey:@"ContainingGroup"] : nil)];
612         }
615 - (BOOL)useContactListGroups
617         return useContactListGroups;
620 - (void)setUseContactListGroups:(BOOL)inFlag
622         if (inFlag != useContactListGroups) {
623                 useContactListGroups = inFlag;
625                 [self _performChangeOfUseContactListGroups];
626         }
629 - (void)_performChangeOfUseContactListGroups
631         NSEnumerator    *enumerator;
632         AIListObject    *listObject;
634         [self delayListObjectNotifications];
636         //Store the preference
637         [[adium preferenceController] setPreference:[NSNumber numberWithBool:!useContactListGroups]
638                                                                                  forKey:KEY_HIDE_CONTACT_LIST_GROUPS
639                                                                                   group:PREF_GROUP_CONTACT_LIST_DISPLAY];
641         //Configure the sort controller to force ignoring of groups as appropriate
642         [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
644         enumerator = [[[[contactList containedObjects] copy] autorelease] objectEnumerator];
646         if (useContactListGroups) { /* We are now using contact list groups, but we weren't before. */
648                 //Restore the grouping of all root-level contacts
649                 while ((listObject = [enumerator nextObject])) {
650                         if ([listObject isKindOfClass:[AIListContact class]]) {
651                                 [(AIListContact *)listObject restoreGrouping];
652                         }
653                 }
655         } else { /* We are no longer using contact list groups, but we were before. */
657                 while ((listObject = [enumerator nextObject])) {
658                         if ([listObject isKindOfClass:[AIListGroup class]]) {
659                                 NSArray                 *containedObjects;
660                                 NSEnumerator    *groupEnumerator;
661                                 AIListObject    *containedListObject;
663                                 containedObjects = [[(AIListGroup *)listObject containedObjects] copy];
664                                 groupEnumerator = [containedObjects objectEnumerator];
665                                 while ((containedListObject = [groupEnumerator nextObject])) {
666                                         if ([containedListObject isKindOfClass:[AIListContact class]]) {
667                                                 [self _moveContactLocally:(AIListContact *)containedListObject
668                                                                                   toGroup:contactList];
669                                         }
670                                 }
671                                 [containedObjects release];
672                         }
673                 }
674         }
676         //Stop delaying object notifications; this will automatically resort the contact list, so we're done.
677         [self endListObjectNotificationsDelay];
680 - (void)prepareShowHideGroups
682         //Load the preference
683         useContactListGroups = ![[[adium preferenceController] preferenceForKey:KEY_HIDE_CONTACT_LIST_GROUPS
684                                                                                                                                           group:PREF_GROUP_CONTACT_LIST_DISPLAY] boolValue];
686         //Show offline contacts menu item
687     menuItem_showGroups = [[NSMenuItem alloc] initWithTitle:SHOW_GROUPS_MENU_TITLE
688                                                                                                         target:self
689                                                                                                         action:@selector(toggleShowGroups:)
690                                                                                          keyEquivalent:@""];
691         [menuItem_showGroups setState:useContactListGroups];
692         [[adium menuController] addMenuItem:menuItem_showGroups toLocation:LOC_View_Toggles];
694         //Toolbar
695         NSToolbarItem   *toolbarItem;
696     toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SHOW_GROUPS_IDENTIFER
697                                                                                                                   label:AILocalizedString(@"Show Groups",nil)
698                                                                                                    paletteLabel:AILocalizedString(@"Toggle Groups Display",nil)
699                                                                                                                 toolTip:AILocalizedString(@"Toggle display of groups",nil)
700                                                                                                                  target:self
701                                                                                                 settingSelector:@selector(setImage:)
702                                                                                                         itemContent:[NSImage imageNamed:(useContactListGroups ?
703                                                                                                                                                                          @"togglegroups_transparent" :
704                                                                                                                                                                          @"togglegroups")
705                                                                                                                                                    forClass:[self class]]
706                                                                                                                  action:@selector(toggleShowGroupsToolbar:)
707                                                                                                                    menu:nil];
708     [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ContactList"];
711 - (IBAction)toggleShowGroups:(id)sender
713         //Flip-flop.
714         useContactListGroups = !useContactListGroups;
715         [menuItem_showGroups setState:useContactListGroups];
717         //Update the contact list.  Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure.
718         [self performSelector:@selector(_performChangeOfUseContactListGroups)
719                            withObject:nil
720                            afterDelay:0.000001];
723 - (IBAction)toggleShowGroupsToolbar:(id)sender
725         [self toggleShowGroups:sender];
727         [sender setImage:[NSImage imageNamed:(useContactListGroups ?
728                                                                                   @"togglegroups_transparent" :
729                                                                                   @"togglegroups")
730                                                                 forClass:[self class]]];
733 - (BOOL)useOfflineGroup
735         return useOfflineGroup;
738 - (void)setUseOfflineGroup:(BOOL)inFlag
740         if (inFlag != useOfflineGroup) {
741                 useOfflineGroup = inFlag;
742                 
743                 if (useOfflineGroup) {
744                         [self registerListObjectObserver:self]; 
745                 } else {
746                         [self updateAllListObjectsForObserver:self];
747                         [self unregisterListObjectObserver:self];       
748                 }
749         }
752 - (AIListGroup *)offlineGroup
754         return [self groupWithUID:AILocalizedString(@"Offline", "Name of offline group")];
757 #pragma mark Meta Contacts
758 //Meta Contacts --------------------------------------------------------------------------------------------------------
760  * @brief Create or load a metaContact
762  * @param inObjectID The objectID of an existing but unloaded metaContact, or nil to create and save a new metaContact
763  */
764 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID
766         NSString                *metaContactDictKey;
767         AIMetaContact   *metaContact;
768         BOOL                    shouldRestoreContacts = YES;
770         //If no object ID is provided, use the next available object ID
771         //(MetaContacts should always have an individually unique object id)
772         if (!inObjectID) {
773                 int topID = [[[adium preferenceController] preferenceForKey:TOP_METACONTACT_ID
774                                                                                                                           group:PREF_GROUP_CONTACT_LIST] intValue];
775                 inObjectID = [NSNumber numberWithInt:topID];
776                 [[adium preferenceController] setPreference:[NSNumber numberWithInt:([inObjectID intValue] + 1)]
777                                                                                          forKey:TOP_METACONTACT_ID
778                                                                                           group:PREF_GROUP_CONTACT_LIST];
780                 //No reason to waste time restoring contacts when none are in the meta contact yet.
781                 shouldRestoreContacts = NO;
782         }
784         //Look for a metacontact with this object ID.  If none is found, create one
785         //and add its contained contacts to it.
786         metaContactDictKey = [AIMetaContact internalObjectIDFromObjectID:inObjectID];
788         metaContact = [metaContactDict objectForKey:metaContactDictKey];
789         if (!metaContact) {
790                 metaContact = [[AIMetaContact alloc] initWithObjectID:inObjectID];
792                 //Keep track of it in our metaContactDict for retrieval by objectID
793                 [metaContactDict setObject:metaContact forKey:metaContactDictKey];
795                 //Add it to our more general contactDict, as well
796                 [contactDict setObject:metaContact forKey:[metaContact internalUniqueObjectID]];
798                 /* We restore contacts (actually, internalIDs for contacts, to be added as necessary later) if the metaContact
799                  * existed before this call to metaContactWithObjectID:
800                  */
801                 if (shouldRestoreContacts) {
802                         if (![self _restoreContactsToMetaContact:metaContact]) {
804                                 //If restoring the metacontact did not actually add any contacts, delete it since it is invalid
805                                 [self breakdownAndRemoveMetaContact:metaContact];
806                                 metaContact = nil;
807                         }
808                 }
809                 
810                 /* As with contactWithService:account:UID, update all attributes so observers are initially informed of
811                  * this object's existence.
812                  */
813                 [self _updateAllAttributesOfObject:metaContact];
814                 
815                 [metaContact release];
816         }
818         return (metaContact);
822  * @brief Associate the appropriate internal IDs for contained contacts with a metaContact
824  * @result YES if one or more contacts was associated with the metaContact; NO if none were.
825  */
826 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact
828         NSDictionary    *allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
829                                                                                                                                                                         group:PREF_GROUP_CONTACT_LIST];
830         NSArray                 *containedContactsArray = [allMetaContactsDict objectForKey:[metaContact internalObjectID]];
831         BOOL                    restoredContacts;
833         if ([containedContactsArray count]) {
834                 [self _restoreContactsToMetaContact:metaContact
835                                  fromContainedContactsArray:containedContactsArray];
837                 restoredContacts = YES;
839         } else {
840                 restoredContacts = NO;
841         }
843         return restoredContacts;
847  * @brief Associate the internal IDs for an array of contacts with a specific metaContact
849  * This does not actually place any AIListContacts within the metaContact.  Instead, it updates the contactToMetaContactLookupDict
850  * dictionary to have metaContact associated with the list contacts specified by containedContactsArray. This
851  * allows us to add them lazily to the metaContact (in contactWithService:account:UID:) as necessary.
853  * @param metaContact The metaContact to which contact referneces are added
854  * @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
855  */
856 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray
858         NSEnumerator            *enumerator = [containedContactsArray objectEnumerator];
859         NSDictionary            *containedContactDict;
861         while ((containedContactDict = [enumerator nextObject])) {
862                 /* Before Adium 0.80, metaContacts could be created within metaContacts. Simply ignore any attempt to restore
863                 * such irroneous data, which will have a YES boolValue for KEY_IS_METACONTACT. */
864                 if (![[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) {
865                         /* Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject.
866                          *
867                          * As listObjects are loaded/created/requested which match this internalObjectID, 
868                          * they will be inserted into the metaContact.
869                          */
870                         NSString        *internalObjectID = [AIListObject internalObjectIDForServiceID:[containedContactDict objectForKey:SERVICE_ID_KEY]
871                                                                                                                                                                    UID:[containedContactDict objectForKey:UID_KEY]];
872                         [contactToMetaContactLookupDict setObject:metaContact
873                                                                                            forKey:internalObjectID];
874                 }
875         }
879 //Add a list object to a meta contact, setting preferences and such
880 //so the association is lasting across program launches.
881 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
883         if (listObject != metaContact) {
885                 //If listObject is a metaContact, perform addListObject:toMetaContact: recursively
886                 if ([listObject isKindOfClass:[AIMetaContact class]]) {
887                         NSEnumerator    *enumerator = [[[[(AIMetaContact *)listObject containedObjects] copy] autorelease] objectEnumerator];
888                         AIListObject    *someObject;
890                         while ((someObject = [enumerator nextObject])) {
892                                 [self addListObject:someObject toMetaContact:metaContact];
893                         }
895                 } else {
896                         AIMetaContact           *oldMetaContact;
898                         //Obtain any metaContact this listObject is currently within, so we can remove it later
899                         oldMetaContact = [contactToMetaContactLookupDict objectForKey:[listObject internalObjectID]];
901                         if ([self _performAddListObject:listObject toMetaContact:metaContact]) {
902                                 //If this listObject was not in this metaContact in any form before, store the change
903                                 if (metaContact != oldMetaContact) {
904                                         //Remove the list object from any other metaContact it is in at present
905                                         if (oldMetaContact) {
906                                                 [self removeListObject:listObject fromMetaContact:oldMetaContact];
907                                         }
909                                         [self _storeListObject:listObject inMetaContact:metaContact];
910                                 }
911                         }
912                 }
913         }
916 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact
918         AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact);
919         NSDictionary            *containedContactDict;
920         NSMutableDictionary     *allMetaContactsDict;
921         NSMutableArray          *containedContactsArray;
923         NSString                        *metaContactInternalObjectID = [metaContact internalObjectID];
925         //Get the dictionary of all metaContacts
926         allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
927                                                                                                                                         group:PREF_GROUP_CONTACT_LIST] mutableCopy];
928         if (!allMetaContactsDict) {
929                 allMetaContactsDict = [[NSMutableDictionary alloc] init];
930         }
932         //Load the array for the new metaContact
933         containedContactsArray = [[allMetaContactsDict objectForKey:metaContactInternalObjectID] mutableCopy];
934         if (!containedContactsArray) containedContactsArray = [[NSMutableArray alloc] init];
935         containedContactDict = nil;
937         //Create the dictionary describing this list object
938         if ([listObject isKindOfClass:[AIMetaContact class]]) {
939                 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
940                         [NSNumber numberWithBool:YES],KEY_IS_METACONTACT,
941                         [(AIMetaContact *)listObject objectID],KEY_OBJECTID,nil];
943         } else if ([listObject isKindOfClass:[AIListContact class]]) {
944                 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
945                         [[listObject service] serviceID],SERVICE_ID_KEY,
946                         [listObject UID],UID_KEY,nil];
947         }
949         //Only add if this dict isn't already in the array
950         if (containedContactDict && ([containedContactsArray indexOfObject:containedContactDict] == NSNotFound)) {
951                 [containedContactsArray addObject:containedContactDict];
952                 [allMetaContactsDict setObject:containedContactsArray forKey:metaContactInternalObjectID];
954                 //Save
955                 [self _saveMetaContacts:allMetaContactsDict];
957                 [[adium contactAlertsController] mergeAndMoveContactAlertsFromListObject:listObject
958                                                                                                                                   intoListObject:metaContact];
959         }
961         [allMetaContactsDict release];
962         [containedContactsArray release];
965 //Actually adds a list object to a meta contact. No preferences are changed.
966 //Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict
967 //for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful.
968 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
970         AIListObject<AIContainingObject>        *localGroup;
971         BOOL                                                            success;
973         localGroup = [listObject containingObject];
975         //Remove the object from its previous containing group
976         if (localGroup && (localGroup != metaContact)) {
977                 [localGroup removeObject:listObject];
978                 [self _listChangedGroup:localGroup object:listObject];
979         }
981         //AIMetaContact will handle reassigning the list object's grouping to being itself
982         if ((success = [metaContact addObject:listObject])) {
983                 [contactToMetaContactLookupDict setObject:metaContact forKey:[listObject internalObjectID]];
985                 [self _listChangedGroup:metaContact object:listObject];
986                 //If the metaContact isn't in a group yet, use the group of the object we just added
987                 if ((![metaContact containingObject]) && localGroup) {
988                         //Add the new meta contact to our list
989                         [(AIMetaContact *)localGroup addObject:metaContact];
990                         [self _listChangedGroup:localGroup object:metaContact];
991                 }
992         }
994         return success;
997 - (void)removeAllListObjectsMatching:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
999         NSEnumerator    *enumerator;
1000         AIListObject    *theObject;
1001         
1002         enumerator = [[self allContactsWithService:[listObject service]
1003                                                                                    UID:[listObject UID]
1004                                                                   existingOnly:YES] objectEnumerator];
1006         //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1007         [contactToMetaContactLookupDict removeObjectForKey:[listObject internalObjectID]];
1009         [self delayListObjectNotifications];
1010         while ((theObject = [enumerator nextObject])) {
1011                 [self removeListObject:theObject fromMetaContact:metaContact];
1012         }
1013         [self endListObjectNotificationsDelay];
1016 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1018         NSEnumerator            *enumerator;
1019         NSArray                         *containedContactsArray;
1020         NSDictionary            *containedContactDict = nil;
1021         NSMutableDictionary     *allMetaContactsDict;
1022         NSString                        *metaContactInternalObjectID = [metaContact internalObjectID];
1024         //Get the dictionary of all metaContacts
1025         allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1026                                                                                                                                    group:PREF_GROUP_CONTACT_LIST];
1028         //Load the array for the metaContact
1029         containedContactsArray = [allMetaContactsDict objectForKey:metaContactInternalObjectID];
1031         //Enumerate it, looking only for the appropriate type of containedContactDict
1032         enumerator = [containedContactsArray objectEnumerator];
1034         if ([listObject isKindOfClass:[AIMetaContact class]]) {
1035                 NSNumber        *listObjectObjectID = [(AIMetaContact *)listObject objectID];
1037                 while ((containedContactDict = [enumerator nextObject])) {
1038                         if (([[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) &&
1039                                 (([(NSNumber *)[containedContactDict objectForKey:KEY_OBJECTID] compare:listObjectObjectID]) == 0)) {
1040                                 break;
1041                         }
1042                 }
1044         } else if ([listObject isKindOfClass:[AIListContact class]]) {
1046                 NSString        *listObjectUID = [listObject UID];
1047                 NSString        *listObjectServiceID = [[listObject service] serviceID];
1049                 while ((containedContactDict = [enumerator nextObject])) {
1050                         if ([[containedContactDict objectForKey:UID_KEY] isEqualToString:listObjectUID] &&
1051                                 [[containedContactDict objectForKey:SERVICE_ID_KEY] isEqualToString:listObjectServiceID]) {
1052                                 break;
1053                         }
1054                 }
1055         }
1057         //If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result
1058         if (containedContactDict) {
1059                 NSMutableArray          *newContainedContactsArray;
1060                 NSMutableDictionary     *newAllMetaContactsDict;
1062                 newContainedContactsArray = [containedContactsArray mutableCopy];
1063                 [newContainedContactsArray removeObjectIdenticalTo:containedContactDict];
1065                 newAllMetaContactsDict = [allMetaContactsDict mutableCopy];
1066                 [newAllMetaContactsDict setObject:newContainedContactsArray
1067                                                                    forKey:metaContactInternalObjectID];
1069                 [self _saveMetaContacts:newAllMetaContactsDict];
1071                 [newContainedContactsArray release];
1072                 [newAllMetaContactsDict release];
1073         }
1075         //The listObject can be within the metaContact without us finding a containedContactDict if we are removing multiple
1076         //listContacts referring to the same UID & serviceID combination - that is, on multiple accounts on the same service.
1077         //We therefore request removal of the object regardless of the if (containedContactDict) check above.
1078         [metaContact removeObject:listObject];
1083  * @brief Groups UIDs for services into a single metacontact
1085  * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to
1086  * a UID and a service, respectively, which together define a contact which should be included in the grouping.
1088  * Assumption: This is only called after the contact list is finished loading, which occurs via
1089  * -(void)controllerDidLoad above.
1091  * @param UIDsArray NSArray of UIDs
1092  * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
1093  */
1094 - (AIMetaContact *)groupUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray
1096         NSMutableSet    *internalObjectIDs = [[NSMutableSet alloc] init];
1097         AIMetaContact   *metaContact = nil;
1098         NSEnumerator    *enumerator;
1099         NSString                *internalObjectID;
1100         int                             count = [UIDsArray count];
1101         int                             i;
1102                                 
1103         //Build an array of all contacts matching this description (multiple accounts on the same service listing
1104         //the same UID mean that we can have multiple AIListContact objects with a UID/service combination)
1105         for (i = 0; i < count; i++) {
1106                 NSString        *serviceID = [servicesArray objectAtIndex:i];
1107                 NSString        *UID = [UIDsArray objectAtIndex:i];
1108                 
1109                 internalObjectID = [AIListObject internalObjectIDForServiceID:serviceID
1110                                                                                                                                   UID:UID];
1111                 if(!metaContact) {
1112                         metaContact = [contactToMetaContactLookupDict objectForKey:internalObjectID];
1113                 }
1114                 
1115                 [internalObjectIDs addObject:internalObjectID];
1116         }
1117         
1118         //Create a new metaContact is we didn't find one.
1119         if (!metaContact) {
1120                 metaContact = [self metaContactWithObjectID:nil];
1121         }
1122         
1123         enumerator = [internalObjectIDs objectEnumerator];
1124         while ((internalObjectID = [enumerator nextObject])) {
1125                 AIListObject    *existingObject;
1126                 if ((existingObject = [self existingListObjectWithUniqueID:internalObjectID])) {
1127                         /* If there is currently an object (or multiple objects) matching this internalObjectID
1128                          * we should add immediately.
1129                          */
1130                         [self addListObject:existingObject
1131                                   toMetaContact:metaContact];   
1132                 } else {
1133                         /* If no objects matching this internalObjectID exist, we can simply add to the 
1134                          * contactToMetaContactLookupDict for use if such an object is created later.
1135                          */
1136                         [contactToMetaContactLookupDict setObject:metaContact
1137                                                                                            forKey:internalObjectID];                    
1138                 }
1139         }
1140         [internalObjectIDs release];
1142         return metaContact;
1145 /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added.
1147  * This will reuse an existing metacontact (for one of the contacts in the array) if possible.
1148  * @param contactsToGroupArray Contacts to group together
1149  */
1150 - (AIMetaContact *)groupListContacts:(NSArray *)contactsToGroupArray
1152         NSEnumerator    *enumerator;
1153         AIListContact   *listContact;
1154         AIMetaContact   *metaContact = nil;
1156         //Look for an existing MetaContact we can use.  The first one we find is the lucky winner.
1157         enumerator = [contactsToGroupArray objectEnumerator];
1158         while ((listContact = [enumerator nextObject]) && (metaContact == nil)) {
1159                 if ([listContact isKindOfClass:[AIMetaContact class]]) {
1160                         metaContact = (AIMetaContact *)listContact;
1161                 } else {
1162                         metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]];
1163                 }
1164         }
1166         //Create a new metaContact is we didn't find one.
1167         if (!metaContact) {
1168                 metaContact = [self metaContactWithObjectID:nil];
1169         }
1170         
1171         /* Add all these contacts to our MetaContact.
1172                 * Some may already be present, but that's fine, as nothing will happen.
1173                 */
1174         enumerator = [contactsToGroupArray objectEnumerator];
1175         while ((listContact = [enumerator nextObject])) {
1176                 [self addListObject:listContact toMetaContact:metaContact];
1177         }
1178         
1179         return metaContact;
1182 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact
1184         //Remove the objects within it from being inside it
1185         NSArray                                                         *containedObjects = [[metaContact containedObjects] copy];
1186         NSEnumerator                                            *metaEnumerator = [containedObjects objectEnumerator];
1187         AIListObject<AIContainingObject>        *containingObject = [metaContact containingObject];
1188         AIListObject                                            *object;
1190         NSMutableDictionary *allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1191                                                                                                                                                                                  group:PREF_GROUP_CONTACT_LIST] mutableCopy];
1193         while ((object = [metaEnumerator nextObject])) {
1195                 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1196                 [contactToMetaContactLookupDict removeObjectForKey:[object internalObjectID]];
1198                 [self removeListObject:object fromMetaContact:metaContact];
1199         }
1201         //Then, procede to remove the metaContact
1203         //Protect!
1204         [metaContact retain];
1206         //Remove it from its containing group
1207         [containingObject removeObject:metaContact];
1209         NSString        *metaContactInternalObjectID = [metaContact internalObjectID];
1211         //Remove our reference to it internally
1212         [metaContactDict removeObjectForKey:metaContactInternalObjectID];
1214         //Remove it from the preferences dictionary
1215         [allMetaContactsDict removeObjectForKey:metaContactInternalObjectID];
1217         //XXX - contactToMetaContactLookupDict
1219         //Post the list changed notification for the old containingObject
1220         [self _listChangedGroup:containingObject object:metaContact];
1222         //Save the updated allMetaContactsDict which no longer lists the metaContact
1223         [self _saveMetaContacts:allMetaContactsDict];
1225         //Protection is overrated.
1226         [metaContact release];
1227         [containedObjects release];
1228         [allMetaContactsDict release];
1231 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict
1233         AILog(@"MetaContacts: Saving!");
1234         [[adium preferenceController] setPreference:allMetaContactsDict
1235                                                                                  forKey:KEY_METACONTACT_OWNERSHIP
1236                                                                                   group:PREF_GROUP_CONTACT_LIST];
1237         [[adium preferenceController] setPreference:[allMetaContactsDict allKeys]
1238                                                                                  forKey:KEY_FLAT_METACONTACTS
1239                                                                                   group:PREF_GROUP_CONTACT_LIST];
1242 //Sort list objects alphabetically by their display name
1243 int contactDisplayNameSort(AIListObject *objectA, AIListObject *objectB, void *context)
1245         return [[objectA displayName] caseInsensitiveCompare:[objectB displayName]];
1248 //Return either the highest metaContact containing this list object, or the list object itself.  Appropriate for when
1249 //preferences should be read from/to the most generalized contact possible.
1250 - (AIListObject *)parentContactForListObject:(AIListObject *)listObject
1252         if ([listObject isKindOfClass:[AIListContact class]]) {
1253                 //Find the highest-up metaContact
1254                 AIListObject    *containingObject;
1255                 while ([(containingObject = [listObject containingObject]) isKindOfClass:[AIMetaContact class]]) {
1256                         listObject = (AIMetaContact *)containingObject;
1257                 }
1258         }
1260         return listObject;
1263 #pragma mark Preference observing
1265 * @brief Preferences changed
1266  */
1267 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
1268                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
1270         if (firstTime ||
1271                 [key isEqualToString:KEY_USE_OFFLINE_GROUP]) {
1273                 [self setUseOfflineGroup:[[prefDict objectForKey:KEY_USE_OFFLINE_GROUP] boolValue]];
1274         }
1277 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1279         if (!inModifiedKeys ||
1280                 [inModifiedKeys containsObject:@"Online"]) {
1282                 if ([inObject isKindOfClass:[AIListContact class]]) {                   
1283                         //If this contact is not its own parent contact, don't bother since we'll get an update for the parent if appropriate
1284                         if (inObject == [(AIListContact *)inObject parentContact]) {
1285                                 if (useOfflineGroup) {
1286                                         AIListObject *containingObject = [inObject containingObject];
1287                                         
1288                                         if ([inObject online] &&
1289                                                 (containingObject == [self offlineGroup])) {
1290                                                 [(AIListContact *)inObject restoreGrouping];
1291                                                 
1292                                         } else if (![inObject online] &&
1293                                                            containingObject &&
1294                                                            (containingObject != [self offlineGroup])) {
1295                                                 [self _moveContactLocally:(AIListContact *)inObject
1296                                                                                   toGroup:[self offlineGroup]];
1297                                         }
1298                                         
1299                                 } else {
1300                                         if ([inObject containingObject] == [self offlineGroup]) {
1301                                                 [(AIListContact *)inObject restoreGrouping];
1302                                         }
1303                                 }
1304                         }
1305                 }
1306         }
1307         
1308         return nil;
1311 //Contact Sorting --------------------------------------------------------------------------------
1312 #pragma mark Contact Sorting
1313 //Register sorting code
1314 - (void)registerListSortController:(AISortController *)inController
1316     [sortControllerArray addObject:inController];
1318 - (NSArray *)sortControllerArray
1320     return sortControllerArray;
1323 //Set and get the active sort controller
1324 - (void)setActiveSortController:(AISortController *)inController
1326     activeSortController = inController;
1328         [activeSortController didBecomeActive];
1330         //The newly-active sort controller needs to know whether it should be forced to ignore groups
1331         [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
1333     //Resort the list
1334     [self sortContactList];
1336 - (AISortController *)activeSortController
1338     return activeSortController;
1341 //Sort the entire contact list
1342 - (void)sortContactList
1344     [contactList sortGroupAndSubGroups:YES sortController:activeSortController];
1345         [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:nil];
1348 //Sort an individual object
1349 - (void)sortListObject:(AIListObject *)inObject
1351         if (updatesAreDelayed) {
1352                 delayedContactChanges++;
1353         } else {
1354                 AIListObject            *group = [inObject containingObject];
1356                 if ([group isKindOfClass:[AIListGroup class]]) {
1357                         //Sort the groups containing this object
1358                         [(AIListGroup *)group sortListObject:inObject sortController:activeSortController];
1359                         [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:inObject];
1360                 }
1361         }
1364 //List object observers ------------------------------------------------------------------------------------------------
1365 #pragma mark List object observers
1366 //Registers code to observe handle status changes
1367 - (void)registerListObjectObserver:(id <AIListObjectObserver>)inObserver
1369         //Add the observer
1370     [contactObservers addObject:[NSValue valueWithNonretainedObject:inObserver]];
1372     //Let the new observer process all existing objects
1373         [self updateAllListObjectsForObserver:inObserver];
1376 - (void)unregisterListObjectObserver:(id)inObserver
1378     [contactObservers removeObject:[NSValue valueWithNonretainedObject:inObserver]];
1383  * @brief Update all contacts for an observer, notifying the observer of each one in turn
1385  * @param contacts The contacts to update, or nil to update all contacts
1386  * @param inObserver The observer
1387  */
1388 - (void)updateContacts:(NSSet *)contacts forObserver:(id <AIListObjectObserver>)inObserver
1390         NSEnumerator    *enumerator;
1391         AIListObject    *listObject;
1393         [self delayListObjectNotifications];
1394         
1395         enumerator = (contacts ? [contacts objectEnumerator] : [contactDict objectEnumerator]);
1396         while ((listObject = [enumerator nextObject])) {
1397                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1398                 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1399                 
1400                 //If this contact is within a meta contact, update the meta contact too
1401                 AIListObject    *containingObject = [listObject containingObject];
1402                 if (containingObject && [containingObject isKindOfClass:[AIMetaContact class]]) {
1403                         NSSet   *attributes = [inObserver updateListObject:containingObject
1404                                                                                                                   keys:nil
1405                                                                                                                 silent:YES];
1406                         if (attributes) [self listObjectAttributesChanged:containingObject
1407                                                                                                  modifiedKeys:attributes];
1408                 }
1409         }
1410         
1411         [self endListObjectNotificationsDelay];
1414 //Instructs a controller to update all available list objects
1415 - (void)updateAllListObjectsForObserver:(id <AIListObjectObserver>)inObserver
1417         NSEnumerator    *enumerator;
1418         AIListObject    *listObject;
1420         [self delayListObjectNotifications];
1422         //All contacts
1423         [self updateContacts:nil forObserver:inObserver];
1425     //Reset all groups
1426         enumerator = [groupDict objectEnumerator];
1427         while ((listObject = [enumerator nextObject])) {
1428                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1429                 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1430         }
1432         //Reset all accounts
1433         enumerator = [[[adium accountController] accounts] objectEnumerator];
1434         while ((listObject = [enumerator nextObject])) {
1435                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1436                 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1437         }
1439         //
1440         [self endListObjectNotificationsDelay];
1444 //Notify observers of a status change.  Returns the modified attribute keys
1445 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
1447         NSMutableSet    *attrChange = nil;
1448         NSEnumerator    *enumerator;
1449         NSValue                 *observerValue;
1450         
1451         //Let our observers know
1452         enumerator = [contactObservers objectEnumerator];
1453         while ((observerValue = [enumerator nextObject])) {
1454                 id <AIListObjectObserver>       observer;
1455                 NSSet                                           *newKeys;
1457                 observer = [observerValue nonretainedObjectValue];
1458                 if ((newKeys = [observer updateListObject:inObject keys:modifiedKeys silent:silent])) {
1459                         if (!attrChange) attrChange = [NSMutableSet set];
1460                         [attrChange unionSet:newKeys];
1461                 }
1462         }
1464         //Send out the notification for other observers
1465         [[adium notificationCenter] postNotificationName:ListObject_StatusChanged
1466                                                                                           object:inObject
1467                                                                                         userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys
1468                                                                                                                                                                                                  forKey:@"Keys"] : nil)];
1470         return attrChange;
1473 //Command all observers to apply their attributes to an object
1474 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject
1476         NSEnumerator    *enumerator = [contactObservers objectEnumerator];
1477         NSValue                 *observerValue;
1479         while ((observerValue = [enumerator nextObject])) {
1480                 id <AIListObjectObserver>       observer;
1481                 
1482                 observer = [observerValue nonretainedObjectValue];
1484                 [observer updateListObject:inObject keys:nil silent:YES];
1485         }
1490 //Contact List Access --------------------------------------------------------------------------------------------------
1491 #pragma mark Contact List Access
1492 //Returns the main contact list group
1493 - (AIListGroup *)contactList
1495     return contactList;
1498 //Return a flat array of all the objects in a group on an account (and all subgroups, if desired)
1499 - (NSMutableArray *)allContactsInGroup:(AIListGroup *)inGroup subgroups:(BOOL)subGroups onAccount:(AIAccount *)inAccount
1501         NSMutableArray  *contactArray = [NSMutableArray array];
1502         NSEnumerator    *enumerator;
1503     AIListObject        *object;
1505         if (inGroup == nil) inGroup = contactList;  //Passing nil scans the entire contact list
1507         enumerator = [[inGroup containedObjects] objectEnumerator];
1508     while ((object = [enumerator nextObject])) {
1509         if ([object isMemberOfClass:[AIMetaContact class]] || [object isMemberOfClass:[AIListGroup class]]) {
1510             if (subGroups) {
1511                                 [contactArray addObjectsFromArray:[self allContactsInGroup:(AIListGroup *)object
1512                                                                                                                                  subgroups:subGroups
1513                                                                                                                                  onAccount:inAccount]];
1514                         }
1515                 } else if ([object isMemberOfClass:[AIListContact class]]) {
1516                         if (!inAccount ||
1517                            ([(AIListContact *)object account] == inAccount)) {
1518                                 [contactArray addObject:object];
1519                         }
1520                 }
1521         }
1523         return contactArray;
1526 //Contact List Menus- --------------------------------------------------------------------------------------------------
1527 #pragma mark Contact List Menus
1529 //Returns a menu containing all the groups within a group
1530 //- Selector called on group selection is selectGroup:
1531 //- The menu items represented object is the group it represents
1532 - (NSMenu *)menuOfAllGroupsInGroup:(AIListGroup *)inGroup withTarget:(id)target
1534         NSMenu  *menu = [[NSMenu alloc] initWithTitle:@""];
1536         [menu setAutoenablesItems:NO];
1537         [self _menuOfAllGroups:menu forGroup:inGroup withTarget:target level:0];
1539         return [menu autorelease];
1541 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level
1543         NSEnumerator    *enumerator;
1544         AIListObject    *object;
1546         //Passing nil scans the entire contact list
1547         if (group == nil) group = contactList;
1549         //Enumerate this group and process all groups we find within it
1550         enumerator = [[group containedObjects] objectEnumerator];
1551         while ((object = [enumerator nextObject])) {
1552                 if ([object isKindOfClass:[AIListGroup class]] && object != [self offlineGroup]) {
1553                         NSMenuItem      *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[object displayName]
1554                                                                                                                                                                                  target:target
1555                                                                                                                                                                                  action:@selector(selectGroup:)
1556                                                                                                                                                                   keyEquivalent:@""];
1557                         [menuItem setRepresentedObject:object];
1558                         if ([menuItem respondsToSelector:@selector(setIndentationLevel:)]) {
1559                                 [menuItem setIndentationLevel:level];
1560                         }
1561                         [menu addItem:menuItem];
1562                         [menuItem release];
1563                         
1564                         [self _menuOfAllGroups:menu forGroup:(AIListGroup *)object withTarget:target level:level+1];
1565                 }
1566         }
1570 //Returns a menu containing all the objects in a group on an account
1571 //- Selector called on contact selection is selectContact:
1572 //- The menu item's represented object is the contact it represents
1573 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target{
1574         return [self menuOfAllContactsInContainingObject:inObject withTarget:target firstLevel:YES];
1576 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target firstLevel:(BOOL)firstLevel
1578     NSEnumerator                                *enumerator;
1579     AIListObject                                *object;
1581         //Prepare our menu
1582         NSMenu *menu = [[NSMenu alloc] init];
1583         [menu setAutoenablesItems:NO];
1585         //Passing nil scans the entire contact list
1586         if (inObject == nil) inObject = contactList;
1588         //The pull down menu needs an extra item at the top of its root menu to handle the selection.
1589         if (firstLevel) [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
1591         //All menu items for all contained objects
1592         enumerator = [[inObject listContacts] objectEnumerator];
1593     while ((object = [enumerator nextObject])) {
1594                 NSImage         *menuServiceImage;
1595                 NSMenuItem      *menuItem;
1596                 BOOL            needToCreateSubmenu;
1597                 BOOL            isGroup = [object isKindOfClass:[AIListGroup class]];
1598                 BOOL            isValidGroup = (isGroup &&
1599                                                                         [[(AIListGroup *)object containedObjects] count]);
1601                 //We don't want to include empty groups
1602                 if (!isGroup || isValidGroup) {
1604                         needToCreateSubmenu = (isValidGroup ||
1605                                                                    ([object isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)object listContacts] count] > 1)));
1607                         
1608                         menuServiceImage = [AIUserIcons menuUserIconForObject:object];
1609                         
1610                         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(needToCreateSubmenu ?
1611                                                                                                                                                                         [object displayName] :
1612                                                                                                                                                                         [object formattedUID])
1613                                                                                                                                                         target:target
1614                                                                                                                                                         action:@selector(selectContact:)
1615                                                                                                                                          keyEquivalent:@""];
1616                         
1617                         if (needToCreateSubmenu) {
1618                                 [menuItem setSubmenu:[self menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)object withTarget:target firstLevel:NO]];
1619                         }
1621                         [menuItem setRepresentedObject:object];
1622                         [menuItem setImage:menuServiceImage];
1623                         [menu addItem:menuItem];
1624                         [menuItem release];
1625                 }
1626         }
1628         return [menu autorelease];
1631 //Retrieving Specific Contacts -----------------------------------------------------------------------------------------
1632 #pragma mark Retrieving Specific Contacts
1634 //Retrieve a contact from the contact list (Creating if necessary)
1635 - (AIListContact *)contactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1637         if (!(inUID && [inUID length] && inService)) return nil; //Ignore invalid requests
1638         
1639         AIListContact   *contact = nil;
1640         NSString                *key = [AIListContact internalUniqueObjectIDForService:inService
1641                                                                                                                                    account:inAccount
1642                                                                                                                                            UID:inUID];
1643         contact = [contactDict objectForKey:key];
1644         if (!contact) {
1645                 //Create
1646                 contact = [[AIListContact alloc] initWithUID:inUID account:inAccount service:inService];
1648                 //Do the update thing
1649                 [self _updateAllAttributesOfObject:contact];
1651                 //Check to see if we should add to a metaContact
1652                 AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:[contact internalObjectID]];
1653                 if (metaContact) {
1654                         /* We already know to add this object to the metaContact, since we did it before with another object,
1655                            but this particular listContact is new and needs to be added directly to the metaContact
1656                            (on future launches, the metaContact will obtain it automatically since all contacts matching this UID
1657                            and serviceID should be included). */
1658                         [self _performAddListObject:contact toMetaContact:metaContact];
1659                 }
1660                 
1661                 //Set the contact as mobile if it is a phone number
1662                 if ([inUID characterAtIndex:0] == '+') {
1663                         [contact setIsMobile:YES notify:NotifyNever];
1664                 }
1666                 //Add
1667                 [contactDict setObject:contact forKey:key];
1668                 [contact release];
1669         }
1671         return contact;
1674 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1676         if (inService && [inUID length]) {
1677                 return [contactDict objectForKey:[AIListContact internalUniqueObjectIDForService:inService
1678                                                                                                                                                                  account:inAccount
1679                                                                                                                                                                          UID:inUID]];
1680         } else {
1681                 return nil;
1682         }
1686  * @brief Return a set of all contacts with a specified UID and service
1688  * @param service The AIService in question
1689  * @param inUID The UID, which should be normalized (lower case, no spaces, etc.) as appropriate for the service
1690  * @param existingOnly If YES, only pre-existing contacts. If NO, an AIListContact is guaranteed to be returned
1691  *                                         on each compatible account, even if one did not previously exist.
1692  */
1693 - (NSSet *)allContactsWithService:(AIService *)service UID:(NSString *)inUID existingOnly:(BOOL)existingOnly
1695         NSEnumerator    *enumerator;
1696         AIAccount               *account;
1697         NSMutableSet    *returnContactSet = [NSMutableSet set];
1699         enumerator = [[[adium accountController] accountsCompatibleWithService:service] objectEnumerator];
1701         while ((account = [enumerator nextObject])) {
1702                 AIListContact   *listContact;
1703                 
1704                 if (existingOnly) {
1705                         listContact = [self existingContactWithService:service
1706                                                                                                    account:account
1707                                                                                                            UID:inUID];
1708                 } else {
1709                         listContact = [self contactWithService:service
1710                                                                                    account:account
1711                                                                                            UID:inUID];
1712                 }
1714                 if (listContact) {
1715                         [returnContactSet addObject:listContact];
1716                 }
1717         }
1719         return returnContactSet;
1722 - (AIListObject *)existingListObjectWithUniqueID:(NSString *)uniqueID
1724         NSEnumerator    *enumerator;
1725         AIListObject    *listObject;
1727         //Contact
1728         enumerator = [contactDict objectEnumerator];
1729         while ((listObject = [enumerator nextObject])) {
1730                 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1731         }
1733         //Group
1734         enumerator = [groupDict objectEnumerator];
1735         while ((listObject = [enumerator nextObject])) {
1736                 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1737         }
1739         //Metacontact
1740         enumerator = [metaContactDict objectEnumerator];
1741         while ((listObject = [enumerator nextObject])) {
1742                 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1743         }
1745         return nil;
1748 - (AIListContact *)preferredContactForContentType:(NSString *)inType forListContact:(AIListContact *)inContact
1750         AIListContact   *returnContact = nil;
1751         AIAccount               *account;
1753         if ([inContact isKindOfClass:[AIMetaContact class]]) {
1754                 AIListObject    *preferredContact;
1755                 NSString                *internalObjectID;
1757                 /* If we've messaged this object previously, prefer the last contact we sent to if that
1758                  * contact is currently in the most-available status the metacontact can offer
1759                  */
1760         internalObjectID = [inContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1761                                                                                                  group:OBJECT_STATUS_CACHE];
1763         if ((internalObjectID) &&
1764                    (preferredContact = [self existingListObjectWithUniqueID:internalObjectID]) &&
1765                    ([preferredContact isKindOfClass:[AIListContact class]]) &&
1766                    ([preferredContact statusSummary] == [inContact statusSummary]) &&
1767                         ([[(AIMetaContact *)inContact containedObjects] containsObject:preferredContact])) {
1768                         returnContact = [self preferredContactForContentType:inType
1769                                                                                                   forListContact:(AIListContact *)preferredContact];
1770         }
1772                 //If the last contact we sent to is not appropriate, use the metaContact's preferredContact
1773                 if (!returnContact) {
1774                         //Recurse into metacontacts if necessary
1775                         AIListContact *firstAvailableContact = nil;
1776                         AIListContact *firstNotOfflineContact = nil;
1778                         AIListContact *thisContact;
1779                         NSEnumerator *contactsEnum = [[(AIMetaContact *)inContact containedObjects] objectEnumerator];
1780                         while ((thisContact = [contactsEnum nextObject])) {
1781                                 AIStatusType statusSummary = [thisContact statusSummary];
1783                                 if (statusSummary != AIOfflineStatus) {
1784                                         if (!firstNotOfflineContact) {
1785                                                 firstNotOfflineContact = thisContact;
1786                                         }
1788                                         if (statusSummary == AIAvailableStatus && ![thisContact isMobile]) {
1789                                                 if (!firstAvailableContact) {
1790                                                         firstAvailableContact = thisContact;
1791                                                 }
1793                                                 break;
1794                                         }
1795                                 }
1796                         }
1798                         returnContact = (firstAvailableContact ?
1799                                                          firstAvailableContact :
1800                                                          (firstNotOfflineContact ? firstNotOfflineContact : [(AIMetaContact *)inContact preferredContact]));
1801                 }
1803         } else {
1804                 //We have a flat contact; find the best account for talking to this contact,
1805                 //and return an AIListContact on that account
1806                 account = [[adium accountController] preferredAccountForSendingContentType:inType
1807                                                                                                                                                  toContact:inContact];
1808                 if (account) {
1809                         if ([inContact account] == account) {
1810                                 returnContact = inContact;
1811                         } else {
1812                                 returnContact = [self contactWithService:[inContact service]
1813                                                                                                  account:account
1814                                                                                                          UID:[inContact UID]];
1815                         }
1816                 }
1817         }
1819         return returnContact;
1822 //Retrieve a list contact matching the UID and serviceID of the passed contact but on the specified account.
1823 //In many cases this will be the same as inContact.
1824 - (AIListContact *)contactOnAccount:(AIAccount *)account fromListContact:(AIListContact *)inContact
1826         if (account && ([inContact account] != account)) {
1827                 return [self contactWithService:[inContact service] account:account UID:[inContact UID]];
1828         } else {
1829                 return inContact;
1830         }
1833 //XXX - This is ridiculous.
1834 - (AIListContact *)preferredContactWithUID:(NSString *)inUID andServiceID:(NSString *)inService forSendingContentType:(NSString *)inType
1836         AIService               *theService = [[adium accountController] firstServiceWithServiceID:inService];
1837         AIListContact   *tempListContact = [[AIListContact alloc] initWithUID:inUID
1838                                                                                                                                 service:theService];
1839         AIAccount               *account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
1840                                                                                                                                                                           toContact:tempListContact
1841                                                                                                                                                                  includeOffline:YES];
1842         [tempListContact release];
1844         return [self contactWithService:theService account:account UID:inUID];
1849  * @brief Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts
1851  * If the destination contact's parent contact differs from the destination contact itself, the chat is with a metaContact.
1852  * If that metaContact's preferred destination for messaging isn't the same as the contact which was just messaged,
1853  * update the preference so that a new chat with this metaContact would default to the proper contact.
1854  */
1855 - (void)didSendContent:(NSNotification *)notification
1857     AIChat                      *chat = [[notification userInfo] objectForKey:@"AIChat"];
1858     AIListContact       *destContact = [chat listObject];
1859     AIListContact       *metaContact;
1861     if (((metaContact = [destContact parentContact]) != destContact)) {
1862                 NSString        *destinationInternalObjectID = [destContact internalObjectID];
1863                 NSString        *currentPreferredDestination = [metaContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1864                                                                                                                                                    group:OBJECT_STATUS_CACHE];
1866                 if (![destinationInternalObjectID isEqualToString:currentPreferredDestination]) {
1867                         [metaContact setPreference:destinationInternalObjectID
1868                                                                 forKey:KEY_PREFERRED_DESTINATION_CONTACT
1869                                                                  group:OBJECT_STATUS_CACHE];
1870                 }
1871     }
1874 //Retrieving Groups ----------------------------------------------------------------------------------------------------
1875 #pragma mark Retrieving Groups
1877 //Retrieve a group from the contact list (Creating if necessary)
1878 - (AIListGroup *)groupWithUID:(NSString *)groupUID
1880         AIListGroup             *group;
1882         if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME]) {
1883                 //Return our root group if it is requested
1884                 group = contactList;
1885         } else {
1886                 if (!(group = [groupDict objectForKey:groupUID])) {
1887                         //Create
1888                         group = [[AIListGroup alloc] initWithUID:groupUID];
1890                         //Add
1891                         [self _updateAllAttributesOfObject:group];
1892                         [groupDict setObject:group forKey:groupUID];
1894                         //Add to the contact list
1895                         [contactList addObject:group];
1896                         [self _listChangedGroup:contactList object:group];
1897                         [group release];
1898                 }
1899         }
1901         return group;
1904 - (AIListGroup *)existingGroupWithUID:(NSString *)groupUID
1906         AIListGroup             *group;
1908         if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME]) {
1909                 //Return our root group if it is requested
1910                 group = contactList;
1911         } else {
1912                 group = [groupDict objectForKey:groupUID];
1913         }
1914         
1915         return group;
1918 //Contact list editing -------------------------------------------------------------------------------------------------
1919 #pragma mark Contact list editing
1920 - (void)removeListObjects:(NSArray *)objectArray
1922         NSEnumerator    *enumerator = [objectArray objectEnumerator];
1923         AIListObject    *listObject;
1925         while ((listObject = [enumerator nextObject])) {
1926                 if ([listObject isKindOfClass:[AIMetaContact class]]) {
1927                         NSSet   *objectsToRemove = nil;
1929                         //If the metaContact only has one listContact, we will remove that contact from all accounts
1930                         if ([[(AIMetaContact *)listObject listContacts] count] == 1) {
1931                                 AIListContact   *listContact = [[(AIMetaContact *)listObject listContacts] objectAtIndex:0];
1932                                 
1933                                 objectsToRemove = [self allContactsWithService:[listContact service]
1934                                                                                                                    UID:[listContact UID]
1935                                                                                                   existingOnly:YES];
1936                         }
1938                         //And actually remove the single contact if applicable
1939                         if (objectsToRemove) {
1940                                 [self removeListObjects:[objectsToRemove allObjects]];
1941                         }
1942                         
1943                         //Now break the metaContact down, taking out all contacts and putting them back in the main list
1944                         [self breakdownAndRemoveMetaContact:(AIMetaContact *)listObject];                               
1946                 } else if ([listObject isKindOfClass:[AIListGroup class]]) {
1947                         AIListObject    *containingObject = [listObject containingObject];
1948                         NSEnumerator    *enumerator;
1949                         AIAccount               *account;
1951                         //If this is a group, delete all the objects within it
1952                         [self removeListObjects:[(AIListGroup *)listObject containedObjects]];
1954                         //Delete the list off of all active accounts
1955                         enumerator = [[[adium accountController] accounts] objectEnumerator];
1956                         while ((account = [enumerator nextObject])) {
1957                                 if ([account online]) {
1958                                         [account deleteGroup:(AIListGroup *)listObject];
1959                                 }
1960                         }
1962                         //Then, procede to delete the group
1963                         [listObject retain];
1964                         [(AIMetaContact *)containingObject removeObject:listObject];
1965                         [groupDict removeObjectForKey:[listObject UID]];
1966                         [self _listChangedGroup:containingObject object:listObject];
1967                         [listObject release];
1969                 } else {
1970                         AIAccount       *account = [(AIListContact *)listObject account];
1971                         if ([account online]) {
1972                                 [account removeContacts:[NSArray arrayWithObject:listObject]];
1973                         }
1974                 }
1975         }
1978 - (void)addContacts:(NSArray *)contactArray toGroup:(AIListGroup *)group
1980         NSEnumerator    *enumerator;
1981         AIListContact   *listObject;
1983         [self delayListObjectNotifications];
1985         enumerator = [contactArray objectEnumerator];
1986         while ((listObject = [enumerator nextObject])) {
1987                 if(![group containsObject:listObject]) //don't add it if it's already there.
1988                         [[listObject account] addContacts:[NSArray arrayWithObject:listObject] toGroup:group];
1989         }
1991         [self endListObjectNotificationsDelay];
1994 - (void)requestAddContactWithUID:(NSString *)contactUID service:(AIService *)inService
1996         NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:contactUID, UID_KEY, inService, @"service",nil];
1997         [[adium notificationCenter] postNotificationName:Contact_AddNewContact
1998                                                                                           object:nil
1999                                                                                         userInfo:userInfo];
2002 - (void)moveListObjects:(NSArray *)objectArray toGroup:(AIListObject<AIContainingObject> *)group index:(int)index
2004         NSEnumerator    *enumerator;
2005         AIListContact   *listContact;
2007         [self delayListObjectNotifications];
2009         if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2010                 [(id)group setDelayContainedObjectSorting:YES];
2011         }
2013         enumerator = [objectArray objectEnumerator];
2014         while ((listContact = [enumerator nextObject])) {
2015                 [self moveContact:listContact toGroup:group];
2017                 //Set the new index / position of the object
2018                 [self _positionObject:listContact atIndex:index inGroup:group];
2019         }
2021         [self endListObjectNotificationsDelay];
2023         if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2024                 [(id)group setDelayContainedObjectSorting:NO];
2025         }
2027         /*
2028          Resort the entire list if we are moving within or between AIListGroup objects
2029          (other containing objects such as metaContacts will handle their own sorting).
2030         */
2031         if ([group isKindOfClass:[AIListGroup class]]) {
2032                 [self sortContactList];
2033         }
2036 - (void)moveContact:(AIListContact *)listContact toGroup:(AIListObject<AIContainingObject> *)group
2038         //Move the object to the new group only if necessary
2039         if (group != [listContact containingObject]) {
2040                 if ([group isKindOfClass:[AIListGroup class]]) {
2041                         //Move a contact into a new group
2042                         if ([listContact isKindOfClass:[AIMetaContact class]]) {
2043                                 //Move the meta contact to this new group
2044                                 [self _moveContactLocally:listContact toGroup:(AIListGroup *)group];
2046                                 NSEnumerator    *enumerator;
2047                                 AIListContact   *actualListContact;
2049                                 //This is a meta contact, move the objects within it.  listContacts will give us a flat array of AIListContacts.
2050                                 enumerator = [[(AIMetaContact *)listContact listContacts] objectEnumerator];
2051                                 while ((actualListContact = [enumerator nextObject])) {
2052                                         //Only move the contact if it is actually listed on the account in question
2053                                         if (![actualListContact isStranger]) {
2054                                                 [self _moveObjectServerside:actualListContact toGroup:(AIListGroup *)group];
2055                                         }
2056                                 }
2058                         } else if ([listContact isKindOfClass:[AIListContact class]]) {
2059                                 //Move the object
2060                                 [self _moveObjectServerside:listContact toGroup:(AIListGroup *)group];
2061                         }
2063                 } else if ([group isKindOfClass:[AIMetaContact class]]) {
2064                         //Moving a contact into a meta contact
2065                         [self addListObject:listContact toMetaContact:(AIMetaContact *)group];
2066                 }
2067         }
2070 //Move an object to another group
2071 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group
2073         AIAccount       *account = [(AIListContact *)listObject account];
2074         if ([account online]) {
2075                 [account moveListObjects:[NSArray arrayWithObject:listObject] toGroup:group];
2076         }
2079 //Rename a group
2080 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName
2082         NSEnumerator    *enumerator = [[[adium accountController] accounts] objectEnumerator];
2083         AIAccount               *account;
2085         //Since Adium has no memory of what accounts a group is on, we have to send this message to all available accounts
2086         //The accounts without this group will just ignore it
2087         while ((account = [enumerator nextObject])) {
2088                 [account renameGroup:listGroup to:newName];
2089         }
2091         //Remove the old group if it's empty
2092         if ([listGroup containedObjectsCount] == 0) {
2093                 [self removeListObjects:[NSArray arrayWithObject:listGroup]];
2094         }
2097 //Position a list object within a group
2098 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inGroup:(AIListObject<AIContainingObject> *)group
2100         if (index == 0) {
2101                 //Moved to the top of a group.  New index is between 0 and the lowest current index
2102                 [listObject setOrderIndex:([group smallestOrder] / 2.0)];
2104         } else if (index >= [group visibleCount]) {
2105                 //Moved to the bottom of a group.  New index is one higher than the highest current index
2106                 [listObject setOrderIndex:([group largestOrder] + 1.0)];
2108         } else {
2109                 //Moved somewhere in the middle.  New index is the average of the next largest and smallest index
2110                 AIListObject    *previousObject = [group objectAtIndex:index-1];
2111                 AIListObject    *nextObject = [group objectAtIndex:index];
2112                 float nextLowest = [previousObject orderIndex];
2113                 float nextHighest = [nextObject orderIndex];
2115                 //
2116                 [listObject setOrderIndex:((nextHighest + nextLowest) / 2.0)];
2117         }
2120 #pragma mark Authorization
2121 - (id)showAuthorizationRequestWithDict:(NSDictionary *)inDict forAccount:(AIAccount *)inAccount
2123         return [adiumAuthorization showAuthorizationRequestWithDict:inDict forAccount:inAccount];
2126 @end