Merged [21073]: When encoding an icon to JPEG, start off at 100% quality. If that...
[adiumx.git] / Source / AIContactController.m
blob8786baac3d400176943568aa067b21bf547bea7f
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/AILoginControllerProtocol.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 inObject:(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 @end
117 @implementation AIContactController
119 //init
120 - (id)init
122         if ((self = [super init])) {
123                 //
124                 contactObservers = [[NSMutableSet alloc] init];
125                 sortControllerArray = [[NSMutableArray alloc] init];
126                 activeSortController = nil;
127                 delayedStatusChanges = 0;
128                 delayedModifiedStatusKeys = [[NSMutableSet alloc] init];
129                 delayedAttributeChanges = 0;
130                 delayedModifiedAttributeKeys = [[NSMutableSet alloc] init];
131                 delayedContactChanges = 0;
132                 delayedUpdateRequests = 0;
133                 updatesAreDelayed = NO;
134                 
135                 //
136                 contactDict = [[NSMutableDictionary alloc] init];
137                 groupDict = [[NSMutableDictionary alloc] init];
138                 metaContactDict = [[NSMutableDictionary alloc] init];
139                 contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
140         }
141         
142         return self;
145 //finish initing
146 - (void)controllerDidLoad
147 {       
148         //Default contact preferences
149         [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:CONTACT_DEFAULT_PREFS
150                                                                                                                                                 forClass:[self class]]
151                                                                                   forGroup:PREF_GROUP_CONTACT_LIST];
152         
153         contactList = [[AIListGroup alloc] initWithUID:ADIUM_ROOT_GROUP_NAME];
154         //Root is always "expanded"
155         [contactList setExpanded:YES];
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                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
263                 NSNumber *objectID = [NSNumber numberWithInt:[[[identifier componentsSeparatedByString:@"-"] objectAtIndex:1] intValue]];
264                 [self metaContactWithObjectID:objectID];
265                 [pool release];
266         }
269 //Flattened array of the contact list content
270 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects
272         NSMutableArray  *array = [NSMutableArray array];
273         NSEnumerator    *enumerator = [listObjects objectEnumerator];;
274         AIListObject    *object;
276         //Create temporary strings outside the loop
277         NSString        *Group = @"Group";
278         NSString        *Type = @"Type";
279         NSString        *Expanded = @"Expanded";
281         while ((object = [enumerator nextObject])) {
282                         [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
283                                 Group, Type,
284                                 [object UID], UID_KEY,
285                                 [NSNumber numberWithBool:[(AIListGroup *)object isExpanded]], Expanded,
286                                 nil]];
287         }
289         return array;
293 //Status and Display updates -------------------------------------------------------------------------------------------
294 #pragma mark Status and Display updates
295 //These delay Contact_ListChanged, ListObject_AttributesChanged, Contact_OrderChanged notificationsDelays,
296 //sorting and redrawing to prevent redundancy when making a large number of changes
297 //Explicit delay.  Call endListObjectNotificationsDelay to end
298 - (void)delayListObjectNotifications
300         delayedUpdateRequests++;
301         updatesAreDelayed = YES;
304 //End an explicit delay
305 - (void)endListObjectNotificationsDelay
307         delayedUpdateRequests--;
308         if (delayedUpdateRequests == 0 && !delayedUpdateTimer) {
309                 [self _performDelayedUpdates:nil];
310         }
313 //Delay all list object notifications until a period of inactivity occurs.  This is useful for accounts that do not
314 //know when they have finished connecting but still want to mute events.
315 - (void)delayListObjectNotificationsUntilInactivity
317     if (!delayedUpdateTimer) {
318                 updatesAreDelayed = YES;
319                 delayedUpdateTimer = [[NSTimer scheduledTimerWithTimeInterval:UPDATE_CLUMP_INTERVAL
320                                                                                                                            target:self
321                                                                                                                          selector:@selector(_performDelayedUpdates:)
322                                                                                                                          userInfo:nil
323                                                                                                                           repeats:YES] retain];
324     } else {
325                 //Reset the timer
326                 [delayedUpdateTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:UPDATE_CLUMP_INTERVAL]];
327         }
330 //Update the status of a list object.  This will update any information that is otherwise too expensive to update
331 //automatically, such as their profile.
332 - (void)updateListContactStatus:(AIListContact *)inContact
334         //If we're handed something that can contain other contacts, update the status of the contacts contained within it
335         if ([inContact conformsToProtocol:@protocol(AIContainingObject)]) {
336                 NSEnumerator    *enumerator = [[(AIListObject<AIContainingObject> *)inContact listContacts] objectEnumerator];
337                 AIListContact   *contact;
339                 while ((contact = [enumerator nextObject])) {
340                         [self updateListContactStatus:contact];
341                 }
343         } else {
344                 AIAccount *account = [inContact account];
345                 if (![account online]) {
346                         account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
347                                                                                                                                                          toContact:inContact];
348                 }
350                 [account updateContactStatus:inContact];
351         }
354 //Called after modifying a contact's status
355 // Silent: Silences all events, notifications, sounds, overlays, etc. that would have been associated with this status change
356 - (void)listObjectStatusChanged:(AIListObject *)inObject modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
358     NSSet                       *modifiedAttributeKeys;
360     //Let all observers know the contact's status has changed before performing any sorting or further notifications
361         modifiedAttributeKeys = [self _informObserversOfObjectStatusChange:inObject withKeys:inModifiedKeys silent:silent];
363     //Resort the contact list
364         if (updatesAreDelayed) {
365                 delayedStatusChanges++;
366                 [delayedModifiedStatusKeys unionSet:inModifiedKeys];
367         } else {
368                 //We can safely skip sorting if we know the modified attributes will invoke a resort later
369                 if (![[self activeSortController] shouldSortForModifiedAttributeKeys:modifiedAttributeKeys] &&
370                    [[self activeSortController] shouldSortForModifiedStatusKeys:inModifiedKeys]) {
371                         [self sortListObject:inObject];
372                 }
373         }
375     //Post an attributes changed message (if necessary)
376     if ([modifiedAttributeKeys count]) {
377                 [self listObjectAttributesChanged:inObject modifiedKeys:modifiedAttributeKeys];
378     }
381 //Call after modifying an object's display attributes
382 //(When modifying display attributes in response to a status change, this is not necessary)
383 - (void)listObjectAttributesChanged:(AIListObject *)inObject modifiedKeys:(NSSet *)inModifiedKeys
385         if (updatesAreDelayed) {
386                 delayedAttributeChanges++;
387                 [delayedModifiedAttributeKeys unionSet:inModifiedKeys];
388         } else {
389         //Resort the contact list if necessary
390         if ([[self activeSortController] shouldSortForModifiedAttributeKeys:inModifiedKeys]) {
391                         [self sortListObject:inObject];
392         }
394         //Post an attributes changed message
395                 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
396                                                                                                   object:inObject
397                                                                                                 userInfo:(inModifiedKeys ?
398                                                                                                                   [NSDictionary dictionaryWithObject:inModifiedKeys
399                                                                                                                                                                           forKey:@"Keys"] :
400                                                                                                                   nil)];
401         }
404 //Performs any delayed list object/handle updates
405 - (void)_performDelayedUpdates:(NSTimer *)timer
407         BOOL    updatesOccured = (delayedStatusChanges || delayedAttributeChanges || delayedContactChanges);
409         //Send out global attribute & status changed notifications (to cover any delayed updates)
410         if (updatesOccured) {
411                 BOOL shouldSort = NO;
413                 //Inform observers of any changes
414                 if (delayedContactChanges) {
415 //                      [[adium notificationCenter] postNotificationName:Contact_ListChanged object:nil];
416                         delayedContactChanges = 0;
417                         shouldSort = YES;
418                 }
419                 if (delayedStatusChanges) {
420                         if (!shouldSort &&
421                            [[self activeSortController] shouldSortForModifiedStatusKeys:delayedModifiedStatusKeys]) {
422                                 shouldSort = YES;
423                         }
424                         [delayedModifiedStatusKeys removeAllObjects];
425                         delayedStatusChanges = 0;
426                 }
427                 if (delayedAttributeChanges) {
428                         if (!shouldSort &&
429                            [[self activeSortController] shouldSortForModifiedAttributeKeys:delayedModifiedAttributeKeys]) {
430                                 shouldSort = YES;
431                         }
432                         [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
433                                                                                                           object:nil
434                                                                                                         userInfo:(delayedModifiedAttributeKeys ?
435                                                                                                                           [NSDictionary dictionaryWithObject:delayedModifiedAttributeKeys
436                                                                                                                                                                                   forKey:@"Keys"] :
437                                                                                                                           nil)];
438                         [delayedModifiedAttributeKeys removeAllObjects];
439                         delayedAttributeChanges = 0;
440                 }
442                 //Sort only if necessary
443                 if (shouldSort) {
444                         [self sortContactList];
445                 }
446         }
448     //If no more updates are left to process, disable the update timer
449         //If there are no delayed update requests, remove the hold
450         if (!delayedUpdateTimer || !updatesOccured) {
451                 if (delayedUpdateTimer) {
452                         [delayedUpdateTimer invalidate];
453                         [delayedUpdateTimer release];
454                         delayedUpdateTimer = nil;
455                 }
456                 if (delayedUpdateRequests == 0) {
457                         updatesAreDelayed = NO;
458                 }
459     }
462 #pragma mark Contact Grouping
463 //Contact Grouping -----------------------------------------------------------------------------------------------------
465 //Redetermine the local grouping of a contact in response to server grouping information or an external change
466 - (void)listObjectRemoteGroupingChanged:(AIListContact *)inContact
468         AIListObject<AIContainingObject>        *containingObject;
469         NSString                                                        *remoteGroupName = [inContact remoteGroupName];
471         [inContact retain];
473         containingObject = [inContact containingObject];
475         if ([containingObject isKindOfClass:[AIMetaContact class]]) {
477                 /* If inContact's containingObject is a metaContact, and that metaContact has no containingObject,
478                  * use inContact's remote grouping as the metaContact's grouping.
479                  */
480                 if (![containingObject containingObject] && [remoteGroupName length]) {
481                         //If no similar objects exist, we add this contact directly to the list
482                         //Create a group for the contact even if contact list groups aren't on,
483                         //otherwise requests for all the contact list groups will return nothing
484                         AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
486                         localGroup = (useContactListGroups ?
487                                                   ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
488                                                   contactList);
490                         [localGroup addObject:containingObject];
491                         [self _listChangedGroup:localGroup object:containingObject];
492 //                      AILog(@"listObjectRemoteGroupingChanged: %@ is in %@, which was moved to %@",inContact,containingObject,localGroup);
493                 }
495         } else {
496                 //If we have a remoteGroupName, add the contact locally to the list
497                 if (remoteGroupName) {
498                         //Create a group for the contact even if contact list groups aren't on,
499                         //otherwise requests for all the contact list groups will return nothing
500                         AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
502                         localGroup = (useContactListGroups ?
503                                                   ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
504                                                   contactList);
506 //                      AILog(@"listObjectRemoteGroupingChanged: %@: remoteGroupName %@ --> %@",inContact,remoteGroupName,localGroup);
508                         [self _moveContactLocally:inContact
509                                                           toGroup:localGroup];
511                 } else {
512                         //If !remoteGroupName, remove the contact from any local groups
513                         if (containingObject) {
514                                 //Remove the object
515                                 [(AIListGroup *)containingObject removeObject:inContact];
517                                 [self _listChangedGroup:(AIListGroup *)containingObject object:inContact];
518                                 
519 //                              AILog(@"listObjectRemoteGroupingChanged: %@: -- !remoteGroupName so removed from %@",inContact,containingObject);
520                         }
521                 }
522         }
524         BOOL    isCurrentlyAStranger = [inContact isStranger];
525         if ((isCurrentlyAStranger && (remoteGroupName != nil)) ||
526            (!isCurrentlyAStranger && (remoteGroupName == nil))) {
527                 [inContact setStatusObject:(remoteGroupName ? [NSNumber numberWithBool:YES] : nil)
528                                                         forKey:@"NotAStranger"
529                                                         notify:NotifyLater];
530                 [inContact notifyOfChangedStatusSilently:YES];
531         }
533         [inContact release];
536 - (void)_moveContactLocally:(AIListContact *)listContact toGroup:(AIListGroup *)localGroup
538         AIListObject    *containingObject;
539         AIListObject    *existingObject;
540         BOOL                    performedGrouping = NO;
542         //Protect with a retain while we are removing and adding the contact to our arrays
543         [listContact retain];
545         //XXX
546 //      AILog(@"Moving %@ to %@",listContact,localGroup);
547         
548         //Remove this object from any local groups we have it in currently
549         if ((containingObject = [listContact containingObject]) &&
550            ([containingObject isKindOfClass:[AIListGroup class]])) {
551                 //Remove the object
552                 [(AIListGroup *)containingObject removeObject:listContact];
553                 [self _listChangedGroup:(AIListGroup *)containingObject object:listContact];
554         }
556         if ((existingObject = [localGroup objectWithService:[listContact service] UID:[listContact UID]])) {
557                 //If an object exists in this group with the same UID and serviceID, create a MetaContact
558                 //for the two.
559                 [self groupListContacts:[NSArray arrayWithObjects:listContact,existingObject,nil]];
560                 performedGrouping = YES;
562         } else {
563                 AIMetaContact   *metaContact;
565                 //If no object exists in this group which matches, we should check if there is already
566                 //a MetaContact holding a matching ListContact, since we should include this contact in it
567                 //If we found a metaContact to which we should add, do it.
568                 if ((metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]])) {
569                         //XXX
570 //                      AILog(@"Found an existing metacontact; adding %@ to %@",listContact,metaContact);
572                         [self addListObject:listContact toMetaContact:metaContact];
573                         performedGrouping = YES;
574                 }
575         }
577         if (!performedGrouping) {
578                 //If no similar objects exist, we add this contact directly to the list
579                 [localGroup addObject:listContact];
581                 //Add
582                 [self _listChangedGroup:localGroup object:listContact];
583         }
585         //Cleanup
586         [listContact release];
589 - (AIListGroup *)remoteGroupForContact:(AIListContact *)inContact
591         AIListGroup             *group;
593         if ([inContact isKindOfClass:[AIMetaContact class]]) {
594                 //For a metaContact, the closest we have to a remote group is the group it is within locally
595                 group = [(AIMetaContact *)inContact parentGroup];
597         } else {
598                 NSString        *remoteGroup = [inContact remoteGroupName];
599                 group = (remoteGroup ? [self groupWithUID:remoteGroup] : nil);
600         }
602         return group;
605 //Post a list grouping changed notification for the object and group
606 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object
608         if (updatesAreDelayed) {
609                 delayedContactChanges++;
610         } else {
611                 [[adium notificationCenter] postNotificationName:Contact_ListChanged
612                                                                                                   object:object
613                                                                                                 userInfo:(group ? [NSDictionary dictionaryWithObject:group forKey:@"ContainingGroup"] : nil)];
614         }
617 - (BOOL)useContactListGroups
619         return useContactListGroups;
622 - (void)setUseContactListGroups:(BOOL)inFlag
624         if (inFlag != useContactListGroups) {
625                 useContactListGroups = inFlag;
627                 [self _performChangeOfUseContactListGroups];
628         }
631 - (void)_performChangeOfUseContactListGroups
633         NSEnumerator    *enumerator;
634         AIListObject    *listObject;
636         [self delayListObjectNotifications];
638         //Store the preference
639         [[adium preferenceController] setPreference:[NSNumber numberWithBool:!useContactListGroups]
640                                                                                  forKey:KEY_HIDE_CONTACT_LIST_GROUPS
641                                                                                   group:PREF_GROUP_CONTACT_LIST_DISPLAY];
643         //Configure the sort controller to force ignoring of groups as appropriate
644         [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
646         enumerator = [[[[contactList containedObjects] copy] autorelease] objectEnumerator];
648         if (useContactListGroups) { /* We are now using contact list groups, but we weren't before. */
650                 //Restore the grouping of all root-level contacts
651                 while ((listObject = [enumerator nextObject])) {
652                         if ([listObject isKindOfClass:[AIListContact class]]) {
653                                 [(AIListContact *)listObject restoreGrouping];
654                         }
655                 }
657         } else { /* We are no longer using contact list groups, but we were before. */
659                 while ((listObject = [enumerator nextObject])) {
660                         if ([listObject isKindOfClass:[AIListGroup class]]) {
661                                 NSArray                 *containedObjects;
662                                 NSEnumerator    *groupEnumerator;
663                                 AIListObject    *containedListObject;
665                                 containedObjects = [[(AIListGroup *)listObject containedObjects] copy];
666                                 groupEnumerator = [containedObjects objectEnumerator];
667                                 while ((containedListObject = [groupEnumerator nextObject])) {
668                                         if ([containedListObject isKindOfClass:[AIListContact class]]) {
669                                                 [self _moveContactLocally:(AIListContact *)containedListObject
670                                                                                   toGroup:contactList];
671                                         }
672                                 }
673                                 [containedObjects release];
674                         }
675                 }
676         }
678         //Stop delaying object notifications; this will automatically resort the contact list, so we're done.
679         [self endListObjectNotificationsDelay];
682 - (void)prepareShowHideGroups
684         //Load the preference
685         useContactListGroups = ![[[adium preferenceController] preferenceForKey:KEY_HIDE_CONTACT_LIST_GROUPS
686                                                                                                                                           group:PREF_GROUP_CONTACT_LIST_DISPLAY] boolValue];
688         //Show offline contacts menu item
689     menuItem_showGroups = [[NSMenuItem alloc] initWithTitle:SHOW_GROUPS_MENU_TITLE
690                                                                                                         target:self
691                                                                                                         action:@selector(toggleShowGroups:)
692                                                                                          keyEquivalent:@""];
693         [menuItem_showGroups setState:useContactListGroups];
694         [[adium menuController] addMenuItem:menuItem_showGroups toLocation:LOC_View_Toggles];
696         //Toolbar
697         NSToolbarItem   *toolbarItem;
698     toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SHOW_GROUPS_IDENTIFER
699                                                                                                                   label:AILocalizedString(@"Show Groups",nil)
700                                                                                                    paletteLabel:AILocalizedString(@"Toggle Groups Display",nil)
701                                                                                                                 toolTip:AILocalizedString(@"Toggle display of groups",nil)
702                                                                                                                  target:self
703                                                                                                 settingSelector:@selector(setImage:)
704                                                                                                         itemContent:[NSImage imageNamed:(useContactListGroups ?
705                                                                                                                                                                          @"togglegroups_transparent" :
706                                                                                                                                                                          @"togglegroups")
707                                                                                                                                                    forClass:[self class]]
708                                                                                                                  action:@selector(toggleShowGroupsToolbar:)
709                                                                                                                    menu:nil];
710     [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ContactList"];
713 - (IBAction)toggleShowGroups:(id)sender
715         //Flip-flop.
716         useContactListGroups = !useContactListGroups;
717         [menuItem_showGroups setState:useContactListGroups];
719         //Update the contact list.  Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure.
720         [self performSelector:@selector(_performChangeOfUseContactListGroups)
721                            withObject:nil
722                            afterDelay:0.000001];
725 - (IBAction)toggleShowGroupsToolbar:(id)sender
727         [self toggleShowGroups:sender];
729         [sender setImage:[NSImage imageNamed:(useContactListGroups ?
730                                                                                   @"togglegroups_transparent" :
731                                                                                   @"togglegroups")
732                                                                 forClass:[self class]]];
735 - (BOOL)useOfflineGroup
737         return useOfflineGroup;
740 - (void)setUseOfflineGroup:(BOOL)inFlag
742         if (inFlag != useOfflineGroup) {
743                 useOfflineGroup = inFlag;
744                 
745                 if (useOfflineGroup) {
746                         [self registerListObjectObserver:self]; 
747                 } else {
748                         [self updateAllListObjectsForObserver:self];
749                         [self unregisterListObjectObserver:self];       
750                 }
751         }
754 - (AIListGroup *)offlineGroup
756         return [self groupWithUID:AILocalizedString(@"Offline", "Name of offline group")];
759 #pragma mark Meta Contacts
760 //Meta Contacts --------------------------------------------------------------------------------------------------------
762  * @brief Create or load a metaContact
764  * @param inObjectID The objectID of an existing but unloaded metaContact, or nil to create and save a new metaContact
765  */
766 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID
768         NSString                *metaContactDictKey;
769         AIMetaContact   *metaContact;
770         BOOL                    shouldRestoreContacts = YES;
772         //If no object ID is provided, use the next available object ID
773         //(MetaContacts should always have an individually unique object id)
774         if (!inObjectID) {
775                 int topID = [[[adium preferenceController] preferenceForKey:TOP_METACONTACT_ID
776                                                                                                                           group:PREF_GROUP_CONTACT_LIST] intValue];
777                 inObjectID = [NSNumber numberWithInt:topID];
778                 [[adium preferenceController] setPreference:[NSNumber numberWithInt:([inObjectID intValue] + 1)]
779                                                                                          forKey:TOP_METACONTACT_ID
780                                                                                           group:PREF_GROUP_CONTACT_LIST];
782                 //No reason to waste time restoring contacts when none are in the meta contact yet.
783                 shouldRestoreContacts = NO;
784         }
786         //Look for a metacontact with this object ID.  If none is found, create one
787         //and add its contained contacts to it.
788         metaContactDictKey = [AIMetaContact internalObjectIDFromObjectID:inObjectID];
790         metaContact = [metaContactDict objectForKey:metaContactDictKey];
791         if (!metaContact) {
792                 metaContact = [[AIMetaContact alloc] initWithObjectID:inObjectID];
794                 //Keep track of it in our metaContactDict for retrieval by objectID
795                 [metaContactDict setObject:metaContact forKey:metaContactDictKey];
797                 //Add it to our more general contactDict, as well
798                 [contactDict setObject:metaContact forKey:[metaContact internalUniqueObjectID]];
800                 /* We restore contacts (actually, internalIDs for contacts, to be added as necessary later) if the metaContact
801                  * existed before this call to metaContactWithObjectID:
802                  */
803                 if (shouldRestoreContacts) {
804                         if (![self _restoreContactsToMetaContact:metaContact]) {
806                                 //If restoring the metacontact did not actually add any contacts, delete it since it is invalid
807                                 [self breakdownAndRemoveMetaContact:metaContact];
808                                 metaContact = nil;
809                         }
810                 }
811                 
812                 /* As with contactWithService:account:UID, update all attributes so observers are initially informed of
813                  * this object's existence.
814                  */
815                 [self _updateAllAttributesOfObject:metaContact];
816                 
817                 [metaContact release];
818         }
820         return (metaContact);
824  * @brief Associate the appropriate internal IDs for contained contacts with a metaContact
826  * @result YES if one or more contacts was associated with the metaContact; NO if none were.
827  */
828 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact
830         NSDictionary    *allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
831                                                                                                                                                                         group:PREF_GROUP_CONTACT_LIST];
832         NSArray                 *containedContactsArray = [allMetaContactsDict objectForKey:[metaContact internalObjectID]];
833         BOOL                    restoredContacts;
835         if ([containedContactsArray count]) {
836                 [self _restoreContactsToMetaContact:metaContact
837                                  fromContainedContactsArray:containedContactsArray];
839                 restoredContacts = YES;
841         } else {
842                 restoredContacts = NO;
843         }
845         return restoredContacts;
849  * @brief Associate the internal IDs for an array of contacts with a specific metaContact
851  * This does not actually place any AIListContacts within the metaContact.  Instead, it updates the contactToMetaContactLookupDict
852  * dictionary to have metaContact associated with the list contacts specified by containedContactsArray. This
853  * allows us to add them lazily to the metaContact (in contactWithService:account:UID:) as necessary.
855  * @param metaContact The metaContact to which contact referneces are added
856  * @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
857  */
858 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray
860         NSEnumerator            *enumerator = [containedContactsArray objectEnumerator];
861         NSDictionary            *containedContactDict;
863         while ((containedContactDict = [enumerator nextObject])) {
864                 /* Before Adium 0.80, metaContacts could be created within metaContacts. Simply ignore any attempt to restore
865                 * such erroneous data, which will have a YES boolValue for KEY_IS_METACONTACT. */
866                 if (![[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) {
867                         /* Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject.
868                          *
869                          * As listObjects are loaded/created/requested which match this internalObjectID, 
870                          * they will be inserted into the metaContact.
871                          */
872                         NSString        *internalObjectID = [AIListObject internalObjectIDForServiceID:[containedContactDict objectForKey:SERVICE_ID_KEY]
873                                                                                                                                                                    UID:[containedContactDict objectForKey:UID_KEY]];
874                         [contactToMetaContactLookupDict setObject:metaContact
875                                                                                            forKey:internalObjectID];
876                 }
877         }
881 //Add a list object to a meta contact, setting preferences and such
882 //so the association is lasting across program launches.
883 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
885         if (!listObject || [listObject isKindOfClass:[AIListGroup class]]) {
886                 //I can't think of why one would want to add an entire group to a metacontact. Let's say you can't.
887                 NSLog(@"Warning: addListObject:toMetaContact: Attempted to add %@ to %@",listObject,metaContact);
888                 return;
889         }
891         if (listObject == metaContact) return;
893         //If listObject contains other contacts, perform addListObject:toMetaContact: recursively
894         if ([listObject conformsToProtocol:@protocol(AIContainingObject)]) {
895                 NSEnumerator    *enumerator = [[[[(AIListObject<AIContainingObject> *)listObject containedObjects] copy] autorelease] objectEnumerator];
896                 AIListObject    *someObject;
898                 while ((someObject = [enumerator nextObject]))
899                         [self addListObject:someObject toMetaContact:metaContact];
901         } else {
902                 //Obtain any metaContact this listObject is currently within, so we can remove it later
903                 AIMetaContact *oldMetaContact = [contactToMetaContactLookupDict objectForKey:[listObject internalObjectID]];
905                 if ([self _performAddListObject:listObject toMetaContact:metaContact]) {
906                         //If this listObject was not in this metaContact in any form before, store the change
907                         if (metaContact != oldMetaContact) {
908                                 //Remove the list object from any other metaContact it is in at present
909                                 if (oldMetaContact)
910                                         [self removeListObject:listObject fromMetaContact:oldMetaContact];
912                                 [self _storeListObject:listObject inMetaContact:metaContact];
913                         }
914                 }
915         }
918 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact
920         //we only allow group->meta->contact, not group->meta->meta->contact
921         NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
922         
923 //      AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact);
924         NSDictionary            *containedContactDict;
925         NSMutableDictionary     *allMetaContactsDict;
926         NSMutableArray          *containedContactsArray;
928         NSString                        *metaContactInternalObjectID = [metaContact internalObjectID];
930         //Get the dictionary of all metaContacts
931         allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
932                                                                                                                                         group:PREF_GROUP_CONTACT_LIST] mutableCopy];
933         if (!allMetaContactsDict) {
934                 allMetaContactsDict = [[NSMutableDictionary alloc] init];
935         }
937         //Load the array for the new metaContact
938         containedContactsArray = [[allMetaContactsDict objectForKey:metaContactInternalObjectID] mutableCopy];
939         if (!containedContactsArray) containedContactsArray = [[NSMutableArray alloc] init];
940         containedContactDict = nil;
942         //Create the dictionary describing this list object
943         containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
944                 [[listObject service] serviceID],SERVICE_ID_KEY,
945                 [listObject UID],UID_KEY,nil];
947         //Only add if this dict isn't already in the array
948         if (containedContactDict && ([containedContactsArray indexOfObject:containedContactDict] == NSNotFound)) {
949                 [containedContactsArray addObject:containedContactDict];
950                 [allMetaContactsDict setObject:containedContactsArray forKey:metaContactInternalObjectID];
952                 //Save
953                 [self _saveMetaContacts:allMetaContactsDict];
955                 [[adium contactAlertsController] mergeAndMoveContactAlertsFromListObject:listObject
956                                                                                                                                   intoListObject:metaContact];
957         }
959         [allMetaContactsDict release];
960         [containedContactsArray release];
963 //Actually adds a list object to a meta contact. No preferences are changed.
964 //Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict
965 //for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful.
966 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
968         //we only allow group->meta->contact, not group->meta->meta->contact
969         //FIXME: deal with smack contacts correctly.
970         //NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);    
971         AIListObject<AIContainingObject>        *localGroup;
972         BOOL                                                            success;
974         localGroup = [listObject containingObject];
976         //Remove the object from its previous containing group
977         if (localGroup && (localGroup != metaContact)) {
978                 [localGroup removeObject:listObject];
979                 [self _listChangedGroup:localGroup object:listObject];
980         }
982         //AIMetaContact will handle reassigning the list object's grouping to being itself
983         if ((success = [metaContact addObject:listObject])) {
984                 [contactToMetaContactLookupDict setObject:metaContact forKey:[listObject internalObjectID]];
986                 [self _listChangedGroup:metaContact object:listObject];
987                 //If the metaContact isn't in a group yet, use the group of the object we just added
988                 if ((![metaContact containingObject]) && localGroup) {
989                         //Add the new meta contact to our list
990                         [(AIMetaContact *)localGroup addObject:metaContact];
991                         [self _listChangedGroup:localGroup object:metaContact];
992                 }
993         }
995         return success;
998 - (void)removeAllListObjectsMatching:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1000         NSEnumerator    *enumerator;
1001         AIListObject    *theObject;
1002         
1003         enumerator = [[self allContactsWithService:[listObject service]
1004                                                                                    UID:[listObject UID]
1005                                                                   existingOnly:YES] objectEnumerator];
1007         //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1008         [contactToMetaContactLookupDict removeObjectForKey:[listObject internalObjectID]];
1010         [self delayListObjectNotifications];
1011         while ((theObject = [enumerator nextObject])) {
1012                 [self removeListObject:theObject fromMetaContact:metaContact];
1013         }
1014         [self endListObjectNotificationsDelay];
1017 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1019         //we only allow group->meta->contact, not group->meta->meta->contact
1020         NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
1021         
1022         NSEnumerator            *enumerator;
1023         NSArray                         *containedContactsArray;
1024         NSDictionary            *containedContactDict = nil;
1025         NSMutableDictionary     *allMetaContactsDict;
1026         NSString                        *metaContactInternalObjectID = [metaContact internalObjectID];
1028         //Get the dictionary of all metaContacts
1029         allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1030                                                                                                                                    group:PREF_GROUP_CONTACT_LIST];
1032         //Load the array for the metaContact
1033         containedContactsArray = [allMetaContactsDict objectForKey:metaContactInternalObjectID];
1035         //Enumerate it, looking only for the appropriate type of containedContactDict
1036         enumerator = [containedContactsArray objectEnumerator];
1038         NSString        *listObjectUID = [listObject UID];
1039         NSString        *listObjectServiceID = [[listObject service] serviceID];
1041         while ((containedContactDict = [enumerator nextObject])) {
1042                 if ([[containedContactDict objectForKey:UID_KEY] isEqualToString:listObjectUID] &&
1043                         [[containedContactDict objectForKey:SERVICE_ID_KEY] isEqualToString:listObjectServiceID]) {
1044                         break;
1045                 }
1046         }
1048         //If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result
1049         if (containedContactDict) {
1050                 NSMutableArray          *newContainedContactsArray;
1051                 NSMutableDictionary     *newAllMetaContactsDict;
1053                 newContainedContactsArray = [containedContactsArray mutableCopy];
1054                 [newContainedContactsArray removeObjectIdenticalTo:containedContactDict];
1056                 newAllMetaContactsDict = [allMetaContactsDict mutableCopy];
1057                 [newAllMetaContactsDict setObject:newContainedContactsArray
1058                                                                    forKey:metaContactInternalObjectID];
1060                 [self _saveMetaContacts:newAllMetaContactsDict];
1062                 [newContainedContactsArray release];
1063                 [newAllMetaContactsDict release];
1064         }
1066         //The listObject can be within the metaContact without us finding a containedContactDict if we are removing multiple
1067         //listContacts referring to the same UID & serviceID combination - that is, on multiple accounts on the same service.
1068         //We therefore request removal of the object regardless of the if (containedContactDict) check above.
1069         [metaContact removeObject:listObject];
1074  * @brief Groups UIDs for services into a single metacontact
1076  * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to
1077  * a UID and a service, respectively, which together define a contact which should be included in the grouping.
1079  * Assumption: This is only called after the contact list is finished loading, which occurs via
1080  * -(void)controllerDidLoad above.
1082  * @param UIDsArray NSArray of UIDs
1083  * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
1084  */
1085 - (AIMetaContact *)groupUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray
1087         NSMutableSet    *internalObjectIDs = [[NSMutableSet alloc] init];
1088         AIMetaContact   *metaContact = nil;
1089         NSEnumerator    *enumerator;
1090         NSString                *internalObjectID;
1091         int                             count = [UIDsArray count];
1092         int                             i;
1093                                 
1094         //Build an array of all contacts matching this description (multiple accounts on the same service listing
1095         //the same UID mean that we can have multiple AIListContact objects with a UID/service combination)
1096         for (i = 0; i < count; i++) {
1097                 NSString        *serviceID = [servicesArray objectAtIndex:i];
1098                 NSString        *UID = [UIDsArray objectAtIndex:i];
1099                 
1100                 internalObjectID = [AIListObject internalObjectIDForServiceID:serviceID
1101                                                                                                                                   UID:UID];
1102                 if(!metaContact) {
1103                         metaContact = [contactToMetaContactLookupDict objectForKey:internalObjectID];
1104                 }
1105                 
1106                 [internalObjectIDs addObject:internalObjectID];
1107         }
1108         
1109         //Create a new metaContact is we didn't find one.
1110         if (!metaContact) {
1111                 metaContact = [self metaContactWithObjectID:nil];
1112         }
1113         
1114         enumerator = [internalObjectIDs objectEnumerator];
1115         while ((internalObjectID = [enumerator nextObject])) {
1116                 AIListObject    *existingObject;
1117                 if ((existingObject = [self existingListObjectWithUniqueID:internalObjectID])) {
1118                         /* If there is currently an object (or multiple objects) matching this internalObjectID
1119                          * we should add immediately.
1120                          */
1121                         [self addListObject:existingObject
1122                                   toMetaContact:metaContact];   
1123                 } else {
1124                         /* If no objects matching this internalObjectID exist, we can simply add to the 
1125                          * contactToMetaContactLookupDict for use if such an object is created later.
1126                          */
1127                         [contactToMetaContactLookupDict setObject:metaContact
1128                                                                                            forKey:internalObjectID];                    
1129                 }
1130         }
1131         [internalObjectIDs release];
1133         return metaContact;
1136 /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added.
1138  * This will reuse an existing metacontact (for one of the contacts in the array) if possible.
1139  * @param contactsToGroupArray Contacts to group together
1140  */
1141 - (AIMetaContact *)groupListContacts:(NSArray *)contactsToGroupArray
1143         NSEnumerator    *enumerator;
1144         AIListContact   *listContact;
1145         AIMetaContact   *metaContact = nil;
1147         //Look for an existing MetaContact we can use.  The first one we find is the lucky winner.
1148         enumerator = [contactsToGroupArray objectEnumerator];
1149         while ((listContact = [enumerator nextObject]) && (metaContact == nil)) {
1150                 if ([listContact isKindOfClass:[AIMetaContact class]]) {
1151                         metaContact = (AIMetaContact *)listContact;
1152                 } else {
1153                         metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]];
1154                 }
1155         }
1157         //Create a new metaContact is we didn't find one.
1158         if (!metaContact) {
1159                 metaContact = [self metaContactWithObjectID:nil];
1160         }
1161         
1162         /* Add all these contacts to our MetaContact.
1163                 * Some may already be present, but that's fine, as nothing will happen.
1164                 */
1165         enumerator = [contactsToGroupArray objectEnumerator];
1166         while ((listContact = [enumerator nextObject])) {
1167                 [self addListObject:listContact toMetaContact:metaContact];
1168         }
1169         
1170         return metaContact;
1173 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact
1175         //Remove the objects within it from being inside it
1176         NSArray                                                         *containedObjects = [[metaContact containedObjects] copy];
1177         NSEnumerator                                            *metaEnumerator = [containedObjects objectEnumerator];
1178         AIListObject<AIContainingObject>        *containingObject = [metaContact containingObject];
1179         AIListObject                                            *object;
1181         NSMutableDictionary *allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1182                                                                                                                                                                                  group:PREF_GROUP_CONTACT_LIST] mutableCopy];
1184         while ((object = [metaEnumerator nextObject])) {
1186                 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1187                 [contactToMetaContactLookupDict removeObjectForKey:[object internalObjectID]];
1189                 [self removeListObject:object fromMetaContact:metaContact];
1190         }
1192         //Then, procede to remove the metaContact
1194         //Protect!
1195         [metaContact retain];
1197         //Remove it from its containing group
1198         [containingObject removeObject:metaContact];
1200         NSString        *metaContactInternalObjectID = [metaContact internalObjectID];
1202         //Remove our reference to it internally
1203         [metaContactDict removeObjectForKey:metaContactInternalObjectID];
1205         //Remove it from the preferences dictionary
1206         [allMetaContactsDict removeObjectForKey:metaContactInternalObjectID];
1208         //XXX - contactToMetaContactLookupDict
1210         //Post the list changed notification for the old containingObject
1211         [self _listChangedGroup:containingObject object:metaContact];
1213         //Save the updated allMetaContactsDict which no longer lists the metaContact
1214         [self _saveMetaContacts:allMetaContactsDict];
1216         //Protection is overrated.
1217         [metaContact release];
1218         [containedObjects release];
1219         [allMetaContactsDict release];
1222 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict
1224         AILog(@"MetaContacts: Saving!");
1225         [[adium preferenceController] setPreference:allMetaContactsDict
1226                                                                                  forKey:KEY_METACONTACT_OWNERSHIP
1227                                                                                   group:PREF_GROUP_CONTACT_LIST];
1228         [[adium preferenceController] setPreference:[allMetaContactsDict allKeys]
1229                                                                                  forKey:KEY_FLAT_METACONTACTS
1230                                                                                   group:PREF_GROUP_CONTACT_LIST];
1233 //Sort list objects alphabetically by their display name
1234 int contactDisplayNameSort(AIListObject *objectA, AIListObject *objectB, void *context)
1236         return [[objectA displayName] caseInsensitiveCompare:[objectB displayName]];
1239 //Return either the highest metaContact containing this list object, or the list object itself.  Appropriate for when
1240 //preferences should be read from/to the most generalized contact possible.
1241 - (AIListObject *)parentContactForListObject:(AIListObject *)listObject
1243         if ([listObject isKindOfClass:[AIListContact class]]) {
1244                 //Find the highest-up metaContact
1245                 AIListObject    *containingObject;
1246                 while ([(containingObject = [listObject containingObject]) isKindOfClass:[AIMetaContact class]]) {
1247                         listObject = (AIMetaContact *)containingObject;
1248                 }
1249         }
1251         return listObject;
1254 #pragma mark Preference observing
1256 * @brief Preferences changed
1257  */
1258 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
1259                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
1261         if (firstTime ||
1262                 [key isEqualToString:KEY_USE_OFFLINE_GROUP] ||
1263                 [key isEqualToString:KEY_SHOW_OFFLINE_CONTACTS]) {
1265                 [self setUseOfflineGroup:([[prefDict objectForKey:KEY_USE_OFFLINE_GROUP] boolValue])];
1266         }
1270  * @brief Move contacts to and from the offline group as necessary as their online state changes.
1271  */
1272 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1274         if (!inModifiedKeys ||
1275                 [inModifiedKeys containsObject:@"Online"]) {
1277                 if ([inObject isKindOfClass:[AIListContact class]]) {                   
1278                         //If this contact is not its own parent contact, don't bother since we'll get an update for the parent if appropriate
1279                         if (inObject == [(AIListContact *)inObject parentContact]) {
1280                                 if (useOfflineGroup) {
1281                                         AIListObject *containingObject = [inObject containingObject];
1282                                         
1283                                         if ([inObject online] &&
1284                                                 (containingObject == [self offlineGroup])) {
1285                                                 [(AIListContact *)inObject restoreGrouping];
1286                                                 
1287                                         } else if (![inObject online] &&
1288                                                            containingObject &&
1289                                                            (containingObject != [self offlineGroup])) {
1290                                                 [self _moveContactLocally:(AIListContact *)inObject
1291                                                                                   toGroup:[self offlineGroup]];
1292                                         }
1293                                         
1294                                 } else {
1295                                         if ([inObject containingObject] == [self offlineGroup]) {
1296                                                 [(AIListContact *)inObject restoreGrouping];
1297                                         }
1298                                 }
1299                         }
1300                 }
1301         }
1302         
1303         return nil;
1306 //Contact Sorting --------------------------------------------------------------------------------
1307 #pragma mark Contact Sorting
1308 //Register sorting code
1309 - (void)registerListSortController:(AISortController *)inController
1311     [sortControllerArray addObject:inController];
1313 - (NSArray *)sortControllerArray
1315     return sortControllerArray;
1318 //Set and get the active sort controller
1319 - (void)setActiveSortController:(AISortController *)inController
1321     activeSortController = inController;
1323         [activeSortController didBecomeActive];
1325         //The newly-active sort controller needs to know whether it should be forced to ignore groups
1326         [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
1328     //Resort the list
1329     [self sortContactList];
1331 - (AISortController *)activeSortController
1333     return activeSortController;
1336 //Sort the entire contact list
1337 - (void)sortContactList
1339     [contactList sortGroupAndSubGroups:YES sortController:activeSortController];
1340         [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:nil];
1343 //Sort an individual object
1344 - (void)sortListObject:(AIListObject *)inObject
1346         if (updatesAreDelayed) {
1347                 delayedContactChanges++;
1348         } else {
1349                 AIListObject            *group = [inObject containingObject];
1351                 if ([group isKindOfClass:[AIListGroup class]]) {
1352                         //Sort the groups containing this object
1353                         [(AIListGroup *)group sortListObject:inObject sortController:activeSortController];
1354                         [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:inObject];
1355                 }
1356         }
1359 //List object observers ------------------------------------------------------------------------------------------------
1360 #pragma mark List object observers
1361 //Registers code to observe handle status changes
1362 - (void)registerListObjectObserver:(id <AIListObjectObserver>)inObserver
1364         //Add the observer
1365     [contactObservers addObject:[NSValue valueWithNonretainedObject:inObserver]];
1367     //Let the new observer process all existing objects
1368         [self updateAllListObjectsForObserver:inObserver];
1371 - (void)unregisterListObjectObserver:(id)inObserver
1373     [contactObservers removeObject:[NSValue valueWithNonretainedObject:inObserver]];
1378  * @brief Update all contacts for an observer, notifying the observer of each one in turn
1380  * @param contacts The contacts to update, or nil to update all contacts
1381  * @param inObserver The observer
1382  */
1383 - (void)updateContacts:(NSSet *)contacts forObserver:(id <AIListObjectObserver>)inObserver
1385         NSEnumerator    *enumerator;
1386         AIListObject    *listObject;
1388         [self delayListObjectNotifications];
1389         
1390         enumerator = (contacts ? [contacts objectEnumerator] : [contactDict objectEnumerator]);
1391         while ((listObject = [enumerator nextObject])) {
1392                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1393                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1394                 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1395                 
1396                 //If this contact is within a meta contact, update the meta contact too
1397                 AIListObject<AIContainingObject>        *containingObject = [listObject containingObject];
1398                 if (containingObject && [containingObject isKindOfClass:[AIMetaContact class]]) {
1399                         NSSet   *attributes = [inObserver updateListObject:containingObject
1400                                                                                                                   keys:nil
1401                                                                                                                 silent:YES];
1402                         if (attributes) [self listObjectAttributesChanged:containingObject
1403                                                                                                  modifiedKeys:attributes];
1404                 }
1405                 [pool release];
1406         }
1407         
1408         [self endListObjectNotificationsDelay];
1411 //Instructs a controller to update all available list objects
1412 - (void)updateAllListObjectsForObserver:(id <AIListObjectObserver>)inObserver
1414         NSEnumerator    *enumerator;
1415         AIListObject    *listObject;
1417         [self delayListObjectNotifications];
1419         //All contacts
1420         [self updateContacts:nil forObserver:inObserver];
1422     //Reset all groups
1423         enumerator = [groupDict objectEnumerator];
1424         while ((listObject = [enumerator nextObject])) {
1425                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1426                 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1427         }
1429         //Reset all accounts
1430         enumerator = [[[adium accountController] accounts] objectEnumerator];
1431         while ((listObject = [enumerator nextObject])) {
1432                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1433                 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1434         }
1436         //
1437         [self endListObjectNotificationsDelay];
1441 //Notify observers of a status change.  Returns the modified attribute keys
1442 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
1444         NSMutableSet    *attrChange = nil;
1445         NSEnumerator    *enumerator;
1446         NSValue                 *observerValue;
1447         
1448         //Let our observers know
1449         enumerator = [contactObservers objectEnumerator];
1450         while ((observerValue = [enumerator nextObject])) {
1451                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1452                 id <AIListObjectObserver>       observer;
1453                 NSSet                                           *newKeys;
1455                 observer = [observerValue nonretainedObjectValue];
1456                 if ((newKeys = [observer updateListObject:inObject keys:modifiedKeys silent:silent])) {
1457                         if (!attrChange) attrChange = [[NSMutableSet alloc] init];
1458                         [attrChange unionSet:newKeys];
1459                 }
1460                 [pool release];
1461         }
1463         //Send out the notification for other observers
1464         [[adium notificationCenter] postNotificationName:ListObject_StatusChanged
1465                                                                                           object:inObject
1466                                                                                         userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys
1467                                                                                                                                                                                                  forKey:@"Keys"] : nil)];
1469         return [attrChange autorelease];
1472 //Command all observers to apply their attributes to an object
1473 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject
1475         NSEnumerator    *enumerator = [contactObservers objectEnumerator];
1476         NSValue                 *observerValue;
1478         while ((observerValue = [enumerator nextObject])) {             
1479                 id <AIListObjectObserver> observer = [observerValue nonretainedObjectValue];
1481                 [observer updateListObject:inObject keys:nil silent:YES];
1482         }
1487 //Contact List Access --------------------------------------------------------------------------------------------------
1488 #pragma mark Contact List Access
1489 //Returns the main contact list group
1490 - (AIListGroup *)contactList
1492     return contactList;
1495 //Returns a flat array of all contacts (by calling through to -allContactsInObject:recurse:onAccount:)
1496 - (NSMutableArray *)allContacts
1498         return [self allContactsInObject:contactList recurse:YES onAccount:nil];
1501 //Returns a flat array of all contacts on a given account (by calling through to -allContactsInObject:recurse:onAccount:)
1502 - (NSMutableArray *)allContactsOnAccount:(AIAccount *)inAccount
1504         return [self allContactsInObject:contactList recurse:YES onAccount:inAccount];
1507 //Return a flat array of all the objects in a group on an account (and all subgroups, if desired)
1508 - (NSMutableArray *)allContactsInObject:(AIListObject<AIContainingObject> *)inGroup recurse:(BOOL)recurse onAccount:(AIAccount *)inAccount
1510         NSParameterAssert(inGroup != nil);
1511         
1512         NSMutableArray  *contactArray = [NSMutableArray array];    
1514         NSEnumerator *enumerator = [[inGroup containedObjects] objectEnumerator];
1515         AIListObject *object;
1516     while ((object = [enumerator nextObject])) {
1517         if (recurse && [object conformsToProtocol:@protocol(AIContainingObject)]) {
1518                         [contactArray addObjectsFromArray:[self allContactsInObject:(AIListObject<AIContainingObject> *)object
1519                                                                                                                                 recurse:recurse
1520                                                                                                                           onAccount:inAccount]];
1521                 } 
1522                 else if ([object isMemberOfClass:[AIListContact class]] && (!inAccount || ([(AIListContact *)object account] == inAccount)))
1523                         [contactArray addObject:object];
1524         }
1526         return contactArray;
1529 //Contact List Menus- --------------------------------------------------------------------------------------------------
1530 #pragma mark Contact List Menus
1532 //Returns a menu containing all the groups within a group
1533 //- Selector called on group selection is selectGroup:
1534 //- The menu items represented object is the group it represents
1535 - (NSMenu *)menuOfAllGroupsInGroup:(AIListGroup *)inGroup withTarget:(id)target
1537         NSMenu  *menu = [[NSMenu alloc] initWithTitle:@""];
1539         [menu setAutoenablesItems:NO];
1540         [self _menuOfAllGroups:menu forGroup:inGroup withTarget:target level:0];
1542         return [menu autorelease];
1544 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level
1546         NSEnumerator    *enumerator;
1547         AIListObject    *object;
1549         //Passing nil scans the entire contact list
1550         if (group == nil) group = contactList;
1552         //Enumerate this group and process all groups we find within it
1553         enumerator = [[group containedObjects] objectEnumerator];
1554         while ((object = [enumerator nextObject])) {
1555                 if ([object isKindOfClass:[AIListGroup class]] && object != [self offlineGroup]) {
1556                         NSMenuItem      *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[object displayName]
1557                                                                                                                                                                                  target:target
1558                                                                                                                                                                                  action:@selector(selectGroup:)
1559                                                                                                                                                                   keyEquivalent:@""];
1560                         [menuItem setRepresentedObject:object];
1561                         if ([menuItem respondsToSelector:@selector(setIndentationLevel:)]) {
1562                                 [menuItem setIndentationLevel:level];
1563                         }
1564                         [menu addItem:menuItem];
1565                         [menuItem release];
1566                         
1567                         [self _menuOfAllGroups:menu forGroup:(AIListGroup *)object withTarget:target level:level+1];
1568                 }
1569         }
1573 //Returns a menu containing all the objects in a group on an account
1574 //- Selector called on contact selection is selectContact:
1575 //- The menu item's represented object is the contact it represents
1576 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target{
1577         return [self menuOfAllContactsInContainingObject:inObject withTarget:target firstLevel:YES];
1579 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target firstLevel:(BOOL)firstLevel
1581     NSEnumerator                                *enumerator;
1582     AIListObject                                *object;
1584         //Prepare our menu
1585         NSMenu *menu = [[NSMenu alloc] init];
1586         [menu setAutoenablesItems:NO];
1588         //Passing nil scans the entire contact list
1589         if (inObject == nil) inObject = contactList;
1591         //The pull down menu needs an extra item at the top of its root menu to handle the selection.
1592         if (firstLevel) [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
1594         //All menu items for all contained objects
1595         enumerator = [[inObject listContacts] objectEnumerator];
1596     while ((object = [enumerator nextObject])) {
1597                 NSImage         *menuServiceImage;
1598                 NSMenuItem      *menuItem;
1599                 BOOL            needToCreateSubmenu;
1600                 BOOL            isGroup = [object isKindOfClass:[AIListGroup class]];
1601                 BOOL            isValidGroup = (isGroup &&
1602                                                                         [[(AIListGroup *)object containedObjects] count]);
1604                 //We don't want to include empty groups
1605                 if (!isGroup || isValidGroup) {
1607                         needToCreateSubmenu = (isValidGroup ||
1608                                                                    ([object isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)object listContacts] count] > 1)));
1610                         
1611                         menuServiceImage = [AIUserIcons menuUserIconForObject:object];
1612                         
1613                         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(needToCreateSubmenu ?
1614                                                                                                                                                                         [object displayName] :
1615                                                                                                                                                                         [object formattedUID])
1616                                                                                                                                                         target:target
1617                                                                                                                                                         action:@selector(selectContact:)
1618                                                                                                                                          keyEquivalent:@""];
1619                         
1620                         if (needToCreateSubmenu) {
1621                                 [menuItem setSubmenu:[self menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)object withTarget:target firstLevel:NO]];
1622                         }
1624                         [menuItem setRepresentedObject:object];
1625                         [menuItem setImage:menuServiceImage];
1626                         [menu addItem:menuItem];
1627                         [menuItem release];
1628                 }
1629         }
1631         return [menu autorelease];
1634 //Retrieving Specific Contacts -----------------------------------------------------------------------------------------
1635 #pragma mark Retrieving Specific Contacts
1637 - (AIListContact *)contactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1639     return [self contactWithService:inService account:inAccount UID:inUID usingClass:[AIListContact class]];
1642 //Retrieve a contact from the contact list (Creating if necessary)
1643 - (AIListContact *)contactWithService:(AIService *)inService 
1644                                                           account:(AIAccount *)inAccount 
1645                                                                   UID:(NSString *)inUID 
1646                                                    usingClass:(Class)ContactClass
1648         if (!(inUID && [inUID length] && inService)) return nil; //Ignore invalid requests
1650         AIListContact   *contact = nil;
1651         NSString                *key = [ContactClass internalUniqueObjectIDForService:inService
1652                                                                                                                                   account:inAccount
1653                                                                                                                                           UID:inUID];
1654         contact = [contactDict objectForKey:key];
1655         if (!contact) {
1656                 //Create
1657                 contact = [[ContactClass alloc] initWithUID:inUID account:inAccount service:inService];
1659                 //Do the update thing
1660                 [self _updateAllAttributesOfObject:contact];
1661                 
1662                 //Check to see if we should add to a metaContact
1663                 AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:[contact internalObjectID]];
1664                 if (metaContact) {
1665                         /* We already know to add this object to the metaContact, since we did it before with another object,
1666                            but this particular listContact is new and needs to be added directly to the metaContact
1667                            (on future launches, the metaContact will obtain it automatically since all contacts matching this UID
1668                            and serviceID should be included). */
1669                         [self _performAddListObject:contact toMetaContact:metaContact];
1670                 }
1671                 
1672                 //Set the contact as mobile if it is a phone number
1673                 if ([inUID characterAtIndex:0] == '+') {
1674                         [contact setIsMobile:YES notify:NotifyNever];
1675                 }
1677                 //Add
1678                 [contactDict setObject:contact forKey:key];
1679                 [contact release];
1680         }
1682         return contact;
1685 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID usingClass:(Class)ContactClass
1687         if (inService && [inUID length]) {
1688                 return [contactDict objectForKey:[ContactClass internalUniqueObjectIDForService:inService
1689                                                                                                                                                                 account:inAccount
1690                                                                                                                                                                         UID:inUID]];
1691         } else {
1692                 return nil;
1693         }
1696 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1698         return [self existingContactWithService:inService account:inAccount UID:inUID usingClass:[AIListContact class]];
1702  * @brief Return a set of all contacts with a specified UID and service
1704  * @param service The AIService in question
1705  * @param inUID The UID, which should be normalized (lower case, no spaces, etc.) as appropriate for the service
1706  * @param existingOnly If YES, only pre-existing contacts. If NO, an AIListContact is guaranteed to be returned
1707  *                                         on each compatible account, even if one did not previously exist.
1708  */
1709 - (NSSet *)allContactsWithService:(AIService *)service UID:(NSString *)inUID existingOnly:(BOOL)existingOnly
1711         NSEnumerator    *enumerator;
1712         AIAccount               *account;
1713         NSMutableSet    *returnContactSet = [NSMutableSet set];
1715         enumerator = [[[adium accountController] accountsCompatibleWithService:service] objectEnumerator];
1717         while ((account = [enumerator nextObject])) {
1718                 AIListContact   *listContact;
1719                 
1720                 if (existingOnly) {
1721                         listContact = [self existingContactWithService:service
1722                                                                                                    account:account
1723                                                                                                            UID:inUID];
1724                 } else {
1725                         listContact = [self contactWithService:service
1726                                                                                    account:account
1727                                                                                            UID:inUID];
1728                 }
1730                 if (listContact) {
1731                         [returnContactSet addObject:listContact];
1732                 }
1733         }
1735         return returnContactSet;
1738 - (AIListObject *)existingListObjectWithUniqueID:(NSString *)uniqueID
1740         NSEnumerator    *enumerator;
1741         AIListObject    *listObject;
1743         //Contact
1744         enumerator = [contactDict objectEnumerator];
1745         while ((listObject = [enumerator nextObject])) {
1746                 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1747         }
1749         //Group
1750         enumerator = [groupDict objectEnumerator];
1751         while ((listObject = [enumerator nextObject])) {
1752                 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1753         }
1755         //Metacontact
1756         enumerator = [metaContactDict objectEnumerator];
1757         while ((listObject = [enumerator nextObject])) {
1758                 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1759         }
1761         return nil;
1764 - (AIListContact *)preferredContactForContentType:(NSString *)inType forListContact:(AIListContact *)inContact
1766         AIListContact   *returnContact = nil;
1767         AIAccount               *account;
1769         if ([inContact containsMultipleContacts]) {
1770                 AIListObject    *preferredContact;
1771                 NSString                *internalObjectID;
1773                 /* If we've messaged this object previously, prefer the last contact we sent to if that
1774                  * contact is currently in the most-available status the metacontact can offer
1775                  */
1776         internalObjectID = [inContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1777                                                                                                  group:OBJECT_STATUS_CACHE];
1779         if ((internalObjectID) &&
1780                    (preferredContact = [self existingListObjectWithUniqueID:internalObjectID]) &&
1781                    ([preferredContact isKindOfClass:[AIListContact class]]) &&
1782                    ([preferredContact statusSummary] == [inContact statusSummary]) &&
1783                         ([[(AIMetaContact *)inContact containedObjects] containsObject:preferredContact])) {
1784                         returnContact = [self preferredContactForContentType:inType
1785                                                                                                   forListContact:(AIListContact *)preferredContact];
1786         }
1788                 /* If the last contact we sent to is not appropriate, use the following algorithm, which differs from -[AIMetaContact preferredContact]
1789                  * in that it doesn't "like" mobile contacts.
1790                  *
1791                  *  1) Prefer available contacts who are not mobile
1792                  *  2) If no available non-mobile contacts, use the first online contact
1793                  *  3) If no online contacts, use the metacontact's preferredContact
1794                  */
1795 #warning This is a casting mess... define a protocol for metacontact-like AIListContact subclasses
1796                 if (!returnContact) {
1797                         //Recurse into metacontacts if necessary
1798                         AIListContact *firstAvailableContact = nil;
1799                         AIListContact *firstNotOfflineContact = nil;
1801                         AIListContact *thisContact;
1802                         NSEnumerator *contactsEnum = [[(AIMetaContact *)inContact containedObjects] objectEnumerator];
1803                         while ((thisContact = [contactsEnum nextObject])) {
1804                                 AIStatusType statusSummary = [thisContact statusSummary];
1806                                 if (statusSummary != AIOfflineStatus) {
1807                                         if (!firstNotOfflineContact) {
1808                                                 firstNotOfflineContact = thisContact;
1809                                         }
1811                                         if (statusSummary == AIAvailableStatus && ![thisContact isMobile]) {
1812                                                 if (!firstAvailableContact) {
1813                                                         firstAvailableContact = thisContact;
1814                                                 }
1816                                                 break;
1817                                         }
1818                                 }
1819                         }
1821                         returnContact = (firstAvailableContact ?
1822                                                          firstAvailableContact :
1823                                                          (firstNotOfflineContact ? firstNotOfflineContact : [(AIMetaContact *)inContact preferredContact]));
1824                 }
1826         } else {
1827                 //This contact doesn't contain multiple contacts... but it might still be a metacontact. Do NOT proceed with a metacontact.
1828                 if ([inContact respondsToSelector:@selector(preferredContact)])
1829                         inContact = [inContact performSelector:@selector(preferredContact)];
1831                 //find the best account for talking to this contact,
1832                 //and return an AIListContact on that account
1833                 account = [[adium accountController] preferredAccountForSendingContentType:inType
1834                                                                                                                                                  toContact:inContact];
1835                 if (account) {
1836                         if ([inContact account] == account) {
1837                                 returnContact = inContact;
1838                         } else {
1839                                 returnContact = [self contactWithService:[inContact service]
1840                                                                                                  account:account
1841                                                                                                          UID:[inContact UID]];
1842                         }
1843                 }
1844         }
1846         return returnContact;
1849 //Retrieve a list contact matching the UID and serviceID of the passed contact but on the specified account.
1850 //In many cases this will be the same as inContact.
1851 - (AIListContact *)contactOnAccount:(AIAccount *)account fromListContact:(AIListContact *)inContact
1853         if (account && ([inContact account] != account)) {
1854                 return [self contactWithService:[inContact service] account:account UID:[inContact UID]];
1855         } else {
1856                 return inContact;
1857         }
1860 //XXX - This is ridiculous.
1861 - (AIListContact *)preferredContactWithUID:(NSString *)inUID andServiceID:(NSString *)inService forSendingContentType:(NSString *)inType
1863         AIService               *theService = [[adium accountController] firstServiceWithServiceID:inService];
1864         AIListContact   *tempListContact = [[AIListContact alloc] initWithUID:inUID
1865                                                                                                                                 service:theService];
1866         AIAccount               *account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
1867                                                                                                                                                                           toContact:tempListContact
1868                                                                                                                                                                  includeOffline:YES];
1869         [tempListContact release];
1871         return [self contactWithService:theService account:account UID:inUID];
1876  * @brief Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts
1878  * If the destination contact's parent contact differs from the destination contact itself, the chat is with a metaContact.
1879  * If that metaContact's preferred destination for messaging isn't the same as the contact which was just messaged,
1880  * update the preference so that a new chat with this metaContact would default to the proper contact.
1881  */
1882 - (void)didSendContent:(NSNotification *)notification
1884         AIChat                  *chat = [[notification userInfo] objectForKey:@"AIChat"];
1885         AIListContact   *destContact = [chat listObject];
1886         AIListContact   *metaContact = [destContact parentContact];
1888         //it's not particularly obvious from the name, but -parentContact can return self
1889         if (metaContact == destContact) return;
1890         
1891         NSString        *destinationInternalObjectID = [destContact internalObjectID];
1892         NSString        *currentPreferredDestination = [metaContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1893                                                                                                                                            group:OBJECT_STATUS_CACHE];
1895         if (![destinationInternalObjectID isEqualToString:currentPreferredDestination]) {
1896                 [metaContact setPreference:destinationInternalObjectID
1897                                                         forKey:KEY_PREFERRED_DESTINATION_CONTACT
1898                                                          group:OBJECT_STATUS_CACHE];
1899         }
1902 //Retrieving Groups ----------------------------------------------------------------------------------------------------
1903 #pragma mark Retrieving Groups
1905 //Retrieve a group from the contact list (Creating if necessary)
1906 - (AIListGroup *)groupWithUID:(NSString *)groupUID
1908         //Return our root group if it is requested. 
1909         //XXX: is this a good idea? it might semi-mask bugs where we accidentally pass nil
1910         if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME])
1911                 return [self contactList];
1913         AIListGroup             *group = nil;
1914         if (!(group = [groupDict objectForKey:groupUID])) {
1915                 //Create
1916                 group = [[AIListGroup alloc] initWithUID:groupUID];
1918                 //Add
1919                 [self _updateAllAttributesOfObject:group];
1920                 [groupDict setObject:group forKey:groupUID];
1922                 //Add to the contact list
1923                 [contactList addObject:group];
1924                 [self _listChangedGroup:contactList object:group];
1925                 [group release];
1926         }
1928         return group;
1931 - (AIListGroup *)existingGroupWithUID:(NSString *)groupUID
1933         //Return our root group if it is requested
1934         //XXX: is this a good idea? it might semi-mask bugs where we accidentally pass nil
1935         if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME])
1936                 return [self contactList];
1937         
1938         return [groupDict objectForKey:groupUID];
1941 //Contact list editing -------------------------------------------------------------------------------------------------
1942 #pragma mark Contact list editing
1943 - (void)removeListObjects:(NSArray *)objectArray
1945         NSEnumerator    *enumerator = [objectArray objectEnumerator];
1946         AIListObject    *listObject;
1948         while ((listObject = [enumerator nextObject])) {
1949                 if ([listObject isKindOfClass:[AIMetaContact class]]) {
1950                         NSSet   *objectsToRemove = nil;
1952                         //If the metaContact only has one listContact, we will remove that contact from all accounts
1953                         if ([[(AIMetaContact *)listObject listContacts] count] == 1) {
1954                                 AIListContact   *listContact = [[(AIMetaContact *)listObject listContacts] objectAtIndex:0];
1955                                 
1956                                 objectsToRemove = [self allContactsWithService:[listContact service]
1957                                                                                                                    UID:[listContact UID]
1958                                                                                                   existingOnly:YES];
1959                         }
1961                         //And actually remove the single contact if applicable
1962                         if (objectsToRemove) {
1963                                 [self removeListObjects:[objectsToRemove allObjects]];
1964                         }
1965                         
1966                         //Now break the metaContact down, taking out all contacts and putting them back in the main list
1967                         [self breakdownAndRemoveMetaContact:(AIMetaContact *)listObject];                               
1969                 } else if ([listObject isKindOfClass:[AIListGroup class]]) {
1970                         AIListObject <AIContainingObject>       *containingObject = [listObject containingObject];
1971                         NSEnumerator    *enumerator;
1972                         AIAccount               *account;
1974                         //If this is a group, delete all the objects within it
1975                         [self removeListObjects:[(AIListGroup *)listObject containedObjects]];
1977                         //Delete the list off of all active accounts
1978                         enumerator = [[[adium accountController] accounts] objectEnumerator];
1979                         while ((account = [enumerator nextObject])) {
1980                                 if ([account online]) {
1981                                         [account deleteGroup:(AIListGroup *)listObject];
1982                                 }
1983                         }
1985                         //Then, procede to delete the group
1986                         [listObject retain];
1987                         [containingObject removeObject:listObject];
1988                         [groupDict removeObjectForKey:[listObject UID]];
1989                         [self _listChangedGroup:containingObject object:listObject];
1990                         [listObject release];
1992                 } else {
1993                         AIAccount       *account = [(AIListContact *)listObject account];
1994                         if ([account online]) {
1995                                 [account removeContacts:[NSArray arrayWithObject:listObject]];
1996                         }
1997                 }
1998         }
2001 - (void)addContacts:(NSArray *)contactArray toGroup:(AIListGroup *)group
2003         NSEnumerator    *enumerator;
2004         AIListContact   *listObject;
2006         [self delayListObjectNotifications];
2008         enumerator = [contactArray objectEnumerator];
2009         while ((listObject = [enumerator nextObject])) {
2010                 if(![group containsObject:listObject]) //don't add it if it's already there.
2011                         [[listObject account] addContacts:[NSArray arrayWithObject:listObject] toGroup:group];
2012         }
2014         [self endListObjectNotificationsDelay];
2017 - (void)requestAddContactWithUID:(NSString *)contactUID service:(AIService *)inService account:(AIAccount *)inAccount
2019         NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:contactUID
2020                                                                                                                                            forKey:UID_KEY];
2021         if (inService) [userInfo setObject:inService forKey:@"AIService"];
2022         if (inAccount) [userInfo setObject:inAccount forKey:@"AIAccount"];
2024         [[adium notificationCenter] postNotificationName:Contact_AddNewContact
2025                                                                                           object:nil
2026                                                                                         userInfo:userInfo];
2029 - (void)moveListObjects:(NSArray *)objectArray intoObject:(AIListObject<AIContainingObject> *)group index:(int)index
2031         NSEnumerator    *enumerator;
2032         AIListContact   *listContact;
2034         [self delayListObjectNotifications];
2036         if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2037                 [(id)group setDelayContainedObjectSorting:YES];
2038         }
2040         enumerator = [objectArray objectEnumerator];
2041         while ((listContact = [enumerator nextObject])) {
2042                 [self moveContact:listContact intoObject:group];
2044                 //Set the new index / position of the object
2045                 [self _positionObject:listContact atIndex:index inObject:group];
2046         }
2048         [self endListObjectNotificationsDelay];
2050         if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2051                 [(id)group setDelayContainedObjectSorting:NO];
2052         }
2054         /*
2055          Resort the entire list if we are moving within or between AIListGroup objects
2056          (other containing objects such as metaContacts will handle their own sorting).
2057         */
2058         if ([group isKindOfClass:[AIListGroup class]])
2059                 [self sortContactList];
2062 - (void)moveContact:(AIListContact *)listContact intoObject:(AIListObject<AIContainingObject> *)group
2064         //Move the object to the new group only if necessary
2065         if (group == [listContact containingObject]) return;
2066         
2067         if ([group isKindOfClass:[AIListGroup class]]) {
2068                 //Move a contact into a new group
2069                 if ([listContact isKindOfClass:[AIMetaContact class]]) {
2070                         //Move the meta contact to this new group
2071                         [self _moveContactLocally:listContact toGroup:(AIListGroup *)group];
2073                         NSEnumerator    *enumerator;
2074                         AIListContact   *actualListContact;
2076                         //This is a meta contact, move the objects within it.  listContacts will give us a flat array of AIListContacts.
2077                         enumerator = [[(AIMetaContact *)listContact listContacts] objectEnumerator];
2078                         while ((actualListContact = [enumerator nextObject])) {
2079                                 //Only move the contact if it is actually listed on the account in question
2080                                 if (![actualListContact isStranger]) {
2081                                         [self _moveObjectServerside:actualListContact toGroup:(AIListGroup *)group];
2082                                 }
2083                         }
2085                 } else if ([listContact isKindOfClass:[AIListContact class]]) {
2086                         //Move the object
2087                         [self _moveObjectServerside:listContact toGroup:(AIListGroup *)group];
2088                 }
2090         } else if ([group isKindOfClass:[AIMetaContact class]]) {
2091                 //Moving a contact into a meta contact
2092                 [self addListObject:listContact toMetaContact:(AIMetaContact *)group];
2093         }
2096 //Move an object to another group
2097 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group
2099         AIAccount       *account = [(AIListContact *)listObject account];
2100         if ([account online]) {
2101                 [account moveListObjects:[NSArray arrayWithObject:listObject] toGroup:group];
2102         }
2105 //Rename a group
2106 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName
2108         NSEnumerator    *enumerator = [[[adium accountController] accounts] objectEnumerator];
2109         AIAccount               *account;
2111         //Since Adium has no memory of what accounts a group is on, we have to send this message to all available accounts
2112         //The accounts without this group will just ignore it
2113         while ((account = [enumerator nextObject])) {
2114                 [account renameGroup:listGroup to:newName];
2115         }
2117         //Remove the old group if it's empty
2118         if ([listGroup containedObjectsCount] == 0) {
2119                 [self removeListObjects:[NSArray arrayWithObject:listGroup]];
2120         }
2123 //Position a list object within a group
2124 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inObject:(AIListObject<AIContainingObject> *)group
2126         if (index == 0) {
2127                 //Moved to the top of a group.  New index is between 0 and the lowest current index
2128                 [listObject setOrderIndex:([group smallestOrder] / 2.0)];
2130         } else if (index >= [group visibleCount]) {
2131                 //Moved to the bottom of a group.  New index is one higher than the highest current index
2132                 [listObject setOrderIndex:([group largestOrder] + 1.0)];
2134         } else {
2135                 //Moved somewhere in the middle.  New index is the average of the next largest and smallest index
2136                 AIListObject    *previousObject = [group objectAtIndex:index-1];
2137                 AIListObject    *nextObject = [group objectAtIndex:index];
2138                 float nextLowest = [previousObject orderIndex];
2139                 float nextHighest = [nextObject orderIndex];
2141                 /* XXX - Fixme as per below
2142                  * It's possible that nextLowest > nextHighest if ordering is not strictly based on the ordering indexes themselves.
2143                  * For example, a group sorted by status then manually could look like (status - ordering index):
2144                  *
2145                  * Away Contact - 100
2146                  * Away Contact - 120
2147                  * Offline Contact - 110
2148                  * Offline Contact - 113
2149                  * Offline Contact - 125
2150                  * 
2151                  * Dropping between Away Contact and Offline Contact should make an Away Contact be > 120 but an Offline Contact be < 110.
2152                  * Only the sort controller knows the answer as to where this contact should be positioned in the end.
2153                  */
2154                 //
2155                 [listObject setOrderIndex:((nextHighest + nextLowest) / 2.0)];
2156         }
2159 #pragma mark Authorization
2160 - (id)showAuthorizationRequestWithDict:(NSDictionary *)inDict forAccount:(AIAccount *)inAccount
2162         return [adiumAuthorization showAuthorizationRequestWithDict:inDict forAccount:inAccount];
2165 @end