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