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/AILoginController.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 inGroup:(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;
115 - (void)_performChangeOfUseOfflineGroup;
119 @implementation AIContactController
124 if ((self = [super init])) {
126 contactObservers = [[NSMutableSet alloc] init];
127 sortControllerArray = [[NSMutableArray alloc] init];
128 activeSortController = nil;
129 delayedStatusChanges = 0;
130 delayedModifiedStatusKeys = [[NSMutableSet alloc] init];
131 delayedAttributeChanges = 0;
132 delayedModifiedAttributeKeys = [[NSMutableSet alloc] init];
133 delayedContactChanges = 0;
134 delayedUpdateRequests = 0;
135 updatesAreDelayed = NO;
138 contactDict = [[NSMutableDictionary alloc] init];
139 groupDict = [[NSMutableDictionary alloc] init];
140 metaContactDict = [[NSMutableDictionary alloc] init];
141 contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
148 - (void)controllerDidLoad
150 //Default contact preferences
151 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:CONTACT_DEFAULT_PREFS
152 forClass:[self class]]
153 forGroup:PREF_GROUP_CONTACT_LIST];
155 contactList = [[AIListGroup alloc] initWithUID:ADIUM_ROOT_GROUP_NAME];
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 NSNumber *objectID = [NSNumber numberWithInt:[[[identifier componentsSeparatedByString:@"-"] objectAtIndex:1] intValue]];
263 [self metaContactWithObjectID:objectID];
267 //Flattened array of the contact list content
268 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects
270 NSMutableArray *array = [NSMutableArray array];
271 NSEnumerator *enumerator = [listObjects objectEnumerator];;
272 AIListObject *object;
274 //Create temporary strings outside the loop
275 NSString *Group = @"Group";
276 NSString *Type = @"Type";
277 NSString *Expanded = @"Expanded";
279 while ((object = [enumerator nextObject])) {
280 [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
282 [object UID], UID_KEY,
283 [NSNumber numberWithBool:[(AIListGroup *)object isExpanded]], Expanded,
291 //Status and Display updates -------------------------------------------------------------------------------------------
292 #pragma mark Status and Display updates
293 //These delay Contact_ListChanged, ListObject_AttributesChanged, Contact_OrderChanged notificationsDelays,
294 //sorting and redrawing to prevent redundancy when making a large number of changes
295 //Explicit delay. Call endListObjectNotificationsDelay to end
296 - (void)delayListObjectNotifications
298 delayedUpdateRequests++;
299 updatesAreDelayed = YES;
302 //End an explicit delay
303 - (void)endListObjectNotificationsDelay
305 delayedUpdateRequests--;
306 if (delayedUpdateRequests == 0 && !delayedUpdateTimer) {
307 [self _performDelayedUpdates:nil];
311 //Delay all list object notifications until a period of inactivity occurs. This is useful for accounts that do not
312 //know when they have finished connecting but still want to mute events.
313 - (void)delayListObjectNotificationsUntilInactivity
315 if (!delayedUpdateTimer) {
316 updatesAreDelayed = YES;
317 delayedUpdateTimer = [[NSTimer scheduledTimerWithTimeInterval:UPDATE_CLUMP_INTERVAL
319 selector:@selector(_performDelayedUpdates:)
321 repeats:YES] retain];
324 [delayedUpdateTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:UPDATE_CLUMP_INTERVAL]];
328 //Update the status of a list object. This will update any information that is otherwise too expensive to update
329 //automatically, such as their profile.
330 - (void)updateListContactStatus:(AIListContact *)inContact
332 //If we're dealing with a meta contact, update the status of the contacts contained within it
333 if ([inContact isKindOfClass:[AIMetaContact class]]) {
334 NSEnumerator *enumerator = [[(AIMetaContact *)inContact listContacts] objectEnumerator];
335 AIListContact *contact;
337 while ((contact = [enumerator nextObject])) {
338 [self updateListContactStatus:contact];
342 AIAccount *account = [inContact account];
343 if (![account online]) {
344 account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
345 toContact:inContact];
348 [account updateContactStatus:inContact];
352 //Called after modifying a contact's status
353 // Silent: Silences all events, notifications, sounds, overlays, etc. that would have been associated with this status change
354 - (void)listObjectStatusChanged:(AIListObject *)inObject modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
356 NSSet *modifiedAttributeKeys;
358 //Let all observers know the contact's status has changed before performing any sorting or further notifications
359 modifiedAttributeKeys = [self _informObserversOfObjectStatusChange:inObject withKeys:inModifiedKeys silent:silent];
361 //Resort the contact list
362 if (updatesAreDelayed) {
363 delayedStatusChanges++;
364 [delayedModifiedStatusKeys unionSet:inModifiedKeys];
366 //We can safely skip sorting if we know the modified attributes will invoke a resort later
367 if (![[self activeSortController] shouldSortForModifiedAttributeKeys:modifiedAttributeKeys] &&
368 [[self activeSortController] shouldSortForModifiedStatusKeys:inModifiedKeys]) {
369 [self sortListObject:inObject];
373 //Post an attributes changed message (if necessary)
374 if ([modifiedAttributeKeys count]) {
375 [self listObjectAttributesChanged:inObject modifiedKeys:modifiedAttributeKeys];
379 //Call after modifying an object's display attributes
380 //(When modifying display attributes in response to a status change, this is not necessary)
381 - (void)listObjectAttributesChanged:(AIListObject *)inObject modifiedKeys:(NSSet *)inModifiedKeys
383 if (updatesAreDelayed) {
384 delayedAttributeChanges++;
385 [delayedModifiedAttributeKeys unionSet:inModifiedKeys];
387 //Resort the contact list if necessary
388 if ([[self activeSortController] shouldSortForModifiedAttributeKeys:inModifiedKeys]) {
389 [self sortListObject:inObject];
392 //Post an attributes changed message
393 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
395 userInfo:(inModifiedKeys ?
396 [NSDictionary dictionaryWithObject:inModifiedKeys
402 //Performs any delayed list object/handle updates
403 - (void)_performDelayedUpdates:(NSTimer *)timer
405 BOOL updatesOccured = (delayedStatusChanges || delayedAttributeChanges || delayedContactChanges);
407 //Send out global attribute & status changed notifications (to cover any delayed updates)
408 if (updatesOccured) {
409 BOOL shouldSort = NO;
411 //Inform observers of any changes
412 if (delayedContactChanges) {
413 // [[adium notificationCenter] postNotificationName:Contact_ListChanged object:nil];
414 delayedContactChanges = 0;
417 if (delayedStatusChanges) {
419 [[self activeSortController] shouldSortForModifiedStatusKeys:delayedModifiedStatusKeys]) {
422 [delayedModifiedStatusKeys removeAllObjects];
423 delayedStatusChanges = 0;
425 if (delayedAttributeChanges) {
427 [[self activeSortController] shouldSortForModifiedAttributeKeys:delayedModifiedAttributeKeys]) {
430 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
432 userInfo:(delayedModifiedAttributeKeys ?
433 [NSDictionary dictionaryWithObject:delayedModifiedAttributeKeys
436 [delayedModifiedAttributeKeys removeAllObjects];
437 delayedAttributeChanges = 0;
440 //Sort only if necessary
442 [self sortContactList];
446 //If no more updates are left to process, disable the update timer
447 //If there are no delayed update requests, remove the hold
448 if (!delayedUpdateTimer || !updatesOccured) {
449 if (delayedUpdateTimer) {
450 [delayedUpdateTimer invalidate];
451 [delayedUpdateTimer release];
452 delayedUpdateTimer = nil;
454 if (delayedUpdateRequests == 0) {
455 updatesAreDelayed = NO;
460 #pragma mark Contact Grouping
461 //Contact Grouping -----------------------------------------------------------------------------------------------------
463 //Redetermine the local grouping of a contact in response to server grouping information or an external change
464 - (void)listObjectRemoteGroupingChanged:(AIListContact *)inContact
466 AIListObject<AIContainingObject> *containingObject;
467 NSString *remoteGroupName = [inContact remoteGroupName];
471 containingObject = [inContact containingObject];
473 if ([containingObject isKindOfClass:[AIMetaContact class]]) {
475 /* If inContact's containingObject is a metaContact, and that metaContact has no containingObject,
476 * use inContact's remote grouping as the metaContact's grouping.
478 if (![containingObject containingObject] && [remoteGroupName length]) {
479 //If no similar objects exist, we add this contact directly to the list
480 //Create a group for the contact even if contact list groups aren't on,
481 //otherwise requests for all the contact list groups will return nothing
482 AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
484 localGroup = (useContactListGroups ?
485 ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
488 [localGroup addObject:containingObject];
489 [self _listChangedGroup:localGroup object:containingObject];
490 AILog(@"listObjectRemoteGroupingChanged: %@ is in %@, which was moved to %@",inContact,containingObject,localGroup);
494 //If we have a remoteGroupName, add the contact locally to the list
495 if (remoteGroupName) {
496 //Create a group for the contact even if contact list groups aren't on,
497 //otherwise requests for all the contact list groups will return nothing
498 AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
500 localGroup = (useContactListGroups ?
501 ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
504 AILog(@"listObjectRemoteGroupingChanged: %@: remoteGroupName %@ --> %@",inContact,remoteGroupName,localGroup);
506 [self _moveContactLocally:inContact
510 //If !remoteGroupName, remove the contact from any local groups
511 if (containingObject) {
513 [(AIListGroup *)containingObject removeObject:inContact];
515 [self _listChangedGroup:(AIListGroup *)containingObject object:inContact];
517 AILog(@"listObjectRemoteGroupingChanged: %@: -- !remoteGroupName so removed from %@",inContact,containingObject);
522 BOOL isCurrentlyAStranger = [inContact isStranger];
523 if ((isCurrentlyAStranger && (remoteGroupName != nil)) ||
524 (!isCurrentlyAStranger && (remoteGroupName == nil))) {
525 [inContact setStatusObject:(remoteGroupName ? [NSNumber numberWithBool:YES] : nil)
526 forKey:@"NotAStranger"
528 [inContact notifyOfChangedStatusSilently:YES];
534 - (void)_moveContactLocally:(AIListContact *)listContact toGroup:(AIListGroup *)localGroup
536 AIListObject *containingObject;
537 AIListObject *existingObject;
538 BOOL performedGrouping = NO;
540 //Protect with a retain while we are removing and adding the contact to our arrays
541 [listContact retain];
544 AILog(@"Moving %@ to %@",listContact,localGroup);
546 //Remove this object from any local groups we have it in currently
547 if ((containingObject = [listContact containingObject]) &&
548 ([containingObject isKindOfClass:[AIListGroup class]])) {
550 [(AIListGroup *)containingObject removeObject:listContact];
551 [self _listChangedGroup:(AIListGroup *)containingObject object:listContact];
554 if ((existingObject = [localGroup objectWithService:[listContact service] UID:[listContact UID]])) {
555 //If an object exists in this group with the same UID and serviceID, create a MetaContact
557 [self groupListContacts:[NSArray arrayWithObjects:listContact,existingObject,nil]];
558 performedGrouping = YES;
561 AIMetaContact *metaContact;
563 //If no object exists in this group which matches, we should check if there is already
564 //a MetaContact holding a matching ListContact, since we should include this contact in it
565 //If we found a metaContact to which we should add, do it.
566 if ((metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]])) {
568 AILog(@"Found an existing metacontact; adding %@ to %@",listContact,metaContact);
570 [self addListObject:listContact toMetaContact:metaContact];
571 performedGrouping = YES;
575 if (!performedGrouping) {
576 //If no similar objects exist, we add this contact directly to the list
577 [localGroup addObject:listContact];
580 [self _listChangedGroup:localGroup object:listContact];
584 [listContact release];
587 - (AIListGroup *)remoteGroupForContact:(AIListContact *)inContact
591 if ([inContact isKindOfClass:[AIMetaContact class]]) {
592 //For a metaContact, the closest we have to a remote group is the group it is within locally
593 group = [(AIMetaContact *)inContact parentGroup];
596 NSString *remoteGroup = [inContact remoteGroupName];
597 group = (remoteGroup ? [self groupWithUID:remoteGroup] : nil);
603 //Post a list grouping changed notification for the object and group
604 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object
606 if (updatesAreDelayed) {
607 delayedContactChanges++;
609 [[adium notificationCenter] postNotificationName:Contact_ListChanged
611 userInfo:(group ? [NSDictionary dictionaryWithObject:group forKey:@"ContainingGroup"] : nil)];
615 - (BOOL)useContactListGroups
617 return useContactListGroups;
620 - (void)setUseContactListGroups:(BOOL)inFlag
622 if (inFlag != useContactListGroups) {
623 useContactListGroups = inFlag;
625 [self _performChangeOfUseContactListGroups];
629 - (void)_performChangeOfUseContactListGroups
631 NSEnumerator *enumerator;
632 AIListObject *listObject;
634 [self delayListObjectNotifications];
636 //Store the preference
637 [[adium preferenceController] setPreference:[NSNumber numberWithBool:!useContactListGroups]
638 forKey:KEY_HIDE_CONTACT_LIST_GROUPS
639 group:PREF_GROUP_CONTACT_LIST_DISPLAY];
641 //Configure the sort controller to force ignoring of groups as appropriate
642 [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
644 enumerator = [[[[contactList containedObjects] copy] autorelease] objectEnumerator];
646 if (useContactListGroups) { /* We are now using contact list groups, but we weren't before. */
648 //Restore the grouping of all root-level contacts
649 while ((listObject = [enumerator nextObject])) {
650 if ([listObject isKindOfClass:[AIListContact class]]) {
651 [(AIListContact *)listObject restoreGrouping];
655 } else { /* We are no longer using contact list groups, but we were before. */
657 while ((listObject = [enumerator nextObject])) {
658 if ([listObject isKindOfClass:[AIListGroup class]]) {
659 NSArray *containedObjects;
660 NSEnumerator *groupEnumerator;
661 AIListObject *containedListObject;
663 containedObjects = [[(AIListGroup *)listObject containedObjects] copy];
664 groupEnumerator = [containedObjects objectEnumerator];
665 while ((containedListObject = [groupEnumerator nextObject])) {
666 if ([containedListObject isKindOfClass:[AIListContact class]]) {
667 [self _moveContactLocally:(AIListContact *)containedListObject
668 toGroup:contactList];
671 [containedObjects release];
676 //Stop delaying object notifications; this will automatically resort the contact list, so we're done.
677 [self endListObjectNotificationsDelay];
680 - (void)prepareShowHideGroups
682 //Load the preference
683 useContactListGroups = ![[[adium preferenceController] preferenceForKey:KEY_HIDE_CONTACT_LIST_GROUPS
684 group:PREF_GROUP_CONTACT_LIST_DISPLAY] boolValue];
686 //Show offline contacts menu item
687 menuItem_showGroups = [[NSMenuItem alloc] initWithTitle:SHOW_GROUPS_MENU_TITLE
689 action:@selector(toggleShowGroups:)
691 [menuItem_showGroups setState:useContactListGroups];
692 [[adium menuController] addMenuItem:menuItem_showGroups toLocation:LOC_View_Toggles];
695 NSToolbarItem *toolbarItem;
696 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SHOW_GROUPS_IDENTIFER
697 label:AILocalizedString(@"Show Groups",nil)
698 paletteLabel:AILocalizedString(@"Toggle Groups Display",nil)
699 toolTip:AILocalizedString(@"Toggle display of groups",nil)
701 settingSelector:@selector(setImage:)
702 itemContent:[NSImage imageNamed:(useContactListGroups ?
703 @"togglegroups_transparent" :
705 forClass:[self class]]
706 action:@selector(toggleShowGroupsToolbar:)
708 [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ContactList"];
711 - (IBAction)toggleShowGroups:(id)sender
714 useContactListGroups = !useContactListGroups;
715 [menuItem_showGroups setState:useContactListGroups];
717 //Update the contact list. Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure.
718 [self performSelector:@selector(_performChangeOfUseContactListGroups)
720 afterDelay:0.000001];
723 - (IBAction)toggleShowGroupsToolbar:(id)sender
725 [self toggleShowGroups:sender];
727 [sender setImage:[NSImage imageNamed:(useContactListGroups ?
728 @"togglegroups_transparent" :
730 forClass:[self class]]];
733 - (BOOL)useOfflineGroup
735 return useOfflineGroup;
738 - (void)setUseOfflineGroup:(BOOL)inFlag
740 if (inFlag != useOfflineGroup) {
741 useOfflineGroup = inFlag;
743 if (useOfflineGroup) {
744 [self registerListObjectObserver:self];
746 [self updateAllListObjectsForObserver:self];
747 [self unregisterListObjectObserver:self];
752 - (AIListGroup *)offlineGroup
754 return [self groupWithUID:AILocalizedString(@"Offline", "Name of offline group")];
757 #pragma mark Meta Contacts
758 //Meta Contacts --------------------------------------------------------------------------------------------------------
760 * @brief Create or load a metaContact
762 * @param inObjectID The objectID of an existing but unloaded metaContact, or nil to create and save a new metaContact
764 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID
766 NSString *metaContactDictKey;
767 AIMetaContact *metaContact;
768 BOOL shouldRestoreContacts = YES;
770 //If no object ID is provided, use the next available object ID
771 //(MetaContacts should always have an individually unique object id)
773 int topID = [[[adium preferenceController] preferenceForKey:TOP_METACONTACT_ID
774 group:PREF_GROUP_CONTACT_LIST] intValue];
775 inObjectID = [NSNumber numberWithInt:topID];
776 [[adium preferenceController] setPreference:[NSNumber numberWithInt:([inObjectID intValue] + 1)]
777 forKey:TOP_METACONTACT_ID
778 group:PREF_GROUP_CONTACT_LIST];
780 //No reason to waste time restoring contacts when none are in the meta contact yet.
781 shouldRestoreContacts = NO;
784 //Look for a metacontact with this object ID. If none is found, create one
785 //and add its contained contacts to it.
786 metaContactDictKey = [AIMetaContact internalObjectIDFromObjectID:inObjectID];
788 metaContact = [metaContactDict objectForKey:metaContactDictKey];
790 metaContact = [[AIMetaContact alloc] initWithObjectID:inObjectID];
792 //Keep track of it in our metaContactDict for retrieval by objectID
793 [metaContactDict setObject:metaContact forKey:metaContactDictKey];
795 //Add it to our more general contactDict, as well
796 [contactDict setObject:metaContact forKey:[metaContact internalUniqueObjectID]];
798 /* We restore contacts (actually, internalIDs for contacts, to be added as necessary later) if the metaContact
799 * existed before this call to metaContactWithObjectID:
801 if (shouldRestoreContacts) {
802 if (![self _restoreContactsToMetaContact:metaContact]) {
804 //If restoring the metacontact did not actually add any contacts, delete it since it is invalid
805 [self breakdownAndRemoveMetaContact:metaContact];
810 /* As with contactWithService:account:UID, update all attributes so observers are initially informed of
811 * this object's existence.
813 [self _updateAllAttributesOfObject:metaContact];
815 [metaContact release];
818 return (metaContact);
822 * @brief Associate the appropriate internal IDs for contained contacts with a metaContact
824 * @result YES if one or more contacts was associated with the metaContact; NO if none were.
826 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact
828 NSDictionary *allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
829 group:PREF_GROUP_CONTACT_LIST];
830 NSArray *containedContactsArray = [allMetaContactsDict objectForKey:[metaContact internalObjectID]];
831 BOOL restoredContacts;
833 if ([containedContactsArray count]) {
834 [self _restoreContactsToMetaContact:metaContact
835 fromContainedContactsArray:containedContactsArray];
837 restoredContacts = YES;
840 restoredContacts = NO;
843 return restoredContacts;
847 * @brief Associate the internal IDs for an array of contacts with a specific metaContact
849 * This does not actually place any AIListContacts within the metaContact. Instead, it updates the contactToMetaContactLookupDict
850 * dictionary to have metaContact associated with the list contacts specified by containedContactsArray. This
851 * allows us to add them lazily to the metaContact (in contactWithService:account:UID:) as necessary.
853 * @param metaContact The metaContact to which contact referneces are added
854 * @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
856 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray
858 NSEnumerator *enumerator = [containedContactsArray objectEnumerator];
859 NSDictionary *containedContactDict;
861 while ((containedContactDict = [enumerator nextObject])) {
862 /* Before Adium 0.80, metaContacts could be created within metaContacts. Simply ignore any attempt to restore
863 * such irroneous data, which will have a YES boolValue for KEY_IS_METACONTACT. */
864 if (![[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) {
865 /* Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject.
867 * As listObjects are loaded/created/requested which match this internalObjectID,
868 * they will be inserted into the metaContact.
870 NSString *internalObjectID = [AIListObject internalObjectIDForServiceID:[containedContactDict objectForKey:SERVICE_ID_KEY]
871 UID:[containedContactDict objectForKey:UID_KEY]];
872 [contactToMetaContactLookupDict setObject:metaContact
873 forKey:internalObjectID];
879 //Add a list object to a meta contact, setting preferences and such
880 //so the association is lasting across program launches.
881 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
883 if (listObject != metaContact) {
885 //If listObject is a metaContact, perform addListObject:toMetaContact: recursively
886 if ([listObject isKindOfClass:[AIMetaContact class]]) {
887 NSEnumerator *enumerator = [[[[(AIMetaContact *)listObject containedObjects] copy] autorelease] objectEnumerator];
888 AIListObject *someObject;
890 while ((someObject = [enumerator nextObject])) {
892 [self addListObject:someObject toMetaContact:metaContact];
896 AIMetaContact *oldMetaContact;
898 //Obtain any metaContact this listObject is currently within, so we can remove it later
899 oldMetaContact = [contactToMetaContactLookupDict objectForKey:[listObject internalObjectID]];
901 if ([self _performAddListObject:listObject toMetaContact:metaContact]) {
902 //If this listObject was not in this metaContact in any form before, store the change
903 if (metaContact != oldMetaContact) {
904 //Remove the list object from any other metaContact it is in at present
905 if (oldMetaContact) {
906 [self removeListObject:listObject fromMetaContact:oldMetaContact];
909 [self _storeListObject:listObject inMetaContact:metaContact];
916 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact
918 AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact);
919 NSDictionary *containedContactDict;
920 NSMutableDictionary *allMetaContactsDict;
921 NSMutableArray *containedContactsArray;
923 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
925 //Get the dictionary of all metaContacts
926 allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
927 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
928 if (!allMetaContactsDict) {
929 allMetaContactsDict = [[NSMutableDictionary alloc] init];
932 //Load the array for the new metaContact
933 containedContactsArray = [[allMetaContactsDict objectForKey:metaContactInternalObjectID] mutableCopy];
934 if (!containedContactsArray) containedContactsArray = [[NSMutableArray alloc] init];
935 containedContactDict = nil;
937 //Create the dictionary describing this list object
938 if ([listObject isKindOfClass:[AIMetaContact class]]) {
939 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
940 [NSNumber numberWithBool:YES],KEY_IS_METACONTACT,
941 [(AIMetaContact *)listObject objectID],KEY_OBJECTID,nil];
943 } else if ([listObject isKindOfClass:[AIListContact class]]) {
944 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
945 [[listObject service] serviceID],SERVICE_ID_KEY,
946 [listObject UID],UID_KEY,nil];
949 //Only add if this dict isn't already in the array
950 if (containedContactDict && ([containedContactsArray indexOfObject:containedContactDict] == NSNotFound)) {
951 [containedContactsArray addObject:containedContactDict];
952 [allMetaContactsDict setObject:containedContactsArray forKey:metaContactInternalObjectID];
955 [self _saveMetaContacts:allMetaContactsDict];
957 [[adium contactAlertsController] mergeAndMoveContactAlertsFromListObject:listObject
958 intoListObject:metaContact];
961 [allMetaContactsDict release];
962 [containedContactsArray release];
965 //Actually adds a list object to a meta contact. No preferences are changed.
966 //Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict
967 //for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful.
968 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
970 AIListObject<AIContainingObject> *localGroup;
973 localGroup = [listObject containingObject];
975 //Remove the object from its previous containing group
976 if (localGroup && (localGroup != metaContact)) {
977 [localGroup removeObject:listObject];
978 [self _listChangedGroup:localGroup object:listObject];
981 //AIMetaContact will handle reassigning the list object's grouping to being itself
982 if ((success = [metaContact addObject:listObject])) {
983 [contactToMetaContactLookupDict setObject:metaContact forKey:[listObject internalObjectID]];
985 [self _listChangedGroup:metaContact object:listObject];
986 //If the metaContact isn't in a group yet, use the group of the object we just added
987 if ((![metaContact containingObject]) && localGroup) {
988 //Add the new meta contact to our list
989 [(AIMetaContact *)localGroup addObject:metaContact];
990 [self _listChangedGroup:localGroup object:metaContact];
997 - (void)removeAllListObjectsMatching:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
999 NSEnumerator *enumerator;
1000 AIListObject *theObject;
1002 enumerator = [[self allContactsWithService:[listObject service]
1003 UID:[listObject UID]
1004 existingOnly:YES] objectEnumerator];
1006 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1007 [contactToMetaContactLookupDict removeObjectForKey:[listObject internalObjectID]];
1009 [self delayListObjectNotifications];
1010 while ((theObject = [enumerator nextObject])) {
1011 [self removeListObject:theObject fromMetaContact:metaContact];
1013 [self endListObjectNotificationsDelay];
1016 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1018 NSEnumerator *enumerator;
1019 NSArray *containedContactsArray;
1020 NSDictionary *containedContactDict = nil;
1021 NSMutableDictionary *allMetaContactsDict;
1022 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
1024 //Get the dictionary of all metaContacts
1025 allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1026 group:PREF_GROUP_CONTACT_LIST];
1028 //Load the array for the metaContact
1029 containedContactsArray = [allMetaContactsDict objectForKey:metaContactInternalObjectID];
1031 //Enumerate it, looking only for the appropriate type of containedContactDict
1032 enumerator = [containedContactsArray objectEnumerator];
1034 if ([listObject isKindOfClass:[AIMetaContact class]]) {
1035 NSNumber *listObjectObjectID = [(AIMetaContact *)listObject objectID];
1037 while ((containedContactDict = [enumerator nextObject])) {
1038 if (([[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) &&
1039 (([(NSNumber *)[containedContactDict objectForKey:KEY_OBJECTID] compare:listObjectObjectID]) == 0)) {
1044 } else if ([listObject isKindOfClass:[AIListContact class]]) {
1046 NSString *listObjectUID = [listObject UID];
1047 NSString *listObjectServiceID = [[listObject service] serviceID];
1049 while ((containedContactDict = [enumerator nextObject])) {
1050 if ([[containedContactDict objectForKey:UID_KEY] isEqualToString:listObjectUID] &&
1051 [[containedContactDict objectForKey:SERVICE_ID_KEY] isEqualToString:listObjectServiceID]) {
1057 //If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result
1058 if (containedContactDict) {
1059 NSMutableArray *newContainedContactsArray;
1060 NSMutableDictionary *newAllMetaContactsDict;
1062 newContainedContactsArray = [containedContactsArray mutableCopy];
1063 [newContainedContactsArray removeObjectIdenticalTo:containedContactDict];
1065 newAllMetaContactsDict = [allMetaContactsDict mutableCopy];
1066 [newAllMetaContactsDict setObject:newContainedContactsArray
1067 forKey:metaContactInternalObjectID];
1069 [self _saveMetaContacts:newAllMetaContactsDict];
1071 [newContainedContactsArray release];
1072 [newAllMetaContactsDict release];
1075 //The listObject can be within the metaContact without us finding a containedContactDict if we are removing multiple
1076 //listContacts referring to the same UID & serviceID combination - that is, on multiple accounts on the same service.
1077 //We therefore request removal of the object regardless of the if (containedContactDict) check above.
1078 [metaContact removeObject:listObject];
1083 * @brief Groups UIDs for services into a single metacontact
1085 * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to
1086 * a UID and a service, respectively, which together define a contact which should be included in the grouping.
1088 * Assumption: This is only called after the contact list is finished loading, which occurs via
1089 * -(void)controllerDidLoad above.
1091 * @param UIDsArray NSArray of UIDs
1092 * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
1094 - (AIMetaContact *)groupUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray
1096 NSMutableSet *internalObjectIDs = [[NSMutableSet alloc] init];
1097 AIMetaContact *metaContact = nil;
1098 NSEnumerator *enumerator;
1099 NSString *internalObjectID;
1100 int count = [UIDsArray count];
1103 //Build an array of all contacts matching this description (multiple accounts on the same service listing
1104 //the same UID mean that we can have multiple AIListContact objects with a UID/service combination)
1105 for (i = 0; i < count; i++) {
1106 NSString *serviceID = [servicesArray objectAtIndex:i];
1107 NSString *UID = [UIDsArray objectAtIndex:i];
1109 internalObjectID = [AIListObject internalObjectIDForServiceID:serviceID
1112 metaContact = [contactToMetaContactLookupDict objectForKey:internalObjectID];
1115 [internalObjectIDs addObject:internalObjectID];
1118 //Create a new metaContact is we didn't find one.
1120 metaContact = [self metaContactWithObjectID:nil];
1123 enumerator = [internalObjectIDs objectEnumerator];
1124 while ((internalObjectID = [enumerator nextObject])) {
1125 AIListObject *existingObject;
1126 if ((existingObject = [self existingListObjectWithUniqueID:internalObjectID])) {
1127 /* If there is currently an object (or multiple objects) matching this internalObjectID
1128 * we should add immediately.
1130 [self addListObject:existingObject
1131 toMetaContact:metaContact];
1133 /* If no objects matching this internalObjectID exist, we can simply add to the
1134 * contactToMetaContactLookupDict for use if such an object is created later.
1136 [contactToMetaContactLookupDict setObject:metaContact
1137 forKey:internalObjectID];
1140 [internalObjectIDs release];
1145 /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added.
1147 * This will reuse an existing metacontact (for one of the contacts in the array) if possible.
1148 * @param contactsToGroupArray Contacts to group together
1150 - (AIMetaContact *)groupListContacts:(NSArray *)contactsToGroupArray
1152 NSEnumerator *enumerator;
1153 AIListContact *listContact;
1154 AIMetaContact *metaContact = nil;
1156 //Look for an existing MetaContact we can use. The first one we find is the lucky winner.
1157 enumerator = [contactsToGroupArray objectEnumerator];
1158 while ((listContact = [enumerator nextObject]) && (metaContact == nil)) {
1159 if ([listContact isKindOfClass:[AIMetaContact class]]) {
1160 metaContact = (AIMetaContact *)listContact;
1162 metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]];
1166 //Create a new metaContact is we didn't find one.
1168 metaContact = [self metaContactWithObjectID:nil];
1171 /* Add all these contacts to our MetaContact.
1172 * Some may already be present, but that's fine, as nothing will happen.
1174 enumerator = [contactsToGroupArray objectEnumerator];
1175 while ((listContact = [enumerator nextObject])) {
1176 [self addListObject:listContact toMetaContact:metaContact];
1182 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact
1184 //Remove the objects within it from being inside it
1185 NSArray *containedObjects = [[metaContact containedObjects] copy];
1186 NSEnumerator *metaEnumerator = [containedObjects objectEnumerator];
1187 AIListObject<AIContainingObject> *containingObject = [metaContact containingObject];
1188 AIListObject *object;
1190 NSMutableDictionary *allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1191 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
1193 while ((object = [metaEnumerator nextObject])) {
1195 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1196 [contactToMetaContactLookupDict removeObjectForKey:[object internalObjectID]];
1198 [self removeListObject:object fromMetaContact:metaContact];
1201 //Then, procede to remove the metaContact
1204 [metaContact retain];
1206 //Remove it from its containing group
1207 [containingObject removeObject:metaContact];
1209 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
1211 //Remove our reference to it internally
1212 [metaContactDict removeObjectForKey:metaContactInternalObjectID];
1214 //Remove it from the preferences dictionary
1215 [allMetaContactsDict removeObjectForKey:metaContactInternalObjectID];
1217 //XXX - contactToMetaContactLookupDict
1219 //Post the list changed notification for the old containingObject
1220 [self _listChangedGroup:containingObject object:metaContact];
1222 //Save the updated allMetaContactsDict which no longer lists the metaContact
1223 [self _saveMetaContacts:allMetaContactsDict];
1225 //Protection is overrated.
1226 [metaContact release];
1227 [containedObjects release];
1228 [allMetaContactsDict release];
1231 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict
1233 AILog(@"MetaContacts: Saving!");
1234 [[adium preferenceController] setPreference:allMetaContactsDict
1235 forKey:KEY_METACONTACT_OWNERSHIP
1236 group:PREF_GROUP_CONTACT_LIST];
1237 [[adium preferenceController] setPreference:[allMetaContactsDict allKeys]
1238 forKey:KEY_FLAT_METACONTACTS
1239 group:PREF_GROUP_CONTACT_LIST];
1242 //Sort list objects alphabetically by their display name
1243 int contactDisplayNameSort(AIListObject *objectA, AIListObject *objectB, void *context)
1245 return [[objectA displayName] caseInsensitiveCompare:[objectB displayName]];
1248 //Return either the highest metaContact containing this list object, or the list object itself. Appropriate for when
1249 //preferences should be read from/to the most generalized contact possible.
1250 - (AIListObject *)parentContactForListObject:(AIListObject *)listObject
1252 if ([listObject isKindOfClass:[AIListContact class]]) {
1253 //Find the highest-up metaContact
1254 AIListObject *containingObject;
1255 while ([(containingObject = [listObject containingObject]) isKindOfClass:[AIMetaContact class]]) {
1256 listObject = (AIMetaContact *)containingObject;
1263 #pragma mark Preference observing
1265 * @brief Preferences changed
1267 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
1268 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
1271 [key isEqualToString:KEY_USE_OFFLINE_GROUP]) {
1273 [self setUseOfflineGroup:[[prefDict objectForKey:KEY_USE_OFFLINE_GROUP] boolValue]];
1277 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1279 if (!inModifiedKeys ||
1280 [inModifiedKeys containsObject:@"Online"]) {
1282 if ([inObject isKindOfClass:[AIListContact class]]) {
1283 //If this contact is not its own parent contact, don't bother since we'll get an update for the parent if appropriate
1284 if (inObject == [(AIListContact *)inObject parentContact]) {
1285 if (useOfflineGroup) {
1286 AIListObject *containingObject = [inObject containingObject];
1288 if ([inObject online] &&
1289 (containingObject == [self offlineGroup])) {
1290 [(AIListContact *)inObject restoreGrouping];
1292 } else if (![inObject online] &&
1294 (containingObject != [self offlineGroup])) {
1295 [self _moveContactLocally:(AIListContact *)inObject
1296 toGroup:[self offlineGroup]];
1300 if ([inObject containingObject] == [self offlineGroup]) {
1301 [(AIListContact *)inObject restoreGrouping];
1311 //Contact Sorting --------------------------------------------------------------------------------
1312 #pragma mark Contact Sorting
1313 //Register sorting code
1314 - (void)registerListSortController:(AISortController *)inController
1316 [sortControllerArray addObject:inController];
1318 - (NSArray *)sortControllerArray
1320 return sortControllerArray;
1323 //Set and get the active sort controller
1324 - (void)setActiveSortController:(AISortController *)inController
1326 activeSortController = inController;
1328 [activeSortController didBecomeActive];
1330 //The newly-active sort controller needs to know whether it should be forced to ignore groups
1331 [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
1334 [self sortContactList];
1336 - (AISortController *)activeSortController
1338 return activeSortController;
1341 //Sort the entire contact list
1342 - (void)sortContactList
1344 [contactList sortGroupAndSubGroups:YES sortController:activeSortController];
1345 [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:nil];
1348 //Sort an individual object
1349 - (void)sortListObject:(AIListObject *)inObject
1351 if (updatesAreDelayed) {
1352 delayedContactChanges++;
1354 AIListObject *group = [inObject containingObject];
1356 if ([group isKindOfClass:[AIListGroup class]]) {
1357 //Sort the groups containing this object
1358 [(AIListGroup *)group sortListObject:inObject sortController:activeSortController];
1359 [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:inObject];
1364 //List object observers ------------------------------------------------------------------------------------------------
1365 #pragma mark List object observers
1366 //Registers code to observe handle status changes
1367 - (void)registerListObjectObserver:(id <AIListObjectObserver>)inObserver
1370 [contactObservers addObject:[NSValue valueWithNonretainedObject:inObserver]];
1372 //Let the new observer process all existing objects
1373 [self updateAllListObjectsForObserver:inObserver];
1376 - (void)unregisterListObjectObserver:(id)inObserver
1378 [contactObservers removeObject:[NSValue valueWithNonretainedObject:inObserver]];
1383 * @brief Update all contacts for an observer, notifying the observer of each one in turn
1385 * @param contacts The contacts to update, or nil to update all contacts
1386 * @param inObserver The observer
1388 - (void)updateContacts:(NSSet *)contacts forObserver:(id <AIListObjectObserver>)inObserver
1390 NSEnumerator *enumerator;
1391 AIListObject *listObject;
1393 [self delayListObjectNotifications];
1395 enumerator = (contacts ? [contacts objectEnumerator] : [contactDict objectEnumerator]);
1396 while ((listObject = [enumerator nextObject])) {
1397 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1398 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1400 //If this contact is within a meta contact, update the meta contact too
1401 AIListObject *containingObject = [listObject containingObject];
1402 if (containingObject && [containingObject isKindOfClass:[AIMetaContact class]]) {
1403 NSSet *attributes = [inObserver updateListObject:containingObject
1406 if (attributes) [self listObjectAttributesChanged:containingObject
1407 modifiedKeys:attributes];
1411 [self endListObjectNotificationsDelay];
1414 //Instructs a controller to update all available list objects
1415 - (void)updateAllListObjectsForObserver:(id <AIListObjectObserver>)inObserver
1417 NSEnumerator *enumerator;
1418 AIListObject *listObject;
1420 [self delayListObjectNotifications];
1423 [self updateContacts:nil forObserver:inObserver];
1426 enumerator = [groupDict objectEnumerator];
1427 while ((listObject = [enumerator nextObject])) {
1428 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1429 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1432 //Reset all accounts
1433 enumerator = [[[adium accountController] accounts] objectEnumerator];
1434 while ((listObject = [enumerator nextObject])) {
1435 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1436 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1440 [self endListObjectNotificationsDelay];
1444 //Notify observers of a status change. Returns the modified attribute keys
1445 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
1447 NSMutableSet *attrChange = nil;
1448 NSEnumerator *enumerator;
1449 NSValue *observerValue;
1451 //Let our observers know
1452 enumerator = [contactObservers objectEnumerator];
1453 while ((observerValue = [enumerator nextObject])) {
1454 id <AIListObjectObserver> observer;
1457 observer = [observerValue nonretainedObjectValue];
1458 if ((newKeys = [observer updateListObject:inObject keys:modifiedKeys silent:silent])) {
1459 if (!attrChange) attrChange = [NSMutableSet set];
1460 [attrChange unionSet:newKeys];
1464 //Send out the notification for other observers
1465 [[adium notificationCenter] postNotificationName:ListObject_StatusChanged
1467 userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys
1468 forKey:@"Keys"] : nil)];
1473 //Command all observers to apply their attributes to an object
1474 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject
1476 NSEnumerator *enumerator = [contactObservers objectEnumerator];
1477 NSValue *observerValue;
1479 while ((observerValue = [enumerator nextObject])) {
1480 id <AIListObjectObserver> observer;
1482 observer = [observerValue nonretainedObjectValue];
1484 [observer updateListObject:inObject keys:nil silent:YES];
1490 //Contact List Access --------------------------------------------------------------------------------------------------
1491 #pragma mark Contact List Access
1492 //Returns the main contact list group
1493 - (AIListGroup *)contactList
1498 //Return a flat array of all the objects in a group on an account (and all subgroups, if desired)
1499 - (NSMutableArray *)allContactsInGroup:(AIListGroup *)inGroup subgroups:(BOOL)subGroups onAccount:(AIAccount *)inAccount
1501 NSMutableArray *contactArray = [NSMutableArray array];
1502 NSEnumerator *enumerator;
1503 AIListObject *object;
1505 if (inGroup == nil) inGroup = contactList; //Passing nil scans the entire contact list
1507 enumerator = [[inGroup containedObjects] objectEnumerator];
1508 while ((object = [enumerator nextObject])) {
1509 if ([object isMemberOfClass:[AIMetaContact class]] || [object isMemberOfClass:[AIListGroup class]]) {
1511 [contactArray addObjectsFromArray:[self allContactsInGroup:(AIListGroup *)object
1513 onAccount:inAccount]];
1515 } else if ([object isMemberOfClass:[AIListContact class]]) {
1517 ([(AIListContact *)object account] == inAccount)) {
1518 [contactArray addObject:object];
1523 return contactArray;
1526 //Contact List Menus- --------------------------------------------------------------------------------------------------
1527 #pragma mark Contact List Menus
1529 //Returns a menu containing all the groups within a group
1530 //- Selector called on group selection is selectGroup:
1531 //- The menu items represented object is the group it represents
1532 - (NSMenu *)menuOfAllGroupsInGroup:(AIListGroup *)inGroup withTarget:(id)target
1534 NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
1536 [menu setAutoenablesItems:NO];
1537 [self _menuOfAllGroups:menu forGroup:inGroup withTarget:target level:0];
1539 return [menu autorelease];
1541 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level
1543 NSEnumerator *enumerator;
1544 AIListObject *object;
1546 //Passing nil scans the entire contact list
1547 if (group == nil) group = contactList;
1549 //Enumerate this group and process all groups we find within it
1550 enumerator = [[group containedObjects] objectEnumerator];
1551 while ((object = [enumerator nextObject])) {
1552 if ([object isKindOfClass:[AIListGroup class]] && object != [self offlineGroup]) {
1553 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[object displayName]
1555 action:@selector(selectGroup:)
1557 [menuItem setRepresentedObject:object];
1558 if ([menuItem respondsToSelector:@selector(setIndentationLevel:)]) {
1559 [menuItem setIndentationLevel:level];
1561 [menu addItem:menuItem];
1564 [self _menuOfAllGroups:menu forGroup:(AIListGroup *)object withTarget:target level:level+1];
1570 //Returns a menu containing all the objects in a group on an account
1571 //- Selector called on contact selection is selectContact:
1572 //- The menu item's represented object is the contact it represents
1573 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target{
1574 return [self menuOfAllContactsInContainingObject:inObject withTarget:target firstLevel:YES];
1576 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target firstLevel:(BOOL)firstLevel
1578 NSEnumerator *enumerator;
1579 AIListObject *object;
1582 NSMenu *menu = [[NSMenu alloc] init];
1583 [menu setAutoenablesItems:NO];
1585 //Passing nil scans the entire contact list
1586 if (inObject == nil) inObject = contactList;
1588 //The pull down menu needs an extra item at the top of its root menu to handle the selection.
1589 if (firstLevel) [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
1591 //All menu items for all contained objects
1592 enumerator = [[inObject listContacts] objectEnumerator];
1593 while ((object = [enumerator nextObject])) {
1594 NSImage *menuServiceImage;
1595 NSMenuItem *menuItem;
1596 BOOL needToCreateSubmenu;
1597 BOOL isGroup = [object isKindOfClass:[AIListGroup class]];
1598 BOOL isValidGroup = (isGroup &&
1599 [[(AIListGroup *)object containedObjects] count]);
1601 //We don't want to include empty groups
1602 if (!isGroup || isValidGroup) {
1604 needToCreateSubmenu = (isValidGroup ||
1605 ([object isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)object listContacts] count] > 1)));
1608 menuServiceImage = [AIUserIcons menuUserIconForObject:object];
1610 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(needToCreateSubmenu ?
1611 [object displayName] :
1612 [object formattedUID])
1614 action:@selector(selectContact:)
1617 if (needToCreateSubmenu) {
1618 [menuItem setSubmenu:[self menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)object withTarget:target firstLevel:NO]];
1621 [menuItem setRepresentedObject:object];
1622 [menuItem setImage:menuServiceImage];
1623 [menu addItem:menuItem];
1628 return [menu autorelease];
1631 //Retrieving Specific Contacts -----------------------------------------------------------------------------------------
1632 #pragma mark Retrieving Specific Contacts
1634 //Retrieve a contact from the contact list (Creating if necessary)
1635 - (AIListContact *)contactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1637 if (!(inUID && [inUID length] && inService)) return nil; //Ignore invalid requests
1639 AIListContact *contact = nil;
1640 NSString *key = [AIListContact internalUniqueObjectIDForService:inService
1643 contact = [contactDict objectForKey:key];
1646 contact = [[AIListContact alloc] initWithUID:inUID account:inAccount service:inService];
1648 //Do the update thing
1649 [self _updateAllAttributesOfObject:contact];
1651 //Check to see if we should add to a metaContact
1652 AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:[contact internalObjectID]];
1654 /* We already know to add this object to the metaContact, since we did it before with another object,
1655 but this particular listContact is new and needs to be added directly to the metaContact
1656 (on future launches, the metaContact will obtain it automatically since all contacts matching this UID
1657 and serviceID should be included). */
1658 [self _performAddListObject:contact toMetaContact:metaContact];
1661 //Set the contact as mobile if it is a phone number
1662 if ([inUID characterAtIndex:0] == '+') {
1663 [contact setIsMobile:YES notify:NotifyNever];
1667 [contactDict setObject:contact forKey:key];
1674 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1676 if (inService && [inUID length]) {
1677 return [contactDict objectForKey:[AIListContact internalUniqueObjectIDForService:inService
1686 * @brief Return a set of all contacts with a specified UID and service
1688 * @param service The AIService in question
1689 * @param inUID The UID, which should be normalized (lower case, no spaces, etc.) as appropriate for the service
1690 * @param existingOnly If YES, only pre-existing contacts. If NO, an AIListContact is guaranteed to be returned
1691 * on each compatible account, even if one did not previously exist.
1693 - (NSSet *)allContactsWithService:(AIService *)service UID:(NSString *)inUID existingOnly:(BOOL)existingOnly
1695 NSEnumerator *enumerator;
1697 NSMutableSet *returnContactSet = [NSMutableSet set];
1699 enumerator = [[[adium accountController] accountsCompatibleWithService:service] objectEnumerator];
1701 while ((account = [enumerator nextObject])) {
1702 AIListContact *listContact;
1705 listContact = [self existingContactWithService:service
1709 listContact = [self contactWithService:service
1715 [returnContactSet addObject:listContact];
1719 return returnContactSet;
1722 - (AIListObject *)existingListObjectWithUniqueID:(NSString *)uniqueID
1724 NSEnumerator *enumerator;
1725 AIListObject *listObject;
1728 enumerator = [contactDict objectEnumerator];
1729 while ((listObject = [enumerator nextObject])) {
1730 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1734 enumerator = [groupDict objectEnumerator];
1735 while ((listObject = [enumerator nextObject])) {
1736 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1740 enumerator = [metaContactDict objectEnumerator];
1741 while ((listObject = [enumerator nextObject])) {
1742 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1748 - (AIListContact *)preferredContactForContentType:(NSString *)inType forListContact:(AIListContact *)inContact
1750 AIListContact *returnContact = nil;
1753 if ([inContact isKindOfClass:[AIMetaContact class]]) {
1754 AIListObject *preferredContact;
1755 NSString *internalObjectID;
1757 /* If we've messaged this object previously, prefer the last contact we sent to if that
1758 * contact is currently in the most-available status the metacontact can offer
1760 internalObjectID = [inContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1761 group:OBJECT_STATUS_CACHE];
1763 if ((internalObjectID) &&
1764 (preferredContact = [self existingListObjectWithUniqueID:internalObjectID]) &&
1765 ([preferredContact isKindOfClass:[AIListContact class]]) &&
1766 ([preferredContact statusSummary] == [inContact statusSummary]) &&
1767 ([[(AIMetaContact *)inContact containedObjects] containsObject:preferredContact])) {
1768 returnContact = [self preferredContactForContentType:inType
1769 forListContact:(AIListContact *)preferredContact];
1772 //If the last contact we sent to is not appropriate, use the metaContact's preferredContact
1773 if (!returnContact) {
1774 //Recurse into metacontacts if necessary
1775 AIListContact *firstAvailableContact = nil;
1776 AIListContact *firstNotOfflineContact = nil;
1778 AIListContact *thisContact;
1779 NSEnumerator *contactsEnum = [[(AIMetaContact *)inContact containedObjects] objectEnumerator];
1780 while ((thisContact = [contactsEnum nextObject])) {
1781 AIStatusType statusSummary = [thisContact statusSummary];
1783 if (statusSummary != AIOfflineStatus) {
1784 if (!firstNotOfflineContact) {
1785 firstNotOfflineContact = thisContact;
1788 if (statusSummary == AIAvailableStatus && ![thisContact isMobile]) {
1789 if (!firstAvailableContact) {
1790 firstAvailableContact = thisContact;
1798 returnContact = (firstAvailableContact ?
1799 firstAvailableContact :
1800 (firstNotOfflineContact ? firstNotOfflineContact : [(AIMetaContact *)inContact preferredContact]));
1804 //We have a flat contact; find the best account for talking to this contact,
1805 //and return an AIListContact on that account
1806 account = [[adium accountController] preferredAccountForSendingContentType:inType
1807 toContact:inContact];
1809 if ([inContact account] == account) {
1810 returnContact = inContact;
1812 returnContact = [self contactWithService:[inContact service]
1814 UID:[inContact UID]];
1819 return returnContact;
1822 //Retrieve a list contact matching the UID and serviceID of the passed contact but on the specified account.
1823 //In many cases this will be the same as inContact.
1824 - (AIListContact *)contactOnAccount:(AIAccount *)account fromListContact:(AIListContact *)inContact
1826 if (account && ([inContact account] != account)) {
1827 return [self contactWithService:[inContact service] account:account UID:[inContact UID]];
1833 //XXX - This is ridiculous.
1834 - (AIListContact *)preferredContactWithUID:(NSString *)inUID andServiceID:(NSString *)inService forSendingContentType:(NSString *)inType
1836 AIService *theService = [[adium accountController] firstServiceWithServiceID:inService];
1837 AIListContact *tempListContact = [[AIListContact alloc] initWithUID:inUID
1838 service:theService];
1839 AIAccount *account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
1840 toContact:tempListContact
1841 includeOffline:YES];
1842 [tempListContact release];
1844 return [self contactWithService:theService account:account UID:inUID];
1849 * @brief Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts
1851 * If the destination contact's parent contact differs from the destination contact itself, the chat is with a metaContact.
1852 * If that metaContact's preferred destination for messaging isn't the same as the contact which was just messaged,
1853 * update the preference so that a new chat with this metaContact would default to the proper contact.
1855 - (void)didSendContent:(NSNotification *)notification
1857 AIChat *chat = [[notification userInfo] objectForKey:@"AIChat"];
1858 AIListContact *destContact = [chat listObject];
1859 AIListContact *metaContact;
1861 if (((metaContact = [destContact parentContact]) != destContact)) {
1862 NSString *destinationInternalObjectID = [destContact internalObjectID];
1863 NSString *currentPreferredDestination = [metaContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1864 group:OBJECT_STATUS_CACHE];
1866 if (![destinationInternalObjectID isEqualToString:currentPreferredDestination]) {
1867 [metaContact setPreference:destinationInternalObjectID
1868 forKey:KEY_PREFERRED_DESTINATION_CONTACT
1869 group:OBJECT_STATUS_CACHE];
1874 //Retrieving Groups ----------------------------------------------------------------------------------------------------
1875 #pragma mark Retrieving Groups
1877 //Retrieve a group from the contact list (Creating if necessary)
1878 - (AIListGroup *)groupWithUID:(NSString *)groupUID
1882 if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME]) {
1883 //Return our root group if it is requested
1884 group = contactList;
1886 if (!(group = [groupDict objectForKey:groupUID])) {
1888 group = [[AIListGroup alloc] initWithUID:groupUID];
1891 [self _updateAllAttributesOfObject:group];
1892 [groupDict setObject:group forKey:groupUID];
1894 //Add to the contact list
1895 [contactList addObject:group];
1896 [self _listChangedGroup:contactList object:group];
1904 - (AIListGroup *)existingGroupWithUID:(NSString *)groupUID
1908 if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME]) {
1909 //Return our root group if it is requested
1910 group = contactList;
1912 group = [groupDict objectForKey:groupUID];
1918 //Contact list editing -------------------------------------------------------------------------------------------------
1919 #pragma mark Contact list editing
1920 - (void)removeListObjects:(NSArray *)objectArray
1922 NSEnumerator *enumerator = [objectArray objectEnumerator];
1923 AIListObject *listObject;
1925 while ((listObject = [enumerator nextObject])) {
1926 if ([listObject isKindOfClass:[AIMetaContact class]]) {
1927 NSSet *objectsToRemove = nil;
1929 //If the metaContact only has one listContact, we will remove that contact from all accounts
1930 if ([[(AIMetaContact *)listObject listContacts] count] == 1) {
1931 AIListContact *listContact = [[(AIMetaContact *)listObject listContacts] objectAtIndex:0];
1933 objectsToRemove = [self allContactsWithService:[listContact service]
1934 UID:[listContact UID]
1938 //And actually remove the single contact if applicable
1939 if (objectsToRemove) {
1940 [self removeListObjects:[objectsToRemove allObjects]];
1943 //Now break the metaContact down, taking out all contacts and putting them back in the main list
1944 [self breakdownAndRemoveMetaContact:(AIMetaContact *)listObject];
1946 } else if ([listObject isKindOfClass:[AIListGroup class]]) {
1947 AIListObject *containingObject = [listObject containingObject];
1948 NSEnumerator *enumerator;
1951 //If this is a group, delete all the objects within it
1952 [self removeListObjects:[(AIListGroup *)listObject containedObjects]];
1954 //Delete the list off of all active accounts
1955 enumerator = [[[adium accountController] accounts] objectEnumerator];
1956 while ((account = [enumerator nextObject])) {
1957 if ([account online]) {
1958 [account deleteGroup:(AIListGroup *)listObject];
1962 //Then, procede to delete the group
1963 [listObject retain];
1964 [(AIMetaContact *)containingObject removeObject:listObject];
1965 [groupDict removeObjectForKey:[listObject UID]];
1966 [self _listChangedGroup:containingObject object:listObject];
1967 [listObject release];
1970 AIAccount *account = [(AIListContact *)listObject account];
1971 if ([account online]) {
1972 [account removeContacts:[NSArray arrayWithObject:listObject]];
1978 - (void)addContacts:(NSArray *)contactArray toGroup:(AIListGroup *)group
1980 NSEnumerator *enumerator;
1981 AIListContact *listObject;
1983 [self delayListObjectNotifications];
1985 enumerator = [contactArray objectEnumerator];
1986 while ((listObject = [enumerator nextObject])) {
1987 if(![group containsObject:listObject]) //don't add it if it's already there.
1988 [[listObject account] addContacts:[NSArray arrayWithObject:listObject] toGroup:group];
1991 [self endListObjectNotificationsDelay];
1994 - (void)requestAddContactWithUID:(NSString *)contactUID service:(AIService *)inService
1996 NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:contactUID, UID_KEY, inService, @"service",nil];
1997 [[adium notificationCenter] postNotificationName:Contact_AddNewContact
2002 - (void)moveListObjects:(NSArray *)objectArray toGroup:(AIListObject<AIContainingObject> *)group index:(int)index
2004 NSEnumerator *enumerator;
2005 AIListContact *listContact;
2007 [self delayListObjectNotifications];
2009 if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2010 [(id)group setDelayContainedObjectSorting:YES];
2013 enumerator = [objectArray objectEnumerator];
2014 while ((listContact = [enumerator nextObject])) {
2015 [self moveContact:listContact toGroup:group];
2017 //Set the new index / position of the object
2018 [self _positionObject:listContact atIndex:index inGroup:group];
2021 [self endListObjectNotificationsDelay];
2023 if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2024 [(id)group setDelayContainedObjectSorting:NO];
2028 Resort the entire list if we are moving within or between AIListGroup objects
2029 (other containing objects such as metaContacts will handle their own sorting).
2031 if ([group isKindOfClass:[AIListGroup class]]) {
2032 [self sortContactList];
2036 - (void)moveContact:(AIListContact *)listContact toGroup:(AIListObject<AIContainingObject> *)group
2038 //Move the object to the new group only if necessary
2039 if (group != [listContact containingObject]) {
2040 if ([group isKindOfClass:[AIListGroup class]]) {
2041 //Move a contact into a new group
2042 if ([listContact isKindOfClass:[AIMetaContact class]]) {
2043 //Move the meta contact to this new group
2044 [self _moveContactLocally:listContact toGroup:(AIListGroup *)group];
2046 NSEnumerator *enumerator;
2047 AIListContact *actualListContact;
2049 //This is a meta contact, move the objects within it. listContacts will give us a flat array of AIListContacts.
2050 enumerator = [[(AIMetaContact *)listContact listContacts] objectEnumerator];
2051 while ((actualListContact = [enumerator nextObject])) {
2052 //Only move the contact if it is actually listed on the account in question
2053 if (![actualListContact isStranger]) {
2054 [self _moveObjectServerside:actualListContact toGroup:(AIListGroup *)group];
2058 } else if ([listContact isKindOfClass:[AIListContact class]]) {
2060 [self _moveObjectServerside:listContact toGroup:(AIListGroup *)group];
2063 } else if ([group isKindOfClass:[AIMetaContact class]]) {
2064 //Moving a contact into a meta contact
2065 [self addListObject:listContact toMetaContact:(AIMetaContact *)group];
2070 //Move an object to another group
2071 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group
2073 AIAccount *account = [(AIListContact *)listObject account];
2074 if ([account online]) {
2075 [account moveListObjects:[NSArray arrayWithObject:listObject] toGroup:group];
2080 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName
2082 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
2085 //Since Adium has no memory of what accounts a group is on, we have to send this message to all available accounts
2086 //The accounts without this group will just ignore it
2087 while ((account = [enumerator nextObject])) {
2088 [account renameGroup:listGroup to:newName];
2091 //Remove the old group if it's empty
2092 if ([listGroup containedObjectsCount] == 0) {
2093 [self removeListObjects:[NSArray arrayWithObject:listGroup]];
2097 //Position a list object within a group
2098 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inGroup:(AIListObject<AIContainingObject> *)group
2101 //Moved to the top of a group. New index is between 0 and the lowest current index
2102 [listObject setOrderIndex:([group smallestOrder] / 2.0)];
2104 } else if (index >= [group visibleCount]) {
2105 //Moved to the bottom of a group. New index is one higher than the highest current index
2106 [listObject setOrderIndex:([group largestOrder] + 1.0)];
2109 //Moved somewhere in the middle. New index is the average of the next largest and smallest index
2110 AIListObject *previousObject = [group objectAtIndex:index-1];
2111 AIListObject *nextObject = [group objectAtIndex:index];
2112 float nextLowest = [previousObject orderIndex];
2113 float nextHighest = [nextObject orderIndex];
2116 [listObject setOrderIndex:((nextHighest + nextLowest) / 2.0)];
2120 #pragma mark Authorization
2121 - (id)showAuthorizationRequestWithDict:(NSDictionary *)inDict forAccount:(AIAccount *)inAccount
2123 return [adiumAuthorization showAuthorizationRequestWithDict:inDict forAccount:inAccount];