Allow localization of the Alert Text label in the Display an Alert action
[adiumx.git] / Source / AIContactController.m
blobb92b776a0209cb663aefd08ae8004f605b76dd12
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 "AISCLViewPlugin.h"
23 #import <Adium/AIAccountControllerProtocol.h>
24 #import <Adium/AIInterfaceControllerProtocol.h>
25 #import <Adium/AILoginControllerProtocol.h>
26 #import <Adium/AIMenuControllerProtocol.h>
27 #import <Adium/AIPreferenceControllerProtocol.h>
28 #import <Adium/AIToolbarControllerProtocol.h>
29 #import <Adium/AIContactAlertsControllerProtocol.h>
31 #import <AIUtilities/AIDictionaryAdditions.h>
32 #import <AIUtilities/AIFileManagerAdditions.h>
33 #import <AIUtilities/AIMenuAdditions.h>
34 #import <AIUtilities/AIToolbarUtilities.h>
35 #import <AIUtilities/AIApplicationAdditions.h>
36 #import <AIUtilities/AIImageAdditions.h>
37 #import <AIUtilities/AIStringAdditions.h>
38 #import <Adium/AIAccount.h>
39 #import <Adium/AIChat.h>
40 #import <Adium/AIContentMessage.h>
41 #import <Adium/AIListContact.h>
42 #import <Adium/AIListGroup.h>
43 #import <Adium/AIListObject.h>
44 #import <Adium/AIMetaContact.h>
45 #import <Adium/AIService.h>
46 #import <Adium/AISortController.h>
47 #import <Adium/AIUserIcons.h>
48 #import <Adium/AIServiceIcons.h>
49 #import <Adium/AIListBookmark.h>
51 #import "AdiumAuthorization.h"
53 #define KEY_FLAT_GROUPS                                 @"FlatGroups"                   //Group storage
54 #define KEY_FLAT_CONTACTS                               @"FlatContacts"                 //Contact storage
55 #define KEY_FLAT_METACONTACTS                   @"FlatMetaContacts"             //Metacontact objectID storage
56 #define KEY_BOOKMARKS                                   @"Bookmarks"
58 #define OBJECT_STATUS_CACHE                             @"Object Status Cache"
60 #define UPDATE_CLUMP_INTERVAL                   1.0
62 #define TOP_METACONTACT_ID                              @"TopMetaContactID"
63 #define KEY_IS_METACONTACT                              @"isMetaContact"
64 #define KEY_OBJECTID                                    @"objectID"
65 #define KEY_METACONTACT_OWNERSHIP               @"MetaContact Ownership"
66 #define CONTACT_DEFAULT_PREFS                   @"ContactPrefs"
68 #define SHOW_GROUPS_MENU_TITLE                  AILocalizedString(@"Show Groups",nil)
69 #define SHOW_GROUPS_IDENTIFER                   @"ShowGroups"
71 #define SERVICE_ID_KEY                                  @"ServiceID"
72 #define UID_KEY                                                 @"UID"
74 @interface AIContactController (PRIVATE)
75 - (AIListGroup *)processGetGroupNamed:(NSString *)serverGroup;
76 - (void)_performDelayedUpdates:(NSTimer *)timer;
78 - (void)saveContactList;
79 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
80 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject;
81 - (void)prepareContactInfo;
83 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inGroup withTarget:(id)target firstLevel:(BOOL)firstLevel;
84 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level;
86 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects;
87 - (void)_loadGroupsFromArray:(NSArray *)array;
89 - (void)_loadBookmarks;
90 - (NSArray *)allBookmarks;
92 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object;
93 - (void)prepareShowHideGroups;
94 - (void)_performChangeOfUseContactListGroups;
96 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inObject:(AIListObject<AIContainingObject> *)group;
97 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group;
98 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName;
100 //MetaContacts
101 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID;
102 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact;
103 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray;
104 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
105 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
106 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact;
107 - (void)_loadMetaContactsFromArray:(NSArray *)array;
108 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict;
109 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact;
110 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact;
112 - (void)_addMenuItemsFromArray:(NSArray *)contactArray toMenu:(NSMenu *)contactMenu target:(id)target offlineContacts:(BOOL)offlineContacts;
114 @end
116 @implementation AIContactController
118 //init
119 - (id)init
121         if ((self = [super init])) {
122                 //
123                 contactObservers = [[NSMutableSet alloc] init];
124                 sortControllerArray = [[NSMutableArray alloc] init];
125                 activeSortController = nil;
126                 delayedStatusChanges = 0;
127                 delayedModifiedStatusKeys = [[NSMutableSet alloc] init];
128                 delayedAttributeChanges = 0;
129                 delayedModifiedAttributeKeys = [[NSMutableSet alloc] init];
130                 delayedContactChanges = 0;
131                 delayedUpdateRequests = 0;
132                 updatesAreDelayed = NO;
133                 
134                 //
135                 contactDict = [[NSMutableDictionary alloc] init];
136                 groupDict = [[NSMutableDictionary alloc] init];
137                 metaContactDict = [[NSMutableDictionary alloc] init];
138                 contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
139                 detachedContactLists = [[NSMutableArray 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];
168         
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;
187         
188         [sortControllerArray release];
189         [delayedModifiedStatusKeys release];
190         [delayedModifiedAttributeKeys release];
192         [contactDict release];
193         [groupDict release];
194         [metaContactDict release];
195         [contactToMetaContactLookupDict release];
196         [detachedContactLists release];
197         
198         [adiumAuthorization release];
199         
200     [super dealloc];
203 - (void)clearAllMetaContactData
205         NSString                *path;
206         NSDictionary    *metaContactDictCopy = [metaContactDict copy];
207         NSEnumerator    *enumerator;
208         AIMetaContact   *metaContact;
210         if ([metaContactDictCopy count]) {
211                 [self delayListObjectNotifications];
213                 //Remove all the metaContacts to get any existing objects out of them
214                 enumerator = [metaContactDictCopy objectEnumerator];
215                 while ((metaContact = [enumerator nextObject])) {
216                         [self breakdownAndRemoveMetaContact:metaContact];
217                 }
219                 [self endListObjectNotificationsDelay];
220         }
222         [metaContactDict release]; metaContactDict = [[NSMutableDictionary alloc] init];
223         [contactToMetaContactLookupDict release]; contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
225         //Clear the preferences for good measure
226         [[adium preferenceController] setPreference:nil
227                                                                                  forKey:KEY_FLAT_METACONTACTS
228                                                                                   group:PREF_GROUP_CONTACT_LIST];
229         [[adium preferenceController] setPreference:nil
230                                                                                  forKey:KEY_METACONTACT_OWNERSHIP
231                                                                                   group:PREF_GROUP_CONTACT_LIST];
233         //Clear out old metacontact files
234         path = [[[adium loginController] userDirectory] stringByAppendingPathComponent:OBJECT_PREFS_PATH];
235         [[NSFileManager defaultManager] removeFilesInDirectory:path
236                                                                                                 withPrefix:@"MetaContact"
237                                                                                          movingToTrash:NO];
238         [[NSFileManager defaultManager] removeFilesInDirectory:[adium cachesPath]
239                                                                                                 withPrefix:@"MetaContact"
240                                                                                          movingToTrash:NO];
242         [metaContactDictCopy release];
245 //Local Contact List Storage -------------------------------------------------------------------------------------------
246 #pragma mark Local Contact List Storage
247 //Load the contact list
248 - (void)loadContactList
250         //We must load all the groups before loading contacts for the ordering system to work correctly.
251         [self _loadMetaContactsFromArray:[[adium preferenceController] preferenceForKey:KEY_FLAT_METACONTACTS
252                                                                                                                                                           group:PREF_GROUP_CONTACT_LIST]];
253         [self _loadBookmarks];
256 //Save the contact list
257 - (void)saveContactList
259         NSEnumerator *enumerator = [groupDict objectEnumerator];
260         AIListGroup      *listGroup;
261         
262         while ((listGroup = [enumerator nextObject])) {
263                 [listGroup setPreference:[NSNumber numberWithBool:[listGroup isExpanded]]
264                                                   forKey:@"IsExpanded"
265                                                    group:PREF_GROUP_CONTACT_LIST];
266         }
267         
268         NSMutableArray *bookmarks = [NSMutableArray array];
269         AIListObject *listObject;
270         enumerator = [[self allBookmarks] objectEnumerator];
271         while ((listObject = [enumerator nextObject])) {
272                 if ([listObject isKindOfClass:[AIListBookmark class]]) {
273                         [bookmarks addObject:[NSKeyedArchiver archivedDataWithRootObject:listObject]];
274                 }
275         }
277         [[adium preferenceController] setPreference:bookmarks
278                                                                                  forKey:KEY_BOOKMARKS
279                                                                                   group:PREF_GROUP_CONTACT_LIST];
282 - (void)_loadBookmarks
284         NSEnumerator    *enumerator = [[[adium preferenceController] preferenceForKey:KEY_BOOKMARKS
285                                                                                                                                                         group:PREF_GROUP_CONTACT_LIST] objectEnumerator];
286         NSData *data;
287         while ((data = [enumerator nextObject])) {
288                 AIListBookmark  *bookmark;
289                 //As a bookmark is initialized, it will add itself to the contact list in the right place
290                 bookmark = [NSKeyedUnarchiver unarchiveObjectWithData:data];    
292                 //It's a newly created object, so set its initial attributes
293                 [self _updateAllAttributesOfObject:bookmark];
294         }
297 - (void)_loadMetaContactsFromArray:(NSArray *)array
299         NSEnumerator    *enumerator = [array objectEnumerator];
300         NSString                *identifier;
302         while ((identifier = [enumerator nextObject])) {
303                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
304                 NSNumber *objectID = [NSNumber numberWithInt:[[[identifier componentsSeparatedByString:@"-"] objectAtIndex:1] intValue]];
305                 [self metaContactWithObjectID:objectID];
306                 [pool release];
307         }
310 //Flattened array of the contact list content
311 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects
313         NSMutableArray  *array = [NSMutableArray array];
314         NSEnumerator    *enumerator = [listObjects objectEnumerator];;
315         AIListObject    *object;
317         //Create temporary strings outside the loop
318         NSString        *Group = @"Group";
319         NSString        *Type = @"Type";
320         NSString        *Expanded = @"Expanded";
322         while ((object = [enumerator nextObject])) {
323                         [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
324                                 Group, Type,
325                                 [object UID], UID_KEY,
326                                 [NSNumber numberWithBool:[(AIListGroup *)object isExpanded]], Expanded,
327                                 nil]];
328         }
330         return array;
334 //Status and Display updates -------------------------------------------------------------------------------------------
335 #pragma mark Status and Display updates
336 //These delay Contact_ListChanged, ListObject_AttributesChanged, Contact_OrderChanged notificationsDelays,
337 //sorting and redrawing to prevent redundancy when making a large number of changes
338 //Explicit delay.  Call endListObjectNotificationsDelay to end
339 - (void)delayListObjectNotifications
341         delayedUpdateRequests++;
342         updatesAreDelayed = YES;
345 //End an explicit delay
346 - (void)endListObjectNotificationsDelay
348         delayedUpdateRequests--;
349         if (delayedUpdateRequests == 0 && !delayedUpdateTimer) {
350                 [self _performDelayedUpdates:nil];
351         }
354 //Delay all list object notifications until a period of inactivity occurs.  This is useful for accounts that do not
355 //know when they have finished connecting but still want to mute events.
356 - (void)delayListObjectNotificationsUntilInactivity
358     if (!delayedUpdateTimer) {
359                 updatesAreDelayed = YES;
360                 delayedUpdateTimer = [[NSTimer scheduledTimerWithTimeInterval:UPDATE_CLUMP_INTERVAL
361                                                                                                                            target:self
362                                                                                                                          selector:@selector(_performDelayedUpdates:)
363                                                                                                                          userInfo:nil
364                                                                                                                           repeats:YES] retain];
365     } else {
366                 //Reset the timer
367                 [delayedUpdateTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:UPDATE_CLUMP_INTERVAL]];
368         }
371 //Update the status of a list object.  This will update any information that is otherwise too expensive to update
372 //automatically, such as their profile.
373 - (void)updateListContactStatus:(AIListContact *)inContact
375         //If we're handed something that can contain other contacts, update the status of the contacts contained within it
376         if ([inContact conformsToProtocol:@protocol(AIContainingObject)]) {
377                 NSEnumerator    *enumerator = [[(AIListObject<AIContainingObject> *)inContact listContacts] objectEnumerator];
378                 AIListContact   *contact;
380                 while ((contact = [enumerator nextObject])) {
381                         [self updateListContactStatus:contact];
382                 }
384         } else {
385                 AIAccount *account = [inContact account];
386                 if (![account online]) {
387                         account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
388                                                                                                                                                          toContact:inContact];
389                 }
391                 [account updateContactStatus:inContact];
392         }
395 //Called after modifying a contact's status
396 // Silent: Silences all events, notifications, sounds, overlays, etc. that would have been associated with this status change
397 - (void)listObjectStatusChanged:(AIListObject *)inObject modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
399     NSSet                       *modifiedAttributeKeys;
401     //Let all observers know the contact's status has changed before performing any sorting or further notifications
402         modifiedAttributeKeys = [self _informObserversOfObjectStatusChange:inObject withKeys:inModifiedKeys silent:silent];
404     //Resort the contact list
405         if (updatesAreDelayed) {
406                 delayedStatusChanges++;
407                 [delayedModifiedStatusKeys unionSet:inModifiedKeys];
408         } else {
409                 //We can safely skip sorting if we know the modified attributes will invoke a resort later
410                 if (![[self activeSortController] shouldSortForModifiedAttributeKeys:modifiedAttributeKeys] &&
411                    [[self activeSortController] shouldSortForModifiedStatusKeys:inModifiedKeys]) {
412                         [self sortListObject:inObject];
413                 }
414         }
416     //Post an attributes changed message (if necessary)
417     if ([modifiedAttributeKeys count]) {
418                 [self listObjectAttributesChanged:inObject modifiedKeys:modifiedAttributeKeys];
419     }
422 //Call after modifying an object's display attributes
423 //(When modifying display attributes in response to a status change, this is not necessary)
424 - (void)listObjectAttributesChanged:(AIListObject *)inObject modifiedKeys:(NSSet *)inModifiedKeys
426         if (updatesAreDelayed) {
427                 delayedAttributeChanges++;
428                 [delayedModifiedAttributeKeys unionSet:inModifiedKeys];
429         } else {
430         //Resort the contact list if necessary
431         if ([[self activeSortController] shouldSortForModifiedAttributeKeys:inModifiedKeys]) {
432                         [self sortListObject:inObject];
433         }
435         //Post an attributes changed message
436                 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
437                                                                                                   object:inObject
438                                                                                                 userInfo:(inModifiedKeys ?
439                                                                                                                   [NSDictionary dictionaryWithObject:inModifiedKeys
440                                                                                                                                                                           forKey:@"Keys"] :
441                                                                                                                   nil)];
442         }
445 //Performs any delayed list object/handle updates
446 - (void)_performDelayedUpdates:(NSTimer *)timer
448         BOOL    updatesOccured = (delayedStatusChanges || delayedAttributeChanges || delayedContactChanges);
450         //Send out global attribute & status changed notifications (to cover any delayed updates)
451         if (updatesOccured) {
452                 BOOL shouldSort = NO;
454                 //Inform observers of any changes
455                 if (delayedContactChanges) {
456                         delayedContactChanges = 0;
457                         shouldSort = YES;
458                 }
459                 if (delayedStatusChanges) {
460                         if (!shouldSort &&
461                            [[self activeSortController] shouldSortForModifiedStatusKeys:delayedModifiedStatusKeys]) {
462                                 shouldSort = YES;
463                         }
464                         [delayedModifiedStatusKeys removeAllObjects];
465                         delayedStatusChanges = 0;
466                 }
467                 if (delayedAttributeChanges) {
468                         if (!shouldSort &&
469                            [[self activeSortController] shouldSortForModifiedAttributeKeys:delayedModifiedAttributeKeys]) {
470                                 shouldSort = YES;
471                         }
472                         [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
473                                                                                                           object:nil
474                                                                                                         userInfo:(delayedModifiedAttributeKeys ?
475                                                                                                                           [NSDictionary dictionaryWithObject:delayedModifiedAttributeKeys
476                                                                                                                                                                                   forKey:@"Keys"] :
477                                                                                                                           nil)];
478                         [delayedModifiedAttributeKeys removeAllObjects];
479                         delayedAttributeChanges = 0;
480                 }
482                 //Sort only if necessary
483                 if (shouldSort) {
484                         [self sortContactList];
485                 }
486         }
488     //If no more updates are left to process, disable the update timer
489         //If there are no delayed update requests, remove the hold
490         if (!delayedUpdateTimer || !updatesOccured) {
491                 if (delayedUpdateTimer) {
492                         [delayedUpdateTimer invalidate];
493                         [delayedUpdateTimer release];
494                         delayedUpdateTimer = nil;
495                 }
496                 if (delayedUpdateRequests == 0) {
497                         updatesAreDelayed = NO;
498                 }
499     }
502 #pragma mark Contact Grouping
503 //Contact Grouping -----------------------------------------------------------------------------------------------------
505 //Redetermine the local grouping of a contact in response to server grouping information or an external change
506 - (void)listObjectRemoteGroupingChanged:(AIListContact *)inContact
508         AIListObject<AIContainingObject>        *containingObject;
509         NSString                                                        *remoteGroupName = [inContact remoteGroupName];
510         [inContact retain];
512         containingObject = [inContact containingObject];
514         if ([containingObject isKindOfClass:[AIMetaContact class]]) {
516                 /* If inContact's containingObject is a metaContact, and that metaContact has no containingObject,
517                  * use inContact's remote grouping as the metaContact's grouping.
518                  */
519                 if (![containingObject containingObject] && [remoteGroupName length]) {
520                         //If no similar objects exist, we add this contact directly to the list
521                         //Create a group for the contact even if contact list groups aren't on,
522                         //otherwise requests for all the contact list groups will return nothing
523                         AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
525                         localGroup = (useContactListGroups ?
526                                                   ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
527                                                   contactList);
529                         [localGroup addObject:containingObject];
530                         
531                         [self _listChangedGroup:localGroup object:containingObject];
532                         [[NSNotificationCenter defaultCenter] postNotificationName:@"Contact_ListChanged"
533                                                                                                                                 object:[localGroup containingObject]
534                                                                                                                           userInfo:nil];
535                         //NSLog(@"listObjectRemoteGroupingChanged: %@ is in %@, which was moved to %@",inContact,containingObject,localGroup);
536                 }
538         } else {
539                 //If we have a remoteGroupName, add the contact locally to the list
540                 if (remoteGroupName) {
541                         //Create a group for the contact even if contact list groups aren't on,
542                         //otherwise requests for all the contact list groups will return nothing
543                         AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
545                         localGroup = (useContactListGroups ?
546                                                   ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
547                                                   contactList);
549                         //NSLog(@"listObjectRemoteGroupingChanged: %@: remoteGroupName %@ --> %@",inContact,remoteGroupName,localGroup);
551                         [self _moveContactLocally:inContact
552                                                           toGroup:localGroup];
553                         
554                         if([[localGroup containingObject] isKindOfClass:[AIListGroup class]])
555                                 [(AIListGroup *)[localGroup containingObject] visibilityOfContainedObject:localGroup changedTo:YES];
556                         
557                 } else {
558                         //If !remoteGroupName, remove the contact from any local groups
559                         if (containingObject) {
560                                 //Remove the object
561                                 [(AIListGroup *)containingObject removeObject:inContact];
563                                 [self _listChangedGroup:(AIListGroup *)containingObject object:inContact];
564                                 
565                                 //NSLog(@"listObjectRemoteGroupingChanged: %@: -- !remoteGroupName so removed from %@",inContact,containingObject);
566                         }
567                 }
568         }
570         BOOL    isCurrentlyAStranger = [inContact isStranger];
571         if ((isCurrentlyAStranger && (remoteGroupName != nil)) ||
572            (!isCurrentlyAStranger && (remoteGroupName == nil))) {
573                 [inContact setStatusObject:(remoteGroupName ? [NSNumber numberWithBool:YES] : nil)
574                                                         forKey:@"NotAStranger"
575                                                         notify:NotifyLater];
576                 [inContact notifyOfChangedStatusSilently:YES];
577         }
579         [inContact release];
582 - (void)_moveContactLocally:(AIListContact *)listContact toGroup:(AIListGroup *)localGroup
584         AIListObject    *containingObject;
585         AIListObject    *existingObject;
586         BOOL                    performedGrouping = NO;
588         //Protect with a retain while we are removing and adding the contact to our arrays
589         [listContact retain];
591         //XXX
592 //      AILog(@"Moving %@ to %@",listContact,localGroup);
593         
594         //Remove this object from any local groups we have it in currently
595         if ((containingObject = [listContact containingObject]) &&
596            ([containingObject isKindOfClass:[AIListGroup class]])) {
597                 //Remove the object
598                 [(AIListGroup *)containingObject removeObject:listContact];
599                 [self _listChangedGroup:(AIListGroup *)containingObject object:listContact];
600         }
602         if ([listContact canJoinMetaContacts]) {
603                 if ((existingObject = [localGroup objectWithService:[listContact service] UID:[listContact UID]])) {
604                         //If an object exists in this group with the same UID and serviceID, create a MetaContact
605                         //for the two.
606                         [self groupListContacts:[NSArray arrayWithObjects:listContact,existingObject,nil]];
607                         performedGrouping = YES;
608                         
609                 } else {
610                         AIMetaContact   *metaContact;
611                         
612                         //If no object exists in this group which matches, we should check if there is already
613                         //a MetaContact holding a matching ListContact, since we should include this contact in it
614                         //If we found a metaContact to which we should add, do it.
615                         if ((metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]])) {
616                                 //XXX
617                                 //                      AILog(@"Found an existing metacontact; adding %@ to %@",listContact,metaContact);
618                                 
619                                 [self addListObject:listContact toMetaContact:metaContact];
620                                 performedGrouping = YES;
621                         }
622                 }
623         }
625         if (!performedGrouping) {
626                 //If no similar objects exist, we add this contact directly to the list
627                 [localGroup addObject:listContact];
629                 //Add
630                 [self _listChangedGroup:localGroup object:listContact];
631         }
633         //Cleanup
634         [listContact release];
637 - (AIListGroup *)remoteGroupForContact:(AIListContact *)inContact
639         AIListGroup             *group;
641         if ([inContact isKindOfClass:[AIMetaContact class]]) {
642                 //For a metaContact, the closest we have to a remote group is the group it is within locally
643                 group = [(AIMetaContact *)inContact parentGroup];
645         } else {
646                 NSString        *remoteGroup = [inContact remoteGroupName];
647                 group = (remoteGroup ? [self groupWithUID:remoteGroup] : nil);
648         }
650         return group;
653 //Post a list grouping changed notification for the object and group
654 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object
656         if (updatesAreDelayed) {
657                 delayedContactChanges++;
658         } else {
659                 [[adium notificationCenter] postNotificationName:Contact_ListChanged
660                                                                                                   object:object
661                                                                                                 userInfo:(group ? [NSDictionary dictionaryWithObject:group forKey:@"ContainingGroup"] : nil)];
662         }
665 - (BOOL)useContactListGroups
667         return useContactListGroups;
670 - (void)setUseContactListGroups:(BOOL)inFlag
672         if (inFlag != useContactListGroups) {
673                 useContactListGroups = inFlag;
675                 [self _performChangeOfUseContactListGroups];
676         }
679 - (void)_performChangeOfUseContactListGroups
681         NSEnumerator    *enumerator;
682         AIListObject    *listObject;
684         [self delayListObjectNotifications];
686         //Store the preference
687         [[adium preferenceController] setPreference:[NSNumber numberWithBool:!useContactListGroups]
688                                                                                  forKey:KEY_HIDE_CONTACT_LIST_GROUPS
689                                                                                   group:PREF_GROUP_CONTACT_LIST_DISPLAY];
691         //Configure the sort controller to force ignoring of groups as appropriate
692         [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
694         enumerator = [[[[contactList containedObjects] copy] autorelease] objectEnumerator];
696         if (useContactListGroups) { /* We are now using contact list groups, but we weren't before. */
698                 //Restore the grouping of all root-level contacts
699                 while ((listObject = [enumerator nextObject])) {
700                         if ([listObject isKindOfClass:[AIListContact class]]) {
701                                 [(AIListContact *)listObject restoreGrouping];
702                         }
703                 }
705         } else { /* We are no longer using contact list groups, but we were before. */
707                 while ((listObject = [enumerator nextObject])) {
708                         if ([listObject isKindOfClass:[AIListGroup class]]) {
709                                 NSArray                 *containedObjects;
710                                 NSEnumerator    *groupEnumerator;
711                                 AIListObject    *containedListObject;
713                                 containedObjects = [[(AIListGroup *)listObject containedObjects] copy];
714                                 groupEnumerator = [containedObjects objectEnumerator];
715                                 while ((containedListObject = [groupEnumerator nextObject])) {
716                                         if ([containedListObject isKindOfClass:[AIListContact class]]) {
717                                                 [self _moveContactLocally:(AIListContact *)containedListObject
718                                                                                   toGroup:contactList];
719                                         }
720                                 }
721                                 [containedObjects release];
722                         }
723                 }
724         }
726         //Stop delaying object notifications; this will automatically resort the contact list, so we're done.
727         [self endListObjectNotificationsDelay];
730 - (void)prepareShowHideGroups
732         //Load the preference
733         useContactListGroups = ![[[adium preferenceController] preferenceForKey:KEY_HIDE_CONTACT_LIST_GROUPS
734                                                                                                                                           group:PREF_GROUP_CONTACT_LIST_DISPLAY] boolValue];
736         //Show offline contacts menu item
737     menuItem_showGroups = [[NSMenuItem alloc] initWithTitle:SHOW_GROUPS_MENU_TITLE
738                                                                                                         target:self
739                                                                                                         action:@selector(toggleShowGroups:)
740                                                                                          keyEquivalent:@""];
741         [menuItem_showGroups setState:useContactListGroups];
742         [[adium menuController] addMenuItem:menuItem_showGroups toLocation:LOC_View_Toggles];
744         //Toolbar
745         NSToolbarItem   *toolbarItem;
746     toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SHOW_GROUPS_IDENTIFER
747                                                                                                                   label:AILocalizedString(@"Show Groups",nil)
748                                                                                                    paletteLabel:AILocalizedString(@"Toggle Groups Display",nil)
749                                                                                                                 toolTip:AILocalizedString(@"Toggle display of groups",nil)
750                                                                                                                  target:self
751                                                                                                 settingSelector:@selector(setImage:)
752                                                                                                         itemContent:[NSImage imageNamed:(useContactListGroups ?
753                                                                                                                                                                          @"togglegroups_transparent" :
754                                                                                                                                                                          @"togglegroups")
755                                                                                                                                                    forClass:[self class]
756                                                                                                                                                  loadLazily:YES]
757                                                                                                                  action:@selector(toggleShowGroupsToolbar:)
758                                                                                                                    menu:nil];
759     [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ContactList"];
762 - (IBAction)toggleShowGroups:(id)sender
764         //Flip-flop.
765         useContactListGroups = !useContactListGroups;
766         [menuItem_showGroups setState:useContactListGroups];
768         //Update the contact list.  Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure.
769         [self performSelector:@selector(_performChangeOfUseContactListGroups)
770                            withObject:nil
771                            afterDelay:0.000001];
774 - (IBAction)toggleShowGroupsToolbar:(id)sender
776         [self toggleShowGroups:sender];
778         [sender setImage:[NSImage imageNamed:(useContactListGroups ?
779                                                                                   @"togglegroups_transparent" :
780                                                                                   @"togglegroups")
781                                                                 forClass:[self class]]];
784 - (BOOL)useOfflineGroup
786         return useOfflineGroup;
789 - (void)setUseOfflineGroup:(BOOL)inFlag
791         if (inFlag != useOfflineGroup) {
792                 useOfflineGroup = inFlag;
793                 
794                 if (useOfflineGroup) {
795                         [self registerListObjectObserver:self]; 
796                 } else {
797                         [self updateAllListObjectsForObserver:self];
798                         [self unregisterListObjectObserver:self];       
799                 }
800         }
803 - (AIListGroup *)offlineGroup
805         return [self groupWithUID:AILocalizedString(@"Offline", "Name of offline group")];
808 #pragma mark Meta Contacts
809 //Meta Contacts --------------------------------------------------------------------------------------------------------
811  * @brief Create or load a metaContact
813  * @param inObjectID The objectID of an existing but unloaded metaContact, or nil to create and save a new metaContact
814  */
815 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID
817         NSString                *metaContactDictKey;
818         AIMetaContact   *metaContact;
819         BOOL                    shouldRestoreContacts = YES;
821         //If no object ID is provided, use the next available object ID
822         //(MetaContacts should always have an individually unique object id)
823         if (!inObjectID) {
824                 int topID = [[[adium preferenceController] preferenceForKey:TOP_METACONTACT_ID
825                                                                                                                           group:PREF_GROUP_CONTACT_LIST] intValue];
826                 inObjectID = [NSNumber numberWithInt:topID];
827                 [[adium preferenceController] setPreference:[NSNumber numberWithInt:([inObjectID intValue] + 1)]
828                                                                                          forKey:TOP_METACONTACT_ID
829                                                                                           group:PREF_GROUP_CONTACT_LIST];
831                 //No reason to waste time restoring contacts when none are in the meta contact yet.
832                 shouldRestoreContacts = NO;
833         }
835         //Look for a metacontact with this object ID.  If none is found, create one
836         //and add its contained contacts to it.
837         metaContactDictKey = [AIMetaContact internalObjectIDFromObjectID:inObjectID];
839         metaContact = [metaContactDict objectForKey:metaContactDictKey];
840         if (!metaContact) {
841                 metaContact = [[AIMetaContact alloc] initWithObjectID:inObjectID];
843                 //Keep track of it in our metaContactDict for retrieval by objectID
844                 [metaContactDict setObject:metaContact forKey:metaContactDictKey];
846                 //Add it to our more general contactDict, as well
847                 [contactDict setObject:metaContact forKey:[metaContact internalUniqueObjectID]];
849                 /* We restore contacts (actually, internalIDs for contacts, to be added as necessary later) if the metaContact
850                  * existed before this call to metaContactWithObjectID:
851                  */
852                 if (shouldRestoreContacts) {
853                         if (![self _restoreContactsToMetaContact:metaContact]) {
855                                 //If restoring the metacontact did not actually add any contacts, delete it since it is invalid
856                                 [self breakdownAndRemoveMetaContact:metaContact];
857                                 metaContact = nil;
858                         }
859                 }
860                 
861                 /* As with contactWithService:account:UID, update all attributes so observers are initially informed of
862                  * this object's existence.
863                  */
864                 [self _updateAllAttributesOfObject:metaContact];
865                 
866                 [metaContact release];
867         }
869         return (metaContact);
873  * @brief Associate the appropriate internal IDs for contained contacts with a metaContact
875  * @result YES if one or more contacts was associated with the metaContact; NO if none were.
876  */
877 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact
879         NSDictionary    *allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
880                                                                                                                                                                         group:PREF_GROUP_CONTACT_LIST];
881         NSArray                 *containedContactsArray = [allMetaContactsDict objectForKey:[metaContact internalObjectID]];
882         BOOL                    restoredContacts;
884         if ([containedContactsArray count]) {
885                 [self _restoreContactsToMetaContact:metaContact
886                                  fromContainedContactsArray:containedContactsArray];
888                 restoredContacts = YES;
890         } else {
891                 restoredContacts = NO;
892         }
894         return restoredContacts;
898  * @brief Associate the internal IDs for an array of contacts with a specific metaContact
900  * This does not actually place any AIListContacts within the metaContact.  Instead, it updates the contactToMetaContactLookupDict
901  * dictionary to have metaContact associated with the list contacts specified by containedContactsArray. This
902  * allows us to add them lazily to the metaContact (in contactWithService:account:UID:) as necessary.
904  * @param metaContact The metaContact to which contact referneces are added
905  * @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
906  */
907 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray
909         NSEnumerator            *enumerator = [containedContactsArray objectEnumerator];
910         NSDictionary            *containedContactDict;
912         while ((containedContactDict = [enumerator nextObject])) {
913                 /* Before Adium 0.80, metaContacts could be created within metaContacts. Simply ignore any attempt to restore
914                 * such erroneous data, which will have a YES boolValue for KEY_IS_METACONTACT. */
915                 if (![[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) {
916                         /* Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject.
917                          *
918                          * As listObjects are loaded/created/requested which match this internalObjectID, 
919                          * they will be inserted into the metaContact.
920                          */
921                         NSString        *internalObjectID = [AIListObject internalObjectIDForServiceID:[containedContactDict objectForKey:SERVICE_ID_KEY]
922                                                                                                                                                                    UID:[containedContactDict objectForKey:UID_KEY]];
923                         [contactToMetaContactLookupDict setObject:metaContact
924                                                                                            forKey:internalObjectID];
925                 }
926         }
930 //Add a list object to a meta contact, setting preferences and such
931 //so the association is lasting across program launches.
932 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
934         if (!listObject || [listObject isKindOfClass:[AIListGroup class]]) {
935                 //I can't think of why one would want to add an entire group to a metacontact. Let's say you can't.
936                 NSLog(@"Warning: addListObject:toMetaContact: Attempted to add %@ to %@",listObject,metaContact);
937                 return;
938         }
940         if (listObject == metaContact) return;
942         //If listObject contains other contacts, perform addListObject:toMetaContact: recursively
943         if ([listObject conformsToProtocol:@protocol(AIContainingObject)]) {
944                 NSEnumerator    *enumerator = [[[[(AIListObject<AIContainingObject> *)listObject containedObjects] copy] autorelease] objectEnumerator];
945                 AIListObject    *someObject;
947                 while ((someObject = [enumerator nextObject]))
948                         [self addListObject:someObject toMetaContact:metaContact];
950         } else {
951                 //Obtain any metaContact this listObject is currently within, so we can remove it later
952                 AIMetaContact *oldMetaContact = [contactToMetaContactLookupDict objectForKey:[listObject internalObjectID]];
954                 if ([self _performAddListObject:listObject toMetaContact:metaContact]) {
955                         //If this listObject was not in this metaContact in any form before, store the change
956                         if (metaContact != oldMetaContact) {
957                                 //Remove the list object from any other metaContact it is in at present
958                                 if (oldMetaContact)
959                                         [self removeListObject:listObject fromMetaContact:oldMetaContact];
961                                 [self _storeListObject:listObject inMetaContact:metaContact];
962                         }
963                 }
964         }
967 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact
969         //we only allow group->meta->contact, not group->meta->meta->contact
970         NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
971         
972 //      AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact);
973         NSDictionary            *containedContactDict;
974         NSMutableDictionary     *allMetaContactsDict;
975         NSMutableArray          *containedContactsArray;
977         NSString                        *metaContactInternalObjectID = [metaContact internalObjectID];
979         //Get the dictionary of all metaContacts
980         allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
981                                                                                                                                         group:PREF_GROUP_CONTACT_LIST] mutableCopy];
982         if (!allMetaContactsDict) {
983                 allMetaContactsDict = [[NSMutableDictionary alloc] init];
984         }
986         //Load the array for the new metaContact
987         containedContactsArray = [[allMetaContactsDict objectForKey:metaContactInternalObjectID] mutableCopy];
988         if (!containedContactsArray) containedContactsArray = [[NSMutableArray alloc] init];
989         containedContactDict = nil;
991         //Create the dictionary describing this list object
992         containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
993                 [[listObject service] serviceID],SERVICE_ID_KEY,
994                 [listObject UID],UID_KEY,nil];
996         //Only add if this dict isn't already in the array
997         if (containedContactDict && ([containedContactsArray indexOfObject:containedContactDict] == NSNotFound)) {
998                 [containedContactsArray addObject:containedContactDict];
999                 [allMetaContactsDict setObject:containedContactsArray forKey:metaContactInternalObjectID];
1001                 //Save
1002                 [self _saveMetaContacts:allMetaContactsDict];
1004                 [[adium contactAlertsController] mergeAndMoveContactAlertsFromListObject:listObject
1005                                                                                                                                   intoListObject:metaContact];
1006         }
1008         [allMetaContactsDict release];
1009         [containedContactsArray release];
1012 //Actually adds a list object to a meta contact. No preferences are changed.
1013 //Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict
1014 //for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful.
1015 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
1017         //we only allow group->meta->contact, not group->meta->meta->contact
1018         //FIXME: deal with smack contacts correctly.
1019         //NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);    
1020         AIListObject<AIContainingObject>        *localGroup;
1021         BOOL                                                            success;
1023         localGroup = [listObject containingObject];
1025         //Remove the object from its previous containing group
1026         if (localGroup && (localGroup != metaContact)) {
1027                 [localGroup removeObject:listObject];
1028                 [self _listChangedGroup:localGroup object:listObject];
1029         }
1031         //AIMetaContact will handle reassigning the list object's grouping to being itself
1032         if ((success = [metaContact addObject:listObject])) {
1033                 [contactToMetaContactLookupDict setObject:metaContact forKey:[listObject internalObjectID]];
1035                 [self _listChangedGroup:metaContact object:listObject];
1036                 //If the metaContact isn't in a group yet, use the group of the object we just added
1037                 if ((![metaContact containingObject]) && localGroup) {
1038                         //Add the new meta contact to our list
1039                         [(AIMetaContact *)localGroup addObject:metaContact];
1040                         [self _listChangedGroup:localGroup object:metaContact];
1041                 }
1042         }
1044         return success;
1047 - (void)removeAllListObjectsMatching:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1049         NSEnumerator    *enumerator;
1050         AIListObject    *theObject;
1051         
1052         enumerator = [[self allContactsWithService:[listObject service]
1053                                                                                    UID:[listObject UID]
1054                                                                   existingOnly:YES] objectEnumerator];
1056         //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1057         [contactToMetaContactLookupDict removeObjectForKey:[listObject internalObjectID]];
1059         [self delayListObjectNotifications];
1060         while ((theObject = [enumerator nextObject])) {
1061                 [self removeListObject:theObject fromMetaContact:metaContact];
1062         }
1063         [self endListObjectNotificationsDelay];
1066 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1068         //we only allow group->meta->contact, not group->meta->meta->contact
1069         NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
1070         
1071         NSEnumerator            *enumerator;
1072         NSArray                         *containedContactsArray;
1073         NSDictionary            *containedContactDict = nil;
1074         NSMutableDictionary     *allMetaContactsDict;
1075         NSString                        *metaContactInternalObjectID = [metaContact internalObjectID];
1077         //Get the dictionary of all metaContacts
1078         allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1079                                                                                                                                    group:PREF_GROUP_CONTACT_LIST];
1081         //Load the array for the metaContact
1082         containedContactsArray = [allMetaContactsDict objectForKey:metaContactInternalObjectID];
1084         //Enumerate it, looking only for the appropriate type of containedContactDict
1085         enumerator = [containedContactsArray objectEnumerator];
1087         NSString        *listObjectUID = [listObject UID];
1088         NSString        *listObjectServiceID = [[listObject service] serviceID];
1090         while ((containedContactDict = [enumerator nextObject])) {
1091                 if ([[containedContactDict objectForKey:UID_KEY] isEqualToString:listObjectUID] &&
1092                         [[containedContactDict objectForKey:SERVICE_ID_KEY] isEqualToString:listObjectServiceID]) {
1093                         break;
1094                 }
1095         }
1097         //If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result
1098         if (containedContactDict) {
1099                 NSMutableArray          *newContainedContactsArray;
1100                 NSMutableDictionary     *newAllMetaContactsDict;
1102                 newContainedContactsArray = [containedContactsArray mutableCopy];
1103                 [newContainedContactsArray removeObjectIdenticalTo:containedContactDict];
1105                 newAllMetaContactsDict = [allMetaContactsDict mutableCopy];
1106                 [newAllMetaContactsDict setObject:newContainedContactsArray
1107                                                                    forKey:metaContactInternalObjectID];
1109                 [self _saveMetaContacts:newAllMetaContactsDict];
1111                 [newContainedContactsArray release];
1112                 [newAllMetaContactsDict release];
1113         }
1115         //The listObject can be within the metaContact without us finding a containedContactDict if we are removing multiple
1116         //listContacts referring to the same UID & serviceID combination - that is, on multiple accounts on the same service.
1117         //We therefore request removal of the object regardless of the if (containedContactDict) check above.
1118         [metaContact removeObject:listObject];
1123  * @brief Groups UIDs for services into a single metacontact
1125  * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to
1126  * a UID and a service, respectively, which together define a contact which should be included in the grouping.
1128  * Assumption: This is only called after the contact list is finished loading, which occurs via
1129  * -(void)controllerDidLoad above.
1131  * @param UIDsArray NSArray of UIDs
1132  * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
1133  */
1134 - (AIMetaContact *)groupUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray
1136         NSMutableSet    *internalObjectIDs = [[NSMutableSet alloc] init];
1137         AIMetaContact   *metaContact = nil;
1138         NSEnumerator    *enumerator;
1139         NSString                *internalObjectID;
1140         int                             count = [UIDsArray count];
1141         int                             i;
1142                                 
1143         //Build an array of all contacts matching this description (multiple accounts on the same service listing
1144         //the same UID mean that we can have multiple AIListContact objects with a UID/service combination)
1145         for (i = 0; i < count; i++) {
1146                 NSString        *serviceID = [servicesArray objectAtIndex:i];
1147                 NSString        *UID = [UIDsArray objectAtIndex:i];
1148                 
1149                 internalObjectID = [AIListObject internalObjectIDForServiceID:serviceID
1150                                                                                                                                   UID:UID];
1151                 if(!metaContact) {
1152                         metaContact = [contactToMetaContactLookupDict objectForKey:internalObjectID];
1153                 }
1154                 
1155                 [internalObjectIDs addObject:internalObjectID];
1156         }
1157         
1158         //Create a new metaContact is we didn't find one.
1159         if (!metaContact) {
1160                 metaContact = [self metaContactWithObjectID:nil];
1161         }
1162         
1163         enumerator = [internalObjectIDs objectEnumerator];
1164         while ((internalObjectID = [enumerator nextObject])) {
1165                 AIListObject    *existingObject;
1166                 if ((existingObject = [self existingListObjectWithUniqueID:internalObjectID])) {
1167                         /* If there is currently an object (or multiple objects) matching this internalObjectID
1168                          * we should add immediately.
1169                          */
1170                         [self addListObject:existingObject
1171                                   toMetaContact:metaContact];   
1172                 } else {
1173                         /* If no objects matching this internalObjectID exist, we can simply add to the 
1174                          * contactToMetaContactLookupDict for use if such an object is created later.
1175                          */
1176                         [contactToMetaContactLookupDict setObject:metaContact
1177                                                                                            forKey:internalObjectID];                    
1178                 }
1179         }
1180         [internalObjectIDs release];
1182         return metaContact;
1185 /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added.
1187  * This will reuse an existing metacontact (for one of the contacts in the array) if possible.
1188  * @param contactsToGroupArray Contacts to group together
1189  */
1190 - (AIMetaContact *)groupListContacts:(NSArray *)contactsToGroupArray
1192         NSEnumerator    *enumerator;
1193         AIListContact   *listContact;
1194         AIMetaContact   *metaContact = nil;
1196         //Look for an existing MetaContact we can use.  The first one we find is the lucky winner.
1197         enumerator = [contactsToGroupArray objectEnumerator];
1198         while ((listContact = [enumerator nextObject]) && (metaContact == nil)) {
1199                 if ([listContact isKindOfClass:[AIMetaContact class]]) {
1200                         metaContact = (AIMetaContact *)listContact;
1201                 } else {
1202                         metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]];
1203                 }
1204         }
1206         //Create a new metaContact is we didn't find one.
1207         if (!metaContact) {
1208                 metaContact = [self metaContactWithObjectID:nil];
1209         }
1210         
1211         /* Add all these contacts to our MetaContact.
1212                 * Some may already be present, but that's fine, as nothing will happen.
1213                 */
1214         enumerator = [contactsToGroupArray objectEnumerator];
1215         while ((listContact = [enumerator nextObject])) {
1216                 [self addListObject:listContact toMetaContact:metaContact];
1217         }
1218         
1219         return metaContact;
1222 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact
1224         //Remove the objects within it from being inside it
1225         NSArray                                                         *containedObjects = [[metaContact containedObjects] copy];
1226         NSEnumerator                                            *metaEnumerator = [containedObjects objectEnumerator];
1227         AIListObject<AIContainingObject>        *containingObject = [metaContact containingObject];
1228         AIListObject                                            *object;
1230         NSMutableDictionary *allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1231                                                                                                                                                                                  group:PREF_GROUP_CONTACT_LIST] mutableCopy];
1233         while ((object = [metaEnumerator nextObject])) {
1235                 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1236                 [contactToMetaContactLookupDict removeObjectForKey:[object internalObjectID]];
1238                 [self removeListObject:object fromMetaContact:metaContact];
1239         }
1241         //Then, procede to remove the metaContact
1243         //Protect!
1244         [metaContact retain];
1246         //Remove it from its containing group
1247         [containingObject removeObject:metaContact];
1249         NSString        *metaContactInternalObjectID = [metaContact internalObjectID];
1251         //Remove our reference to it internally
1252         [metaContactDict removeObjectForKey:metaContactInternalObjectID];
1254         //Remove it from the preferences dictionary
1255         [allMetaContactsDict removeObjectForKey:metaContactInternalObjectID];
1257         //XXX - contactToMetaContactLookupDict
1259         //Post the list changed notification for the old containingObject
1260         [self _listChangedGroup:containingObject object:metaContact];
1262         //Save the updated allMetaContactsDict which no longer lists the metaContact
1263         [self _saveMetaContacts:allMetaContactsDict];
1265         //Protection is overrated.
1266         [metaContact release];
1267         [containedObjects release];
1268         [allMetaContactsDict release];
1271 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict
1273         AILog(@"MetaContacts: Saving!");
1274         [[adium preferenceController] setPreference:allMetaContactsDict
1275                                                                                  forKey:KEY_METACONTACT_OWNERSHIP
1276                                                                                   group:PREF_GROUP_CONTACT_LIST];
1277         [[adium preferenceController] setPreference:[allMetaContactsDict allKeys]
1278                                                                                  forKey:KEY_FLAT_METACONTACTS
1279                                                                                   group:PREF_GROUP_CONTACT_LIST];
1282 //Sort list objects alphabetically by their display name
1283 int contactDisplayNameSort(AIListObject *objectA, AIListObject *objectB, void *context)
1285         return [[objectA displayName] caseInsensitiveCompare:[objectB displayName]];
1288 //Return either the highest metaContact containing this list object, or the list object itself.  Appropriate for when
1289 //preferences should be read from/to the most generalized contact possible.
1290 - (AIListObject *)parentContactForListObject:(AIListObject *)listObject
1292         if ([listObject isKindOfClass:[AIListContact class]]) {
1293                 //Find the highest-up metaContact
1294                 AIListObject    *containingObject;
1295                 while ([(containingObject = [listObject containingObject]) isKindOfClass:[AIMetaContact class]]) {
1296                         listObject = (AIMetaContact *)containingObject;
1297                 }
1298         }
1300         return listObject;
1303 #pragma mark Preference observing
1305 * @brief Preferences changed
1306  */
1307 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
1308                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
1310         [self setUseOfflineGroup:((![[prefDict objectForKey:KEY_HIDE_CONTACTS] boolValue] ||
1311                                                           [[prefDict objectForKey:KEY_SHOW_OFFLINE_CONTACTS] boolValue]) &&
1312                                                           [[prefDict objectForKey:KEY_USE_OFFLINE_GROUP] boolValue])];
1316  * @brief Move contacts to and from the offline group as necessary as their online state changes.
1317  */
1318 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1320         if (!inModifiedKeys ||
1321                 [inModifiedKeys containsObject:@"Online"]) {
1323                 if ([inObject isKindOfClass:[AIListContact class]]) {                   
1324                         //If this contact is not its own parent contact, don't bother since we'll get an update for the parent if appropriate
1325                         if (inObject == [(AIListContact *)inObject parentContact]) {
1326                                 if (useOfflineGroup) {
1327                                         AIListObject *containingObject = [inObject containingObject];
1328                                         
1329                                         if ([inObject online] &&
1330                                                 (containingObject == [self offlineGroup])) {
1331                                                 [(AIListContact *)inObject restoreGrouping];
1332                                                 
1333                                         } else if (![inObject online] &&
1334                                                            containingObject &&
1335                                                            (containingObject != [self offlineGroup])) {
1336                                                 [self _moveContactLocally:(AIListContact *)inObject
1337                                                                                   toGroup:[self offlineGroup]];
1338                                         }
1339                                         
1340                                 } else {
1341                                         if ([inObject containingObject] == [self offlineGroup]) {
1342                                                 [(AIListContact *)inObject restoreGrouping];
1343                                         }
1344                                 }
1345                         }
1346                 }
1347         }
1348         
1349         return nil;
1352 //Contact Sorting --------------------------------------------------------------------------------
1353 #pragma mark Contact Sorting
1354 //Register sorting code
1355 - (void)registerListSortController:(AISortController *)inController
1357     [sortControllerArray addObject:inController];
1359 - (NSArray *)sortControllerArray
1361     return sortControllerArray;
1364 //Set and get the active sort controller
1365 - (void)setActiveSortController:(AISortController *)inController
1367     activeSortController = inController;
1369         [activeSortController didBecomeActive];
1371         //The newly-active sort controller needs to know whether it should be forced to ignore groups
1372         [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
1374     //Resort the list
1375     [self sortContactList];
1377 - (AISortController *)activeSortController
1379     return activeSortController;
1382 //Sort the entire contact list
1383 - (void)sortContactList
1385         // Detached lists
1386         NSEnumerator *i = [detachedContactLists objectEnumerator];
1387         AIListGroup *list;
1388         while((list = [i nextObject])){
1389                 [list sortGroupAndSubGroups:YES sortController:activeSortController];
1390         }
1391         
1392         // Main contact list
1393     [contactList sortGroupAndSubGroups:YES sortController:activeSortController];
1394         [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:nil];
1396 - (void)sortAContactList:(AIListGroup *)group
1398         [group sortGroupAndSubGroups:YES sortController:activeSortController];
1399         [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:nil];
1402 //Sort an individual object
1403 - (void)sortListObject:(AIListObject *)inObject
1405         if (updatesAreDelayed) {
1406                 delayedContactChanges++;
1407         } else {
1408                 AIListObject            *group = [inObject containingObject];
1410                 if ([group isKindOfClass:[AIListGroup class]]) {
1411                         //Sort the groups containing this object
1412                         [(AIListGroup *)group sortListObject:inObject sortController:activeSortController];
1413                         [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:inObject];
1414                 }
1415         }
1418 //List object observers ------------------------------------------------------------------------------------------------
1419 #pragma mark List object observers
1420 //Registers code to observe handle status changes
1421 - (void)registerListObjectObserver:(id <AIListObjectObserver>)inObserver
1423         //Add the observer
1424     [contactObservers addObject:[NSValue valueWithNonretainedObject:inObserver]];
1426     //Let the new observer process all existing objects
1427         [self updateAllListObjectsForObserver:inObserver];
1430 - (void)unregisterListObjectObserver:(id)inObserver
1432     [contactObservers removeObject:[NSValue valueWithNonretainedObject:inObserver]];
1437  * @brief Update all contacts for an observer, notifying the observer of each one in turn
1439  * @param contacts The contacts to update, or nil to update all contacts
1440  * @param inObserver The observer
1441  */
1442 - (void)updateContacts:(NSSet *)contacts forObserver:(id <AIListObjectObserver>)inObserver
1444         NSEnumerator    *enumerator;
1445         AIListObject    *listObject;
1447         [self delayListObjectNotifications];
1448         
1449         enumerator = (contacts ? [contacts objectEnumerator] : [contactDict objectEnumerator]);
1450         while ((listObject = [enumerator nextObject])) {
1451                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1452                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1453                 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1454                 
1455                 //If this contact is within a meta contact, update the meta contact too
1456                 AIListObject<AIContainingObject>        *containingObject = [listObject containingObject];
1457                 if (containingObject && [containingObject isKindOfClass:[AIMetaContact class]]) {
1458                         NSSet   *attributes = [inObserver updateListObject:containingObject
1459                                                                                                                   keys:nil
1460                                                                                                                 silent:YES];
1461                         if (attributes) [self listObjectAttributesChanged:containingObject
1462                                                                                                  modifiedKeys:attributes];
1463                 }
1464                 [pool release];
1465         }
1466         
1467         [self endListObjectNotificationsDelay];
1470 //Instructs a controller to update all available list objects
1471 - (void)updateAllListObjectsForObserver:(id <AIListObjectObserver>)inObserver
1473         NSEnumerator    *enumerator;
1474         AIListObject    *listObject;
1476         [self delayListObjectNotifications];
1478         //All contacts
1479         [self updateContacts:nil forObserver:inObserver];
1481     //Reset all groups
1482         enumerator = [groupDict objectEnumerator];
1483         while ((listObject = [enumerator nextObject])) {
1484                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1485                 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1486         }
1488         //Reset all accounts
1489         enumerator = [[[adium accountController] accounts] objectEnumerator];
1490         while ((listObject = [enumerator nextObject])) {
1491                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1492                 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1493         }
1495         [self endListObjectNotificationsDelay];
1499 //Notify observers of a status change.  Returns the modified attribute keys
1500 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
1502         NSMutableSet    *attrChange = nil;
1503         NSEnumerator    *enumerator;
1504         NSValue                 *observerValue;
1505         
1506         //Let our observers know
1507         enumerator = [contactObservers objectEnumerator];
1508         while ((observerValue = [enumerator nextObject])) {
1509                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1510                 id <AIListObjectObserver>       observer;
1511                 NSSet                                           *newKeys;
1513                 observer = [observerValue nonretainedObjectValue];
1514                 if ((newKeys = [observer updateListObject:inObject keys:modifiedKeys silent:silent])) {
1515                         if (!attrChange) attrChange = [[NSMutableSet alloc] init];
1516                         [attrChange unionSet:newKeys];
1517                 }
1518                 [pool release];
1519         }
1521         //Send out the notification for other observers
1522         [[adium notificationCenter] postNotificationName:ListObject_StatusChanged
1523                                                                                           object:inObject
1524                                                                                         userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys
1525                                                                                                                                                                                                  forKey:@"Keys"] : nil)];
1527         return [attrChange autorelease];
1530 //Command all observers to apply their attributes to an object
1531 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject
1533         NSEnumerator    *enumerator = [contactObservers objectEnumerator];
1534         NSValue                 *observerValue;
1536         while ((observerValue = [enumerator nextObject])) {             
1537                 id <AIListObjectObserver> observer = [observerValue nonretainedObjectValue];
1539                 [observer updateListObject:inObject keys:nil silent:YES];
1540         }
1545 //Contact List Access --------------------------------------------------------------------------------------------------
1546 #pragma mark Contact List Access
1547 //Returns the main contact list group
1548 - (AIListGroup *)contactList
1550     return contactList;
1554  * @brief Return an array of all contact list groups, detached or not
1555  */
1556 - (NSMutableArray *)allGroups
1558         NSEnumerator            *enumerator = [detachedContactLists objectEnumerator];
1559         NSMutableArray          *groups = [NSMutableArray array];
1560         AIListGroup                     *detachedGroup;
1562         // Main contact list
1563         [groups addObjectsFromArray:[contactList containedObjects]];
1564         
1565         // Detached groups
1566         while ((detachedGroup = [enumerator nextObject])) {
1567                 [groups addObjectsFromArray:[detachedGroup containedObjects]];
1568         }
1569         
1570         return groups;
1573 //Returns a flat array of all contacts (by calling through to -allContactsInObject:recurse:onAccount:)
1574 - (NSMutableArray *)allContacts
1576         return [self allContactsOnAccount:nil];
1579 //Returns a flat array of all contacts on a given account (by calling through to -allContactsInObject:recurse:onAccount:)
1580 - (NSMutableArray *)allContactsOnAccount:(AIAccount *)inAccount
1582         NSMutableArray *result = [self allContactsInObject:contactList recurse:YES onAccount:inAccount];
1583         
1584         /** Could be perfected I'm sure */
1585         NSEnumerator *enumerator = [detachedContactLists objectEnumerator];
1586         AIListGroup *detached;
1587         while((detached = [enumerator nextObject])){
1588                 [result addObjectsFromArray:[self allContactsInObject:detached recurse:YES onAccount:inAccount]];
1589         }
1590         
1591         return result;  
1594 //Return a flat array of all the objects in a group on an account (and all subgroups, if desired)
1595 - (NSMutableArray *)allContactsInObject:(AIListObject<AIContainingObject> *)inGroup recurse:(BOOL)recurse onAccount:(AIAccount *)inAccount
1597         NSParameterAssert(inGroup != nil);
1598         
1599         NSMutableArray  *contactArray = [NSMutableArray array];    
1601         NSEnumerator *enumerator = [[inGroup containedObjects] objectEnumerator];
1602         AIListObject *object;
1603     while ((object = [enumerator nextObject])) {
1604         if (recurse && [object conformsToProtocol:@protocol(AIContainingObject)]) {
1605                         [contactArray addObjectsFromArray:[self allContactsInObject:(AIListObject<AIContainingObject> *)object
1606                                                                                                                                 recurse:recurse
1607                                                                                                                           onAccount:inAccount]];
1608                 } else if ([object isMemberOfClass:[AIListContact class]] && (!inAccount || ([(AIListContact *)object account] == inAccount)))
1609                         [contactArray addObject:object];
1610         }
1612         return contactArray;
1615 //Return a flat array of all the bookmarks in a group on an account (and all subgroups, if desired)
1616 - (NSMutableArray *)allBookmarksInObject:(AIListObject<AIContainingObject> *)inGroup recurse:(BOOL)recurse onAccount:(AIAccount *)inAccount
1618         NSParameterAssert(inGroup != nil);
1619         
1620         NSMutableArray  *bookmarkArray = [NSMutableArray array];    
1621         
1622         NSEnumerator *enumerator = [[inGroup containedObjects] objectEnumerator];
1623         AIListObject *object;
1624     while ((object = [enumerator nextObject])) {
1625         if (recurse && [object conformsToProtocol:@protocol(AIContainingObject)]) {
1626                         [bookmarkArray addObjectsFromArray:[self allBookmarksInObject:(AIListObject<AIContainingObject> *)object
1627                                                                                                                                   recurse:recurse
1628                                                                                                                                 onAccount:inAccount]];
1629                 } else if ([object isMemberOfClass:[AIListBookmark class]] && (!inAccount || ([(AIListBookmark *)object account] == inAccount))) {
1630                         [bookmarkArray addObject:object];
1631                 }
1632         }
1634         return bookmarkArray;
1637 - (NSArray *)allBookmarks
1639         NSMutableArray *result = [self allBookmarksInObject:contactList recurse:YES onAccount:nil];
1640         
1641         /** Could be perfected I'm sure */
1642         NSEnumerator *enumerator = [detachedContactLists objectEnumerator];
1643         AIListGroup *detached;
1644         while((detached = [enumerator nextObject])){
1645                 [result addObjectsFromArray:[self allBookmarksInObject:detached recurse:YES onAccount:nil]];
1646         }
1647         
1648         return result;  
1651 //Contact List Menus- --------------------------------------------------------------------------------------------------
1652 #pragma mark Contact List Menus
1654 //Returns a menu containing all the groups within a group
1655 //- Selector called on group selection is selectGroup:
1656 //- The menu items represented object is the group it represents
1657 - (NSMenu *)menuOfAllGroupsInGroup:(AIListGroup *)inGroup withTarget:(id)target
1659         NSMenu  *menu = [[NSMenu alloc] initWithTitle:@""];
1661         [menu setAutoenablesItems:NO];
1662         [self _menuOfAllGroups:menu forGroup:inGroup withTarget:target level:0];
1664         return [menu autorelease];
1666 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level
1668         NSMutableArray  *fromGroups;
1669         NSEnumerator    *detachedEnumerator;
1670         
1671         //Passing nil scans the entire contact list
1672         if (group == nil){
1673                 fromGroups = [NSMutableArray arrayWithArray:detachedContactLists];
1674                 [fromGroups addObject:contactList];
1675         }else{
1676                 fromGroups = [NSMutableArray arrayWithObject:group];
1677         }
1679         detachedEnumerator = [fromGroups objectEnumerator];
1680         
1681         while((group = [detachedEnumerator nextObject])){
1682                 //Enumerate this group and process all groups we find within it
1683                 NSEnumerator    *enumerator;
1684                 AIListObject    *object;
1685                 enumerator = [[group containedObjects] objectEnumerator];
1686                 while ((object = [enumerator nextObject])) {
1687                         if ([object isKindOfClass:[AIListGroup class]] && object != [self offlineGroup]) {
1688                                 NSMenuItem      *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[object displayName]
1689                                                                                                                                                                                          target:target
1690                                                                                                                                                                                          action:@selector(selectGroup:)
1691                                                                                                                                                                           keyEquivalent:@""];
1692                                 [menuItem setRepresentedObject:object];
1693                                 if ([menuItem respondsToSelector:@selector(setIndentationLevel:)]) {
1694                                         [menuItem setIndentationLevel:level];
1695                                 }
1696                                 [menu addItem:menuItem];
1697                                 [menuItem release];
1698                                 
1699                                 [self _menuOfAllGroups:menu forGroup:(AIListGroup *)object withTarget:target level:level+1];
1700                         }
1701                 }
1702         }
1706 //Returns a menu containing all the objects in a group on an account
1707 //- Selector called on contact selection is selectContact:
1708 //- The menu item's represented object is the contact it represents
1709 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target{
1710         return [self menuOfAllContactsInContainingObject:inObject withTarget:target firstLevel:YES];
1712 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target firstLevel:(BOOL)firstLevel
1714     NSEnumerator                                *enumerator;
1715     AIListObject                                *object;
1717         //Prepare our menu
1718         NSMenu *menu = [[NSMenu alloc] init];
1719         [menu setAutoenablesItems:NO];
1721         //Passing nil scans the entire contact list
1722         if (inObject == nil) inObject = contactList;
1724         //The pull down menu needs an extra item at the top of its root menu to handle the selection.
1725         if (firstLevel) [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
1727         //All menu items for all contained objects
1728         enumerator = [[inObject listContacts] objectEnumerator];
1729     while ((object = [enumerator nextObject])) {
1730                 NSImage         *menuServiceImage;
1731                 NSMenuItem      *menuItem;
1732                 BOOL            needToCreateSubmenu;
1733                 BOOL            isGroup = [object isKindOfClass:[AIListGroup class]];
1734                 BOOL            isValidGroup = (isGroup &&
1735                                                                         [(AIListGroup *)object containedObjectsCount]);
1737                 //We don't want to include empty groups
1738                 if (!isGroup || isValidGroup) {
1740                         needToCreateSubmenu = (isValidGroup ||
1741                                                                    ([object isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)object listContacts] count] > 1)));
1743                         
1744                         menuServiceImage = [AIUserIcons menuUserIconForObject:object];
1745                         
1746                         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(needToCreateSubmenu ?
1747                                                                                                                                                                         [object displayName] :
1748                                                                                                                                                                         [object formattedUID])
1749                                                                                                                                                         target:target
1750                                                                                                                                                         action:@selector(selectContact:)
1751                                                                                                                                          keyEquivalent:@""];
1752                         
1753                         if (needToCreateSubmenu) {
1754                                 [menuItem setSubmenu:[self menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)object withTarget:target firstLevel:NO]];
1755                         }
1757                         [menuItem setRepresentedObject:object];
1758                         [menuItem setImage:menuServiceImage];
1759                         [menu addItem:menuItem];
1760                         [menuItem release];
1761                 }
1762         }
1764         return [menu autorelease];
1767 //Retrieving Specific Contacts -----------------------------------------------------------------------------------------
1768 #pragma mark Retrieving Specific Contacts
1770 - (AIListContact *)contactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1772     return [self contactWithService:inService account:inAccount UID:inUID usingClass:[AIListContact class]];
1775 //Retrieve a contact from the contact list (Creating if necessary)
1776 - (AIListContact *)contactWithService:(AIService *)inService 
1777                                                           account:(AIAccount *)inAccount 
1778                                                                   UID:(NSString *)inUID 
1779                                                    usingClass:(Class)ContactClass
1781         if (!(inUID && [inUID length] && inService)) return nil; //Ignore invalid requests
1783         AIListContact   *contact = nil;
1784         NSString                *key = [ContactClass internalUniqueObjectIDForService:inService
1785                                                                                                                                   account:inAccount
1786                                                                                                                                           UID:inUID];
1787         contact = [contactDict objectForKey:key];
1788         if (!contact) {
1789                 //Create
1790                 contact = [[ContactClass alloc] initWithUID:inUID account:inAccount service:inService];
1792                 //Do the update thing
1793                 [self _updateAllAttributesOfObject:contact];
1794                 
1795                 //Check to see if we should add to a metaContact
1796                 AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:[contact internalObjectID]];
1797                 if (metaContact) {
1798                         /* We already know to add this object to the metaContact, since we did it before with another object,
1799                            but this particular listContact is new and needs to be added directly to the metaContact
1800                            (on future launches, the metaContact will obtain it automatically since all contacts matching this UID
1801                            and serviceID should be included). */
1802                         [self _performAddListObject:contact toMetaContact:metaContact];
1803                 }
1804                 
1805                 //Set the contact as mobile if it is a phone number
1806                 if ([inUID characterAtIndex:0] == '+') {
1807                         [contact setIsMobile:YES notify:NotifyNever];
1808                 }
1810                 //Add
1811                 [contactDict setObject:contact forKey:key];
1812                 [contact release];
1813         }
1815         return contact;
1818 - (AIListBookmark *)bookmarkForChat:(AIChat *)inChat
1820         AIListBookmark *bookmark = [[AIListBookmark alloc] initWithChat:inChat];
1822         //Do the update thing
1823         [self _updateAllAttributesOfObject:bookmark];
1825         return [bookmark autorelease];
1828 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID usingClass:(Class)ContactClass
1830         if (inService && [inUID length]) {
1831                 return [contactDict objectForKey:[ContactClass internalUniqueObjectIDForService:inService
1832                                                                                                                                                                 account:inAccount
1833                                                                                                                                                                         UID:inUID]];
1834         } else {
1835                 return nil;
1836         }
1839 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1841         return [self existingContactWithService:inService account:inAccount UID:inUID usingClass:[AIListContact class]];
1845  * @brief Return a set of all contacts with a specified UID and service
1847  * @param service The AIService in question
1848  * @param inUID The UID, which should be normalized (lower case, no spaces, etc.) as appropriate for the service
1849  * @param existingOnly If YES, only pre-existing contacts. If NO, an AIListContact is guaranteed to be returned
1850  *                                         on each compatible account, even if one did not previously exist.
1851  */
1852 - (NSSet *)allContactsWithService:(AIService *)service UID:(NSString *)inUID existingOnly:(BOOL)existingOnly
1854         NSEnumerator    *enumerator;
1855         AIAccount               *account;
1856         NSMutableSet    *returnContactSet = [NSMutableSet set];
1858         enumerator = [[[adium accountController] accountsCompatibleWithService:service] objectEnumerator];
1860         while ((account = [enumerator nextObject])) {
1861                 AIListContact   *listContact;
1862                 
1863                 if (existingOnly) {
1864                         listContact = [self existingContactWithService:service
1865                                                                                                    account:account
1866                                                                                                            UID:inUID];
1867                 } else {
1868                         listContact = [self contactWithService:service
1869                                                                                    account:account
1870                                                                                            UID:inUID];
1871                 }
1873                 if (listContact) {
1874                         [returnContactSet addObject:listContact];
1875                 }
1876         }
1878         return returnContactSet;
1881 - (AIListObject *)existingListObjectWithUniqueID:(NSString *)uniqueID
1883         NSEnumerator    *enumerator;
1884         AIListObject    *listObject;
1886         //Contact
1887         enumerator = [contactDict objectEnumerator];
1888         while ((listObject = [enumerator nextObject])) {
1889                 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1890         }
1892         //Group
1893         enumerator = [groupDict objectEnumerator];
1894         while ((listObject = [enumerator nextObject])) {
1895                 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1896         }
1898         //Metacontact
1899         enumerator = [metaContactDict objectEnumerator];
1900         while ((listObject = [enumerator nextObject])) {
1901                 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1902         }
1904         return nil;
1907 - (AIListContact *)preferredContactForContentType:(NSString *)inType forListContact:(AIListContact *)inContact
1909         AIListContact   *returnContact = nil;
1910         AIAccount               *account;
1912         if ([inContact containsMultipleContacts]) {
1913                 AIListObject    *preferredContact;
1914                 NSString                *internalObjectID;
1916                 /* If we've messaged this object previously, prefer the last contact we sent to if that
1917                  * contact is currently in the most-available status the metacontact can offer
1918                  */
1919         internalObjectID = [inContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1920                                                                                                  group:OBJECT_STATUS_CACHE];
1922         if ((internalObjectID) &&
1923                    (preferredContact = [self existingListObjectWithUniqueID:internalObjectID]) &&
1924                    ([preferredContact isKindOfClass:[AIListContact class]]) &&
1925                    ([preferredContact statusSummary] == [inContact statusSummary]) &&
1926                         ([[(AIMetaContact *)inContact containedObjects] containsObject:preferredContact])) {
1927                         returnContact = [self preferredContactForContentType:inType
1928                                                                                                   forListContact:(AIListContact *)preferredContact];
1929         }
1931                 /* If the last contact we sent to is not appropriate, use the following algorithm, which differs from -[AIMetaContact preferredContact]
1932                  * in that it doesn't "like" mobile contacts.
1933                  *
1934                  *  1) Prefer available contacts who are not mobile
1935                  *  2) If no available non-mobile contacts, use the first online contact
1936                  *  3) If no online contacts, use the metacontact's preferredContact
1937                  */
1938 #warning This is a casting mess... define a protocol for metacontact-like AIListContact subclasses
1939                 if (!returnContact) {
1940                         //Recurse into metacontacts if necessary
1941                         AIListContact *firstAvailableContact = nil;
1942                         AIListContact *firstNotOfflineContact = nil;
1944                         AIListContact *thisContact;
1945                         NSEnumerator *contactsEnum = [[(AIMetaContact *)inContact containedObjects] objectEnumerator];
1946                         while ((thisContact = [contactsEnum nextObject])) {
1947                                 AIStatusType statusSummary = [thisContact statusSummary];
1949                                 if (statusSummary != AIOfflineStatus) {
1950                                         if (!firstNotOfflineContact) {
1951                                                 firstNotOfflineContact = thisContact;
1952                                         }
1954                                         if (statusSummary == AIAvailableStatus && ![thisContact isMobile]) {
1955                                                 if (!firstAvailableContact) {
1956                                                         firstAvailableContact = thisContact;
1957                                                 }
1959                                                 break;
1960                                         }
1961                                 }
1962                         }
1964                         returnContact = (firstAvailableContact ?
1965                                                          firstAvailableContact :
1966                                                          (firstNotOfflineContact ? firstNotOfflineContact : [(AIMetaContact *)inContact preferredContact]));
1967                 }
1969         } else {
1970                 //This contact doesn't contain multiple contacts... but it might still be a metacontact. Do NOT proceed with a metacontact.
1971                 if ([inContact respondsToSelector:@selector(preferredContact)])
1972                         inContact = [inContact performSelector:@selector(preferredContact)];
1974                 //find the best account for talking to this contact,
1975                 //and return an AIListContact on that account
1976                 account = [[adium accountController] preferredAccountForSendingContentType:inType
1977                                                                                                                                                  toContact:inContact];
1978                 if (account) {
1979                         if ([inContact account] == account) {
1980                                 returnContact = inContact;
1981                         } else {
1982                                 returnContact = [self contactWithService:[inContact service]
1983                                                                                                  account:account
1984                                                                                                          UID:[inContact UID]];
1985                         }
1986                 }
1987         }
1989         return returnContact;
1992 //Retrieve a list contact matching the UID and serviceID of the passed contact but on the specified account.
1993 //In many cases this will be the same as inContact.
1994 - (AIListContact *)contactOnAccount:(AIAccount *)account fromListContact:(AIListContact *)inContact
1996         if (account && ([inContact account] != account)) {
1997                 return [self contactWithService:[inContact service] account:account UID:[inContact UID]];
1998         } else {
1999                 return inContact;
2000         }
2003 //XXX - This is ridiculous.
2004 - (AIListContact *)preferredContactWithUID:(NSString *)inUID andServiceID:(NSString *)inService forSendingContentType:(NSString *)inType
2006         AIService               *theService = [[adium accountController] firstServiceWithServiceID:inService];
2007         AIListContact   *tempListContact = [[AIListContact alloc] initWithUID:inUID
2008                                                                                                                                 service:theService];
2009         AIAccount               *account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
2010                                                                                                                                                                           toContact:tempListContact
2011                                                                                                                                                                  includeOffline:YES];
2012         [tempListContact release];
2014         return [self contactWithService:theService account:account UID:inUID];
2019  * @brief Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts
2021  * If the destination contact's parent contact differs from the destination contact itself, the chat is with a metaContact.
2022  * If that metaContact's preferred destination for messaging isn't the same as the contact which was just messaged,
2023  * update the preference so that a new chat with this metaContact would default to the proper contact.
2024  */
2025 - (void)didSendContent:(NSNotification *)notification
2027         AIChat                  *chat = [[notification userInfo] objectForKey:@"AIChat"];
2028         AIListContact   *destContact = [chat listObject];
2029         AIListContact   *metaContact = [destContact parentContact];
2031         //it's not particularly obvious from the name, but -parentContact can return self
2032         if (metaContact == destContact) return;
2033         
2034         NSString        *destinationInternalObjectID = [destContact internalObjectID];
2035         NSString        *currentPreferredDestination = [metaContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
2036                                                                                                                                            group:OBJECT_STATUS_CACHE];
2038         if (![destinationInternalObjectID isEqualToString:currentPreferredDestination]) {
2039                 [metaContact setPreference:destinationInternalObjectID
2040                                                         forKey:KEY_PREFERRED_DESTINATION_CONTACT
2041                                                          group:OBJECT_STATUS_CACHE];
2042         }
2045 //Retrieving Groups ----------------------------------------------------------------------------------------------------
2046 #pragma mark Retrieving Groups
2048 //Retrieve a group from the contact list (Creating if necessary)
2049 - (AIListGroup *)groupWithUID:(NSString *)groupUID
2051         //Return our root group if it is requested. 
2052         //XXX: is this a good idea? it might semi-mask bugs where we accidentally pass nil
2053         if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME])
2054                 return [self contactList];
2056         AIListGroup             *group = nil;
2057         if (!(group = [groupDict objectForKey:[groupUID lowercaseString]])) {
2058                 //Create
2059                 group = [[AIListGroup alloc] initWithUID:groupUID];
2060                 
2061                 //Add
2062                 [self _updateAllAttributesOfObject:group];
2063                 [groupDict setObject:group forKey:[groupUID lowercaseString]];
2065                 //Add to the contact list
2066                 [contactList addObject:group];
2067                 [self _listChangedGroup:contactList object:group];
2068                 [group release];
2069         }
2071         return group;
2074 - (AIListGroup *)existingGroupWithUID:(NSString *)groupUID
2076         //Return our root group if it is requested
2077         //XXX: is this a good idea? it might semi-mask bugs where we accidentally pass nil
2078         if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME])
2079                 return [self contactList];
2081         return [groupDict objectForKey:groupUID];
2084 //Contact list editing -------------------------------------------------------------------------------------------------
2085 #pragma mark Contact list editing
2086 - (void)removeListObjects:(NSArray *)objectArray
2088         NSEnumerator    *enumerator = [objectArray objectEnumerator];
2089         AIListObject    *listObject;
2091         while ((listObject = [enumerator nextObject])) {
2092                 if ([listObject isKindOfClass:[AIMetaContact class]]) {
2093                         NSSet   *objectsToRemove = nil;
2095                         //If the metaContact only has one listContact, we will remove that contact from all accounts
2096                         if ([[(AIMetaContact *)listObject listContacts] count] == 1) {
2097                                 AIListContact   *listContact = [[(AIMetaContact *)listObject listContacts] objectAtIndex:0];
2098                                 
2099                                 objectsToRemove = [self allContactsWithService:[listContact service]
2100                                                                                                                    UID:[listContact UID]
2101                                                                                                   existingOnly:YES];
2102                         }
2104                         //And actually remove the single contact if applicable
2105                         if (objectsToRemove) {
2106                                 [self removeListObjects:[objectsToRemove allObjects]];
2107                         }
2108                         
2109                         //Now break the metaContact down, taking out all contacts and putting them back in the main list
2110                         [self breakdownAndRemoveMetaContact:(AIMetaContact *)listObject];                               
2112                 } else if ([listObject isKindOfClass:[AIListGroup class]]) {
2113                         AIListObject <AIContainingObject>       *containingObject = [listObject containingObject];
2114                         NSEnumerator    *enumerator;
2115                         AIAccount               *account;
2117                         //If this is a group, delete all the objects within it
2118                         [self removeListObjects:[(AIListGroup *)listObject containedObjects]];
2120                         //Delete the list off of all active accounts
2121                         enumerator = [[[adium accountController] accounts] objectEnumerator];
2122                         while ((account = [enumerator nextObject])) {
2123                                 if ([account online]) {
2124                                         [account deleteGroup:(AIListGroup *)listObject];
2125                                 }
2126                         }
2128                         //Then, procede to delete the group
2129                         [listObject retain];
2130                         [containingObject removeObject:listObject];
2131                         [groupDict removeObjectForKey:[[listObject UID] lowercaseString]];
2132                         [self _listChangedGroup:containingObject object:listObject];
2133                         [listObject release];
2135                 } else {
2136                         AIAccount       *account = [(AIListContact *)listObject account];
2137                         if ([account online]) {
2138                                 [account removeContacts:[NSArray arrayWithObject:listObject]];
2139                         }
2140                 }
2141         }
2144 - (void)addContacts:(NSArray *)contactArray toGroup:(AIListGroup *)group
2146         NSEnumerator    *enumerator;
2147         AIListContact   *listObject;
2149         [self delayListObjectNotifications];
2151         enumerator = [contactArray objectEnumerator];
2152         while ((listObject = [enumerator nextObject])) {
2153                 if(![group containsObject:listObject]) //don't add it if it's already there.
2154                         [[listObject account] addContacts:[NSArray arrayWithObject:listObject] toGroup:group];
2155         }
2157         [self endListObjectNotificationsDelay];
2160 - (void)requestAddContactWithUID:(NSString *)contactUID service:(AIService *)inService account:(AIAccount *)inAccount
2162         NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:contactUID
2163                                                                                                                                            forKey:UID_KEY];
2164         if (inService) [userInfo setObject:inService forKey:@"AIService"];
2165         if (inAccount) [userInfo setObject:inAccount forKey:@"AIAccount"];
2167         [[adium notificationCenter] postNotificationName:Contact_AddNewContact
2168                                                                                           object:nil
2169                                                                                         userInfo:userInfo];
2172 - (void)moveListObjects:(NSArray *)objectArray intoObject:(AIListObject<AIContainingObject> *)group index:(int)index
2174         NSEnumerator    *enumerator;
2175         AIListContact   *listContact;
2177         [self delayListObjectNotifications];
2179         if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2180                 [(id)group setDelayContainedObjectSorting:YES];
2181         }
2183         enumerator = [objectArray objectEnumerator];
2184         while ((listContact = [enumerator nextObject])) {
2185                 [self moveContact:listContact intoObject:group];
2187                 //Set the new index / position of the object
2188                 [self _positionObject:listContact atIndex:index inObject:group];
2189         }
2191         [self endListObjectNotificationsDelay];
2193         if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2194                 [(id)group setDelayContainedObjectSorting:NO];
2195         }
2197         /*
2198          Resort the entire list if we are moving within or between AIListGroup objects
2199          (other containing objects such as metaContacts will handle their own sorting).
2200         */
2201         if ([group isKindOfClass:[AIListGroup class]]){
2202                 [(AIListGroup *)group visibilityOfContainedObject:group changedTo:YES];
2203                 [self sortAContactList:(AIListGroup *)group];
2204         }
2207 - (void)moveContact:(AIListContact *)listContact intoObject:(AIListObject<AIContainingObject> *)group
2209         //Move the object to the new group only if necessary
2210         if (group == [listContact containingObject]) return;
2211         
2212         if ([group isKindOfClass:[AIListGroup class]]) {
2213                 //Move a contact into a new group
2214                 if ([listContact isKindOfClass:[AIListBookmark class]]) {
2215                         [self _moveContactLocally:listContact toGroup:(AIListGroup *)group];
2217                 } else if ([listContact isKindOfClass:[AIMetaContact class]]) {
2218                         //Move the meta contact to this new group
2219                         [self _moveContactLocally:listContact toGroup:(AIListGroup *)group];
2221                         NSEnumerator    *enumerator;
2222                         AIListContact   *actualListContact;
2224                         //This is a meta contact, move the objects within it.  listContacts will give us a flat array of AIListContacts.
2225                         enumerator = [[(AIMetaContact *)listContact listContacts] objectEnumerator];
2226                         while ((actualListContact = [enumerator nextObject])) {
2227                                 //Only move the contact if it is actually listed on the account in question
2228                                 if (![actualListContact isStranger]) {
2229                                         [self _moveObjectServerside:actualListContact toGroup:(AIListGroup *)group];
2230                                 }
2231                         }
2232                 } else if ([listContact isKindOfClass:[AIListContact class]]) {
2233                         //Move the object
2234                         [self _moveObjectServerside:listContact toGroup:(AIListGroup *)group];
2235                 } else if ([listContact isKindOfClass:[AIListGroup class]]) {
2236                         // Move contact from one contact list to another
2237                         [(AIListGroup *)listContact moveGroupFrom:[(AIListGroup *)listContact containingObject] to:group];
2238                 } else {
2239                         AILogWithSignature(@"I don't know what to do with %@",listContact);
2240                 }
2241         } else if ([group isKindOfClass:[AIMetaContact class]]) {
2242                 //Moving a contact into a meta contact
2243                 [self addListObject:listContact toMetaContact:(AIMetaContact *)group];
2244         }
2247 //Move an object to another group
2248 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group
2250         AIAccount       *account = [(AIListContact *)listObject account];
2251         if ([account online]) {
2252                 [account moveListObjects:[NSArray arrayWithObject:listObject] toGroup:group];
2253         }
2256 //Rename a group
2257 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName
2259         NSEnumerator    *enumerator = [[[adium accountController] accounts] objectEnumerator];
2260         AIAccount               *account;
2262         //Since Adium has no memory of what accounts a group is on, we have to send this message to all available accounts
2263         //The accounts without this group will just ignore it
2264         while ((account = [enumerator nextObject])) {
2265                 [account renameGroup:listGroup to:newName];
2266         }
2268         //Remove the old group if it's empty
2269         if ([listGroup containedObjectsCount] == 0) {
2270                 [self removeListObjects:[NSArray arrayWithObject:listGroup]];
2271         }
2274 //Position a list object within a group
2275 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inObject:(AIListObject<AIContainingObject> *)group
2277         if (index == 0) {
2278                 //Moved to the top of a group.  New index is between 0 and the lowest current index
2279                 [listObject setOrderIndex:([group smallestOrder] / 2.0)];
2281         } else if (index >= [group visibleCount]) {
2282                 //Moved to the bottom of a group.  New index is one higher than the highest current index
2283                 [listObject setOrderIndex:([group largestOrder] + 1.0)];
2285         } else {
2286                 //Moved somewhere in the middle.  New index is the average of the next largest and smallest index
2287                 AIListObject    *previousObject = [group objectAtIndex:index-1];
2288                 AIListObject    *nextObject = [group objectAtIndex:index];
2289                 float nextLowest = [previousObject orderIndex];
2290                 float nextHighest = [nextObject orderIndex];
2292                 /* XXX - Fixme as per below
2293                  * It's possible that nextLowest > nextHighest if ordering is not strictly based on the ordering indexes themselves.
2294                  * For example, a group sorted by status then manually could look like (status - ordering index):
2295                  *
2296                  * Away Contact - 100
2297                  * Away Contact - 120
2298                  * Offline Contact - 110
2299                  * Offline Contact - 113
2300                  * Offline Contact - 125
2301                  * 
2302                  * Dropping between Away Contact and Offline Contact should make an Away Contact be > 120 but an Offline Contact be < 110.
2303                  * Only the sort controller knows the answer as to where this contact should be positioned in the end.
2304                  */
2305                 //
2306                 [listObject setOrderIndex:((nextHighest + nextLowest) / 2.0)];
2307         }
2310 #pragma mark Authorization
2311 - (id)showAuthorizationRequestWithDict:(NSDictionary *)inDict forAccount:(AIAccount *)inAccount
2313         return [adiumAuthorization showAuthorizationRequestWithDict:inDict forAccount:inAccount];
2316 //Detached Contact Lists ----------------------------------------------------------------------------------------------------
2317 #pragma mark Detached Contact Lists
2320  * @returns Empty contact list
2321  */
2322 - (AIListGroup *)createDetachedContactList{
2323         static int count = 0;
2324         AIListGroup * list = [[AIListGroup alloc] initWithUID:[NSString stringWithFormat:@"Detached%d",count++]];
2325         [detachedContactLists addObject:list];
2326         [list release];
2327         return list;
2331  * @brief Removes detached contact list
2332  */
2333 - (void)removeDetachedContactList:(AIListGroup *)detachedList{
2334         [detachedContactLists removeObject:detachedList];
2338  * @breif Checks if a particular group is in a detached contact list 
2339  */
2340 - (BOOL)isGroupDetached:(AIListObject *)group{
2341         NSEnumerator *i = [detachedContactLists objectEnumerator];
2342         AIListGroup *currentGroup;
2343         
2344         while((currentGroup = [i nextObject])){
2345                         if(currentGroup == group)
2346                                 return YES;
2347         }
2348         return NO;
2352  * @returns Number of contact lists (ie. both main contact list and all detached contact lists)
2353  */
2354 - (unsigned)contactListCount {
2355         return (contactList!=nil) + [detachedContactLists count];
2359 @end