Source/AIContactController.m
author Zachary West <zacw@adium.im>
Fri Nov 06 12:37:12 2009 -0500 (2009-11-06)
changeset 2875 8983d9241c62
parent 2805 9b757472094b
child 2936 366008549f0c
permissions -rw-r--r--
Backed out changeset 791780625051

This is causing regressions in a few other locations. Backing out for the next beta to investigate after. Refs #13221.
     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  */
    16 
    17 #import "AIContactController.h"
    18 
    19 #import "AISCLViewPlugin.h"
    20 #import <Adium/AIContactHidingController.h>
    21 
    22 #import <Adium/AIAccountControllerProtocol.h>
    23 #import <Adium/AIInterfaceControllerProtocol.h>
    24 #import <Adium/AILoginControllerProtocol.h>
    25 #import <Adium/AIMenuControllerProtocol.h>
    26 #import <Adium/AIToolbarControllerProtocol.h>
    27 #import <Adium/AIContactAlertsControllerProtocol.h>
    28 
    29 #import <AIUtilities/AIArrayAdditions.h>
    30 #import <AIUtilities/AIDictionaryAdditions.h>
    31 #import <AIUtilities/AIFileManagerAdditions.h>
    32 #import <AIUtilities/AIMenuAdditions.h>
    33 #import <AIUtilities/AIToolbarUtilities.h>
    34 #import <AIUtilities/AIApplicationAdditions.h>
    35 #import <AIUtilities/AIImageAdditions.h>
    36 #import <AIUtilities/AIStringAdditions.h>
    37 #import <Adium/AIAccount.h>
    38 #import <Adium/AIChat.h>
    39 #import <Adium/AIContentMessage.h>
    40 #import <Adium/AIListContact.h>
    41 #import <Adium/AIListGroup.h>
    42 #import <Adium/AIListObject.h>
    43 #import <Adium/AIMetaContact.h>
    44 #import <Adium/AIService.h>
    45 #import <Adium/AISortController.h>
    46 #import <Adium/AIUserIcons.h>
    47 #import <Adium/AIServiceIcons.h>
    48 #import <Adium/AIListBookmark.h>
    49 #import <Adium/AIContactList.h>
    50 
    51 #define KEY_FLAT_GROUPS					@"FlatGroups"			//Group storage
    52 #define KEY_FLAT_CONTACTS				@"FlatContacts"			//Contact storage
    53 #define KEY_FLAT_METACONTACTS			@"FlatMetaContacts"		//Metacontact objectID storage
    54 #define KEY_BOOKMARKS					@"Bookmarks"
    55 
    56 #define	OBJECT_STATUS_CACHE				@"Object Status Cache"
    57 
    58 
    59 #define TOP_METACONTACT_ID				@"TopMetaContactID"
    60 #define KEY_IS_METACONTACT				@"isMetaContact"
    61 #define KEY_OBJECTID					@"objectID"
    62 #define KEY_METACONTACT_OWNERSHIP		@"MetaContact Ownership"
    63 #define CONTACT_DEFAULT_PREFS			@"ContactPrefs"
    64 
    65 #define	SHOW_GROUPS_MENU_TITLE			AILocalizedString(@"Show Groups",nil)
    66 
    67 #define SHOW_GROUPS_IDENTIFER			@"ShowGroups"
    68 
    69 #define SERVICE_ID_KEY					@"ServiceID"
    70 #define UID_KEY							@"UID"
    71 
    72 @interface AIListObject ()
    73 @property (readwrite, nonatomic) CGFloat orderIndex;
    74 @end
    75 
    76 @interface AIMetaContact ()
    77 - (void)removeObject:(AIListObject *)inObject;
    78 - (BOOL)addObject:(AIListObject *)inObject;
    79 - (AIListContact *)preferredContactForContentType:(NSString *)inType;
    80 @end
    81 
    82 @interface AIListGroup ()
    83 - (void)removeObject:(AIListObject *)inObject;
    84 - (BOOL)addObject:(AIListObject *)inObject;
    85 @end
    86 
    87 @interface AIContactList ()
    88 - (void)removeObject:(AIListObject *)inObject;
    89 - (BOOL)addObject:(AIListObject *)inObject;
    90 @end
    91 
    92 @interface AIListBookmark ()
    93 //Freshly minted bookmarks don't know where to restore to, since they have no serverside counterpart. This tells them.
    94 - (void)setInitialGroup:(AIListGroup *)inGroup;
    95 @end
    96 
    97 @interface AIContactController ()
    98 @property (readwrite, nonatomic) BOOL useOfflineGroup;
    99 - (void)saveContactList;
   100 - (void)_loadBookmarks;
   101 - (void)_didChangeContainer:(AIListObject<AIContainingObject> *)inContainingObject object:(AIListObject *)object;
   102 - (void)prepareShowHideGroups;
   103 - (void)_performChangeOfUseContactListGroups;
   104 
   105 //MetaContacts
   106 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact;
   107 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray;
   108 - (void)addContact:(AIListContact *)inContact toMetaContact:(AIMetaContact *)metaContact;
   109 - (BOOL)_performAddContact:(AIListContact *)inContact toMetaContact:(AIMetaContact *)metaContact;
   110 - (void)removeContact:(AIListContact *)inContact fromMetaContact:(AIMetaContact *)metaContact;
   111 - (void)_loadMetaContactsFromArray:(NSArray *)array;
   112 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict;
   113 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact;
   114 @end
   115 
   116 @implementation AIContactController
   117 
   118 - (id)init
   119 {
   120 	if ((self = [super init])) {
   121 		//
   122 		contactDict = [[NSMutableDictionary alloc] init];
   123 		groupDict = [[NSMutableDictionary alloc] init];
   124 		metaContactDict = [[NSMutableDictionary alloc] init];
   125 		bookmarkDict = [[NSMutableDictionary alloc] init];
   126 		contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
   127 		contactLists = [[NSMutableArray alloc] init];
   128 
   129 		contactPropertiesObserverManager = [AIContactObserverManager sharedManager];
   130 	}
   131 	
   132 	return self;
   133 }
   134 
   135 - (void)controllerDidLoad
   136 {	
   137 	//Default contact preferences
   138 	[adium.preferenceController registerDefaults:[NSDictionary dictionaryNamed:CONTACT_DEFAULT_PREFS
   139 																		forClass:[self class]]
   140 										  forGroup:PREF_GROUP_CONTACT_LIST];
   141 	
   142 	contactList = [[AIContactList alloc] initWithUID:ADIUM_ROOT_GROUP_NAME];
   143 	[contactLists addObject:contactList];
   144 	//Root is always "expanded"
   145 	[contactList setExpanded:YES];
   146 	
   147 	//Show Groups menu item
   148 	[self prepareShowHideGroups];
   149 	
   150 	//Observe content (for preferredContactForContentType:forListContact:)
   151     [[NSNotificationCenter defaultCenter] addObserver:self
   152                                    selector:@selector(didSendContent:)
   153                                        name:CONTENT_MESSAGE_SENT
   154                                      object:nil];
   155 	
   156 	[self loadContactList];
   157 	[self sortContactList];
   158 	
   159 	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST_DISPLAY];
   160 }
   161 
   162 - (void)controllerWillClose
   163 {
   164 	[self saveContactList];
   165 }
   166 
   167 - (void)dealloc
   168 {
   169 	[adium.preferenceController unregisterPreferenceObserver:self];
   170 		
   171 	[contactDict release];
   172 	[groupDict release];
   173 	[metaContactDict release];
   174 	[contactToMetaContactLookupDict release];
   175 	[contactLists release];
   176 	[bookmarkDict release];
   177 	
   178 	[contactPropertiesObserverManager release];
   179 
   180 	[super dealloc];
   181 }
   182 
   183 - (void)clearAllMetaContactData
   184 {
   185 	if (metaContactDict.count) {
   186 		[contactPropertiesObserverManager delayListObjectNotifications];
   187 		
   188 		//Remove all the metaContacts to get any existing objects out of them
   189 		for (AIMetaContact *metaContact in [[[metaContactDict copy] autorelease] objectEnumerator]) {
   190 			[self explodeMetaContact:metaContact];
   191 		}
   192 		
   193 		[contactPropertiesObserverManager endListObjectNotificationsDelay];
   194 	}
   195 	
   196 	[metaContactDict release]; metaContactDict = [[NSMutableDictionary alloc] init];
   197 	[contactToMetaContactLookupDict release]; contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
   198 	
   199 	//Clear the preferences for good measure
   200 	[adium.preferenceController setPreference:nil
   201 										 forKey:KEY_FLAT_METACONTACTS
   202 										  group:PREF_GROUP_CONTACT_LIST];
   203 	[adium.preferenceController setPreference:nil
   204 										 forKey:KEY_METACONTACT_OWNERSHIP
   205 										  group:PREF_GROUP_CONTACT_LIST];
   206 	
   207 	//Clear out old metacontact files
   208 	[[NSFileManager defaultManager] removeFilesInDirectory:[[adium.loginController userDirectory] stringByAppendingPathComponent:OBJECT_PREFS_PATH]
   209 												withPrefix:@"MetaContact"
   210 											 movingToTrash:NO];
   211 	[[NSFileManager defaultManager] removeFilesInDirectory:[adium cachesPath]
   212 												withPrefix:@"MetaContact"
   213 											 movingToTrash:NO];
   214 }
   215 
   216 #pragma mark Local Contact List Storage
   217 //Load the contact list
   218 - (void)loadContactList
   219 {
   220 	//We must load all the groups before loading contacts for the ordering system to work correctly.
   221 	[self _loadMetaContactsFromArray:[adium.preferenceController preferenceForKey:KEY_FLAT_METACONTACTS
   222 																			  group:PREF_GROUP_CONTACT_LIST]];
   223 	[self _loadBookmarks];
   224 }
   225 
   226 //Save the contact list
   227 - (void)saveContactList
   228 {
   229 	for (AIListGroup *listGroup in [groupDict objectEnumerator]) {
   230 		[listGroup setPreference:[NSNumber numberWithBool:[listGroup isExpanded]]
   231 						  forKey:KEY_EXPANDED
   232 						   group:PREF_GROUP_CONTACT_LIST];
   233 	}
   234 	
   235 	NSMutableArray *bookmarks = [NSMutableArray array];
   236 	for (AIListBookmark *bookmark in self.allBookmarks) {
   237 		[bookmarks addObject:[NSKeyedArchiver archivedDataWithRootObject:bookmark]];
   238 	}
   239 	
   240 	[adium.preferenceController setPreference:bookmarks
   241 									   forKey:KEY_BOOKMARKS
   242 										group:PREF_GROUP_CONTACT_LIST];
   243 }
   244 
   245 - (void)_loadBookmarks
   246 {
   247 	for (NSData *data in [adium.preferenceController preferenceForKey:KEY_BOOKMARKS group:PREF_GROUP_CONTACT_LIST]) {
   248 		//As a bookmark is initialized, it will add itself to the contact list in the right place
   249 		AIListBookmark	*bookmark = [NSKeyedUnarchiver unarchiveObjectWithData:data];
   250 
   251 		if(bookmark) {
   252 			if ([bookmarkDict objectForKey:bookmark.internalObjectID]) {
   253 				// In case we end up with two bookmarks with the same internalObjectID; this should be almost impossible.
   254 				[self removeBookmark:[bookmarkDict objectForKey:bookmark.internalObjectID]];
   255 			}
   256 			
   257 			[bookmarkDict setObject:bookmark forKey:bookmark.internalObjectID];
   258 			
   259 			//It's a newly created object, so set its initial attributes
   260 			[contactPropertiesObserverManager _updateAllAttributesOfObject:bookmark];
   261 		}
   262 	}
   263 }
   264 
   265 - (void)_loadMetaContactsFromArray:(NSArray *)array
   266 {	
   267 	for (NSString *identifier in array) {
   268 		NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   269 		NSNumber *objectID = [NSNumber numberWithInteger:[[[identifier componentsSeparatedByString:@"-"] objectAtIndex:1] integerValue]];
   270 		[self metaContactWithObjectID:objectID];
   271 		[pool release];
   272 	}
   273 }
   274 
   275 #pragma mark Contact Grouping
   276 
   277 - (void)_addContactLocally:(AIListContact *)listContact toGroup:(AIListGroup *)localGroup
   278 {
   279 	BOOL			performedGrouping = NO;
   280 	
   281 	//Protect with a retain while we are removing and adding the contact to our arrays
   282 	[listContact retain];
   283 	
   284 	if (listContact.canJoinMetaContacts) {
   285 		AIListObject *existingObject = [localGroup objectWithService:listContact.service UID:listContact.UID];
   286 		if (existingObject) {
   287 			//If an object exists in this group with the same UID and serviceID, create a MetaContact
   288 			//for the two.
   289 			[self groupContacts:[NSArray arrayWithObjects:listContact,existingObject,nil]];
   290 			performedGrouping = YES;
   291 			
   292 		} else {
   293 			AIMetaContact	*metaContact = [contactToMetaContactLookupDict objectForKey:listContact.internalObjectID];
   294 			
   295 			//If no object exists in this group which matches, we should check if there is already
   296 			//a MetaContact holding a matching ListContact, since we should include this contact in it
   297 			//If we found a metaContact to which we should add, do it.
   298 			if (metaContact) {
   299 				[self addContact:listContact toMetaContact:metaContact];
   300 				performedGrouping = YES;
   301 			}
   302 		}
   303 	}
   304 	
   305 	if (!performedGrouping) {
   306 		//If no similar objects exist, we add this contact directly to the list
   307 		[localGroup addObject:listContact];
   308 		
   309 		//Add
   310 		[self _didChangeContainer:localGroup object:listContact];
   311 	}
   312 	
   313 	//Cleanup
   314 	[listContact release];
   315 }
   316 
   317 - (void)_moveContactLocally:(AIListContact *)listContact fromGroups:(NSSet *)oldGroups toGroups:(NSSet *)groups
   318 {
   319 	//Protect with a retain while we are removing and adding the contact to our arrays
   320 	[listContact retain];
   321 	
   322 	[contactPropertiesObserverManager delayListObjectNotifications];
   323 	
   324 	//Remove this object from any local groups we have it in currently
   325 	for (AIListGroup *group in oldGroups) {
   326 		[group removeObject:listContact];
   327 		[self _didChangeContainer:group object:listContact];
   328 	}
   329 	
   330 	for (AIListGroup *group in groups)
   331 		[self _addContactLocally:listContact toGroup:group];
   332 	
   333 	[contactPropertiesObserverManager endListObjectNotificationsDelay];
   334 	
   335 	[listContact release];
   336 }
   337 
   338 //Post a list grouping changed notification for the object and containing object
   339 - (void)_didChangeContainer:(AIListObject<AIContainingObject> *)inContainingObject object:(AIListObject *)object
   340 {
   341 	if ([contactPropertiesObserverManager updatesAreDelayed]) {
   342 		[contactPropertiesObserverManager noteContactChanged:object];
   343 
   344 	} else {
   345 		[[NSNotificationCenter defaultCenter] postNotificationName:Contact_ListChanged
   346 												  object:inContainingObject
   347 												userInfo:nil];
   348 	}
   349 }
   350 
   351 - (BOOL)useContactListGroups
   352 {
   353 	return useContactListGroups;
   354 }
   355 
   356 - (void)setUseContactListGroups:(BOOL)inFlag
   357 {
   358 	if (inFlag != useContactListGroups) {
   359 		useContactListGroups = inFlag;
   360 		
   361 		[self _performChangeOfUseContactListGroups];
   362 	}
   363 }
   364 
   365 - (void)_performChangeOfUseContactListGroups
   366 {
   367 	[contactPropertiesObserverManager delayListObjectNotifications];
   368 	
   369 	//Store the preference
   370 	[adium.preferenceController setPreference:[NSNumber numberWithBool:!useContactListGroups]
   371 										 forKey:KEY_HIDE_CONTACT_LIST_GROUPS
   372 										  group:PREF_GROUP_CONTACT_LIST_DISPLAY];
   373 	
   374 	//Configure the sort controller to force ignoring of groups as appropriate
   375 	[[AISortController activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
   376 	
   377 	//Restore the grouping of all root-level contacts
   378 	for (AIListContact *contact in [self contactEnumerator]) {
   379 		[contact restoreGrouping];
   380 	}
   381 	
   382 	//Restore the grouping of all list bookmarks
   383 	for (AIListBookmark *bookmark in [self allBookmarks]) {
   384 		[bookmark restoreGrouping];
   385 	}
   386 
   387 	//Stop delaying object notifications; this will automatically resort the contact list, so we're done.
   388 	[contactPropertiesObserverManager endListObjectNotificationsDelay];
   389 }
   390 
   391 - (void)prepareShowHideGroups
   392 {
   393 	//Load the preference
   394 	useContactListGroups = ![[adium.preferenceController preferenceForKey:KEY_HIDE_CONTACT_LIST_GROUPS
   395 																	  group:PREF_GROUP_CONTACT_LIST_DISPLAY] boolValue];
   396 	
   397 	//Show offline contacts menu item
   398     menuItem_showGroups = [[NSMenuItem alloc] initWithTitle:SHOW_GROUPS_MENU_TITLE
   399 													 target:self
   400 													 action:@selector(toggleShowGroups:)
   401 											  keyEquivalent:@""];
   402 	
   403 	[menuItem_showGroups setState:useContactListGroups];
   404 	
   405 	[adium.menuController addMenuItem:menuItem_showGroups toLocation:LOC_View_Toggles];
   406 	
   407 	//Toolbar
   408 	NSToolbarItem	*toolbarItem;
   409     toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SHOW_GROUPS_IDENTIFER
   410 														  label:AILocalizedString(@"Show Groups",nil)
   411 												   paletteLabel:AILocalizedString(@"Toggle Groups Display",nil)
   412 														toolTip:AILocalizedString(@"Toggle display of groups",nil)
   413 														 target:self
   414 												settingSelector:@selector(setImage:)
   415 													itemContent:[NSImage imageNamed:(useContactListGroups ?
   416 																					 @"togglegroups_transparent" :
   417 																					 @"togglegroups")
   418 																		   forClass:[self class]
   419 																		 loadLazily:YES]
   420 														 action:@selector(toggleShowGroupsToolbar:)
   421 														   menu:nil];
   422     [adium.toolbarController registerToolbarItem:toolbarItem forToolbarType:@"ContactList"];
   423 }
   424 
   425 - (IBAction)toggleShowGroups:(id)sender
   426 {
   427 	//Flip-flop.
   428 	useContactListGroups = !useContactListGroups;
   429 	[menuItem_showGroups setState:useContactListGroups];
   430 
   431 	//Update the contact list.  Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure.
   432 	[self performSelector:@selector(_performChangeOfUseContactListGroups)
   433 			   withObject:nil
   434 			   afterDelay:0];
   435 }
   436 
   437 - (IBAction)toggleShowGroupsToolbar:(id)sender
   438 {
   439 	[self toggleShowGroups:sender];
   440 	
   441 	[sender setImage:[NSImage imageNamed:(useContactListGroups ?
   442 										  @"togglegroups_transparent" :
   443 										  @"togglegroups")
   444 								forClass:[self class]]];
   445 }
   446 
   447 @synthesize useOfflineGroup;
   448 
   449 - (AIListGroup *)offlineGroup
   450 {
   451 	return [self groupWithUID:AILocalizedString(@"Offline", "Name of offline group")];
   452 }
   453 
   454 #pragma mark Meta Contacts
   455 
   456 /*!
   457  * @brief Create or load a metaContact
   458  *
   459  * @param inObjectID The objectID of an existing but unloaded metaContact, or nil to create and save a new metaContact
   460  */
   461 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID
   462 {
   463 	BOOL			shouldRestoreContacts = YES;
   464 	
   465 	//If no object ID is provided, use the next available object ID
   466 	//(MetaContacts should always have an individually unique object id)
   467 	if (!inObjectID) {
   468 		NSInteger topID = [[adium.preferenceController preferenceForKey:TOP_METACONTACT_ID
   469 															  group:PREF_GROUP_CONTACT_LIST] integerValue];
   470 		inObjectID = [NSNumber numberWithInteger:topID];
   471 		[adium.preferenceController setPreference:[NSNumber numberWithInteger:([inObjectID integerValue] + 1)]
   472 											 forKey:TOP_METACONTACT_ID
   473 											  group:PREF_GROUP_CONTACT_LIST];
   474 		
   475 		//No reason to waste time restoring contacts when none are in the meta contact yet.
   476 		shouldRestoreContacts = NO;
   477 	}
   478 	
   479 	//Look for a metacontact with this object ID.  If none is found, create one
   480 	//and add its contained contacts to it.
   481 	NSString		*metaContactDictKey = [AIMetaContact internalObjectIDFromObjectID:inObjectID];
   482 	
   483 	AIMetaContact   *metaContact = [metaContactDict objectForKey:metaContactDictKey];
   484 	if (!metaContact) {
   485 		metaContact = [(AIMetaContact *)[AIMetaContact alloc] initWithObjectID:inObjectID];
   486 		
   487 		//Keep track of it in our metaContactDict for retrieval by objectID
   488 		[metaContactDict setObject:metaContact forKey:metaContactDictKey];
   489 		
   490 		//Add it to our more general contactDict, as well
   491 		[contactDict setObject:metaContact forKey:[metaContact internalUniqueObjectID]];
   492 		
   493 		/* We restore contacts (actually, internalIDs for contacts, to be added as necessary later) if the metaContact
   494 		 * existed before this call to metaContactWithObjectID:
   495 		 */
   496 		if (shouldRestoreContacts)
   497 			[self _restoreContactsToMetaContact:metaContact];
   498 		
   499 		/* As with contactWithService:account:UID, update all attributes so observers are initially informed of
   500 		 * this object's existence.
   501 		 */
   502 		[contactPropertiesObserverManager _updateAllAttributesOfObject:metaContact];
   503 		
   504 		[metaContact release];
   505 	}
   506 	
   507 	return metaContact;
   508 }
   509 
   510 /*!
   511  * @brief Associate the appropriate internal IDs for contained contacts with a metaContact
   512  *
   513  * @result YES if one or more contacts was associated with the metaContact; NO if none were.
   514  */
   515 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact
   516 {
   517 	NSDictionary	*allMetaContactsDict = [adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
   518 																				 group:PREF_GROUP_CONTACT_LIST];
   519 	NSArray			*containedContactsArray = [allMetaContactsDict objectForKey:metaContact.internalObjectID];
   520 	
   521 	if (containedContactsArray.count) {
   522 		[self _restoreContactsToMetaContact:metaContact
   523 				 fromContainedContactsArray:containedContactsArray];
   524 		
   525 		return YES;
   526 		
   527 	}
   528 	
   529 	return NO;
   530 }
   531 
   532 /*!
   533  * @brief Associate the internal IDs for an array of contacts with a specific metaContact
   534  *
   535  * This does not actually place any AIListContacts within the metaContact.  Instead, it updates the contactToMetaContactLookupDict
   536  * dictionary to have metaContact associated with the list contacts specified by containedContactsArray. This
   537  * allows us to add them lazily to the metaContact (in contactWithService:account:UID:) as necessary.
   538  *
   539  * @param metaContact The metaContact to which contact referneces are added
   540  * @param containedContactsArray An array of NSDictionary objects, each of which has SERVICE_ID_KEY and UID_KEY which together specify an internalObjectID of an AIListContact
   541  */
   542 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray
   543 {	
   544 	for (NSDictionary *containedContactDict in containedContactsArray) {
   545 		/* Before Adium 0.80, metaContacts could be created within metaContacts. Simply ignore any attempt to restore
   546 		 * such erroneous data, which will have a YES boolValue for KEY_IS_METACONTACT. */
   547 		if (![[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) {
   548 			/* Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject.
   549 			 *
   550 			 * As listObjects are loaded/created/requested which match this internalObjectID, 
   551 			 * they will be inserted into the metaContact.
   552 			 */
   553 			NSString	*internalObjectID = [AIListObject internalObjectIDForServiceID:[containedContactDict objectForKey:SERVICE_ID_KEY]
   554 																				UID:[containedContactDict objectForKey:UID_KEY]];
   555 			[contactToMetaContactLookupDict setObject:metaContact
   556 											   forKey:internalObjectID];
   557 		}
   558 	}
   559 }
   560 
   561 //Add a list object to a meta contact, setting preferences and such
   562 //so the association is lasting across program launches.
   563 - (void)addContact:(AIListContact *)inContact toMetaContact:(AIMetaContact *)metaContact
   564 {
   565 	if (!inContact) {
   566 		//I can't think of why one would want to add an entire group to a metacontact. Let's say you can't.
   567 		NSLog(@"Warning: addContact:toMetaContact: Attempted to add %@ to %@",inContact,metaContact);
   568 		return;
   569 	}
   570 	
   571 	if (inContact == metaContact) return;
   572 	
   573 	//If listObject contains other contacts, perform addContact:toMetaContact: recursively
   574 	if ([inContact conformsToProtocol:@protocol(AIContainingObject)]) {
   575 		for (AIListContact *someObject in ((AIListObject<AIContainingObject> *)inContact).containedObjects) {
   576 			[self addContact:someObject toMetaContact:metaContact];
   577 		}
   578 		
   579 	} else {
   580 		//Obtain any metaContact this listObject is currently within, so we can remove it later
   581 		AIMetaContact *oldMetaContact = [contactToMetaContactLookupDict objectForKey:[inContact internalObjectID]];
   582 		
   583 		if ([self _performAddContact:inContact toMetaContact:metaContact] && metaContact != oldMetaContact) {
   584 			//If this listObject was not in this metaContact in any form before, store the change
   585 			//Remove the list object from any other metaContact it is in at present
   586 			if (oldMetaContact)
   587 				[self removeContact:inContact fromMetaContact:oldMetaContact];
   588 			
   589 			[self _storeListObject:inContact inMetaContact:metaContact];
   590 
   591 			//Do the update thing
   592 			[contactPropertiesObserverManager _updateAllAttributesOfObject:metaContact];
   593 		}
   594 	}
   595 }
   596 
   597 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact
   598 {
   599 	//we only allow group->meta->contact, not group->meta->meta->contact
   600 	NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
   601 	
   602 	//	AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact);
   603 	NSDictionary		*containedContactDict;
   604 	NSMutableDictionary	*allMetaContactsDict;
   605 	NSMutableArray		*containedContactsArray;
   606 	
   607 	NSString			*metaContactInternalObjectID = [metaContact internalObjectID];
   608 	
   609 	//Get the dictionary of all metaContacts
   610 	allMetaContactsDict = [[adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
   611 																	group:PREF_GROUP_CONTACT_LIST] mutableCopy];
   612 	if (!allMetaContactsDict) {
   613 		allMetaContactsDict = [[NSMutableDictionary alloc] init];
   614 	}
   615 	
   616 	//Load the array for the new metaContact
   617 	containedContactsArray = [[allMetaContactsDict objectForKey:metaContactInternalObjectID] mutableCopy];
   618 	if (!containedContactsArray) containedContactsArray = [[NSMutableArray alloc] init];
   619 	containedContactDict = nil;
   620 	
   621 	//Create the dictionary describing this list object
   622 	containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
   623 							listObject.service.serviceID, SERVICE_ID_KEY,
   624 							listObject.UID, UID_KEY, nil];
   625 	
   626 	//Only add if this dict isn't already in the array
   627 	if (containedContactDict && ([containedContactsArray indexOfObject:containedContactDict] == NSNotFound)) {
   628 		[containedContactsArray addObject:containedContactDict];
   629 		[allMetaContactsDict setObject:containedContactsArray forKey:metaContactInternalObjectID];
   630 		
   631 		//Save
   632 		[self _saveMetaContacts:allMetaContactsDict];
   633 		
   634 		[adium.contactAlertsController mergeAndMoveContactAlertsFromListObject:listObject
   635 																  intoListObject:metaContact];
   636 	}
   637 	
   638 	[allMetaContactsDict release];
   639 	[containedContactsArray release];
   640 }
   641 
   642 //Actually adds a list contact to a meta contact. No preferences are changed.
   643 //Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict
   644 //for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful.
   645 - (BOOL)_performAddContact:(AIListContact *)inContact toMetaContact:(AIMetaContact *)metaContact
   646 {
   647 	//we only allow group->meta->contact, not group->meta->meta->contact
   648 	NSParameterAssert([metaContact canContainObject:inContact]);
   649 
   650 	BOOL								success;
   651 	
   652 	//Remove the object from its previous containing groups
   653 	[self _moveContactLocally:inContact fromGroups:inContact.groups toGroups:[NSSet set]];
   654 	
   655 	//AIMetaContact will handle reassigning the list object's grouping to being itself
   656 	if ((success = [metaContact addObject:inContact])) {
   657 		[contactToMetaContactLookupDict setObject:metaContact forKey:[inContact internalObjectID]];
   658 		
   659 		[self _didChangeContainer:metaContact object:inContact];
   660 
   661 		//Ensure the metacontact ends up in the appropriate groups
   662 		[metaContact restoreGrouping];
   663 	}
   664 	
   665 	return success;
   666 }
   667 
   668 - (void)removeContact:(AIListContact *)inContact fromMetaContact:(AIMetaContact *)metaContact
   669 {
   670 	//we only allow group->meta->contact, not group->meta->meta->contact
   671 	NSParameterAssert(![inContact conformsToProtocol:@protocol(AIContainingObject)]);
   672 	
   673 	NSString			*metaContactInternalObjectID = [metaContact internalObjectID];
   674 	
   675 	//Get the dictionary of all metaContacts
   676 	NSMutableDictionary *allMetaContactsDict = [adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
   677 																   group:PREF_GROUP_CONTACT_LIST];
   678 	
   679 	//Load the array for the metaContact
   680 	NSArray *containedContactsArray = [allMetaContactsDict objectForKey:metaContactInternalObjectID];
   681 	
   682 	//Enumerate it, looking only for the appropriate type of containedContactDict
   683 	
   684 	NSString	*listObjectUID = inContact.UID;
   685 	NSString	*listObjectServiceID = inContact.service.serviceID;
   686 	
   687 	NSDictionary *containedContactDict = nil;
   688 	for (containedContactDict in containedContactsArray) {
   689 		if ([[containedContactDict objectForKey:UID_KEY] isEqualToString:listObjectUID] &&
   690 			[[containedContactDict objectForKey:SERVICE_ID_KEY] isEqualToString:listObjectServiceID]) {
   691 			break;
   692 		}
   693 	}
   694 	
   695 	//If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result
   696 	if (containedContactDict) {
   697 		NSMutableArray		*newContainedContactsArray;
   698 		NSMutableDictionary	*newAllMetaContactsDict;
   699 		
   700 		newContainedContactsArray = [containedContactsArray mutableCopy];
   701 		[newContainedContactsArray removeObjectIdenticalTo:containedContactDict];
   702 		
   703 		newAllMetaContactsDict = [allMetaContactsDict mutableCopy];
   704 		[newAllMetaContactsDict setObject:newContainedContactsArray
   705 								   forKey:metaContactInternalObjectID];
   706 		
   707 		[self _saveMetaContacts:newAllMetaContactsDict];
   708 		
   709 		[newContainedContactsArray release];
   710 		[newAllMetaContactsDict release];
   711 	}
   712 	
   713 	//The listObject can be within the metaContact without us finding a containedContactDict if we are removing multiple
   714 	//listContacts referring to the same UID & serviceID combination - that is, on multiple accounts on the same service.
   715 	//We therefore request removal of the object regardless of the if (containedContactDict) check above.
   716 	[metaContact removeObject:inContact];
   717 	
   718 	[self _didChangeContainer:metaContact object:inContact];
   719 }
   720 
   721 /*!
   722  * @brief Determine the existing metacontact into which a grouping of UIDs and services would be placed
   723  *
   724  * @param UIDsArray NSArray of UIDs
   725  * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
   726  * 
   727  * @result Either the existing AIMetaContact -[self groupUIDs:forServices:usingMetaContactHint:] would return if passed a nil metaContactHint,
   728  *         or nil (if no existing metacontact would be used).
   729  */
   730 - (AIMetaContact *)knownMetaContactForGroupingUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray
   731 {
   732 	AIMetaContact	*metaContact = nil;
   733 	NSInteger count = [UIDsArray count];
   734 	
   735 	for (NSInteger i = 0; i < count; i++) {
   736 		if ((metaContact = [contactToMetaContactLookupDict objectForKey:[AIListObject internalObjectIDForServiceID:[servicesArray objectAtIndex:i]
   737 																											   UID:[UIDsArray objectAtIndex:i]]])) {
   738 			break;
   739 		}
   740 	}
   741 	
   742 	return metaContact;
   743 }
   744 
   745 /*!
   746  * @brief Groups UIDs for services into a single metacontact
   747  *
   748  * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to
   749  * a UID and a service, respectively, which together define a contact which should be included in the grouping.
   750  *
   751  * Assumption: This is only called after the contact list is finished loading, which occurs via
   752  * -(void)controllerDidLoad above.
   753  *
   754  * @param UIDsArray NSArray of UIDs
   755  * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
   756  * @param metaContactHint If passed, an AIMetaContact to use for the grouping if an existing one isn't found. If nil, a new metacontact will be craeted in that case.
   757  */
   758 - (AIMetaContact *)groupUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray usingMetaContactHint:(AIMetaContact *)metaContactHint
   759 {
   760 	NSMutableSet *internalObjectIDs = [[NSMutableSet alloc] init];
   761 	AIMetaContact *metaContact = nil;
   762 	NSString *internalObjectID;
   763 	NSInteger count = [UIDsArray count];
   764 	
   765 	/* Build an array of all contacts matching this description (multiple accounts on the same service listing
   766 	 * the same UID mean that we can have multiple AIListContact objects with a UID/service combination)
   767 	 */
   768 	for (NSUInteger i = 0; i < count; i++) {
   769 		NSString	*serviceID = [servicesArray objectAtIndex:i];
   770 		NSString	*UID = [UIDsArray objectAtIndex:i];
   771 		
   772 		internalObjectID = [AIListObject internalObjectIDForServiceID:serviceID
   773 																  UID:UID];
   774 		if(!metaContact) {
   775 			metaContact = [contactToMetaContactLookupDict objectForKey:internalObjectID];
   776 		}
   777 		
   778 		[internalObjectIDs addObject:internalObjectID];
   779 	}
   780 	
   781 	if ([internalObjectIDs count] > 1) {
   782 		//Create a new metaContact is we didn't find one and weren't supplied a hint
   783 		if (!metaContact && !(metaContact = metaContactHint)) {
   784 			AILogWithSignature(@"New metacontact to group %@ on %@", UIDsArray, servicesArray);
   785 			metaContact = [self metaContactWithObjectID:nil];
   786 		}
   787 		
   788 		for (internalObjectID in internalObjectIDs) {
   789 			AIListObject	*existingObject;
   790 			if ((existingObject = [self existingListObjectWithUniqueID:internalObjectID])) {
   791 				/* If there is currently an object (or multiple objects) matching this internalObjectID
   792 				 * we should add immediately.
   793 				 */
   794 				NSAssert([metaContact canContainObject:existingObject], @"Attempting to add something metacontacts can't hold to a metacontact");
   795 				[self addContact:(id)existingObject
   796 					  toMetaContact:metaContact];	
   797 			} else {
   798 				/* If no objects matching this internalObjectID exist, we can simply add to the 
   799 				 * contactToMetaContactLookupDict for use if such an object is created later.
   800 				 */
   801 				[contactToMetaContactLookupDict setObject:metaContact
   802 												   forKey:internalObjectID];			
   803 			}
   804 		}
   805 	}
   806 
   807 	[internalObjectIDs release];
   808 	
   809 	return metaContact;
   810 }
   811 
   812 /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added.
   813  *
   814  * This will reuse an existing metacontact (for one of the contacts in the array) if possible.
   815  * @param contactsToGroupArray Contacts to group together
   816  */
   817 - (AIMetaContact *)groupContacts:(NSArray *)contactsToGroupArray
   818 {
   819 	AIMetaContact   *metaContact = nil;
   820 
   821 	//Look for a metacontact we were passed directly
   822 	for (AIListContact *listContact in contactsToGroupArray) {
   823 		if ([listContact isKindOfClass:[AIMetaContact class]]) {
   824 			metaContact = (AIMetaContact *)listContact;
   825 			break;
   826 		}
   827 	}
   828 
   829 	//If we weren't passed a metacontact, look for an existing metacontact associated with a passed contact
   830 	if (!metaContact) {
   831 		for (AIListContact *listContact in contactsToGroupArray) {
   832 			if (![listContact isKindOfClass:[AIMetaContact class]] &&
   833 				(metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]])) {
   834 					break;
   835 			}
   836 		}
   837 	}
   838 
   839 	//Create a new metaContact is we didn't find one.
   840 	if (!metaContact) {
   841 		AILogWithSignature(@"New metacontact to group %@", contactsToGroupArray);
   842 		metaContact = [self metaContactWithObjectID:nil];
   843 	}
   844 	
   845 	/* Add all these contacts to our MetaContact.
   846 	 * Some may already be present, but that's fine, as nothing will happen.
   847 	 */
   848 	for (AIListContact *listContact in contactsToGroupArray) {
   849 		[self addContact:listContact toMetaContact:metaContact];
   850 	}
   851 	
   852 	return metaContact;
   853 }
   854 
   855 - (void)explodeMetaContact:(AIMetaContact *)metaContact
   856 {
   857 	//Remove the objects within it from being inside it
   858 	[contactPropertiesObserverManager delayListObjectNotifications];
   859 	NSArray	*containedObjects = metaContact.containedObjects;
   860 	
   861 	NSMutableDictionary *allMetaContactsDict = [[adium.preferenceController preferenceForKey:KEY_METACONTACT_OWNERSHIP
   862 																						 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
   863 	
   864 	for (AIListContact *object in containedObjects) {
   865 		
   866 		//Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
   867 		[contactToMetaContactLookupDict removeObjectForKey:[object internalObjectID]];
   868 		
   869 		[self removeContact:object fromMetaContact:metaContact];
   870 	}
   871 	
   872 	//Then, procede to remove the metaContact
   873 	
   874 	//Protect!
   875 	[metaContact retain];
   876 	
   877 	//Remove it from its containing groups (no contained contacts == present in no groups)
   878 	[metaContact restoreGrouping];
   879 	
   880 	NSString	*metaContactInternalObjectID = [metaContact internalObjectID];
   881 	
   882 	//Remove our reference to it internally
   883 	[metaContactDict removeObjectForKey:metaContactInternalObjectID];
   884 	
   885 	//Remove it from the preferences dictionary
   886 	[allMetaContactsDict removeObjectForKey:metaContactInternalObjectID];
   887 	
   888 	//Save the updated allMetaContactsDict which no longer lists the metaContact
   889 	[self _saveMetaContacts:allMetaContactsDict];
   890 	
   891 	[contactPropertiesObserverManager endListObjectNotificationsDelay];
   892 	
   893 	//Protection is overrated.
   894 	[metaContact release];
   895 	[allMetaContactsDict release];
   896 }
   897 
   898 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict
   899 {
   900 	AILog(@"MetaContacts: Saving!");
   901 	[adium.preferenceController setPreference:allMetaContactsDict
   902 										 forKey:KEY_METACONTACT_OWNERSHIP
   903 										  group:PREF_GROUP_CONTACT_LIST];
   904 	[adium.preferenceController setPreference:[allMetaContactsDict allKeys]
   905 										 forKey:KEY_FLAT_METACONTACTS
   906 										  group:PREF_GROUP_CONTACT_LIST];
   907 }
   908 
   909 //Sort list objects alphabetically by their display name
   910 NSInteger contactDisplayNameSort(AIListObject *objectA, AIListObject *objectB, void *context)
   911 {
   912 	return [objectA.displayName caseInsensitiveCompare:objectB.displayName];
   913 }
   914 
   915 #pragma mark Preference observing
   916 /*!
   917  * @brief Preferences changed
   918  */
   919 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
   920 							object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
   921 {
   922 	if (key &&
   923 		![key isEqualToString:KEY_HIDE_CONTACTS] &&
   924 		![key isEqualToString:KEY_SHOW_OFFLINE_CONTACTS] &&
   925 		![key isEqualToString:KEY_USE_OFFLINE_GROUP] &&
   926 		![key isEqualToString:KEY_HIDE_CONTACT_LIST_GROUPS]) {
   927 		return;
   928 	}
   929 
   930 	BOOL shouldUseOfflineGroup = ((![[prefDict objectForKey:KEY_HIDE_CONTACTS] boolValue] ||
   931 								   [[prefDict objectForKey:KEY_SHOW_OFFLINE_CONTACTS] boolValue]) &&
   932 								  [[prefDict objectForKey:KEY_USE_OFFLINE_GROUP] boolValue]);
   933 	
   934 	if (shouldUseOfflineGroup != self.useOfflineGroup || !key) {
   935 		self.useOfflineGroup = shouldUseOfflineGroup;
   936 		
   937 		if (self.useOfflineGroup)
   938 			[contactPropertiesObserverManager registerListObjectObserver:self];
   939 		
   940 		[contactPropertiesObserverManager updateAllListObjectsForObserver:self];	
   941 		
   942 		if (!self.useOfflineGroup)
   943 			[contactPropertiesObserverManager unregisterListObjectObserver:self];    
   944 	}
   945 }
   946 
   947 /*!
   948  * @brief Move contacts to and from the offline group as necessary as their online state changes.
   949  */
   950 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
   951 {
   952 	if ((!inModifiedKeys || [inModifiedKeys containsObject:@"Online"]) && [inObject isKindOfClass:[AIListContact class]]) {
   953 		[((AIListContact *)inObject).parentContact restoreGrouping];
   954 	}
   955 	
   956 	return nil;
   957 }
   958 
   959 #pragma mark Contact Sorting
   960 
   961 //Sort the entire contact list
   962 - (void)sortContactList
   963 {
   964 	[self sortContactLists:contactLists];
   965 }
   966 
   967 - (void)sortContactLists:(NSArray *)lists
   968 {
   969 	for(AIContactList *list in lists) {
   970 		[list sort];
   971 	}
   972 	[[NSNotificationCenter defaultCenter] postNotificationName:Contact_OrderChanged object:nil];
   973 }
   974 
   975 //Sort an individual object
   976 - (void)sortListObject:(AIListObject *)inObject
   977 {
   978 	if ([contactPropertiesObserverManager updatesAreDelayed]) {
   979 		[contactPropertiesObserverManager noteContactChanged:inObject];
   980 
   981 	} else {
   982 		for (AIListGroup *group in inObject.groups) {
   983 			//Sort the groups containing this object
   984 			[group sortListObject:inObject];
   985 			[[NSNotificationCenter defaultCenter] postNotificationName:Contact_OrderChanged object:group];
   986 		}
   987 	}
   988 }
   989 
   990 #pragma mark Contact List Access
   991 
   992 @synthesize contactList;
   993 
   994 /*!
   995  * @brief Return an array of all contact list groups
   996  */
   997 - (NSArray *)allGroups
   998 {
   999 	return [groupDict allValues];
  1000 }
  1001 
  1002 /*!
  1003  * @brief Returns a flat array of all contacts
  1004  *
  1005  * This does not include metacontacts
  1006  */
  1007 - (NSArray *)allContacts
  1008 {
  1009 	NSMutableArray *result = [[[NSMutableArray alloc] init] autorelease];
  1010 
  1011 	for (AIListContact *contact in self.contactEnumerator) {
  1012 		/* We want only contacts, not metacontacts. For a given contact, -[contact parentContact] could be used to access the meta. */
  1013 		if (![contact conformsToProtocol:@protocol(AIContainingObject)])
  1014 			[result addObject:contact];
  1015 	}
  1016 	
  1017 	return result;
  1018 }
  1019 
  1020 /*!
  1021  * @brief Returns a flat array of all bookmarks.
  1022  */
  1023 - (NSArray *)allBookmarks
  1024 {
  1025 	return [[[bookmarkDict allValues] copy] autorelease];
  1026 }
  1027 
  1028 /*!
  1029  * @brief Returns a flat array of all metacontacts
  1030  */
  1031 - (NSArray *)allMetaContacts
  1032 {
  1033 	return [metaContactDict allValues];
  1034 }
  1035 
  1036 //Return a flat array of all the objects in a group on an account (and all subgroups, if desired)
  1037 - (NSArray *)allContactsInObject:(AIListObject<AIContainingObject> *)inGroup onAccount:(AIAccount *)inAccount
  1038 {
  1039 	NSParameterAssert(inGroup != nil);
  1040 	
  1041 	NSMutableArray	*contactArray = [NSMutableArray array];    
  1042 	
  1043 	for (AIListObject *object in inGroup) {
  1044 		if ([object conformsToProtocol:@protocol(AIContainingObject)]) {
  1045 			[contactArray addObjectsFromArray:[self allContactsInObject:(AIListObject<AIContainingObject> *)object
  1046 															  onAccount:inAccount]];
  1047 		} else if ([object isMemberOfClass:[AIListContact class]] && (!inAccount || ([(AIListContact *)object account] == inAccount)))
  1048 			[contactArray addObject:object];
  1049 	}
  1050 	
  1051 	return contactArray;
  1052 }
  1053 
  1054 #pragma mark Contact List Menus
  1055 
  1056 //Returns a menu containing all the groups within a group
  1057 //- Selector called on group selection is selectGroup:
  1058 //- The menu items represented object is the group it represents
  1059 - (NSMenu *)groupMenuWithTarget:(id)target
  1060 {
  1061 	NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
  1062 	[menu setAutoenablesItems:NO];
  1063 	    
  1064     // Separate groups by the contact list they're on.
  1065     NSMutableDictionary *groupsByList = [NSMutableDictionary dictionaryWithCapacity:contactLists.count];
  1066 	for (AIListGroup *group in self.allGroups) {
  1067 		if (group != self.offlineGroup) {
  1068             NSMutableArray *groups = [groupsByList objectForKey:group.contactList.UID];
  1069             if (!groups) {
  1070                 groups = [NSMutableArray array];
  1071                 [groupsByList setObject:groups forKey:group.contactList.UID];
  1072             }
  1073             [groups addObject:group];
  1074 		}
  1075 	}
  1076     
  1077     // Now traverse the contactLists array in order and build a menu showing the groups in each list with separators in between.
  1078     NSInteger i = 0;
  1079     for (AIContactList *list in contactLists) {
  1080         NSMutableArray *groups = [groupsByList objectForKey:list.UID];
  1081         [groups sortUsingActiveSortControllerInContainer:list];
  1082         for (AIListGroup *group in groups) {
  1083             NSMenuItem	*menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:group.displayName
  1084                                                                                         target:target
  1085                                                                                         action:@selector(selectGroup:)
  1086                                                                                  keyEquivalent:@""];
  1087             [menuItem setRepresentedObject:group];
  1088             [menu addItem:menuItem];
  1089             [menuItem release];
  1090         }
  1091                 
  1092         i++;
  1093 
  1094         // Add the separator unless this is the last list.
  1095         if (i < contactLists.count) {
  1096             [menu addItem:[NSMenuItem separatorItem]];
  1097         }
  1098 
  1099     }
  1100     
  1101 	
  1102 	return [menu autorelease];
  1103 }
  1104 
  1105 #pragma mark Retrieving Specific Contacts
  1106 
  1107 /*!
  1108  * @brief Change the UID for a contact
  1109  *
  1110  * @param UID The new UID to set
  1111  * @param contact The contact whose UID is going to be changed
  1112  *
  1113  * Preserves our reference to the contact as the UID changes, allowing us to continue 
  1114  * returning it when asked about the new UID in the future.
  1115  */
  1116 - (void)setUID:(NSString *)UID forContact:(AIListContact *)contact
  1117 {
  1118 	[contact retain];
  1119 	
  1120 	// Remove the old value, its internal ID is going to change.
  1121 	[contactDict removeObjectForKey:contact.internalUniqueObjectID];
  1122 	
  1123 	// Set the UID.
  1124 	[contact setUID:UID];
  1125 	
  1126 	// Add it back int othe dict.
  1127 	[contactDict setObject:contact forKey:contact.internalUniqueObjectID];
  1128 	
  1129 	[contact release];
  1130 }
  1131 
  1132 - (AIListContact *)contactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
  1133 {
  1134 	if (!(inUID && [inUID length] && inService)) return nil; //Ignore invalid requests
  1135 	
  1136 	AIListContact	*contact = nil;
  1137 	NSString		*key = [AIListContact internalUniqueObjectIDForService:inService
  1138 															account:inAccount
  1139 																UID:inUID];
  1140 	contact = [contactDict objectForKey:key];
  1141 	if (!contact) {
  1142 		//Create
  1143 		contact = [[AIListContact alloc] initWithUID:inUID account:inAccount service:inService];
  1144 		
  1145 		//Check to see if we should add to a metaContact
  1146 		AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:[contact internalObjectID]];
  1147 		if (metaContact) {
  1148 			/* We already know to add this object to the metaContact, since we did it before with another object,
  1149 			 but this particular listContact is new and needs to be added directly to the metaContact
  1150 			 (on future launches, the metaContact will obtain it automatically since all contacts matching this UID
  1151 			 and serviceID should be included). */
  1152 			[self _performAddContact:contact toMetaContact:metaContact];
  1153 		}
  1154 		
  1155 		//Set the contact as mobile if it is a phone number
  1156 		if ([inUID hasPrefix:@"+"]) {
  1157 			[contact setIsMobile:YES notify:NotifyNever];
  1158 		}
  1159 		
  1160 		//Add
  1161 		[contactDict setObject:contact forKey:key];
  1162 
  1163 		//Do the update thing
  1164 		[contactPropertiesObserverManager _updateAllAttributesOfObject:contact];
  1165 
  1166 		[contact release];
  1167 	}
  1168 	
  1169 	return contact;
  1170 }
  1171 
  1172 - (void)accountDidStopTrackingContact:(AIListContact *)inContact
  1173 {
  1174 	[[inContact retain] autorelease];
  1175 
  1176 	/* Remove after a short delay. Otherwise, the removal may be visible as the object remains in the contact
  1177 	 * list until a display delay is over, which would show up as the name going blank on metacontacts and other
  1178 	 * odd behavior.
  1179 	 *
  1180 	 * Of course, this really means that the object delay code is somehow failing to actually delay all updates.
  1181 	 * I can't figure out where or why, so this is a hack around it. Ugh. -evands 10/08
  1182 	 */
  1183 	for (AIListObject<AIContainingObject> *container in inContact.containingObjects) {
  1184 		[container performSelector:@selector(removeObjectAfterAccountStopsTracking:)
  1185 		 withObject:inContact
  1186 		 afterDelay:1];
  1187 	}
  1188 	
  1189 	[contactDict removeObjectForKey:inContact.internalUniqueObjectID];
  1190 }
  1191 
  1192 /*!
  1193  * @brief Find an existing bookmark
  1194  *
  1195  * Finds an existing bookmark for a given AIChat
  1196  */
  1197 - (AIListBookmark *)existingBookmarkForChat:(AIChat *)inChat
  1198 {
  1199 	return [self existingBookmarkForChatName:inChat.name
  1200 								   onAccount:inChat.account
  1201 							chatCreationInfo:inChat.chatCreationDictionary];
  1202 }
  1203 
  1204 /*!
  1205  * @brief Find an existing bookmark
  1206  *
  1207  * Finds an existing bookmark for given information.
  1208  */
  1209 - (AIListBookmark *)existingBookmarkForChatName:(NSString *)inName
  1210 									  onAccount:(AIAccount *)inAccount
  1211 							   chatCreationInfo:(NSDictionary *)inCreationInfo
  1212 {
  1213 	AIListBookmark *existingBookmark = nil;
  1214 	
  1215 	for(AIListBookmark *listBookmark in self.allBookmarks) {
  1216 		if([listBookmark.name isEqualToString:[inAccount.service normalizeChatName:inName]] &&
  1217 			listBookmark.account == inAccount &&
  1218 			((!listBookmark.chatCreationDictionary && !inCreationInfo) ||
  1219 			 ([listBookmark.chatCreationDictionary isEqualToDictionary:inCreationInfo]))) {
  1220 			existingBookmark = listBookmark;
  1221 			break;
  1222 		}
  1223 	}
  1224 	
  1225 	return existingBookmark;
  1226 }
  1227 
  1228 
  1229 /*!
  1230  * @brief Find or create a bookmark for a chat
  1231  */
  1232 - (AIListBookmark *)bookmarkForChat:(AIChat *)inChat inGroup:(AIListGroup *)group
  1233 {
  1234 	AIListBookmark *bookmark = [self existingBookmarkForChat:inChat];
  1235 	
  1236 	if (!bookmark) {
  1237 		bookmark = [[[AIListBookmark alloc] initWithChat:inChat] autorelease];
  1238 		
  1239 		if ([bookmarkDict objectForKey:bookmark.internalObjectID]) {
  1240 			// In case we end up with two bookmarks with the same internalObjectID; this should be almost impossible.
  1241 			[self removeBookmark:[bookmarkDict objectForKey:bookmark.internalObjectID]];
  1242 		}
  1243 		
  1244 		[bookmarkDict setObject:bookmark forKey:bookmark.internalObjectID];
  1245 		
  1246 		[bookmark setInitialGroup:group];
  1247 		
  1248 		[self saveContactList];
  1249 	}
  1250 	
  1251 	[bookmark restoreGrouping];
  1252 	
  1253 	//Do the update thing
  1254 	[contactPropertiesObserverManager _updateAllAttributesOfObject:bookmark];
  1255 	
  1256 	return bookmark;
  1257 }
  1258 
  1259 /*!
  1260  * @brief Remove a bookmark
  1261  */
  1262 - (void)removeBookmark:(AIListBookmark *)listBookmark
  1263 {
  1264 	[self moveContact:listBookmark fromGroups:listBookmark.groups intoGroups:[NSSet set]];
  1265 	[bookmarkDict removeObjectForKey:listBookmark.internalObjectID];
  1266 	
  1267 	[self saveContactList];
  1268 }
  1269 
  1270 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
  1271 {
  1272 	if (inService && [inUID length]) {
  1273 		return [contactDict objectForKey:[AIListContact internalUniqueObjectIDForService:inService
  1274 											account:inAccount
  1275 											    UID:inUID]];
  1276 	}
  1277 	return nil;
  1278 }
  1279 
  1280 /*!
  1281  * @brief Return a set of all contacts with a specified UID and service
  1282  *
  1283  * @param service The AIService in question
  1284  * @param inUID The UID, which should be normalized (lower case, no spaces, etc.) as appropriate for the service
  1285  */
  1286 - (NSSet *)allContactsWithService:(AIService *)service UID:(NSString *)inUID
  1287 {
  1288 	NSMutableSet	*returnContactSet = [NSMutableSet set];
  1289 
  1290 	for (AIAccount *account in [adium.accountController accountsCompatibleWithService:service]) {
  1291 		AIListContact *listContact = [self existingContactWithService:service
  1292 														account:account
  1293 															UID:inUID];
  1294 		
  1295 		if (listContact) {
  1296 			[returnContactSet addObject:listContact];
  1297 		}
  1298 	}
  1299 	
  1300 	return returnContactSet;
  1301 }
  1302 
  1303 - (AIListObject *)existingListObjectWithUniqueID:(NSString *)uniqueID
  1304 {	
  1305 	//Contact
  1306 	for (AIListObject *listObject in contactDict.objectEnumerator) {
  1307 		if ([listObject.internalObjectID isEqualToString:uniqueID]) return listObject;
  1308 	}
  1309 	
  1310 	//Group
  1311 	for (AIListGroup *listObject in groupDict.objectEnumerator) {
  1312 		if ([listObject.internalObjectID isEqualToString:uniqueID]) return listObject;
  1313 	}
  1314 	
  1315 	//Metacontact
  1316 	for (AIMetaContact *listObject in metaContactDict.objectEnumerator) {
  1317 		if ([listObject.internalObjectID isEqualToString:uniqueID]) return listObject;
  1318 	}
  1319 	
  1320 	return nil;
  1321 }
  1322 
  1323 /*!
  1324  * @brief Get the best AIListContact to send a given content type to a contat
  1325  *
  1326  * The resulting AIListContact will be the most available individual contact (not metacontact) on the best account to
  1327  * receive the specified content type.
  1328  *
  1329  * @result The contact, or nil if it is impossible to send inType to inContact
  1330  */
  1331 - (AIListContact *)preferredContactForContentType:(NSString *)inType forListContact:(AIListContact *)inContact
  1332 {
  1333 	if ([inContact isKindOfClass:[AIMetaContact class]])
  1334 		inContact = [(AIMetaContact *)inContact preferredContactForContentType:inType];
  1335 
  1336 	/* Find the best account for talking to this contact, and return an AIListContact on that account.
  1337 	 * We'll get nil if no account can send inType to inContact.
  1338 	 */
  1339 	AIAccount *account = [adium.accountController preferredAccountForSendingContentType:inType toContact:inContact];
  1340 
  1341 	if (account)
  1342 		return [self contactWithService:inContact.service account:account UID:inContact.UID];
  1343 
  1344 	return nil;
  1345 }
  1346 
  1347 //XXX - This is ridiculous.
  1348 - (AIListContact *)preferredContactWithUID:(NSString *)inUID andServiceID:(NSString *)inService forSendingContentType:(NSString *)inType
  1349 {
  1350 	AIService		*theService = [adium.accountController firstServiceWithServiceID:inService];
  1351 	AIListContact	*tempListContact = [[AIListContact alloc] initWithUID:inUID
  1352 																service:theService];
  1353 	AIAccount		*account = [adium.accountController preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
  1354 																				 toContact:tempListContact];
  1355 	[tempListContact release];
  1356 
  1357 	return [self contactWithService:theService account:account UID:inUID];
  1358 }
  1359 
  1360 
  1361 /*!
  1362  * @brief Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts
  1363  *
  1364  * If the destination contact's parent contact differs from the destination contact itself, the chat is with a metaContact.
  1365  * If that metaContact's preferred destination for messaging isn't the same as the contact which was just messaged,
  1366  * update the preference so that a new chat with this metaContact would default to the proper contact.
  1367  */
  1368 - (void)didSendContent:(NSNotification *)notification
  1369 {
  1370 	AIChat			*chat = [[notification userInfo] objectForKey:@"AIChat"];
  1371 	AIListContact	*destContact = chat.listObject;
  1372 	AIListContact	*metaContact = destContact.metaContact;
  1373 	
  1374 	if (!metaContact) 
  1375 		return;
  1376 	
  1377 	NSString	*destinationInternalObjectID = destContact.internalObjectID;
  1378 	NSString	*currentPreferredDestination = [metaContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT group:OBJECT_STATUS_CACHE];
  1379 	
  1380 	if (![destinationInternalObjectID isEqualToString:currentPreferredDestination]) {
  1381 		[metaContact setPreference:destinationInternalObjectID
  1382 							forKey:KEY_PREFERRED_DESTINATION_CONTACT
  1383 							 group:OBJECT_STATUS_CACHE];
  1384 	}
  1385 }
  1386 
  1387 #pragma mark Retrieving Groups
  1388 
  1389 //Retrieve a group from the contact list (Creating if necessary)
  1390 - (AIListGroup *)groupWithUID:(NSString *)groupUID
  1391 {
  1392 	NSParameterAssert(groupUID != nil);
  1393 	
  1394 	//Return our root group if it is requested. 
  1395 	if ([groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME])
  1396 		return [self contactList];
  1397 	
  1398 	AIListGroup		*group = nil;
  1399 	if (!(group = [groupDict objectForKey:[groupUID lowercaseString]])) {
  1400 		//Create
  1401 		group = [[AIListGroup alloc] initWithUID:groupUID];
  1402 		
  1403 		//Add
  1404 		//Update afterwards, in case it's being called inside updateListObject already.
  1405 		[contactPropertiesObserverManager performSelector:@selector(_updateAllAttributesOfObject:) withObject:group afterDelay:0.0];
  1406 		[groupDict setObject:group forKey:[groupUID lowercaseString]];
  1407 		
  1408 		//Add to the contact list
  1409 		[contactList addObject:group];
  1410 		[self _didChangeContainer:contactList object:group];
  1411 		[group release];
  1412 	}
  1413 	
  1414 	return group;
  1415 }
  1416 
  1417 #pragma mark Contact list editing
  1418 - (void)removeListGroup:(AIListGroup *)group
  1419 {
  1420 	AIContactList	*containingObject = group.contactList;
  1421 	
  1422 	//Remove all the contacts from this group
  1423 	for (AIListContact *contact in group.containedObjects) {
  1424 		[contact removeFromGroup:group];
  1425 	}
  1426 	
  1427 	//Delete the group from all active accounts
  1428 	for (AIAccount *account in adium.accountController.accounts) {
  1429 		if (account.online) {
  1430 			[account deleteGroup:group];
  1431 		}
  1432 	}
  1433 	
  1434 	//Then, procede to delete the group
  1435 	[group retain];
  1436 	[containingObject removeObject:group];
  1437 	[groupDict removeObjectForKey:[group.UID lowercaseString]];
  1438 	[self _didChangeContainer:containingObject object:group];
  1439 	[group release];
  1440 }
  1441 
  1442 - (void)requestAddContactWithUID:(NSString *)contactUID service:(AIService *)inService account:(AIAccount *)inAccount
  1443 {
  1444 	NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:contactUID
  1445 																	   forKey:UID_KEY];
  1446 	if (inService) [userInfo setObject:inService forKey:@"AIService"];
  1447 	if (inAccount) [userInfo setObject:inAccount forKey:@"AIAccount"];
  1448 	
  1449 	[[NSNotificationCenter defaultCenter] postNotificationName:Contact_AddNewContact
  1450 											  object:nil
  1451 											userInfo:userInfo];
  1452 }
  1453 
  1454 - (void)moveContact:(AIListContact *)contact fromGroups:(NSSet *)oldGroups intoGroups:(NSSet *)groups
  1455 {
  1456 	[contactPropertiesObserverManager delayListObjectNotifications];
  1457 	if (contact.metaContact) {
  1458 		AIMetaContact *meta = contact.metaContact;
  1459 		//Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
  1460 		[contactToMetaContactLookupDict removeObjectForKey:contact.internalObjectID];
  1461 		
  1462 		for (AIListContact *matchingContact in [self allContactsWithService:contact.service UID:contact.UID]) {
  1463 			[self removeContact:matchingContact fromMetaContact:meta];
  1464 		}
  1465 	}
  1466 	
  1467 	if (contact.existsServerside) {
  1468 		if (contact.account.online)
  1469 			[contact.account moveListObjects:[NSArray arrayWithObject:contact] fromGroups:oldGroups toGroups:groups];
  1470 	} else {
  1471 		[self _moveContactLocally:contact fromGroups:oldGroups toGroups:groups];
  1472 		
  1473 		if ([contact conformsToProtocol:@protocol(AIContainingObject)]) {
  1474 			id<AIContainingObject> container = (id<AIContainingObject>)contact;
  1475 			
  1476 			//This is a meta contact, move the objects within it.
  1477 			for (AIListContact *child in container) {
  1478 				//Only move the contact if it is actually listed on the account in question
  1479 				if (child.account.online && !child.isStranger)
  1480 					[child.account moveListObjects:[NSArray arrayWithObject:child] fromGroups:oldGroups toGroups:groups];
  1481 			}
  1482 		}		
  1483 	}
  1484 	[contactPropertiesObserverManager endListObjectNotificationsDelay];
  1485 }
  1486 
  1487 #pragma mark Detached Contact Lists
  1488 - (void)moveGroup:(AIListGroup *)group fromContactList:(AIContactList *)oldContactList toContactList:(AIContactList *)newContactList
  1489 {
  1490 	if (![oldContactList containsObject:group] || [newContactList containsObject:group]) {
  1491 		return;
  1492 	}
  1493 	
  1494 	[oldContactList removeObject:group];
  1495 	[newContactList addObject:group];
  1496 
  1497 	[[NSNotificationCenter defaultCenter] postNotificationName:Contact_ListChanged
  1498 														object:newContactList
  1499 													  userInfo:nil];
  1500 	
  1501 	if (!oldContactList.containedObjects.count) {
  1502 		[[NSNotificationCenter defaultCenter] postNotificationName:DetachedContactListIsEmpty
  1503 															object:oldContactList
  1504 														  userInfo:nil];
  1505 	} else {
  1506 		[[NSNotificationCenter defaultCenter] postNotificationName:Contact_ListChanged
  1507 															object:oldContactList
  1508 														  userInfo:nil];
  1509 	}
  1510 }
  1511 
  1512 /*!
  1513  * @returns Empty contact list
  1514  */
  1515 - (AIContactList *)createDetachedContactList
  1516 {
  1517 	static NSInteger count = 0;
  1518 	AIContactList *list = [[AIContactList alloc] initWithUID:[NSString stringWithFormat:@"Detached%ld",count++]];
  1519 	[contactLists addObject:list];
  1520 	[list release];
  1521 	return list;
  1522 }
  1523 
  1524 /*!
  1525  * @brief Removes detached contact list
  1526  */
  1527 - (void)removeDetachedContactList:(AIContactList *)detachedList
  1528 {
  1529 	[contactLists removeObject:detachedList];
  1530 }
  1531 
  1532 @end
  1533 
  1534 @implementation AIContactController (ContactControllerHelperAccess)
  1535 - (NSEnumerator *)contactEnumerator
  1536 {
  1537 	return [contactDict objectEnumerator];
  1538 }
  1539 - (NSEnumerator *)groupEnumerator
  1540 {
  1541 	return [groupDict objectEnumerator];
  1542 }
  1543 @end