2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 #import "AIContactController.h"
21 #import <Adium/AIAccountControllerProtocol.h>
22 #import <Adium/AIInterfaceControllerProtocol.h>
23 #import <Adium/AILoginControllerProtocol.h>
24 #import <Adium/AIMenuControllerProtocol.h>
25 #import <Adium/AIPreferenceControllerProtocol.h>
26 #import <Adium/AIToolbarControllerProtocol.h>
27 #import <Adium/AIContactAlertsControllerProtocol.h>
29 #import <AIUtilities/AIDictionaryAdditions.h>
30 #import <AIUtilities/AIFileManagerAdditions.h>
31 #import <AIUtilities/AIMenuAdditions.h>
32 #import <AIUtilities/AIToolbarUtilities.h>
33 #import <AIUtilities/AIApplicationAdditions.h>
34 #import <AIUtilities/AIImageAdditions.h>
35 #import <AIUtilities/AIStringAdditions.h>
36 #import <Adium/AIAccount.h>
37 #import <Adium/AIChat.h>
38 #import <Adium/AIContentMessage.h>
39 #import <Adium/AIListContact.h>
40 #import <Adium/AIListGroup.h>
41 #import <Adium/AIListObject.h>
42 #import <Adium/AIMetaContact.h>
43 #import <Adium/AIService.h>
44 #import <Adium/AISortController.h>
45 #import <Adium/AIUserIcons.h>
46 #import <Adium/AIServiceIcons.h>
48 #import "AdiumAuthorization.h"
51 #define PREF_GROUP_CONTACT_LIST @"Contact List" //Contact list preference group
52 #define KEY_FLAT_GROUPS @"FlatGroups" //Group storage
53 #define KEY_FLAT_CONTACTS @"FlatContacts" //Contact storage
54 #define KEY_FLAT_METACONTACTS @"FlatMetaContacts" //Metacontact objectID storage
56 #define OBJECT_STATUS_CACHE @"Object Status Cache"
58 #define UPDATE_CLUMP_INTERVAL 1.0
60 #define TOP_METACONTACT_ID @"TopMetaContactID"
61 #define KEY_IS_METACONTACT @"isMetaContact"
62 #define KEY_OBJECTID @"objectID"
63 #define KEY_METACONTACT_OWNERSHIP @"MetaContact Ownership"
64 #define CONTACT_DEFAULT_PREFS @"ContactPrefs"
66 #define SHOW_GROUPS_MENU_TITLE AILocalizedString(@"Show Groups",nil)
67 #define SHOW_GROUPS_IDENTIFER @"ShowGroups"
69 #define KEY_HIDE_CONTACT_LIST_GROUPS @"Hide Contact List Groups"
70 #define KEY_USE_OFFLINE_GROUP @"Use Offline Group"
71 #define KEY_SHOW_OFFLINE_CONTACTS @"Show Offline Contacts"
73 #define PREF_GROUP_CONTACT_LIST_DISPLAY @"Contact List Display"
75 #define SERVICE_ID_KEY @"ServiceID"
76 #define UID_KEY @"UID"
78 @interface AIContactController (PRIVATE)
79 - (AIListGroup *)processGetGroupNamed:(NSString *)serverGroup;
80 - (void)_performDelayedUpdates:(NSTimer *)timer;
81 - (void)loadContactList;
82 - (void)saveContactList;
83 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
84 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject;
85 - (void)prepareContactInfo;
87 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inGroup withTarget:(id)target firstLevel:(BOOL)firstLevel;
88 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level;
90 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects;
91 - (void)_loadGroupsFromArray:(NSArray *)array;
93 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object;
94 - (void)prepareShowHideGroups;
95 - (void)_performChangeOfUseContactListGroups;
97 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inObject:(AIListObject<AIContainingObject> *)group;
98 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group;
99 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName;
102 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID;
103 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact;
104 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray;
105 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
106 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
107 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact;
108 - (void)_loadMetaContactsFromArray:(NSArray *)array;
109 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict;
110 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact;
111 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact;
113 - (void)_addMenuItemsFromArray:(NSArray *)contactArray toMenu:(NSMenu *)contactMenu target:(id)target offlineContacts:(BOOL)offlineContacts;
117 @implementation AIContactController
122 if ((self = [super init])) {
124 contactObservers = [[NSMutableSet alloc] init];
125 sortControllerArray = [[NSMutableArray alloc] init];
126 activeSortController = nil;
127 delayedStatusChanges = 0;
128 delayedModifiedStatusKeys = [[NSMutableSet alloc] init];
129 delayedAttributeChanges = 0;
130 delayedModifiedAttributeKeys = [[NSMutableSet alloc] init];
131 delayedContactChanges = 0;
132 delayedUpdateRequests = 0;
133 updatesAreDelayed = NO;
136 contactDict = [[NSMutableDictionary alloc] init];
137 groupDict = [[NSMutableDictionary alloc] init];
138 metaContactDict = [[NSMutableDictionary alloc] init];
139 contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
146 - (void)controllerDidLoad
148 //Default contact preferences
149 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:CONTACT_DEFAULT_PREFS
150 forClass:[self class]]
151 forGroup:PREF_GROUP_CONTACT_LIST];
153 contactList = [[AIListGroup alloc] initWithUID:ADIUM_ROOT_GROUP_NAME];
154 //Root is always "expanded"
155 [contactList setExpanded:YES];
157 //Show Groups menu item
158 [self prepareShowHideGroups];
160 //Observe content (for preferredContactForContentType:forListContact:)
161 [[adium notificationCenter] addObserver:self
162 selector:@selector(didSendContent:)
163 name:CONTENT_MESSAGE_SENT
166 [self loadContactList];
167 [self sortContactList];
169 adiumAuthorization = [[AdiumAuthorization alloc] init];
171 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST_DISPLAY];
175 - (void)controllerWillClose
177 [self saveContactList];
183 [[adium preferenceController] unregisterPreferenceObserver:self];
185 [contactList release];
186 [contactObservers release]; contactObservers = nil;
191 - (void)clearAllMetaContactData
194 NSDictionary *metaContactDictCopy = [metaContactDict copy];
195 NSEnumerator *enumerator;
196 AIMetaContact *metaContact;
198 if ([metaContactDictCopy count]) {
199 [self delayListObjectNotifications];
201 //Remove all the metaContacts to get any existing objects out of them
202 enumerator = [metaContactDictCopy objectEnumerator];
203 while ((metaContact = [enumerator nextObject])) {
204 [self breakdownAndRemoveMetaContact:metaContact];
207 [self endListObjectNotificationsDelay];
210 [metaContactDict release]; metaContactDict = [[NSMutableDictionary alloc] init];
211 [contactToMetaContactLookupDict release]; contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
213 //Clear the preferences for good measure
214 [[adium preferenceController] setPreference:nil
215 forKey:KEY_FLAT_METACONTACTS
216 group:PREF_GROUP_CONTACT_LIST];
217 [[adium preferenceController] setPreference:nil
218 forKey:KEY_METACONTACT_OWNERSHIP
219 group:PREF_GROUP_CONTACT_LIST];
221 //Clear out old metacontact files
222 path = [[[adium loginController] userDirectory] stringByAppendingPathComponent:OBJECT_PREFS_PATH];
223 [[NSFileManager defaultManager] removeFilesInDirectory:path
224 withPrefix:@"MetaContact"
226 [[NSFileManager defaultManager] removeFilesInDirectory:[adium cachesPath]
227 withPrefix:@"MetaContact"
230 [metaContactDictCopy release];
233 //Local Contact List Storage -------------------------------------------------------------------------------------------
234 #pragma mark Local Contact List Storage
235 //Load the contact list
236 - (void)loadContactList
238 //We must load all the groups before loading contacts for the ordering system to work correctly.
239 [self _loadMetaContactsFromArray:[[adium preferenceController] preferenceForKey:KEY_FLAT_METACONTACTS
240 group:PREF_GROUP_CONTACT_LIST]];
243 //Save the contact list
244 - (void)saveContactList
246 NSEnumerator *enumerator = [groupDict objectEnumerator];
247 AIListGroup *listGroup;
249 while ((listGroup = [enumerator nextObject])) {
250 [listGroup setPreference:[NSNumber numberWithBool:[listGroup isExpanded]]
252 group:PREF_GROUP_CONTACT_LIST];
256 - (void)_loadMetaContactsFromArray:(NSArray *)array
258 NSEnumerator *enumerator = [array objectEnumerator];
259 NSString *identifier;
261 while ((identifier = [enumerator nextObject])) {
262 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
263 NSNumber *objectID = [NSNumber numberWithInt:[[[identifier componentsSeparatedByString:@"-"] objectAtIndex:1] intValue]];
264 [self metaContactWithObjectID:objectID];
269 //Flattened array of the contact list content
270 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects
272 NSMutableArray *array = [NSMutableArray array];
273 NSEnumerator *enumerator = [listObjects objectEnumerator];;
274 AIListObject *object;
276 //Create temporary strings outside the loop
277 NSString *Group = @"Group";
278 NSString *Type = @"Type";
279 NSString *Expanded = @"Expanded";
281 while ((object = [enumerator nextObject])) {
282 [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
284 [object UID], UID_KEY,
285 [NSNumber numberWithBool:[(AIListGroup *)object isExpanded]], Expanded,
293 //Status and Display updates -------------------------------------------------------------------------------------------
294 #pragma mark Status and Display updates
295 //These delay Contact_ListChanged, ListObject_AttributesChanged, Contact_OrderChanged notificationsDelays,
296 //sorting and redrawing to prevent redundancy when making a large number of changes
297 //Explicit delay. Call endListObjectNotificationsDelay to end
298 - (void)delayListObjectNotifications
300 delayedUpdateRequests++;
301 updatesAreDelayed = YES;
304 //End an explicit delay
305 - (void)endListObjectNotificationsDelay
307 delayedUpdateRequests--;
308 if (delayedUpdateRequests == 0 && !delayedUpdateTimer) {
309 [self _performDelayedUpdates:nil];
313 //Delay all list object notifications until a period of inactivity occurs. This is useful for accounts that do not
314 //know when they have finished connecting but still want to mute events.
315 - (void)delayListObjectNotificationsUntilInactivity
317 if (!delayedUpdateTimer) {
318 updatesAreDelayed = YES;
319 delayedUpdateTimer = [[NSTimer scheduledTimerWithTimeInterval:UPDATE_CLUMP_INTERVAL
321 selector:@selector(_performDelayedUpdates:)
323 repeats:YES] retain];
326 [delayedUpdateTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:UPDATE_CLUMP_INTERVAL]];
330 //Update the status of a list object. This will update any information that is otherwise too expensive to update
331 //automatically, such as their profile.
332 - (void)updateListContactStatus:(AIListContact *)inContact
334 //If we're handed something that can contain other contacts, update the status of the contacts contained within it
335 if ([inContact conformsToProtocol:@protocol(AIContainingObject)]) {
336 NSEnumerator *enumerator = [[(AIListObject<AIContainingObject> *)inContact listContacts] objectEnumerator];
337 AIListContact *contact;
339 while ((contact = [enumerator nextObject])) {
340 [self updateListContactStatus:contact];
344 AIAccount *account = [inContact account];
345 if (![account online]) {
346 account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
347 toContact:inContact];
350 [account updateContactStatus:inContact];
354 //Called after modifying a contact's status
355 // Silent: Silences all events, notifications, sounds, overlays, etc. that would have been associated with this status change
356 - (void)listObjectStatusChanged:(AIListObject *)inObject modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
358 NSSet *modifiedAttributeKeys;
360 //Let all observers know the contact's status has changed before performing any sorting or further notifications
361 modifiedAttributeKeys = [self _informObserversOfObjectStatusChange:inObject withKeys:inModifiedKeys silent:silent];
363 //Resort the contact list
364 if (updatesAreDelayed) {
365 delayedStatusChanges++;
366 [delayedModifiedStatusKeys unionSet:inModifiedKeys];
368 //We can safely skip sorting if we know the modified attributes will invoke a resort later
369 if (![[self activeSortController] shouldSortForModifiedAttributeKeys:modifiedAttributeKeys] &&
370 [[self activeSortController] shouldSortForModifiedStatusKeys:inModifiedKeys]) {
371 [self sortListObject:inObject];
375 //Post an attributes changed message (if necessary)
376 if ([modifiedAttributeKeys count]) {
377 [self listObjectAttributesChanged:inObject modifiedKeys:modifiedAttributeKeys];
381 //Call after modifying an object's display attributes
382 //(When modifying display attributes in response to a status change, this is not necessary)
383 - (void)listObjectAttributesChanged:(AIListObject *)inObject modifiedKeys:(NSSet *)inModifiedKeys
385 if (updatesAreDelayed) {
386 delayedAttributeChanges++;
387 [delayedModifiedAttributeKeys unionSet:inModifiedKeys];
389 //Resort the contact list if necessary
390 if ([[self activeSortController] shouldSortForModifiedAttributeKeys:inModifiedKeys]) {
391 [self sortListObject:inObject];
394 //Post an attributes changed message
395 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
397 userInfo:(inModifiedKeys ?
398 [NSDictionary dictionaryWithObject:inModifiedKeys
404 //Performs any delayed list object/handle updates
405 - (void)_performDelayedUpdates:(NSTimer *)timer
407 BOOL updatesOccured = (delayedStatusChanges || delayedAttributeChanges || delayedContactChanges);
409 //Send out global attribute & status changed notifications (to cover any delayed updates)
410 if (updatesOccured) {
411 BOOL shouldSort = NO;
413 //Inform observers of any changes
414 if (delayedContactChanges) {
415 // [[adium notificationCenter] postNotificationName:Contact_ListChanged object:nil];
416 delayedContactChanges = 0;
419 if (delayedStatusChanges) {
421 [[self activeSortController] shouldSortForModifiedStatusKeys:delayedModifiedStatusKeys]) {
424 [delayedModifiedStatusKeys removeAllObjects];
425 delayedStatusChanges = 0;
427 if (delayedAttributeChanges) {
429 [[self activeSortController] shouldSortForModifiedAttributeKeys:delayedModifiedAttributeKeys]) {
432 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
434 userInfo:(delayedModifiedAttributeKeys ?
435 [NSDictionary dictionaryWithObject:delayedModifiedAttributeKeys
438 [delayedModifiedAttributeKeys removeAllObjects];
439 delayedAttributeChanges = 0;
442 //Sort only if necessary
444 [self sortContactList];
448 //If no more updates are left to process, disable the update timer
449 //If there are no delayed update requests, remove the hold
450 if (!delayedUpdateTimer || !updatesOccured) {
451 if (delayedUpdateTimer) {
452 [delayedUpdateTimer invalidate];
453 [delayedUpdateTimer release];
454 delayedUpdateTimer = nil;
456 if (delayedUpdateRequests == 0) {
457 updatesAreDelayed = NO;
462 #pragma mark Contact Grouping
463 //Contact Grouping -----------------------------------------------------------------------------------------------------
465 //Redetermine the local grouping of a contact in response to server grouping information or an external change
466 - (void)listObjectRemoteGroupingChanged:(AIListContact *)inContact
468 AIListObject<AIContainingObject> *containingObject;
469 NSString *remoteGroupName = [inContact remoteGroupName];
473 containingObject = [inContact containingObject];
475 if ([containingObject isKindOfClass:[AIMetaContact class]]) {
477 /* If inContact's containingObject is a metaContact, and that metaContact has no containingObject,
478 * use inContact's remote grouping as the metaContact's grouping.
480 if (![containingObject containingObject] && [remoteGroupName length]) {
481 //If no similar objects exist, we add this contact directly to the list
482 //Create a group for the contact even if contact list groups aren't on,
483 //otherwise requests for all the contact list groups will return nothing
484 AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
486 localGroup = (useContactListGroups ?
487 ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
490 [localGroup addObject:containingObject];
491 [self _listChangedGroup:localGroup object:containingObject];
492 // AILog(@"listObjectRemoteGroupingChanged: %@ is in %@, which was moved to %@",inContact,containingObject,localGroup);
496 //If we have a remoteGroupName, add the contact locally to the list
497 if (remoteGroupName) {
498 //Create a group for the contact even if contact list groups aren't on,
499 //otherwise requests for all the contact list groups will return nothing
500 AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
502 localGroup = (useContactListGroups ?
503 ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
506 // AILog(@"listObjectRemoteGroupingChanged: %@: remoteGroupName %@ --> %@",inContact,remoteGroupName,localGroup);
508 [self _moveContactLocally:inContact
512 //If !remoteGroupName, remove the contact from any local groups
513 if (containingObject) {
515 [(AIListGroup *)containingObject removeObject:inContact];
517 [self _listChangedGroup:(AIListGroup *)containingObject object:inContact];
519 // AILog(@"listObjectRemoteGroupingChanged: %@: -- !remoteGroupName so removed from %@",inContact,containingObject);
524 BOOL isCurrentlyAStranger = [inContact isStranger];
525 if ((isCurrentlyAStranger && (remoteGroupName != nil)) ||
526 (!isCurrentlyAStranger && (remoteGroupName == nil))) {
527 [inContact setStatusObject:(remoteGroupName ? [NSNumber numberWithBool:YES] : nil)
528 forKey:@"NotAStranger"
530 [inContact notifyOfChangedStatusSilently:YES];
536 - (void)_moveContactLocally:(AIListContact *)listContact toGroup:(AIListGroup *)localGroup
538 AIListObject *containingObject;
539 AIListObject *existingObject;
540 BOOL performedGrouping = NO;
542 //Protect with a retain while we are removing and adding the contact to our arrays
543 [listContact retain];
546 // AILog(@"Moving %@ to %@",listContact,localGroup);
548 //Remove this object from any local groups we have it in currently
549 if ((containingObject = [listContact containingObject]) &&
550 ([containingObject isKindOfClass:[AIListGroup class]])) {
552 [(AIListGroup *)containingObject removeObject:listContact];
553 [self _listChangedGroup:(AIListGroup *)containingObject object:listContact];
556 if ((existingObject = [localGroup objectWithService:[listContact service] UID:[listContact UID]])) {
557 //If an object exists in this group with the same UID and serviceID, create a MetaContact
559 [self groupListContacts:[NSArray arrayWithObjects:listContact,existingObject,nil]];
560 performedGrouping = YES;
563 AIMetaContact *metaContact;
565 //If no object exists in this group which matches, we should check if there is already
566 //a MetaContact holding a matching ListContact, since we should include this contact in it
567 //If we found a metaContact to which we should add, do it.
568 if ((metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]])) {
570 // AILog(@"Found an existing metacontact; adding %@ to %@",listContact,metaContact);
572 [self addListObject:listContact toMetaContact:metaContact];
573 performedGrouping = YES;
577 if (!performedGrouping) {
578 //If no similar objects exist, we add this contact directly to the list
579 [localGroup addObject:listContact];
582 [self _listChangedGroup:localGroup object:listContact];
586 [listContact release];
589 - (AIListGroup *)remoteGroupForContact:(AIListContact *)inContact
593 if ([inContact isKindOfClass:[AIMetaContact class]]) {
594 //For a metaContact, the closest we have to a remote group is the group it is within locally
595 group = [(AIMetaContact *)inContact parentGroup];
598 NSString *remoteGroup = [inContact remoteGroupName];
599 group = (remoteGroup ? [self groupWithUID:remoteGroup] : nil);
605 //Post a list grouping changed notification for the object and group
606 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object
608 if (updatesAreDelayed) {
609 delayedContactChanges++;
611 [[adium notificationCenter] postNotificationName:Contact_ListChanged
613 userInfo:(group ? [NSDictionary dictionaryWithObject:group forKey:@"ContainingGroup"] : nil)];
617 - (BOOL)useContactListGroups
619 return useContactListGroups;
622 - (void)setUseContactListGroups:(BOOL)inFlag
624 if (inFlag != useContactListGroups) {
625 useContactListGroups = inFlag;
627 [self _performChangeOfUseContactListGroups];
631 - (void)_performChangeOfUseContactListGroups
633 NSEnumerator *enumerator;
634 AIListObject *listObject;
636 [self delayListObjectNotifications];
638 //Store the preference
639 [[adium preferenceController] setPreference:[NSNumber numberWithBool:!useContactListGroups]
640 forKey:KEY_HIDE_CONTACT_LIST_GROUPS
641 group:PREF_GROUP_CONTACT_LIST_DISPLAY];
643 //Configure the sort controller to force ignoring of groups as appropriate
644 [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
646 enumerator = [[[[contactList containedObjects] copy] autorelease] objectEnumerator];
648 if (useContactListGroups) { /* We are now using contact list groups, but we weren't before. */
650 //Restore the grouping of all root-level contacts
651 while ((listObject = [enumerator nextObject])) {
652 if ([listObject isKindOfClass:[AIListContact class]]) {
653 [(AIListContact *)listObject restoreGrouping];
657 } else { /* We are no longer using contact list groups, but we were before. */
659 while ((listObject = [enumerator nextObject])) {
660 if ([listObject isKindOfClass:[AIListGroup class]]) {
661 NSArray *containedObjects;
662 NSEnumerator *groupEnumerator;
663 AIListObject *containedListObject;
665 containedObjects = [[(AIListGroup *)listObject containedObjects] copy];
666 groupEnumerator = [containedObjects objectEnumerator];
667 while ((containedListObject = [groupEnumerator nextObject])) {
668 if ([containedListObject isKindOfClass:[AIListContact class]]) {
669 [self _moveContactLocally:(AIListContact *)containedListObject
670 toGroup:contactList];
673 [containedObjects release];
678 //Stop delaying object notifications; this will automatically resort the contact list, so we're done.
679 [self endListObjectNotificationsDelay];
682 - (void)prepareShowHideGroups
684 //Load the preference
685 useContactListGroups = ![[[adium preferenceController] preferenceForKey:KEY_HIDE_CONTACT_LIST_GROUPS
686 group:PREF_GROUP_CONTACT_LIST_DISPLAY] boolValue];
688 //Show offline contacts menu item
689 menuItem_showGroups = [[NSMenuItem alloc] initWithTitle:SHOW_GROUPS_MENU_TITLE
691 action:@selector(toggleShowGroups:)
693 [menuItem_showGroups setState:useContactListGroups];
694 [[adium menuController] addMenuItem:menuItem_showGroups toLocation:LOC_View_Toggles];
697 NSToolbarItem *toolbarItem;
698 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SHOW_GROUPS_IDENTIFER
699 label:AILocalizedString(@"Show Groups",nil)
700 paletteLabel:AILocalizedString(@"Toggle Groups Display",nil)
701 toolTip:AILocalizedString(@"Toggle display of groups",nil)
703 settingSelector:@selector(setImage:)
704 itemContent:[NSImage imageNamed:(useContactListGroups ?
705 @"togglegroups_transparent" :
707 forClass:[self class]]
708 action:@selector(toggleShowGroupsToolbar:)
710 [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ContactList"];
713 - (IBAction)toggleShowGroups:(id)sender
716 useContactListGroups = !useContactListGroups;
717 [menuItem_showGroups setState:useContactListGroups];
719 //Update the contact list. Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure.
720 [self performSelector:@selector(_performChangeOfUseContactListGroups)
722 afterDelay:0.000001];
725 - (IBAction)toggleShowGroupsToolbar:(id)sender
727 [self toggleShowGroups:sender];
729 [sender setImage:[NSImage imageNamed:(useContactListGroups ?
730 @"togglegroups_transparent" :
732 forClass:[self class]]];
735 - (BOOL)useOfflineGroup
737 return useOfflineGroup;
740 - (void)setUseOfflineGroup:(BOOL)inFlag
742 if (inFlag != useOfflineGroup) {
743 useOfflineGroup = inFlag;
745 if (useOfflineGroup) {
746 [self registerListObjectObserver:self];
748 [self updateAllListObjectsForObserver:self];
749 [self unregisterListObjectObserver:self];
754 - (AIListGroup *)offlineGroup
756 return [self groupWithUID:AILocalizedString(@"Offline", "Name of offline group")];
759 #pragma mark Meta Contacts
760 //Meta Contacts --------------------------------------------------------------------------------------------------------
762 * @brief Create or load a metaContact
764 * @param inObjectID The objectID of an existing but unloaded metaContact, or nil to create and save a new metaContact
766 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID
768 NSString *metaContactDictKey;
769 AIMetaContact *metaContact;
770 BOOL shouldRestoreContacts = YES;
772 //If no object ID is provided, use the next available object ID
773 //(MetaContacts should always have an individually unique object id)
775 int topID = [[[adium preferenceController] preferenceForKey:TOP_METACONTACT_ID
776 group:PREF_GROUP_CONTACT_LIST] intValue];
777 inObjectID = [NSNumber numberWithInt:topID];
778 [[adium preferenceController] setPreference:[NSNumber numberWithInt:([inObjectID intValue] + 1)]
779 forKey:TOP_METACONTACT_ID
780 group:PREF_GROUP_CONTACT_LIST];
782 //No reason to waste time restoring contacts when none are in the meta contact yet.
783 shouldRestoreContacts = NO;
786 //Look for a metacontact with this object ID. If none is found, create one
787 //and add its contained contacts to it.
788 metaContactDictKey = [AIMetaContact internalObjectIDFromObjectID:inObjectID];
790 metaContact = [metaContactDict objectForKey:metaContactDictKey];
792 metaContact = [[AIMetaContact alloc] initWithObjectID:inObjectID];
794 //Keep track of it in our metaContactDict for retrieval by objectID
795 [metaContactDict setObject:metaContact forKey:metaContactDictKey];
797 //Add it to our more general contactDict, as well
798 [contactDict setObject:metaContact forKey:[metaContact internalUniqueObjectID]];
800 /* We restore contacts (actually, internalIDs for contacts, to be added as necessary later) if the metaContact
801 * existed before this call to metaContactWithObjectID:
803 if (shouldRestoreContacts) {
804 if (![self _restoreContactsToMetaContact:metaContact]) {
806 //If restoring the metacontact did not actually add any contacts, delete it since it is invalid
807 [self breakdownAndRemoveMetaContact:metaContact];
812 /* As with contactWithService:account:UID, update all attributes so observers are initially informed of
813 * this object's existence.
815 [self _updateAllAttributesOfObject:metaContact];
817 [metaContact release];
820 return (metaContact);
824 * @brief Associate the appropriate internal IDs for contained contacts with a metaContact
826 * @result YES if one or more contacts was associated with the metaContact; NO if none were.
828 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact
830 NSDictionary *allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
831 group:PREF_GROUP_CONTACT_LIST];
832 NSArray *containedContactsArray = [allMetaContactsDict objectForKey:[metaContact internalObjectID]];
833 BOOL restoredContacts;
835 if ([containedContactsArray count]) {
836 [self _restoreContactsToMetaContact:metaContact
837 fromContainedContactsArray:containedContactsArray];
839 restoredContacts = YES;
842 restoredContacts = NO;
845 return restoredContacts;
849 * @brief Associate the internal IDs for an array of contacts with a specific metaContact
851 * This does not actually place any AIListContacts within the metaContact. Instead, it updates the contactToMetaContactLookupDict
852 * dictionary to have metaContact associated with the list contacts specified by containedContactsArray. This
853 * allows us to add them lazily to the metaContact (in contactWithService:account:UID:) as necessary.
855 * @param metaContact The metaContact to which contact referneces are added
856 * @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
858 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray
860 NSEnumerator *enumerator = [containedContactsArray objectEnumerator];
861 NSDictionary *containedContactDict;
863 while ((containedContactDict = [enumerator nextObject])) {
864 /* Before Adium 0.80, metaContacts could be created within metaContacts. Simply ignore any attempt to restore
865 * such erroneous data, which will have a YES boolValue for KEY_IS_METACONTACT. */
866 if (![[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) {
867 /* Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject.
869 * As listObjects are loaded/created/requested which match this internalObjectID,
870 * they will be inserted into the metaContact.
872 NSString *internalObjectID = [AIListObject internalObjectIDForServiceID:[containedContactDict objectForKey:SERVICE_ID_KEY]
873 UID:[containedContactDict objectForKey:UID_KEY]];
874 [contactToMetaContactLookupDict setObject:metaContact
875 forKey:internalObjectID];
881 //Add a list object to a meta contact, setting preferences and such
882 //so the association is lasting across program launches.
883 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
885 if (!listObject || [listObject isKindOfClass:[AIListGroup class]]) {
886 //I can't think of why one would want to add an entire group to a metacontact. Let's say you can't.
887 NSLog(@"Warning: addListObject:toMetaContact: Attempted to add %@ to %@",listObject,metaContact);
891 if (listObject == metaContact) return;
893 //If listObject contains other contacts, perform addListObject:toMetaContact: recursively
894 if ([listObject conformsToProtocol:@protocol(AIContainingObject)]) {
895 NSEnumerator *enumerator = [[[[(AIListObject<AIContainingObject> *)listObject containedObjects] copy] autorelease] objectEnumerator];
896 AIListObject *someObject;
898 while ((someObject = [enumerator nextObject]))
899 [self addListObject:someObject toMetaContact:metaContact];
902 //Obtain any metaContact this listObject is currently within, so we can remove it later
903 AIMetaContact *oldMetaContact = [contactToMetaContactLookupDict objectForKey:[listObject internalObjectID]];
905 if ([self _performAddListObject:listObject toMetaContact:metaContact]) {
906 //If this listObject was not in this metaContact in any form before, store the change
907 if (metaContact != oldMetaContact) {
908 //Remove the list object from any other metaContact it is in at present
910 [self removeListObject:listObject fromMetaContact:oldMetaContact];
912 [self _storeListObject:listObject inMetaContact:metaContact];
918 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact
920 //we only allow group->meta->contact, not group->meta->meta->contact
921 NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
923 // AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact);
924 NSDictionary *containedContactDict;
925 NSMutableDictionary *allMetaContactsDict;
926 NSMutableArray *containedContactsArray;
928 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
930 //Get the dictionary of all metaContacts
931 allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
932 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
933 if (!allMetaContactsDict) {
934 allMetaContactsDict = [[NSMutableDictionary alloc] init];
937 //Load the array for the new metaContact
938 containedContactsArray = [[allMetaContactsDict objectForKey:metaContactInternalObjectID] mutableCopy];
939 if (!containedContactsArray) containedContactsArray = [[NSMutableArray alloc] init];
940 containedContactDict = nil;
942 //Create the dictionary describing this list object
943 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
944 [[listObject service] serviceID],SERVICE_ID_KEY,
945 [listObject UID],UID_KEY,nil];
947 //Only add if this dict isn't already in the array
948 if (containedContactDict && ([containedContactsArray indexOfObject:containedContactDict] == NSNotFound)) {
949 [containedContactsArray addObject:containedContactDict];
950 [allMetaContactsDict setObject:containedContactsArray forKey:metaContactInternalObjectID];
953 [self _saveMetaContacts:allMetaContactsDict];
955 [[adium contactAlertsController] mergeAndMoveContactAlertsFromListObject:listObject
956 intoListObject:metaContact];
959 [allMetaContactsDict release];
960 [containedContactsArray release];
963 //Actually adds a list object to a meta contact. No preferences are changed.
964 //Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict
965 //for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful.
966 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
968 //we only allow group->meta->contact, not group->meta->meta->contact
969 //FIXME: deal with smack contacts correctly.
970 //NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
971 AIListObject<AIContainingObject> *localGroup;
974 localGroup = [listObject containingObject];
976 //Remove the object from its previous containing group
977 if (localGroup && (localGroup != metaContact)) {
978 [localGroup removeObject:listObject];
979 [self _listChangedGroup:localGroup object:listObject];
982 //AIMetaContact will handle reassigning the list object's grouping to being itself
983 if ((success = [metaContact addObject:listObject])) {
984 [contactToMetaContactLookupDict setObject:metaContact forKey:[listObject internalObjectID]];
986 [self _listChangedGroup:metaContact object:listObject];
987 //If the metaContact isn't in a group yet, use the group of the object we just added
988 if ((![metaContact containingObject]) && localGroup) {
989 //Add the new meta contact to our list
990 [(AIMetaContact *)localGroup addObject:metaContact];
991 [self _listChangedGroup:localGroup object:metaContact];
998 - (void)removeAllListObjectsMatching:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1000 NSEnumerator *enumerator;
1001 AIListObject *theObject;
1003 enumerator = [[self allContactsWithService:[listObject service]
1004 UID:[listObject UID]
1005 existingOnly:YES] objectEnumerator];
1007 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1008 [contactToMetaContactLookupDict removeObjectForKey:[listObject internalObjectID]];
1010 [self delayListObjectNotifications];
1011 while ((theObject = [enumerator nextObject])) {
1012 [self removeListObject:theObject fromMetaContact:metaContact];
1014 [self endListObjectNotificationsDelay];
1017 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1019 //we only allow group->meta->contact, not group->meta->meta->contact
1020 NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
1022 NSEnumerator *enumerator;
1023 NSArray *containedContactsArray;
1024 NSDictionary *containedContactDict = nil;
1025 NSMutableDictionary *allMetaContactsDict;
1026 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
1028 //Get the dictionary of all metaContacts
1029 allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1030 group:PREF_GROUP_CONTACT_LIST];
1032 //Load the array for the metaContact
1033 containedContactsArray = [allMetaContactsDict objectForKey:metaContactInternalObjectID];
1035 //Enumerate it, looking only for the appropriate type of containedContactDict
1036 enumerator = [containedContactsArray objectEnumerator];
1038 NSString *listObjectUID = [listObject UID];
1039 NSString *listObjectServiceID = [[listObject service] serviceID];
1041 while ((containedContactDict = [enumerator nextObject])) {
1042 if ([[containedContactDict objectForKey:UID_KEY] isEqualToString:listObjectUID] &&
1043 [[containedContactDict objectForKey:SERVICE_ID_KEY] isEqualToString:listObjectServiceID]) {
1048 //If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result
1049 if (containedContactDict) {
1050 NSMutableArray *newContainedContactsArray;
1051 NSMutableDictionary *newAllMetaContactsDict;
1053 newContainedContactsArray = [containedContactsArray mutableCopy];
1054 [newContainedContactsArray removeObjectIdenticalTo:containedContactDict];
1056 newAllMetaContactsDict = [allMetaContactsDict mutableCopy];
1057 [newAllMetaContactsDict setObject:newContainedContactsArray
1058 forKey:metaContactInternalObjectID];
1060 [self _saveMetaContacts:newAllMetaContactsDict];
1062 [newContainedContactsArray release];
1063 [newAllMetaContactsDict release];
1066 //The listObject can be within the metaContact without us finding a containedContactDict if we are removing multiple
1067 //listContacts referring to the same UID & serviceID combination - that is, on multiple accounts on the same service.
1068 //We therefore request removal of the object regardless of the if (containedContactDict) check above.
1069 [metaContact removeObject:listObject];
1074 * @brief Groups UIDs for services into a single metacontact
1076 * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to
1077 * a UID and a service, respectively, which together define a contact which should be included in the grouping.
1079 * Assumption: This is only called after the contact list is finished loading, which occurs via
1080 * -(void)controllerDidLoad above.
1082 * @param UIDsArray NSArray of UIDs
1083 * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
1085 - (AIMetaContact *)groupUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray
1087 NSMutableSet *internalObjectIDs = [[NSMutableSet alloc] init];
1088 AIMetaContact *metaContact = nil;
1089 NSEnumerator *enumerator;
1090 NSString *internalObjectID;
1091 int count = [UIDsArray count];
1094 //Build an array of all contacts matching this description (multiple accounts on the same service listing
1095 //the same UID mean that we can have multiple AIListContact objects with a UID/service combination)
1096 for (i = 0; i < count; i++) {
1097 NSString *serviceID = [servicesArray objectAtIndex:i];
1098 NSString *UID = [UIDsArray objectAtIndex:i];
1100 internalObjectID = [AIListObject internalObjectIDForServiceID:serviceID
1103 metaContact = [contactToMetaContactLookupDict objectForKey:internalObjectID];
1106 [internalObjectIDs addObject:internalObjectID];
1109 //Create a new metaContact is we didn't find one.
1111 metaContact = [self metaContactWithObjectID:nil];
1114 enumerator = [internalObjectIDs objectEnumerator];
1115 while ((internalObjectID = [enumerator nextObject])) {
1116 AIListObject *existingObject;
1117 if ((existingObject = [self existingListObjectWithUniqueID:internalObjectID])) {
1118 /* If there is currently an object (or multiple objects) matching this internalObjectID
1119 * we should add immediately.
1121 [self addListObject:existingObject
1122 toMetaContact:metaContact];
1124 /* If no objects matching this internalObjectID exist, we can simply add to the
1125 * contactToMetaContactLookupDict for use if such an object is created later.
1127 [contactToMetaContactLookupDict setObject:metaContact
1128 forKey:internalObjectID];
1131 [internalObjectIDs release];
1136 /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added.
1138 * This will reuse an existing metacontact (for one of the contacts in the array) if possible.
1139 * @param contactsToGroupArray Contacts to group together
1141 - (AIMetaContact *)groupListContacts:(NSArray *)contactsToGroupArray
1143 NSEnumerator *enumerator;
1144 AIListContact *listContact;
1145 AIMetaContact *metaContact = nil;
1147 //Look for an existing MetaContact we can use. The first one we find is the lucky winner.
1148 enumerator = [contactsToGroupArray objectEnumerator];
1149 while ((listContact = [enumerator nextObject]) && (metaContact == nil)) {
1150 if ([listContact isKindOfClass:[AIMetaContact class]]) {
1151 metaContact = (AIMetaContact *)listContact;
1153 metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]];
1157 //Create a new metaContact is we didn't find one.
1159 metaContact = [self metaContactWithObjectID:nil];
1162 /* Add all these contacts to our MetaContact.
1163 * Some may already be present, but that's fine, as nothing will happen.
1165 enumerator = [contactsToGroupArray objectEnumerator];
1166 while ((listContact = [enumerator nextObject])) {
1167 [self addListObject:listContact toMetaContact:metaContact];
1173 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact
1175 //Remove the objects within it from being inside it
1176 NSArray *containedObjects = [[metaContact containedObjects] copy];
1177 NSEnumerator *metaEnumerator = [containedObjects objectEnumerator];
1178 AIListObject<AIContainingObject> *containingObject = [metaContact containingObject];
1179 AIListObject *object;
1181 NSMutableDictionary *allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1182 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
1184 while ((object = [metaEnumerator nextObject])) {
1186 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1187 [contactToMetaContactLookupDict removeObjectForKey:[object internalObjectID]];
1189 [self removeListObject:object fromMetaContact:metaContact];
1192 //Then, procede to remove the metaContact
1195 [metaContact retain];
1197 //Remove it from its containing group
1198 [containingObject removeObject:metaContact];
1200 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
1202 //Remove our reference to it internally
1203 [metaContactDict removeObjectForKey:metaContactInternalObjectID];
1205 //Remove it from the preferences dictionary
1206 [allMetaContactsDict removeObjectForKey:metaContactInternalObjectID];
1208 //XXX - contactToMetaContactLookupDict
1210 //Post the list changed notification for the old containingObject
1211 [self _listChangedGroup:containingObject object:metaContact];
1213 //Save the updated allMetaContactsDict which no longer lists the metaContact
1214 [self _saveMetaContacts:allMetaContactsDict];
1216 //Protection is overrated.
1217 [metaContact release];
1218 [containedObjects release];
1219 [allMetaContactsDict release];
1222 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict
1224 AILog(@"MetaContacts: Saving!");
1225 [[adium preferenceController] setPreference:allMetaContactsDict
1226 forKey:KEY_METACONTACT_OWNERSHIP
1227 group:PREF_GROUP_CONTACT_LIST];
1228 [[adium preferenceController] setPreference:[allMetaContactsDict allKeys]
1229 forKey:KEY_FLAT_METACONTACTS
1230 group:PREF_GROUP_CONTACT_LIST];
1233 //Sort list objects alphabetically by their display name
1234 int contactDisplayNameSort(AIListObject *objectA, AIListObject *objectB, void *context)
1236 return [[objectA displayName] caseInsensitiveCompare:[objectB displayName]];
1239 //Return either the highest metaContact containing this list object, or the list object itself. Appropriate for when
1240 //preferences should be read from/to the most generalized contact possible.
1241 - (AIListObject *)parentContactForListObject:(AIListObject *)listObject
1243 if ([listObject isKindOfClass:[AIListContact class]]) {
1244 //Find the highest-up metaContact
1245 AIListObject *containingObject;
1246 while ([(containingObject = [listObject containingObject]) isKindOfClass:[AIMetaContact class]]) {
1247 listObject = (AIMetaContact *)containingObject;
1254 #pragma mark Preference observing
1256 * @brief Preferences changed
1258 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
1259 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
1262 [key isEqualToString:KEY_USE_OFFLINE_GROUP] ||
1263 [key isEqualToString:KEY_SHOW_OFFLINE_CONTACTS]) {
1265 [self setUseOfflineGroup:([[prefDict objectForKey:KEY_USE_OFFLINE_GROUP] boolValue])];
1270 * @brief Move contacts to and from the offline group as necessary as their online state changes.
1272 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1274 if (!inModifiedKeys ||
1275 [inModifiedKeys containsObject:@"Online"]) {
1277 if ([inObject isKindOfClass:[AIListContact class]]) {
1278 //If this contact is not its own parent contact, don't bother since we'll get an update for the parent if appropriate
1279 if (inObject == [(AIListContact *)inObject parentContact]) {
1280 if (useOfflineGroup) {
1281 AIListObject *containingObject = [inObject containingObject];
1283 if ([inObject online] &&
1284 (containingObject == [self offlineGroup])) {
1285 [(AIListContact *)inObject restoreGrouping];
1287 } else if (![inObject online] &&
1289 (containingObject != [self offlineGroup])) {
1290 [self _moveContactLocally:(AIListContact *)inObject
1291 toGroup:[self offlineGroup]];
1295 if ([inObject containingObject] == [self offlineGroup]) {
1296 [(AIListContact *)inObject restoreGrouping];
1306 //Contact Sorting --------------------------------------------------------------------------------
1307 #pragma mark Contact Sorting
1308 //Register sorting code
1309 - (void)registerListSortController:(AISortController *)inController
1311 [sortControllerArray addObject:inController];
1313 - (NSArray *)sortControllerArray
1315 return sortControllerArray;
1318 //Set and get the active sort controller
1319 - (void)setActiveSortController:(AISortController *)inController
1321 activeSortController = inController;
1323 [activeSortController didBecomeActive];
1325 //The newly-active sort controller needs to know whether it should be forced to ignore groups
1326 [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
1329 [self sortContactList];
1331 - (AISortController *)activeSortController
1333 return activeSortController;
1336 //Sort the entire contact list
1337 - (void)sortContactList
1339 [contactList sortGroupAndSubGroups:YES sortController:activeSortController];
1340 [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:nil];
1343 //Sort an individual object
1344 - (void)sortListObject:(AIListObject *)inObject
1346 if (updatesAreDelayed) {
1347 delayedContactChanges++;
1349 AIListObject *group = [inObject containingObject];
1351 if ([group isKindOfClass:[AIListGroup class]]) {
1352 //Sort the groups containing this object
1353 [(AIListGroup *)group sortListObject:inObject sortController:activeSortController];
1354 [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:inObject];
1359 //List object observers ------------------------------------------------------------------------------------------------
1360 #pragma mark List object observers
1361 //Registers code to observe handle status changes
1362 - (void)registerListObjectObserver:(id <AIListObjectObserver>)inObserver
1365 [contactObservers addObject:[NSValue valueWithNonretainedObject:inObserver]];
1367 //Let the new observer process all existing objects
1368 [self updateAllListObjectsForObserver:inObserver];
1371 - (void)unregisterListObjectObserver:(id)inObserver
1373 [contactObservers removeObject:[NSValue valueWithNonretainedObject:inObserver]];
1378 * @brief Update all contacts for an observer, notifying the observer of each one in turn
1380 * @param contacts The contacts to update, or nil to update all contacts
1381 * @param inObserver The observer
1383 - (void)updateContacts:(NSSet *)contacts forObserver:(id <AIListObjectObserver>)inObserver
1385 NSEnumerator *enumerator;
1386 AIListObject *listObject;
1388 [self delayListObjectNotifications];
1390 enumerator = (contacts ? [contacts objectEnumerator] : [contactDict objectEnumerator]);
1391 while ((listObject = [enumerator nextObject])) {
1392 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1393 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1394 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1396 //If this contact is within a meta contact, update the meta contact too
1397 AIListObject<AIContainingObject> *containingObject = [listObject containingObject];
1398 if (containingObject && [containingObject isKindOfClass:[AIMetaContact class]]) {
1399 NSSet *attributes = [inObserver updateListObject:containingObject
1402 if (attributes) [self listObjectAttributesChanged:containingObject
1403 modifiedKeys:attributes];
1408 [self endListObjectNotificationsDelay];
1411 //Instructs a controller to update all available list objects
1412 - (void)updateAllListObjectsForObserver:(id <AIListObjectObserver>)inObserver
1414 NSEnumerator *enumerator;
1415 AIListObject *listObject;
1417 [self delayListObjectNotifications];
1420 [self updateContacts:nil forObserver:inObserver];
1423 enumerator = [groupDict objectEnumerator];
1424 while ((listObject = [enumerator nextObject])) {
1425 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1426 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1429 //Reset all accounts
1430 enumerator = [[[adium accountController] accounts] objectEnumerator];
1431 while ((listObject = [enumerator nextObject])) {
1432 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1433 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1437 [self endListObjectNotificationsDelay];
1441 //Notify observers of a status change. Returns the modified attribute keys
1442 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
1444 NSMutableSet *attrChange = nil;
1445 NSEnumerator *enumerator;
1446 NSValue *observerValue;
1448 //Let our observers know
1449 enumerator = [contactObservers objectEnumerator];
1450 while ((observerValue = [enumerator nextObject])) {
1451 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1452 id <AIListObjectObserver> observer;
1455 observer = [observerValue nonretainedObjectValue];
1456 if ((newKeys = [observer updateListObject:inObject keys:modifiedKeys silent:silent])) {
1457 if (!attrChange) attrChange = [[NSMutableSet alloc] init];
1458 [attrChange unionSet:newKeys];
1463 //Send out the notification for other observers
1464 [[adium notificationCenter] postNotificationName:ListObject_StatusChanged
1466 userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys
1467 forKey:@"Keys"] : nil)];
1469 return [attrChange autorelease];
1472 //Command all observers to apply their attributes to an object
1473 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject
1475 NSEnumerator *enumerator = [contactObservers objectEnumerator];
1476 NSValue *observerValue;
1478 while ((observerValue = [enumerator nextObject])) {
1479 id <AIListObjectObserver> observer = [observerValue nonretainedObjectValue];
1481 [observer updateListObject:inObject keys:nil silent:YES];
1487 //Contact List Access --------------------------------------------------------------------------------------------------
1488 #pragma mark Contact List Access
1489 //Returns the main contact list group
1490 - (AIListGroup *)contactList
1495 //Returns a flat array of all contacts (by calling through to -allContactsInObject:recurse:onAccount:)
1496 - (NSMutableArray *)allContacts
1498 return [self allContactsInObject:contactList recurse:YES onAccount:nil];
1501 //Returns a flat array of all contacts on a given account (by calling through to -allContactsInObject:recurse:onAccount:)
1502 - (NSMutableArray *)allContactsOnAccount:(AIAccount *)inAccount
1504 return [self allContactsInObject:contactList recurse:YES onAccount:inAccount];
1507 //Return a flat array of all the objects in a group on an account (and all subgroups, if desired)
1508 - (NSMutableArray *)allContactsInObject:(AIListObject<AIContainingObject> *)inGroup recurse:(BOOL)recurse onAccount:(AIAccount *)inAccount
1510 NSParameterAssert(inGroup != nil);
1512 NSMutableArray *contactArray = [NSMutableArray array];
1514 NSEnumerator *enumerator = [[inGroup containedObjects] objectEnumerator];
1515 AIListObject *object;
1516 while ((object = [enumerator nextObject])) {
1517 if (recurse && [object conformsToProtocol:@protocol(AIContainingObject)]) {
1518 [contactArray addObjectsFromArray:[self allContactsInObject:(AIListObject<AIContainingObject> *)object
1520 onAccount:inAccount]];
1522 else if ([object isMemberOfClass:[AIListContact class]] && (!inAccount || ([(AIListContact *)object account] == inAccount)))
1523 [contactArray addObject:object];
1526 return contactArray;
1529 //Contact List Menus- --------------------------------------------------------------------------------------------------
1530 #pragma mark Contact List Menus
1532 //Returns a menu containing all the groups within a group
1533 //- Selector called on group selection is selectGroup:
1534 //- The menu items represented object is the group it represents
1535 - (NSMenu *)menuOfAllGroupsInGroup:(AIListGroup *)inGroup withTarget:(id)target
1537 NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
1539 [menu setAutoenablesItems:NO];
1540 [self _menuOfAllGroups:menu forGroup:inGroup withTarget:target level:0];
1542 return [menu autorelease];
1544 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level
1546 NSEnumerator *enumerator;
1547 AIListObject *object;
1549 //Passing nil scans the entire contact list
1550 if (group == nil) group = contactList;
1552 //Enumerate this group and process all groups we find within it
1553 enumerator = [[group containedObjects] objectEnumerator];
1554 while ((object = [enumerator nextObject])) {
1555 if ([object isKindOfClass:[AIListGroup class]] && object != [self offlineGroup]) {
1556 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[object displayName]
1558 action:@selector(selectGroup:)
1560 [menuItem setRepresentedObject:object];
1561 if ([menuItem respondsToSelector:@selector(setIndentationLevel:)]) {
1562 [menuItem setIndentationLevel:level];
1564 [menu addItem:menuItem];
1567 [self _menuOfAllGroups:menu forGroup:(AIListGroup *)object withTarget:target level:level+1];
1573 //Returns a menu containing all the objects in a group on an account
1574 //- Selector called on contact selection is selectContact:
1575 //- The menu item's represented object is the contact it represents
1576 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target{
1577 return [self menuOfAllContactsInContainingObject:inObject withTarget:target firstLevel:YES];
1579 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target firstLevel:(BOOL)firstLevel
1581 NSEnumerator *enumerator;
1582 AIListObject *object;
1585 NSMenu *menu = [[NSMenu alloc] init];
1586 [menu setAutoenablesItems:NO];
1588 //Passing nil scans the entire contact list
1589 if (inObject == nil) inObject = contactList;
1591 //The pull down menu needs an extra item at the top of its root menu to handle the selection.
1592 if (firstLevel) [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
1594 //All menu items for all contained objects
1595 enumerator = [[inObject listContacts] objectEnumerator];
1596 while ((object = [enumerator nextObject])) {
1597 NSImage *menuServiceImage;
1598 NSMenuItem *menuItem;
1599 BOOL needToCreateSubmenu;
1600 BOOL isGroup = [object isKindOfClass:[AIListGroup class]];
1601 BOOL isValidGroup = (isGroup &&
1602 [[(AIListGroup *)object containedObjects] count]);
1604 //We don't want to include empty groups
1605 if (!isGroup || isValidGroup) {
1607 needToCreateSubmenu = (isValidGroup ||
1608 ([object isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)object listContacts] count] > 1)));
1611 menuServiceImage = [AIUserIcons menuUserIconForObject:object];
1613 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(needToCreateSubmenu ?
1614 [object displayName] :
1615 [object formattedUID])
1617 action:@selector(selectContact:)
1620 if (needToCreateSubmenu) {
1621 [menuItem setSubmenu:[self menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)object withTarget:target firstLevel:NO]];
1624 [menuItem setRepresentedObject:object];
1625 [menuItem setImage:menuServiceImage];
1626 [menu addItem:menuItem];
1631 return [menu autorelease];
1634 //Retrieving Specific Contacts -----------------------------------------------------------------------------------------
1635 #pragma mark Retrieving Specific Contacts
1637 - (AIListContact *)contactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1639 return [self contactWithService:inService account:inAccount UID:inUID usingClass:[AIListContact class]];
1642 //Retrieve a contact from the contact list (Creating if necessary)
1643 - (AIListContact *)contactWithService:(AIService *)inService
1644 account:(AIAccount *)inAccount
1645 UID:(NSString *)inUID
1646 usingClass:(Class)ContactClass
1648 if (!(inUID && [inUID length] && inService)) return nil; //Ignore invalid requests
1650 AIListContact *contact = nil;
1651 NSString *key = [ContactClass internalUniqueObjectIDForService:inService
1654 contact = [contactDict objectForKey:key];
1657 contact = [[ContactClass alloc] initWithUID:inUID account:inAccount service:inService];
1659 //Do the update thing
1660 [self _updateAllAttributesOfObject:contact];
1662 //Check to see if we should add to a metaContact
1663 AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:[contact internalObjectID]];
1665 /* We already know to add this object to the metaContact, since we did it before with another object,
1666 but this particular listContact is new and needs to be added directly to the metaContact
1667 (on future launches, the metaContact will obtain it automatically since all contacts matching this UID
1668 and serviceID should be included). */
1669 [self _performAddListObject:contact toMetaContact:metaContact];
1672 //Set the contact as mobile if it is a phone number
1673 if ([inUID characterAtIndex:0] == '+') {
1674 [contact setIsMobile:YES notify:NotifyNever];
1678 [contactDict setObject:contact forKey:key];
1685 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID usingClass:(Class)ContactClass
1687 if (inService && [inUID length]) {
1688 return [contactDict objectForKey:[ContactClass internalUniqueObjectIDForService:inService
1696 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1698 return [self existingContactWithService:inService account:inAccount UID:inUID usingClass:[AIListContact class]];
1702 * @brief Return a set of all contacts with a specified UID and service
1704 * @param service The AIService in question
1705 * @param inUID The UID, which should be normalized (lower case, no spaces, etc.) as appropriate for the service
1706 * @param existingOnly If YES, only pre-existing contacts. If NO, an AIListContact is guaranteed to be returned
1707 * on each compatible account, even if one did not previously exist.
1709 - (NSSet *)allContactsWithService:(AIService *)service UID:(NSString *)inUID existingOnly:(BOOL)existingOnly
1711 NSEnumerator *enumerator;
1713 NSMutableSet *returnContactSet = [NSMutableSet set];
1715 enumerator = [[[adium accountController] accountsCompatibleWithService:service] objectEnumerator];
1717 while ((account = [enumerator nextObject])) {
1718 AIListContact *listContact;
1721 listContact = [self existingContactWithService:service
1725 listContact = [self contactWithService:service
1731 [returnContactSet addObject:listContact];
1735 return returnContactSet;
1738 - (AIListObject *)existingListObjectWithUniqueID:(NSString *)uniqueID
1740 NSEnumerator *enumerator;
1741 AIListObject *listObject;
1744 enumerator = [contactDict objectEnumerator];
1745 while ((listObject = [enumerator nextObject])) {
1746 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1750 enumerator = [groupDict objectEnumerator];
1751 while ((listObject = [enumerator nextObject])) {
1752 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1756 enumerator = [metaContactDict objectEnumerator];
1757 while ((listObject = [enumerator nextObject])) {
1758 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1764 - (AIListContact *)preferredContactForContentType:(NSString *)inType forListContact:(AIListContact *)inContact
1766 AIListContact *returnContact = nil;
1769 if ([inContact containsMultipleContacts]) {
1770 AIListObject *preferredContact;
1771 NSString *internalObjectID;
1773 /* If we've messaged this object previously, prefer the last contact we sent to if that
1774 * contact is currently in the most-available status the metacontact can offer
1776 internalObjectID = [inContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1777 group:OBJECT_STATUS_CACHE];
1779 if ((internalObjectID) &&
1780 (preferredContact = [self existingListObjectWithUniqueID:internalObjectID]) &&
1781 ([preferredContact isKindOfClass:[AIListContact class]]) &&
1782 ([preferredContact statusSummary] == [inContact statusSummary]) &&
1783 ([[(AIMetaContact *)inContact containedObjects] containsObject:preferredContact])) {
1784 returnContact = [self preferredContactForContentType:inType
1785 forListContact:(AIListContact *)preferredContact];
1788 /* If the last contact we sent to is not appropriate, use the following algorithm, which differs from -[AIMetaContact preferredContact]
1789 * in that it doesn't "like" mobile contacts.
1791 * 1) Prefer available contacts who are not mobile
1792 * 2) If no available non-mobile contacts, use the first online contact
1793 * 3) If no online contacts, use the metacontact's preferredContact
1795 #warning This is a casting mess... define a protocol for metacontact-like AIListContact subclasses
1796 if (!returnContact) {
1797 //Recurse into metacontacts if necessary
1798 AIListContact *firstAvailableContact = nil;
1799 AIListContact *firstNotOfflineContact = nil;
1801 AIListContact *thisContact;
1802 NSEnumerator *contactsEnum = [[(AIMetaContact *)inContact containedObjects] objectEnumerator];
1803 while ((thisContact = [contactsEnum nextObject])) {
1804 AIStatusType statusSummary = [thisContact statusSummary];
1806 if (statusSummary != AIOfflineStatus) {
1807 if (!firstNotOfflineContact) {
1808 firstNotOfflineContact = thisContact;
1811 if (statusSummary == AIAvailableStatus && ![thisContact isMobile]) {
1812 if (!firstAvailableContact) {
1813 firstAvailableContact = thisContact;
1821 returnContact = (firstAvailableContact ?
1822 firstAvailableContact :
1823 (firstNotOfflineContact ? firstNotOfflineContact : [(AIMetaContact *)inContact preferredContact]));
1827 //This contact doesn't contain multiple contacts... but it might still be a metacontact. Do NOT proceed with a metacontact.
1828 if ([inContact respondsToSelector:@selector(preferredContact)])
1829 inContact = [inContact performSelector:@selector(preferredContact)];
1831 //find the best account for talking to this contact,
1832 //and return an AIListContact on that account
1833 account = [[adium accountController] preferredAccountForSendingContentType:inType
1834 toContact:inContact];
1836 if ([inContact account] == account) {
1837 returnContact = inContact;
1839 returnContact = [self contactWithService:[inContact service]
1841 UID:[inContact UID]];
1846 return returnContact;
1849 //Retrieve a list contact matching the UID and serviceID of the passed contact but on the specified account.
1850 //In many cases this will be the same as inContact.
1851 - (AIListContact *)contactOnAccount:(AIAccount *)account fromListContact:(AIListContact *)inContact
1853 if (account && ([inContact account] != account)) {
1854 return [self contactWithService:[inContact service] account:account UID:[inContact UID]];
1860 //XXX - This is ridiculous.
1861 - (AIListContact *)preferredContactWithUID:(NSString *)inUID andServiceID:(NSString *)inService forSendingContentType:(NSString *)inType
1863 AIService *theService = [[adium accountController] firstServiceWithServiceID:inService];
1864 AIListContact *tempListContact = [[AIListContact alloc] initWithUID:inUID
1865 service:theService];
1866 AIAccount *account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
1867 toContact:tempListContact
1868 includeOffline:YES];
1869 [tempListContact release];
1871 return [self contactWithService:theService account:account UID:inUID];
1876 * @brief Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts
1878 * If the destination contact's parent contact differs from the destination contact itself, the chat is with a metaContact.
1879 * If that metaContact's preferred destination for messaging isn't the same as the contact which was just messaged,
1880 * update the preference so that a new chat with this metaContact would default to the proper contact.
1882 - (void)didSendContent:(NSNotification *)notification
1884 AIChat *chat = [[notification userInfo] objectForKey:@"AIChat"];
1885 AIListContact *destContact = [chat listObject];
1886 AIListContact *metaContact = [destContact parentContact];
1888 //it's not particularly obvious from the name, but -parentContact can return self
1889 if (metaContact == destContact) return;
1891 NSString *destinationInternalObjectID = [destContact internalObjectID];
1892 NSString *currentPreferredDestination = [metaContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1893 group:OBJECT_STATUS_CACHE];
1895 if (![destinationInternalObjectID isEqualToString:currentPreferredDestination]) {
1896 [metaContact setPreference:destinationInternalObjectID
1897 forKey:KEY_PREFERRED_DESTINATION_CONTACT
1898 group:OBJECT_STATUS_CACHE];
1902 //Retrieving Groups ----------------------------------------------------------------------------------------------------
1903 #pragma mark Retrieving Groups
1905 //Retrieve a group from the contact list (Creating if necessary)
1906 - (AIListGroup *)groupWithUID:(NSString *)groupUID
1908 //Return our root group if it is requested.
1909 //XXX: is this a good idea? it might semi-mask bugs where we accidentally pass nil
1910 if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME])
1911 return [self contactList];
1913 AIListGroup *group = nil;
1914 if (!(group = [groupDict objectForKey:groupUID])) {
1916 group = [[AIListGroup alloc] initWithUID:groupUID];
1919 [self _updateAllAttributesOfObject:group];
1920 [groupDict setObject:group forKey:groupUID];
1922 //Add to the contact list
1923 [contactList addObject:group];
1924 [self _listChangedGroup:contactList object:group];
1931 - (AIListGroup *)existingGroupWithUID:(NSString *)groupUID
1933 //Return our root group if it is requested
1934 //XXX: is this a good idea? it might semi-mask bugs where we accidentally pass nil
1935 if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME])
1936 return [self contactList];
1938 return [groupDict objectForKey:groupUID];
1941 //Contact list editing -------------------------------------------------------------------------------------------------
1942 #pragma mark Contact list editing
1943 - (void)removeListObjects:(NSArray *)objectArray
1945 NSEnumerator *enumerator = [objectArray objectEnumerator];
1946 AIListObject *listObject;
1948 while ((listObject = [enumerator nextObject])) {
1949 if ([listObject isKindOfClass:[AIMetaContact class]]) {
1950 NSSet *objectsToRemove = nil;
1952 //If the metaContact only has one listContact, we will remove that contact from all accounts
1953 if ([[(AIMetaContact *)listObject listContacts] count] == 1) {
1954 AIListContact *listContact = [[(AIMetaContact *)listObject listContacts] objectAtIndex:0];
1956 objectsToRemove = [self allContactsWithService:[listContact service]
1957 UID:[listContact UID]
1961 //And actually remove the single contact if applicable
1962 if (objectsToRemove) {
1963 [self removeListObjects:[objectsToRemove allObjects]];
1966 //Now break the metaContact down, taking out all contacts and putting them back in the main list
1967 [self breakdownAndRemoveMetaContact:(AIMetaContact *)listObject];
1969 } else if ([listObject isKindOfClass:[AIListGroup class]]) {
1970 AIListObject <AIContainingObject> *containingObject = [listObject containingObject];
1971 NSEnumerator *enumerator;
1974 //If this is a group, delete all the objects within it
1975 [self removeListObjects:[(AIListGroup *)listObject containedObjects]];
1977 //Delete the list off of all active accounts
1978 enumerator = [[[adium accountController] accounts] objectEnumerator];
1979 while ((account = [enumerator nextObject])) {
1980 if ([account online]) {
1981 [account deleteGroup:(AIListGroup *)listObject];
1985 //Then, procede to delete the group
1986 [listObject retain];
1987 [containingObject removeObject:listObject];
1988 [groupDict removeObjectForKey:[listObject UID]];
1989 [self _listChangedGroup:containingObject object:listObject];
1990 [listObject release];
1993 AIAccount *account = [(AIListContact *)listObject account];
1994 if ([account online]) {
1995 [account removeContacts:[NSArray arrayWithObject:listObject]];
2001 - (void)addContacts:(NSArray *)contactArray toGroup:(AIListGroup *)group
2003 NSEnumerator *enumerator;
2004 AIListContact *listObject;
2006 [self delayListObjectNotifications];
2008 enumerator = [contactArray objectEnumerator];
2009 while ((listObject = [enumerator nextObject])) {
2010 if(![group containsObject:listObject]) //don't add it if it's already there.
2011 [[listObject account] addContacts:[NSArray arrayWithObject:listObject] toGroup:group];
2014 [self endListObjectNotificationsDelay];
2017 - (void)requestAddContactWithUID:(NSString *)contactUID service:(AIService *)inService account:(AIAccount *)inAccount
2019 NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:contactUID
2021 if (inService) [userInfo setObject:inService forKey:@"AIService"];
2022 if (inAccount) [userInfo setObject:inAccount forKey:@"AIAccount"];
2024 [[adium notificationCenter] postNotificationName:Contact_AddNewContact
2029 - (void)moveListObjects:(NSArray *)objectArray intoObject:(AIListObject<AIContainingObject> *)group index:(int)index
2031 NSEnumerator *enumerator;
2032 AIListContact *listContact;
2034 [self delayListObjectNotifications];
2036 if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2037 [(id)group setDelayContainedObjectSorting:YES];
2040 enumerator = [objectArray objectEnumerator];
2041 while ((listContact = [enumerator nextObject])) {
2042 [self moveContact:listContact intoObject:group];
2044 //Set the new index / position of the object
2045 [self _positionObject:listContact atIndex:index inObject:group];
2048 [self endListObjectNotificationsDelay];
2050 if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2051 [(id)group setDelayContainedObjectSorting:NO];
2055 Resort the entire list if we are moving within or between AIListGroup objects
2056 (other containing objects such as metaContacts will handle their own sorting).
2058 if ([group isKindOfClass:[AIListGroup class]])
2059 [self sortContactList];
2062 - (void)moveContact:(AIListContact *)listContact intoObject:(AIListObject<AIContainingObject> *)group
2064 //Move the object to the new group only if necessary
2065 if (group == [listContact containingObject]) return;
2067 if ([group isKindOfClass:[AIListGroup class]]) {
2068 //Move a contact into a new group
2069 if ([listContact isKindOfClass:[AIMetaContact class]]) {
2070 //Move the meta contact to this new group
2071 [self _moveContactLocally:listContact toGroup:(AIListGroup *)group];
2073 NSEnumerator *enumerator;
2074 AIListContact *actualListContact;
2076 //This is a meta contact, move the objects within it. listContacts will give us a flat array of AIListContacts.
2077 enumerator = [[(AIMetaContact *)listContact listContacts] objectEnumerator];
2078 while ((actualListContact = [enumerator nextObject])) {
2079 //Only move the contact if it is actually listed on the account in question
2080 if (![actualListContact isStranger]) {
2081 [self _moveObjectServerside:actualListContact toGroup:(AIListGroup *)group];
2085 } else if ([listContact isKindOfClass:[AIListContact class]]) {
2087 [self _moveObjectServerside:listContact toGroup:(AIListGroup *)group];
2090 } else if ([group isKindOfClass:[AIMetaContact class]]) {
2091 //Moving a contact into a meta contact
2092 [self addListObject:listContact toMetaContact:(AIMetaContact *)group];
2096 //Move an object to another group
2097 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group
2099 AIAccount *account = [(AIListContact *)listObject account];
2100 if ([account online]) {
2101 [account moveListObjects:[NSArray arrayWithObject:listObject] toGroup:group];
2106 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName
2108 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
2111 //Since Adium has no memory of what accounts a group is on, we have to send this message to all available accounts
2112 //The accounts without this group will just ignore it
2113 while ((account = [enumerator nextObject])) {
2114 [account renameGroup:listGroup to:newName];
2117 //Remove the old group if it's empty
2118 if ([listGroup containedObjectsCount] == 0) {
2119 [self removeListObjects:[NSArray arrayWithObject:listGroup]];
2123 //Position a list object within a group
2124 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inObject:(AIListObject<AIContainingObject> *)group
2127 //Moved to the top of a group. New index is between 0 and the lowest current index
2128 [listObject setOrderIndex:([group smallestOrder] / 2.0)];
2130 } else if (index >= [group visibleCount]) {
2131 //Moved to the bottom of a group. New index is one higher than the highest current index
2132 [listObject setOrderIndex:([group largestOrder] + 1.0)];
2135 //Moved somewhere in the middle. New index is the average of the next largest and smallest index
2136 AIListObject *previousObject = [group objectAtIndex:index-1];
2137 AIListObject *nextObject = [group objectAtIndex:index];
2138 float nextLowest = [previousObject orderIndex];
2139 float nextHighest = [nextObject orderIndex];
2141 /* XXX - Fixme as per below
2142 * It's possible that nextLowest > nextHighest if ordering is not strictly based on the ordering indexes themselves.
2143 * For example, a group sorted by status then manually could look like (status - ordering index):
2145 * Away Contact - 100
2146 * Away Contact - 120
2147 * Offline Contact - 110
2148 * Offline Contact - 113
2149 * Offline Contact - 125
2151 * Dropping between Away Contact and Offline Contact should make an Away Contact be > 120 but an Offline Contact be < 110.
2152 * Only the sort controller knows the answer as to where this contact should be positioned in the end.
2155 [listObject setOrderIndex:((nextHighest + nextLowest) / 2.0)];
2159 #pragma mark Authorization
2160 - (id)showAuthorizationRequestWithDict:(NSDictionary *)inDict forAccount:(AIAccount *)inAccount
2162 return [adiumAuthorization showAuthorizationRequestWithDict:inDict forAccount:inAccount];