Source/AIContactController.m
author Zachary West <zacw@adium.im>
Fri Nov 06 12:37:12 2009 -0500 (2009-11-06)
changeset 2738 b820c57cac38
parent 2676 791780625051
child 3033 0b981d5c2c76
permissions -rw-r--r--
Backed out changeset 791780625051

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