Source/AIContactController.m
author Zachary West <zacw@adium.im>
Wed Oct 28 21:04:52 2009 -0400 (2009-10-28)
changeset 2676 791780625051
parent 2399 0d364fd70a02
child 2738 b820c57cac38
permissions -rw-r--r--
Include bookmarks in the contact controller's contactDict, so that a -contactEnumerator also contains them. By way of updating properly, fixes #13221.

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