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 //#define CONTACTS_INFO_WITH_PROMPT
21 #import "AIAccountController.h"
22 #import "AIContactController.h"
23 #import "AIContactInfoWindowController.h"
24 #import "AIInterfaceController.h"
25 #import "AILoginController.h"
26 #import "AIMenuController.h"
27 #import "AIPreferenceController.h"
28 #import "AIToolbarController.h"
29 #import "AIToolbarController.h"
30 #import "ESContactAlertsController.h"
31 #import <AIUtilities/AIDictionaryAdditions.h>
32 #import <AIUtilities/AIFileManagerAdditions.h>
33 #import <AIUtilities/AIMenuAdditions.h>
34 #import <AIUtilities/AIToolbarUtilities.h>
35 #import <AIUtilities/CBApplicationAdditions.h>
36 #import <AIUtilities/ESImageAdditions.h>
37 #import <Adium/AIAccount.h>
38 #import <Adium/AIChat.h>
39 #import <Adium/AIContentMessage.h>
40 #import <Adium/AIListContact.h>
41 #import <Adium/AIListGroup.h>
42 #import <Adium/AIListObject.h>
43 #import <Adium/AIMetaContact.h>
44 #import <Adium/AIService.h>
45 #import <Adium/AISortController.h>
46 #import <Adium/AIUserIcons.h>
48 #ifdef CONTACTS_INFO_WITH_PROMPT
49 #import "ESShowContactInfoPromptController.h"
52 #define PREF_GROUP_CONTACT_LIST @"Contact List" //Contact list preference group
53 #define KEY_FLAT_GROUPS @"FlatGroups" //Group storage
54 #define KEY_FLAT_CONTACTS @"FlatContacts" //Contact storage
55 #define KEY_FLAT_METACONTACTS @"FlatMetaContacts" //Metacontact objectID storage
57 #define OBJECT_STATUS_CACHE @"Object Status Cache"
59 #define VIEW_CONTACTS_INFO AILocalizedString(@"Get Info",nil)
60 #define VIEW_CONTACTS_INFO_WITH_PROMPT AILocalizedString(@"Get Info...",nil)
61 #define GET_INFO_MASK (NSCommandKeyMask | NSShiftKeyMask)
62 #define ALTERNATE_GET_INFO_MASK (NSCommandKeyMask | NSShiftKeyMask | NSControlKeyMask)
64 #define TITLE_SHOW_INFO AILocalizedString(@"Show Info",nil)
66 #define UPDATE_CLUMP_INTERVAL 1.0
68 #define TOP_METACONTACT_ID @"TopMetaContactID"
69 #define KEY_IS_METACONTACT @"isMetaContact"
70 #define KEY_OBJECTID @"objectID"
71 #define KEY_METACONTACT_OWNERSHIP @"MetaContact Ownership"
72 #define CONTACT_DEFAULT_PREFS @"ContactPrefs"
74 #define SHOW_GROUPS_MENU_TITLE AILocalizedString(@"Show Groups",nil)
75 #define SHOW_GROUPS_IDENTIFER @"ShowGroups"
77 #define KEY_HIDE_CONTACT_LIST_GROUPS @"Hide Contact List Groups"
78 #define PREF_GROUP_CONTACT_LIST_DISPLAY @"Contact List Display"
80 #define SERVICE_ID_KEY @"ServiceID"
81 #define UID_KEY @"UID"
83 @interface AIContactController (PRIVATE)
84 - (AIListGroup *)processGetGroupNamed:(NSString *)serverGroup;
85 - (void)_performDelayedUpdates:(NSTimer *)timer;
86 - (void)loadContactList;
87 - (void)saveContactList;
88 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
89 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject;
90 - (void)prepareContactInfo;
92 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inGroup withTarget:(id)target firstLevel:(BOOL)firstLevel;
93 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level;
95 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector;
96 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector conformingToProtocol:(Protocol *)protocol;
98 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects;
99 - (void)_loadGroupsFromArray:(NSArray *)array;
101 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object;
102 - (void)prepareShowHideGroups;
103 - (void)_performChangeOfUseContactListGroups;
105 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inGroup:(AIListObject<AIContainingObject> *)group;
106 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group;
107 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName;
110 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID;
111 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact updatingPreferences:(BOOL)updatePreferences;
112 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray updatingPreferences:(BOOL)updatePreferences;
113 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
114 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
115 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact;
116 - (void)_loadMetaContactsFromArray:(NSArray *)array;
117 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict;
118 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact;
119 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact;
121 - (NSArray *)allContactsWithServiceID:(NSString *)inServiceID UID:(NSString *)inUID;
123 - (void)_addMenuItemsFromArray:(NSArray *)contactArray toMenu:(NSMenu *)contactMenu target:(id)target offlineContacts:(BOOL)offlineContacts;
126 @implementation AIContactController
129 - (void)initController
131 //Default account preferences
132 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:CONTACT_DEFAULT_PREFS
133 forClass:[self class]]
134 forGroup:PREF_GROUP_CONTACT_LIST];
137 contactObservers = [[NSMutableSet alloc] init];
138 sortControllerArray = [[NSMutableArray alloc] init];
139 activeSortController = nil;
140 delayedStatusChanges = 0;
141 delayedModifiedStatusKeys = [[NSMutableSet alloc] init];
142 delayedAttributeChanges = 0;
143 delayedModifiedAttributeKeys = [[NSMutableSet alloc] init];
144 delayedContactChanges = 0;
145 delayedUpdateRequests = 0;
146 updatesAreDelayed = NO;
149 contactDict = [[NSMutableDictionary alloc] init];
150 groupDict = [[NSMutableDictionary alloc] init];
151 metaContactDict = [[NSMutableDictionary alloc] init];
152 contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
154 contactList = [[AIListGroup alloc] initWithUID:ADIUM_ROOT_GROUP_NAME];
156 //Get Info window and menu items
157 [self prepareContactInfo];
159 //Show Groups menu item
160 [self prepareShowHideGroups];
162 //Observe content (for preferredContactForContentType:forListContact:)
163 [[adium notificationCenter] addObserver:self
164 selector:@selector(didSendContent:)
165 name:CONTENT_MESSAGE_SENT
170 - (void)finishIniting
172 [self loadContactList];
173 [self sortContactList];
177 - (void)closeController
179 [self saveContactList];
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 _loadGroupsFromArray:[[adium preferenceController] preferenceForKey:KEY_FLAT_GROUPS
240 group:PREF_GROUP_CONTACT_LIST]];
241 [self _loadMetaContactsFromArray:[[adium preferenceController] preferenceForKey:KEY_FLAT_METACONTACTS
242 group:PREF_GROUP_CONTACT_LIST]];
245 //Save the contact list
246 - (void)saveContactList
248 [[adium preferenceController] setPreference:[self _arrayRepresentationOfListObjects:[groupDict allValues]]
249 forKey:KEY_FLAT_GROUPS
250 group:PREF_GROUP_CONTACT_LIST];
253 //List objects from flattened array
254 - (void)_loadGroupsFromArray:(NSArray *)array
256 NSEnumerator *enumerator = [array objectEnumerator];
257 NSDictionary *infoDict;
259 NSString *Expanded = @"Expanded";
261 while(infoDict = [enumerator nextObject]) {
262 AIListObject *object = nil;
264 object = [self groupWithUID:[infoDict objectForKey:UID_KEY]];
265 [(AIListGroup *)object setExpanded:[[infoDict objectForKey:Expanded] boolValue]];
269 - (void)_loadMetaContactsFromArray:(NSArray *)array
271 NSEnumerator *enumerator = [array objectEnumerator];
272 NSString *identifier;
274 while (identifier = [enumerator nextObject]) {
275 NSNumber *objectID = [NSNumber numberWithInt:[[[identifier componentsSeparatedByString:@"-"] objectAtIndex:1] intValue]];
276 [self metaContactWithObjectID:objectID];
280 //Flattened array of the contact list content
281 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects
283 NSMutableArray *array = [NSMutableArray array];
284 NSEnumerator *enumerator = [listObjects objectEnumerator];;
285 AIListObject *object;
287 //Create temporary strings outside the loop
288 NSString *Group = @"Group";
289 NSString *Type = @"Type";
290 NSString *Expanded = @"Expanded";
292 while(object = [enumerator nextObject]) {
293 [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
295 [object UID], UID_KEY,
296 [NSNumber numberWithBool:[(AIListGroup *)object isExpanded]], Expanded,
304 //Status and Display updates -------------------------------------------------------------------------------------------
305 #pragma mark Status and Display updates
306 //These delay Contact_ListChanged, ListObject_AttributesChanged, Contact_OrderChanged notificationsDelays,
307 //sorting and redrawing to prevent redundancy when making a large number of changes
308 //Explicit delay. Call endListObjectNotificationsDelay to end
309 - (void)delayListObjectNotifications
311 delayedUpdateRequests++;
312 updatesAreDelayed = YES;
315 //End an explicit delay
316 - (void)endListObjectNotificationsDelay
318 delayedUpdateRequests--;
319 if(delayedUpdateRequests == 0 && !delayedUpdateTimer) {
320 [self _performDelayedUpdates:nil];
324 //Delay all list object notifications until a period of inactivity occurs. This is useful for accounts that do not
325 //know when they have finished connecting but still want to mute events.
326 - (void)delayListObjectNotificationsUntilInactivity
328 if(!delayedUpdateTimer) {
329 updatesAreDelayed = YES;
330 delayedUpdateTimer = [[NSTimer scheduledTimerWithTimeInterval:UPDATE_CLUMP_INTERVAL
332 selector:@selector(_performDelayedUpdates:)
334 repeats:YES] retain];
337 [delayedUpdateTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:UPDATE_CLUMP_INTERVAL]];
341 //Update the status of a list object. This will update any information that is otherwise too expensive to update
342 //automatically, such as their profile.
343 - (void)updateListContactStatus:(AIListContact *)inContact
345 //If we're dealing with a meta contact, update the status of the contacts contained within it
346 if([inContact isKindOfClass:[AIMetaContact class]]) {
347 NSEnumerator *enumerator = [[(AIMetaContact *)inContact listContacts] objectEnumerator];
348 AIListContact *contact;
350 while(contact = [enumerator nextObject]) {
351 [self updateListContactStatus:contact];
355 AIAccount *account = [inContact account];
356 if(![account online]) {
357 account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
358 toContact:inContact];
361 [account updateContactStatus:inContact];
365 //Called after modifying a contact's status
366 // Silent: Silences all events, notifications, sounds, overlays, etc. that would have been associated with this status change
367 - (void)listObjectStatusChanged:(AIListObject *)inObject modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
369 NSSet *modifiedAttributeKeys;
371 //Let all observers know the contact's status has changed before performing any sorting or further notifications
372 modifiedAttributeKeys = [self _informObserversOfObjectStatusChange:inObject withKeys:inModifiedKeys silent:silent];
374 //Resort the contact list
375 if(updatesAreDelayed) {
376 delayedStatusChanges++;
377 [delayedModifiedStatusKeys unionSet:inModifiedKeys];
379 //We can safely skip sorting if we know the modified attributes will invoke a resort later
380 if(![[self activeSortController] shouldSortForModifiedAttributeKeys:modifiedAttributeKeys] &&
381 [[self activeSortController] shouldSortForModifiedStatusKeys:inModifiedKeys]) {
382 [self sortListObject:inObject];
386 //Post an attributes changed message (if necessary)
387 if([modifiedAttributeKeys count]) {
388 [self listObjectAttributesChanged:inObject modifiedKeys:modifiedAttributeKeys];
392 //Call after modifying an object's display attributes
393 //(When modifying display attributes in response to a status change, this is not necessary)
394 - (void)listObjectAttributesChanged:(AIListObject *)inObject modifiedKeys:(NSSet *)inModifiedKeys
396 if(updatesAreDelayed) {
397 delayedAttributeChanges++;
398 [delayedModifiedAttributeKeys unionSet:inModifiedKeys];
400 //Resort the contact list if necessary
401 if([[self activeSortController] shouldSortForModifiedAttributeKeys:inModifiedKeys]) {
402 [self sortListObject:inObject];
405 //Post an attributes changed message
406 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
408 userInfo:(inModifiedKeys ?
409 [NSDictionary dictionaryWithObject:inModifiedKeys
415 //Performs any delayed list object/handle updates
416 - (void)_performDelayedUpdates:(NSTimer *)timer
418 BOOL updatesOccured = (delayedStatusChanges || delayedAttributeChanges || delayedContactChanges);
420 //Send out global attribute & status changed notifications (to cover any delayed updates)
422 BOOL shouldSort = NO;
424 //Inform observers of any changes
425 if(delayedContactChanges) {
426 // [[adium notificationCenter] postNotificationName:Contact_ListChanged object:nil];
427 delayedContactChanges = 0;
430 if (delayedStatusChanges) {
432 [[self activeSortController] shouldSortForModifiedStatusKeys:delayedModifiedStatusKeys]) {
435 [delayedModifiedStatusKeys removeAllObjects];
436 delayedStatusChanges = 0;
438 if(delayedAttributeChanges) {
440 [[self activeSortController] shouldSortForModifiedAttributeKeys:delayedModifiedAttributeKeys]) {
443 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
445 userInfo:(delayedModifiedAttributeKeys ?
446 [NSDictionary dictionaryWithObject:delayedModifiedAttributeKeys
449 [delayedModifiedAttributeKeys removeAllObjects];
450 delayedAttributeChanges = 0;
453 //Sort only if necessary
455 [self sortContactList];
459 //If no more updates are left to process, disable the update timer
460 //If there are no delayed update requests, remove the hold
461 if(!delayedUpdateTimer || !updatesOccured) {
462 if(delayedUpdateTimer) {
463 [delayedUpdateTimer invalidate];
464 [delayedUpdateTimer release];
465 delayedUpdateTimer = nil;
467 if(delayedUpdateRequests == 0) {
468 updatesAreDelayed = NO;
473 #pragma mark Contact Grouping
474 //Contact Grouping -----------------------------------------------------------------------------------------------------
476 //Redetermine the local grouping of a contact in response to server grouping information or an external change
477 - (void)listObjectRemoteGroupingChanged:(AIListContact *)inContact
479 NSString *remoteGroupName = [inContact remoteGroupName];
480 AIListObject *containingObject;
484 containingObject = [inContact containingObject];
486 if ([containingObject isKindOfClass:[AIMetaContact class]]) {
488 //Make sure we traverse metaContacts as close to the root as possible; an automatic metaContact may be within
489 //a manually created one, for example, in the most common situation.
490 containingObject = [self parentContactForListObject:containingObject];
492 //If the object's 'group' is a metaContact, and that metaContact isn't in our list yet
493 //use the object's own remote grouping (or the contactList root) as our grouping.
494 if (![containingObject containingObject] && [remoteGroupName length]) {
496 //If no similar objects exist, we add this contact directly to the list
497 AIListGroup *targetGroup;
499 targetGroup = (useContactListGroups ? [self groupWithUID:remoteGroupName] : contactList);
501 [targetGroup addObject:containingObject];
502 [self _listChangedGroup:targetGroup object:containingObject];
506 //If we have a remoteGroupName, add the contact locally to the list
507 if(remoteGroupName) {
508 AIListGroup *localGroup;
510 localGroup = (useContactListGroups ? [self groupWithUID:remoteGroupName] : contactList);
511 [self _moveContactLocally:inContact
515 //If !remoteGroupName, remove the contact from any local groups
516 if(containingObject) {
518 [(AIListGroup *)containingObject removeObject:inContact];
520 [self _listChangedGroup:(AIListGroup *)containingObject object:inContact];
525 BOOL isCurrentlyAStranger = [inContact isStranger];
526 if((isCurrentlyAStranger && (remoteGroupName != nil)) ||
527 (!isCurrentlyAStranger && (remoteGroupName == nil))){
528 [inContact setStatusObject:(remoteGroupName ? [NSNumber numberWithBool:YES] : nil)
529 forKey:@"NotAStranger"
531 [inContact notifyOfChangedStatusSilently:YES];
537 - (void)_moveContactLocally:(AIListContact *)listContact toGroup:(AIListGroup *)localGroup
539 AIListObject *containingObject;
540 AIListObject *existingObject;
541 BOOL performedGrouping = NO;
543 //Protect with a retain while we are removing and adding the contact to our arrays
544 [listContact retain];
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]]) {
567 [self addListObject:listContact toMetaContact:metaContact];
568 performedGrouping = YES;
572 if (!performedGrouping) {
573 //If no similar objects exist, we add this contact directly to the list
574 [localGroup addObject:listContact];
577 [self _listChangedGroup:localGroup object:listContact];
581 [listContact release];
584 - (AIListGroup *)remoteGroupForContact:(AIListContact *)inContact
588 if ([inContact isKindOfClass:[AIMetaContact class]]) {
589 //For a metaContact, the closest we have to a remote group is the group it is within locally
590 group = [inContact parentGroup];
593 NSString *remoteGroup = [inContact remoteGroupName];
594 group = (remoteGroup ? [self groupWithUID:remoteGroup] : nil);
600 //Post a list grouping changed notification for the object and group
601 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object
603 if(updatesAreDelayed) {
604 delayedContactChanges++;
606 [[adium notificationCenter] postNotificationName:Contact_ListChanged
608 userInfo:(group ? [NSDictionary dictionaryWithObject:group forKey:@"ContainingGroup"] : nil)];
612 - (BOOL)useContactListGroups
614 return(useContactListGroups);
617 - (void)setUseContactListGroups:(BOOL)inFlag
619 if (inFlag != useContactListGroups) {
621 useContactListGroups = inFlag;
623 [self _performChangeOfUseContactListGroups];
627 - (void)_performChangeOfUseContactListGroups
629 NSEnumerator *enumerator;
630 AIListObject *listObject;
632 [self delayListObjectNotifications];
634 //Store the preference
635 [[adium preferenceController] setPreference:[NSNumber numberWithBool:!useContactListGroups]
636 forKey:KEY_HIDE_CONTACT_LIST_GROUPS
637 group:PREF_GROUP_CONTACT_LIST_DISPLAY];
639 //Configure the sort controller to force ignoring of groups as appropriate
640 [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
642 enumerator = [[[[contactList containedObjects] copy] autorelease] objectEnumerator];
644 if (useContactListGroups) { /* We are now using contact list groups, but we weren't before. */
646 //Restore the grouping of all root-level contacts
647 while(listObject = [enumerator nextObject]) {
648 if([listObject isKindOfClass:[AIListContact class]]) {
649 [(AIListContact *)listObject restoreGrouping];
653 } else { /* We are no longer using contact list groups, but we were before. */
655 while(listObject = [enumerator nextObject]) {
656 if([listObject isKindOfClass:[AIListGroup class]]) {
657 NSArray *containedObjects;
658 NSEnumerator *groupEnumerator;
659 AIListObject *containedListObject;
661 containedObjects = [[(AIListGroup *)listObject containedObjects] copy];
662 groupEnumerator = [containedObjects objectEnumerator];
663 while(containedListObject = [groupEnumerator nextObject]) {
664 if([containedListObject isKindOfClass:[AIListContact class]]) {
665 [self _moveContactLocally:(AIListContact *)containedListObject
666 toGroup:contactList];
669 [containedObjects release];
674 //Stop delaying object notifications; this will automatically resort the contact list, so we're done.
675 [self endListObjectNotificationsDelay];
678 - (void)prepareShowHideGroups
680 //Load the preference
681 useContactListGroups = ![[[adium preferenceController] preferenceForKey:KEY_HIDE_CONTACT_LIST_GROUPS
682 group:PREF_GROUP_CONTACT_LIST_DISPLAY] boolValue];
684 //Show offline contacts menu item
685 showGroupsMenuItem = [[NSMenuItem alloc] initWithTitle:SHOW_GROUPS_MENU_TITLE
687 action:@selector(toggleShowGroups:)
689 [showGroupsMenuItem setState:useContactListGroups];
690 [[adium menuController] addMenuItem:showGroupsMenuItem toLocation:LOC_View_Toggles];
693 NSToolbarItem *toolbarItem;
694 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SHOW_GROUPS_IDENTIFER
695 label:AILocalizedString(@"Show Groups",nil)
696 paletteLabel:AILocalizedString(@"Toggle Groups Display",nil)
697 toolTip:AILocalizedString(@"Toggle display of groups",nil)
699 settingSelector:@selector(setImage:)
700 itemContent:[NSImage imageNamed:(useContactListGroups ?
701 @"togglegroups_transparent" :
703 forClass:[self class]]
704 action:@selector(toggleShowGroupsToolbar:)
706 [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ContactList"];
709 - (IBAction)toggleShowGroups:(id)sender
712 useContactListGroups = !useContactListGroups;
713 [showGroupsMenuItem setState:useContactListGroups];
715 //Update the contact list. Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure.
716 [self performSelector:@selector(_performChangeOfUseContactListGroups)
718 afterDelay:0.000001];
721 - (IBAction)toggleShowGroupsToolbar:(id)sender
723 [self toggleShowGroups:sender];
725 [sender setImage:[NSImage imageNamed:(useContactListGroups ?
726 @"togglegroups_transparent" :
728 forClass:[self class]]];
731 #pragma mark Meta Contacts
732 //Meta Contacts --------------------------------------------------------------------------------------------------------
733 //Returns a metaContact with the specified object ID. Pass nil to create a new, unique metaContact
734 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID
736 NSString *metaContactDictKey;
737 AIMetaContact *metaContact;
738 BOOL shouldRestoreContacts = YES;
740 //If no object ID is provided, use the next available object ID
741 //(MetaContacts should always have an individually unique object id)
743 int topID = [[[adium preferenceController] preferenceForKey:TOP_METACONTACT_ID
744 group:PREF_GROUP_CONTACT_LIST] intValue];
745 inObjectID = [NSNumber numberWithInt:topID];
746 [[adium preferenceController] setPreference:[NSNumber numberWithInt:([inObjectID intValue] + 1)]
747 forKey:TOP_METACONTACT_ID
748 group:PREF_GROUP_CONTACT_LIST];
750 //No reason to waste time restoring contacts when none are in the meta contact yet.
751 shouldRestoreContacts = NO;
754 //Look for a metacontact with this object ID. If none is found, create one
755 //and add its contained contacts to it.
756 metaContactDictKey = [AIMetaContact internalObjectIDFromObjectID:inObjectID];
758 metaContact = [metaContactDict objectForKey:metaContactDictKey];
760 metaContact = [[AIMetaContact alloc] initWithObjectID:inObjectID];
762 //Keep track of it in our metaContactDict for retrieval by objectID
763 [metaContactDict setObject:metaContact forKey:metaContactDictKey];
765 //Add it to our more general contactDict, as well
766 [contactDict setObject:metaContact forKey:[metaContact internalUniqueObjectID]];
768 if (shouldRestoreContacts) {
769 if(![self _restoreContactsToMetaContact:metaContact updatingPreferences:NO]) {
771 //If restoring the metacontact did not actually add any contacts, delete it as it is invalid
772 [self breakdownAndRemoveMetaContact:metaContact];
777 /* As with contactWithService:account:UID, update all attributes so observers are initially informed of
778 * this object's existence.
780 [self _updateAllAttributesOfObject:metaContact];
782 [metaContact release];
785 return (metaContact);
788 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact updatingPreferences:(BOOL)updatePreferences
790 NSDictionary *allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
791 group:PREF_GROUP_CONTACT_LIST];
792 NSArray *containedContactsArray = [allMetaContactsDict objectForKey:[metaContact internalObjectID]];
793 BOOL restoredContacts;
795 if([containedContactsArray count]) {
796 [metaContact setDelayContainedObjectSorting:YES];
798 [self _restoreContactsToMetaContact:metaContact
799 fromContainedContactsArray:containedContactsArray
800 updatingPreferences:updatePreferences];
802 [metaContact setDelayContainedObjectSorting:NO];
804 restoredContacts = YES;
807 restoredContacts = NO;
810 return restoredContacts;
813 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray updatingPreferences:(BOOL)updatePreferences
815 NSEnumerator *enumerator = [containedContactsArray objectEnumerator];
816 NSMutableDictionary *allMetaContactsDict = nil;
817 NSDictionary *containedContact;
818 BOOL shouldSaveMetaContacts = NO;
820 while(containedContact = [enumerator nextObject]) {
821 if ([[containedContact objectForKey:KEY_IS_METACONTACT] boolValue]) {
822 /* As of Adium 0.80, these keys should never be created as we no longer create metaContacts within metaContacts.
823 This check exists to import old preferences. */
824 NSArray *newContainedContactsArray;
827 if(!allMetaContactsDict) {
828 allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
829 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
832 //Retrieve the containedContactsArray for the metaContact with the specified objectID
833 objectID = [containedContact objectForKey:KEY_OBJECTID];
834 newContainedContactsArray = [allMetaContactsDict objectForKey:objectID];
836 //Recursively call ourself, saving the preferences, to make this merge unnecessary next time
837 [self _restoreContactsToMetaContact:metaContact
838 fromContainedContactsArray:newContainedContactsArray
839 updatingPreferences:YES];
841 //We should never need this metaContact again.
842 [allMetaContactsDict removeObjectForKey:objectID];
844 shouldSaveMetaContacts = YES;
848 //Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject.
849 //As listObjects are loaded/created/requested which match this internalObjectID, they will be inserted into the metaContact.
850 [contactToMetaContactLookupDict setObject:metaContact
851 forKey:[AIListObject internalObjectIDForServiceID:[containedContact objectForKey:SERVICE_ID_KEY]
852 UID:[containedContact objectForKey:UID_KEY]]];
856 if(shouldSaveMetaContacts) {
857 AILog(@"MetaContacts: Should save for %@",containedContactsArray);
858 [self _saveMetaContacts:allMetaContactsDict];
862 [allMetaContactsDict release];
866 //Add a list object to a meta contact, setting preferences and such
867 //so the association is lasting across program launches.
868 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
870 if (listObject != metaContact) {
872 //If listObject is a metaContact, perform addListObject:toMetaContact: recursively
873 if([listObject isKindOfClass:[AIMetaContact class]]) {
874 NSEnumerator *enumerator = [[[[(AIMetaContact *)listObject containedObjects] copy] autorelease] objectEnumerator];
875 AIListObject *someObject;
877 while(someObject = [enumerator nextObject]) {
879 [self addListObject:someObject toMetaContact:metaContact];
883 AIMetaContact *oldMetaContact;
885 //Obtain any metaContact this listObject is currently within, so we can remove it later
886 oldMetaContact = [contactToMetaContactLookupDict objectForKey:[listObject internalObjectID]];
888 if ([self _performAddListObject:listObject toMetaContact:metaContact]) {
889 //If this listObject was not in this metaContact in any form before, store the change
890 if (metaContact != oldMetaContact) {
891 //Remove the list object from any other metaContact it is in at present
892 if (oldMetaContact) {
893 [self removeListObject:listObject fromMetaContact:oldMetaContact];
896 [self _storeListObject:listObject inMetaContact:metaContact];
903 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact
905 AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact);
906 NSDictionary *containedContactDict;
907 NSMutableDictionary *allMetaContactsDict;
908 NSMutableArray *containedContactsArray;
910 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
912 //Get the dictionary of all metaContacts
913 allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
914 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
915 if (!allMetaContactsDict) {
916 allMetaContactsDict = [[NSMutableDictionary alloc] init];
919 //Load the array for the new metaContact
920 containedContactsArray = [[allMetaContactsDict objectForKey:metaContactInternalObjectID] mutableCopy];
921 if (!containedContactsArray) containedContactsArray = [[NSMutableArray alloc] init];
922 containedContactDict = nil;
924 //Create the dictionary describing this list object
925 if ([listObject isKindOfClass:[AIMetaContact class]]) {
926 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
927 [NSNumber numberWithBool:YES],KEY_IS_METACONTACT,
928 [(AIMetaContact *)listObject objectID],KEY_OBJECTID,nil];
930 } else if ([listObject isKindOfClass:[AIListContact class]]) {
931 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
932 [[listObject service] serviceID],SERVICE_ID_KEY,
933 [listObject UID],UID_KEY,nil];
936 //Only add if this dict isn't already in the array
937 if (containedContactDict && ([containedContactsArray indexOfObject:containedContactDict] == NSNotFound)) {
938 [containedContactsArray addObject:containedContactDict];
939 [allMetaContactsDict setObject:containedContactsArray forKey:metaContactInternalObjectID];
942 AILog(@"MetaContacts: _storeListObject saving: %@",listObject);
943 [self _saveMetaContacts:allMetaContactsDict];
945 [[adium contactAlertsController] mergeAndMoveContactAlertsFromListObject:listObject
946 intoListObject:metaContact];
949 [allMetaContactsDict release];
950 [containedContactsArray release];
953 //Actually adds a list object to a meta contact. No preferences are changed.
954 //Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict
955 //for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful.
956 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
958 AIListObject<AIContainingObject> *localGroup;
961 localGroup = [listObject containingObject];
963 //Remove the object from its previous containing group
964 if (localGroup && (localGroup != metaContact)) {
965 [localGroup removeObject:listObject];
966 [self _listChangedGroup:localGroup object:listObject];
969 //AIMetaContact will handle reassigning the list object's grouping to being itself
970 if (success = [metaContact addObject:listObject]) {
971 [contactToMetaContactLookupDict setObject:metaContact forKey:[listObject internalObjectID]];
973 [self _listChangedGroup:metaContact object:listObject];
975 //If the metaContact isn't in a group yet, use the group of the object we just added
976 if ((![metaContact containingObject]) && localGroup) {
977 //Add the new meta contact to our list
978 [(AIMetaContact *)localGroup addObject:metaContact];
979 [self _listChangedGroup:localGroup object:metaContact];
986 - (void)removeAllListObjectsMatching:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
988 NSEnumerator *enumerator;
989 AIListObject *theObject;
991 enumerator = [[self allContactsWithService:[listObject service]
992 UID:[listObject UID]] objectEnumerator];
994 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
995 [contactToMetaContactLookupDict removeObjectForKey:[listObject internalObjectID]];
997 [self delayListObjectNotifications];
998 while (theObject = [enumerator nextObject]) {
999 [self removeListObject:theObject fromMetaContact:metaContact];
1001 [self endListObjectNotificationsDelay];
1004 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1006 NSEnumerator *enumerator;
1007 NSArray *containedContactsArray;
1008 NSDictionary *containedContactDict = nil;
1009 NSMutableDictionary *allMetaContactsDict;
1010 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
1012 //Get the dictionary of all metaContacts
1013 allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1014 group:PREF_GROUP_CONTACT_LIST];
1016 //Load the array for the metaContact
1017 containedContactsArray = [allMetaContactsDict objectForKey:metaContactInternalObjectID];
1019 //Enumerate it, looking only for the appropriate type of containedContactDict
1020 enumerator = [containedContactsArray objectEnumerator];
1022 if ([listObject isKindOfClass:[AIMetaContact class]]) {
1023 NSNumber *listObjectObjectID = [(AIMetaContact *)listObject objectID];
1025 while (containedContactDict = [enumerator nextObject]) {
1026 if (([[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) &&
1027 (([(NSNumber *)[containedContactDict objectForKey:KEY_OBJECTID] compare:listObjectObjectID]) == 0)) {
1032 } else if ([listObject isKindOfClass:[AIListContact class]]) {
1034 NSString *listObjectUID = [listObject UID];
1035 NSString *listObjectServiceID = [[listObject service] serviceID];
1037 while (containedContactDict = [enumerator nextObject]) {
1038 if ([[containedContactDict objectForKey:UID_KEY] isEqualToString:listObjectUID] &&
1039 [[containedContactDict objectForKey:SERVICE_ID_KEY] isEqualToString:listObjectServiceID]) {
1045 //If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result
1046 if (containedContactDict) {
1047 NSMutableArray *newContainedContactsArray;
1048 NSMutableDictionary *newAllMetaContactsDict;
1050 newContainedContactsArray = [containedContactsArray mutableCopy];
1051 [newContainedContactsArray removeObjectIdenticalTo:containedContactDict];
1053 newAllMetaContactsDict = [allMetaContactsDict mutableCopy];
1054 [newAllMetaContactsDict setObject:newContainedContactsArray
1055 forKey:metaContactInternalObjectID];
1057 [self _saveMetaContacts:newAllMetaContactsDict];
1059 [newContainedContactsArray release];
1060 [newAllMetaContactsDict release];
1063 //The listObject can be within the metaContact without us finding a containedContactDict if we are removing multiple
1064 //listContacts referring to the same UID & serviceID combination - that is, on multiple accounts on the same service.
1065 //We therefore request removal of the object regardless of the if (containedContactDict) check above.
1066 [metaContact removeObject:listObject];
1071 * @brief Groups UIDs for services into a single metacontact
1073 * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to
1074 * a UID and a service, respectively, which together define a contact which should be included in the grouping.
1076 * Assumption: This is only called after the contact list is finished loading, which occurs via
1077 * -(void)finishIniting above.
1079 * @param UIDsArray NSArray of UIDs
1080 * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
1082 - (AIMetaContact *)groupUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray
1084 NSMutableArray *contactsToGroupArray = [NSMutableArray array];
1086 int count = [UIDsArray count];
1089 //Build an array of all contacts matching this description (multiple accounts on the same service listing
1090 //the same UID mean that we can have multiple AIListContact objects with a UID/service combination)
1091 for (i = 0; i < count; i++) {
1092 [contactsToGroupArray addObjectsFromArray:[self allContactsWithServiceID:[servicesArray objectAtIndex:i]
1093 UID:[UIDsArray objectAtIndex:i]]];
1096 return([self groupListContacts:contactsToGroupArray]);
1099 /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added.
1101 * This will reuse an existing metacontact (for one of the contacts in the array) if possible.
1102 * @param contactsToGroupArray Contacts to group together
1104 - (AIMetaContact *)groupListContacts:(NSArray *)contactsToGroupArray
1106 NSEnumerator *enumerator;
1107 AIListContact *listContact;
1108 AIMetaContact *metaContact = nil;
1110 //Look for an existing MetaContact we can use. The first one we find is the lucky winner.
1111 enumerator = [contactsToGroupArray objectEnumerator];
1112 while ((listContact = [enumerator nextObject]) && (metaContact == nil)) {
1113 if ([listContact isKindOfClass:[AIMetaContact class]]) {
1114 metaContact = (AIMetaContact *)listContact;
1116 metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]];
1120 //Create a new MetaContact is we didn't find one.
1122 metaContact = [self metaContactWithObjectID:nil];
1125 /* Add all these contacts to our MetaContact.
1126 * Some may already be present, but that's fine, as nothing will happen.
1128 enumerator = [contactsToGroupArray objectEnumerator];
1129 while (listContact = [enumerator nextObject]) {
1130 [self addListObject:listContact toMetaContact:metaContact];
1133 return(metaContact);
1136 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact
1138 //Remove the objects within it from being inside it
1139 NSArray *containedObjects = [[metaContact containedObjects] copy];
1140 NSEnumerator *metaEnumerator = [containedObjects objectEnumerator];
1141 AIListObject<AIContainingObject> *containingObject = [metaContact containingObject];
1142 AIListObject *object;
1144 NSMutableDictionary *allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1145 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
1147 while (object = [metaEnumerator nextObject]) {
1149 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1150 [contactToMetaContactLookupDict removeObjectForKey:[object internalObjectID]];
1152 [self removeListObject:object fromMetaContact:metaContact];
1155 //Then, procede to remove the metaContact
1158 [metaContact retain];
1160 //Remove it from its containing group
1161 [containingObject removeObject:metaContact];
1163 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
1165 //Remove our reference to it internally
1166 [metaContactDict removeObjectForKey:metaContactInternalObjectID];
1168 //Remove it from the preferences dictionary
1169 [allMetaContactsDict removeObjectForKey:metaContactInternalObjectID];
1171 //XXX - contactToMetaContactLookupDict
1173 //Post the list changed notification for the old containingObject
1174 [self _listChangedGroup:containingObject object:metaContact];
1176 //Save the updated allMetaContactsDict which no longer lists the metaContact
1177 [self _saveMetaContacts:allMetaContactsDict];
1179 //Protection is overrated.
1180 [metaContact release];
1181 [containedObjects release];
1182 [allMetaContactsDict release];
1185 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict
1187 AILog(@"MetaContacts: Saving!");
1188 [[adium preferenceController] setPreference:allMetaContactsDict
1189 forKey:KEY_METACONTACT_OWNERSHIP
1190 group:PREF_GROUP_CONTACT_LIST];
1191 [[adium preferenceController] setPreference:[allMetaContactsDict allKeys]
1192 forKey:KEY_FLAT_METACONTACTS
1193 group:PREF_GROUP_CONTACT_LIST];
1196 //Sort list objects alphabetically by their display name
1197 int contactDisplayNameSort(AIListObject *objectA, AIListObject *objectB, void *context)
1199 return [[objectA displayName] caseInsensitiveCompare:[objectB displayName]];
1202 //Return a menu of contacts which are within inContact on a given service. Pass nil for service to retrive all contacts.
1203 //The menuItems have a target of target and a selector of @selector(selectContainedContact:).
1204 - (NSMenu *)menuOfContainedContacts:(AIListObject *)inContact forService:(AIService *)service withTarget:(id)target includeOffline:(BOOL)includeOffline
1206 NSMenu *contactMenu = [[NSMenu alloc] initWithTitle:@""];
1207 NSArray *contactArray;
1209 if([inContact isKindOfClass:[AIMetaContact class]]) {
1213 dict = [(AIMetaContact *)inContact dictionaryOfServiceClassesAndListContacts];
1214 contactArray = [dict objectForKey:[service serviceClass]];
1216 //If service is nil, get ALL contained contacts
1217 contactArray = [(AIMetaContact *)inContact listContacts];
1220 contactArray = [NSArray arrayWithObject:inContact];
1223 //Sort the array by display name
1224 contactArray = [contactArray sortedArrayUsingFunction:contactDisplayNameSort context:nil];
1226 [self _addMenuItemsFromArray:contactArray
1229 offlineContacts:NO];
1231 if (includeOffline) {
1232 //Separate the online from the offline
1233 if ([contactMenu numberOfItems] > 0) {
1234 [contactMenu addItem:[NSMenuItem separatorItem]];
1237 [self _addMenuItemsFromArray:contactArray
1240 offlineContacts:YES];
1243 return [contactMenu autorelease];
1246 //Add the contacts from contactArray to the specified menu. If offlineContacts is NO, only add online ones.
1247 //If offlineContacts is YES, only add offline ones.
1248 - (void)_addMenuItemsFromArray:(NSArray *)contactArray toMenu:(NSMenu *)contactMenu target:(id)target offlineContacts:(BOOL)offlineContacts
1250 NSEnumerator *enumerator;
1251 AIListContact *contact;
1253 enumerator = [contactArray objectEnumerator];
1255 while(contact = [enumerator nextObject]) {
1256 BOOL contactIsOnline = [contact online];
1258 //If the contact is online and we aren't doing only offline contacts, add it.
1259 //If the contact is offline, and we are doing only offline contacts, add it if any account is available for sending to it.
1260 if ((contactIsOnline && !offlineContacts) ||
1261 (!contactIsOnline &&
1263 ([[adium accountController] firstAccountAvailableForSendingContentType:CONTENT_MESSAGE_TYPE
1265 includeOffline:NO] != nil))) {
1266 NSImage *menuServiceImage;
1267 NSMenuItem *menuItem;
1269 menuServiceImage = [AIUserIcons menuUserIconForObject:contact];
1271 menuItem = [[NSMenuItem alloc] initWithTitle:[contact formattedUID]
1273 action:@selector(selectContainedContact:)
1275 [menuItem setRepresentedObject:contact];
1276 [menuItem setImage:menuServiceImage];
1277 [contactMenu addItem:menuItem];
1284 - (NSMenu *)menuOfContainedContacts:(AIListObject *)inContact withTarget:(id)target
1286 return( [self menuOfContainedContacts:inContact forService:nil withTarget:target includeOffline:YES] );
1289 //Return either the highest metaContact containing this list object, or the list object itself. Appropriate for when
1290 //preferences should be read from/to the most generalized contact possible.
1291 - (AIListObject *)parentContactForListObject:(AIListObject *)listObject
1293 if ([listObject isKindOfClass:[AIListContact class]]) {
1294 //Find the highest-up metaContact
1295 AIListObject *containingObject;
1296 while ([(containingObject = [listObject containingObject]) isKindOfClass:[AIMetaContact class]]) {
1297 listObject = (AIMetaContact *)containingObject;
1303 //Contact Info --------------------------------------------------------------------------------
1304 #pragma mark Contact Info
1305 //Show info for the selected contact
1306 - (IBAction)showContactInfo:(id)sender
1308 AIListObject *listObject = nil;
1310 if ((sender == menuItem_getInfoContextualContact) || (sender == menuItem_getInfoContextualGroup)) {
1311 listObject = [[adium menuController] currentContextMenuObject];
1313 listObject = [self selectedListObject];
1317 [NSApp activateIgnoringOtherApps:YES];
1318 [[[AIContactInfoWindowController showInfoWindowForListObject:listObject] window] makeKeyAndOrderFront:nil];
1322 #ifdef CONTACTS_INFO_WITH_PROMPT
1323 - (void)showSpecifiedContactInfo:(id)sender
1325 [ESShowContactInfoPromptController showPrompt];
1329 //Add a contact info view
1330 - (void)addContactInfoPane:(AIContactInfoPane *)inPane
1332 [contactInfoPanes addObject:inPane];
1335 //Prepare the contact info menu and toolbar items
1336 - (void)prepareContactInfo
1338 contactInfoPanes = [[NSMutableArray alloc] init];
1340 //Add our get info contextual menu item
1341 menuItem_getInfoContextualContact = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_CONTACTS_INFO
1343 action:@selector(showContactInfo:)
1345 [[adium menuController] addContextualMenuItem:menuItem_getInfoContextualContact
1346 toLocation:Context_Contact_Manage];
1348 menuItem_getInfoContextualGroup = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_CONTACTS_INFO
1350 action:@selector(showContactInfo:)
1352 [[adium menuController] addContextualMenuItem:menuItem_getInfoContextualGroup
1353 toLocation:Context_Group_Manage];
1355 //Install the standard Get Info menu item which will always be command-shift-I
1356 menuItem_getInfo = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_CONTACTS_INFO
1358 action:@selector(showContactInfo:)
1359 keyEquivalent:@"i"];
1360 [menuItem_getInfo setKeyEquivalentModifierMask:GET_INFO_MASK];
1361 [[adium menuController] addMenuItem:menuItem_getInfo toLocation:LOC_Contact_Info];
1363 if([NSApp isOnPantherOrBetter]) {
1364 /* Install the alternate Get Info menu item which will be alternately command-I and command-shift-I, in the contact list
1365 * and in all other places, respectively.
1367 menuItem_getInfoAlternate = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_CONTACTS_INFO
1369 action:@selector(showContactInfo:)
1370 keyEquivalent:@"i"];
1371 [menuItem_getInfoAlternate setKeyEquivalentModifierMask:ALTERNATE_GET_INFO_MASK];
1372 [menuItem_getInfoAlternate setAlternate:YES];
1373 [[adium menuController] addMenuItem:menuItem_getInfoAlternate toLocation:LOC_Contact_Info];
1375 //Register for the contact list notifications
1376 [[adium notificationCenter] addObserver:self selector:@selector(contactListDidBecomeMain:)
1377 name:Interface_ContactListDidBecomeMain
1379 [[adium notificationCenter] addObserver:self selector:@selector(contactListDidResignMain:)
1380 name:Interface_ContactListDidResignMain
1383 //Watch changes in viewContactInfoMenuItem_alternate's menu so we can maintain its alternate status
1384 //(it will expand into showing both the normal and the alternate items when the menu changes)
1385 [[adium notificationCenter] addObserver:self selector:@selector(menuChanged:)
1387 object:[menuItem_getInfoAlternate menu]];
1391 #ifdef CONTACTS_INFO_WITH_PROMPT
1392 //Install the Get Info (prompting for a contact name) menu item
1393 menuItem_getInfo = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_CONTACTS_INFO_WITH_PROMPT
1395 action:@selector(showSpecifiedContactInfo:)
1397 [[adium menuController] addMenuItem:menuItem_getInfo toLocation:LOC_Contact_Info];
1400 //Add our get info toolbar item
1401 NSToolbarItem *toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"ShowInfo"
1402 label:AILocalizedString(@"Info",nil)
1403 paletteLabel:TITLE_SHOW_INFO
1404 toolTip:TITLE_SHOW_INFO
1406 settingSelector:@selector(setImage:)
1407 itemContent:[NSImage imageNamed:@"info" forClass:[self class]]
1408 action:@selector(showContactInfo:)
1410 [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ListObject"];
1413 //Always be able to show the inspector
1414 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
1416 if((menuItem == menuItem_getInfo) || (menuItem == menuItem_getInfoAlternate)) {
1417 return([self selectedListObject] != nil);
1419 } else if((menuItem == menuItem_getInfoContextualContact) || (menuItem == menuItem_getInfoContextualGroup)) {
1420 return([[adium menuController] currentContextMenuObject] != nil);
1428 - (NSArray *)contactInfoPanes
1430 return(contactInfoPanes);
1433 - (void)contactListDidBecomeMain:(NSNotification *)notification
1435 [[adium menuController] removeItalicsKeyEquivalent];
1436 [menuItem_getInfoAlternate setKeyEquivalentModifierMask:(NSCommandKeyMask)];
1437 [menuItem_getInfoAlternate setAlternate:YES];
1440 - (void)contactListDidResignMain:(NSNotification *)notification
1442 //set our alternate modifier mask back to the obscure combination
1443 [menuItem_getInfoAlternate setKeyEquivalent:@"i"];
1444 [menuItem_getInfoAlternate setKeyEquivalentModifierMask:ALTERNATE_GET_INFO_MASK];
1445 [menuItem_getInfoAlternate setAlternate:YES];
1447 //Now give the italics its combination back
1448 [[adium menuController] restoreItalicsKeyEquivalent];
1451 - (void)menuChanged:(NSNotification *)notification
1453 [NSMenu updateAlternateMenuItem:menuItem_getInfoAlternate];
1457 //Selected contact ------------------------------------------------
1458 #pragma mark Selected contact
1459 //Returns the "selected"(represented) contact (By finding the first responder that returns a contact)
1460 //If no listObject is found, try to find a list object selected in a group chat
1461 - (AIListObject *)selectedListObject
1463 AIListObject *listObject = [self _performSelectorOnFirstAvailableResponder:@selector(listObject)];
1465 listObject = [self _performSelectorOnFirstAvailableResponder:@selector(preferredListObject)];
1469 - (AIListObject *)selectedListObjectInContactList
1471 return([self _performSelectorOnFirstAvailableResponder:@selector(listObject) conformingToProtocol:@protocol(ContactListOutlineView)]);
1473 - (NSArray *)arrayOfSelectedListObjectsInContactList
1475 return([self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjects) conformingToProtocol:@protocol(ContactListOutlineView)]);
1478 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector
1480 NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
1481 //Check the first responder
1482 if([responder respondsToSelector:selector]) {
1483 return([responder performSelector:selector]);
1486 //Search the responder chain
1488 responder = [responder nextResponder];
1489 if([responder respondsToSelector:selector]) {
1490 return([responder performSelector:selector]);
1493 } while(responder != nil);
1495 //None found, return nil
1498 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector conformingToProtocol:(Protocol *)protocol
1500 NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
1501 //Check the first responder
1502 if([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
1503 return([responder performSelector:selector]);
1506 //Search the responder chain
1508 responder = [responder nextResponder];
1509 if([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
1510 return([responder performSelector:selector]);
1513 } while(responder != nil);
1515 //None found, return nil
1519 //Contact Sorting --------------------------------------------------------------------------------
1520 #pragma mark Contact Sorting
1521 //Register sorting code
1522 - (void)registerListSortController:(AISortController *)inController
1524 [sortControllerArray addObject:inController];
1526 - (NSArray *)sortControllerArray
1528 return(sortControllerArray);
1531 //Set and get the active sort controller
1532 - (void)setActiveSortController:(AISortController *)inController
1534 activeSortController = inController;
1536 [activeSortController didBecomeActive];
1538 //The newly-active sort controller needs to know whether it should be forced to ignore groups
1539 [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
1542 [self sortContactList];
1544 - (AISortController *)activeSortController
1546 return(activeSortController);
1549 //Sort the entire contact list
1550 - (void)sortContactList
1552 [contactList sortGroupAndSubGroups:YES sortController:activeSortController];
1553 [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:nil];
1556 //Sort an individual object
1557 - (void)sortListObject:(AIListObject *)inObject
1559 if(updatesAreDelayed) {
1560 delayedContactChanges++;
1562 AIListObject *group = [inObject containingObject];
1564 if([group isKindOfClass:[AIListGroup class]]) {
1565 //Sort the groups containing this object
1566 [(AIListGroup *)group sortListObject:inObject sortController:activeSortController];
1567 [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:inObject];
1572 //List object observers ------------------------------------------------------------------------------------------------
1573 #pragma mark List object observers
1574 //Registers code to observe handle status changes
1575 - (void)registerListObjectObserver:(id <AIListObjectObserver>)inObserver
1578 [contactObservers addObject:[NSValue valueWithNonretainedObject:inObserver]];
1580 //Let the new observer process all existing objects
1581 [self updateAllListObjectsForObserver:inObserver];
1584 - (void)unregisterListObjectObserver:(id)inObserver
1586 [contactObservers removeObject:[NSValue valueWithNonretainedObject:inObserver]];
1589 //Instructs a controller to update all available list objects
1590 - (void)updateAllListObjectsForObserver:(id <AIListObjectObserver>)inObserver
1592 NSEnumerator *enumerator;
1593 AIListObject *listObject;
1595 [self delayListObjectNotifications];
1597 //Reset all contacts
1598 enumerator = [contactDict objectEnumerator];
1599 while(listObject = [enumerator nextObject]) {
1600 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1601 if(attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1603 //If this contact is within a meta contact, update the meta contact too
1604 AIListObject *containingObject = [listObject containingObject];
1605 if(containingObject && [containingObject isKindOfClass:[AIMetaContact class]]) {
1606 NSSet *attributes = [inObserver updateListObject:containingObject
1609 if(attributes) [self listObjectAttributesChanged:containingObject
1610 modifiedKeys:attributes];
1615 enumerator = [groupDict objectEnumerator];
1616 while(listObject = [enumerator nextObject]) {
1617 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1618 if(attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1621 //Reset all accounts
1622 enumerator = [[[adium accountController] accountArray] objectEnumerator];
1623 while(listObject = [enumerator nextObject]) {
1624 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1625 if(attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1629 [self endListObjectNotificationsDelay];
1632 //Notify observers of a status change. Returns the modified attribute keys
1633 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
1635 NSMutableSet *attrChange = nil;
1636 NSEnumerator *enumerator;
1637 NSValue *observerValue;
1639 //Let our observers know
1640 enumerator = [contactObservers objectEnumerator];
1641 while((observerValue = [enumerator nextObject])) {
1642 id <AIListObjectObserver> observer;
1645 observer = [observerValue nonretainedObjectValue];
1646 if((newKeys = [observer updateListObject:inObject keys:modifiedKeys silent:silent])) {
1647 if (!attrChange) attrChange = [NSMutableSet set];
1648 [attrChange unionSet:newKeys];
1652 //Send out the notification for other observers
1653 [[adium notificationCenter] postNotificationName:ListObject_StatusChanged
1655 userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys
1656 forKey:@"Keys"] : nil)];
1661 //Command all observers to apply their attributes to an object
1662 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject
1664 NSEnumerator *enumerator = [contactObservers objectEnumerator];
1665 NSValue *observerValue;
1667 while((observerValue = [enumerator nextObject])) {
1668 id <AIListObjectObserver> observer;
1670 observer = [observerValue nonretainedObjectValue];
1672 [observer updateListObject:inObject keys:nil silent:YES];
1678 //Contact List Access --------------------------------------------------------------------------------------------------
1679 #pragma mark Contact List Access
1680 //Returns the main contact list group
1681 - (AIListGroup *)contactList
1683 return(contactList);
1686 //Return a flat array of all the objects in a group on an account (and all subgroups, if desired)
1687 - (NSMutableArray *)allContactsInGroup:(AIListGroup *)inGroup subgroups:(BOOL)subGroups onAccount:(AIAccount *)inAccount
1689 NSMutableArray *contactArray = [NSMutableArray array];
1690 NSEnumerator *enumerator;
1691 AIListObject *object;
1693 if(inGroup == nil) inGroup = contactList; //Passing nil scans the entire contact list
1695 enumerator = [inGroup objectEnumerator];
1696 while((object = [enumerator nextObject])) {
1697 if([object isMemberOfClass:[AIMetaContact class]] || [object isMemberOfClass:[AIListGroup class]]) {
1699 [contactArray addObjectsFromArray:[self allContactsInGroup:(AIListGroup *)object
1701 onAccount:inAccount]];
1703 } else if([object isMemberOfClass:[AIListContact class]]) {
1705 ([(AIListContact *)object account] == inAccount)) {
1706 [contactArray addObject:object];
1711 return(contactArray);
1714 //Contact List Menus- --------------------------------------------------------------------------------------------------
1715 #pragma mark Contact List Menus
1717 //Returns a menu containing all the groups within a group
1718 //- Selector called on group selection is selectGroup:
1719 //- The menu items represented object is the group it represents
1720 - (NSMenu *)menuOfAllGroupsInGroup:(AIListGroup *)inGroup withTarget:(id)target
1722 NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
1724 [menu setAutoenablesItems:NO];
1725 [self _menuOfAllGroups:menu forGroup:inGroup withTarget:target level:0];
1727 return [menu autorelease];
1729 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level
1731 NSEnumerator *enumerator;
1732 AIListObject *object;
1734 //Passing nil scans the entire contact list
1735 if(group == nil) group = contactList;
1737 //Enumerate this group and process all groups we find within it
1738 enumerator = [group objectEnumerator];
1739 while(object = [enumerator nextObject]) {
1740 if([object isKindOfClass:[AIListGroup class]]) {
1741 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[object displayName]
1743 action:@selector(selectGroup:)
1745 [menuItem setRepresentedObject:object];
1746 if([menuItem respondsToSelector:@selector(setIndentationLevel:)]) {
1747 [menuItem setIndentationLevel:level];
1749 [menu addItem:menuItem];
1752 [self _menuOfAllGroups:menu forGroup:(AIListGroup *)object withTarget:target level:level+1];
1758 //Returns a menu containing all the objects in a group on an account
1759 //- Selector called on contact selection is selectContact:
1760 //- The menu item's represented object is the contact it represents
1761 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target{
1762 return([self menuOfAllContactsInContainingObject:inObject withTarget:target firstLevel:YES]);
1764 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target firstLevel:(BOOL)firstLevel
1766 NSEnumerator *enumerator;
1767 AIListObject *object;
1770 NSMenu *menu = [[NSMenu alloc] init];
1771 [menu setAutoenablesItems:NO];
1773 //Passing nil scans the entire contact list
1774 if(inObject == nil) inObject = contactList;
1776 //The pull down menu needs an extra item at the top of its root menu to handle the selection.
1777 if(firstLevel) [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
1779 //All menu items for all contained objects
1780 enumerator = [inObject listContactsEnumerator];
1781 while((object = [enumerator nextObject])) {
1782 NSImage *menuServiceImage;
1783 NSMenuItem *menuItem;
1784 BOOL needToCreateSubmenu;
1785 BOOL isGroup = [object isKindOfClass:[AIListGroup class]];
1786 BOOL isValidGroup = (isGroup &&
1787 [[(AIListGroup *)object containedObjects] count]);
1789 //We don't want to include empty groups
1790 if (!isGroup || isValidGroup) {
1792 needToCreateSubmenu = (isValidGroup ||
1793 ([object isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)object listContacts] count] > 1)));
1796 menuServiceImage = [AIUserIcons menuUserIconForObject:object];
1798 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(needToCreateSubmenu ?
1799 [object displayName] :
1800 [object formattedUID])
1802 action:@selector(selectContact:)
1805 if(needToCreateSubmenu) {
1806 [menuItem setSubmenu:[self menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)object withTarget:target firstLevel:NO]];
1809 [menuItem setRepresentedObject:object];
1810 [menuItem setImage:menuServiceImage];
1811 [menu addItem:menuItem];
1816 return [menu autorelease];
1819 //Retrieving Specific Contacts -----------------------------------------------------------------------------------------
1820 #pragma mark Retrieving Specific Contacts
1822 //Retrieve a contact from the contact list (Creating if necessary)
1823 - (AIListContact *)contactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1825 AIListContact *contact = nil;
1827 if(inUID && [inUID length] && inService) { //Ignore invalid requests
1828 NSString *key = [AIListContact internalUniqueObjectIDForService:inService
1832 contact = [contactDict objectForKey:key];
1835 contact = [[AIListContact alloc] initWithUID:inUID account:inAccount service:inService];
1837 //Make sure this contact's order index isn't bigger than our current nextOrderIndex we'll vend to new contacts
1838 float orderIndex = [contact orderIndex];
1839 if (orderIndex > nextOrderIndex) nextOrderIndex = orderIndex + 1;
1841 //Do the update thing
1842 [self _updateAllAttributesOfObject:contact];
1844 //Check to see if we should add to a metaContact
1845 AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:[contact internalObjectID]];
1847 /* We already know to add this object to the metaContact, since we did it before with another object,
1848 but this particular listContact is new and needs to be added directly to the metaContact
1849 (on future launches, the metaContact will obtain it automatically since all contacts matching this UID
1850 and serviceID should be included). */
1851 [self _performAddListObject:contact toMetaContact:metaContact];
1854 //Set the contact as mobile if it is a phone number
1855 if([inUID characterAtIndex:0] == '+'){
1856 [contact setIsMobile:YES notify:NotifyNever];
1860 [contactDict setObject:contact forKey:key];
1868 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1870 if(inService && [inUID length]) {
1871 return([contactDict objectForKey:[AIListContact internalUniqueObjectIDForService:inService
1879 - (NSArray *)allContactsWithServiceID:(NSString *)inServiceID UID:(NSString *)inUID
1881 return([self allContactsWithService:[[adium accountController] firstServiceWithServiceID:inServiceID]
1885 - (NSArray *)allContactsWithService:(AIService *)service UID:(NSString *)inUID
1887 NSEnumerator *enumerator;
1889 NSMutableArray *returnContactArray = [NSMutableArray array];
1891 enumerator = [[[adium accountController] accountsWithServiceClassOfService:service] objectEnumerator];
1893 while(account = [enumerator nextObject]) {
1894 [returnContactArray addObject:[self contactWithService:service
1899 return (returnContactArray);
1902 - (AIListObject *)existingListObjectWithUniqueID:(NSString *)uniqueID
1904 NSEnumerator *enumerator;
1905 AIListObject *listObject;
1908 enumerator = [contactDict objectEnumerator];
1909 while(listObject = [enumerator nextObject]) {
1910 if([[listObject internalObjectID] isEqualToString:uniqueID]) return(listObject);
1914 enumerator = [groupDict objectEnumerator];
1915 while(listObject = [enumerator nextObject]) {
1916 if([[listObject internalObjectID] isEqualToString:uniqueID]) return(listObject);
1920 enumerator = [metaContactDict objectEnumerator];
1921 while(listObject = [enumerator nextObject]) {
1922 if([[listObject internalObjectID] isEqualToString:uniqueID]) return(listObject);
1928 - (AIListContact *)preferredContactForContentType:(NSString *)inType forListContact:(AIListContact *)inContact
1930 AIListContact *returnContact = nil;
1933 if ([inContact isKindOfClass:[AIMetaContact class]]) {
1934 AIListObject *preferredContact;
1935 NSString *internalObjectID;
1938 //If we've messaged this object previously, prefer the last contact we sent to if that
1939 //contact is currently available
1940 internalObjectID = [inContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1941 group:OBJECT_STATUS_CACHE];
1943 if((internalObjectID) &&
1944 (preferredContact = [self existingListObjectWithUniqueID:internalObjectID]) &&
1945 ([preferredContact isKindOfClass:[AIListContact class]]) &&
1946 ([preferredContact statusSummary] == AIAvailableStatus)) {
1947 returnContact = [self preferredContactForContentType:inType
1948 forListContact:(AIListContact *)preferredContact];
1951 //If the last contact we sent to is not available, use the metaContact's preferredContact
1952 if (!returnContact || ![returnContact online]) {
1953 //Recurse into metacontacts if necessary
1954 returnContact = [self preferredContactForContentType:inType
1955 forListContact:[(AIMetaContact *)inContact preferredContact]];
1960 //We have a flat contact; find the best account for talking to this contact,
1961 //and return an AIListContact on that account
1962 account = [[adium accountController] preferredAccountForSendingContentType:inType
1963 toContact:inContact];
1965 if ([inContact account] == account) {
1966 returnContact = inContact;
1968 returnContact = [self contactWithService:[inContact service]
1970 UID:[inContact UID]];
1975 return(returnContact);
1978 //Retrieve a list contact matching the UID and serviceID of the passed contact but on the specified account.
1979 //In many cases this will be the same as inContact.
1980 - (AIListContact *)contactOnAccount:(AIAccount *)account fromListContact:(AIListContact *)inContact
1982 if(account && ([inContact account] != account)) {
1983 return([self contactWithService:[inContact service] account:account UID:[inContact UID]]);
1989 //XXX - This is ridiculous.
1990 - (AIListContact *)preferredContactWithUID:(NSString *)inUID andServiceID:(NSString *)inService forSendingContentType:(NSString *)inType
1992 AIService *theService = [[adium accountController] firstServiceWithServiceID:inService];
1993 AIListContact *tempListContact = [[AIListContact alloc] initWithUID:inUID
1994 service:theService];
1995 AIAccount *account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
1996 toContact:tempListContact
1997 includeOffline:YES];
1998 [tempListContact release];
2000 return([self contactWithService:theService account:account UID:inUID]);
2004 //Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts
2005 - (void)didSendContent:(NSNotification *)notification
2007 AIChat *chat = [[notification userInfo] objectForKey:@"AIChat"];
2008 AIListContact *destContact = [chat listObject];
2009 AIListObject *metaContact;
2011 //Note: parentContactForListObject returns the passed contact if it is not in a metaContact;
2012 //this is a quick and easy check.
2015 ((metaContact = [self parentContactForListObject:destContact]) != destContact)) {
2017 [metaContact setPreference:[destContact internalObjectID]
2018 forKey:KEY_PREFERRED_DESTINATION_CONTACT
2019 group:OBJECT_STATUS_CACHE];
2023 //Retrieving Groups ----------------------------------------------------------------------------------------------------
2024 #pragma mark Retrieving Groups
2026 //Retrieve a group from the contact list (Creating if necessary)
2027 - (AIListGroup *)groupWithUID:(NSString *)groupUID
2031 if(!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME]) {
2032 //Return our root group if it is requested
2033 group = contactList;
2035 if(!(group = [groupDict objectForKey:groupUID])) {
2037 group = [[AIListGroup alloc] initWithUID:groupUID];
2039 //Place new groups at the bottom of our list (by giving them the largest ordering index)
2040 float orderIndex = [group orderIndex];
2041 if (orderIndex > nextOrderIndex) nextOrderIndex = orderIndex + 1;
2044 [self _updateAllAttributesOfObject:group];
2045 [groupDict setObject:group forKey:groupUID];
2047 //Add to target group
2048 [contactList addObject:group];
2049 [self _listChangedGroup:contactList object:group];
2051 //Add to the contact list
2052 [contactList addObject:group];
2053 [self _listChangedGroup:contactList object:group];
2062 - (AIListGroup *)existingGroupWithUID:(NSString *)groupUID
2066 if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME]) {
2067 //Return our root group if it is requested
2068 group = contactList;
2070 group = [groupDict objectForKey:groupUID];
2076 //Contact list editing -------------------------------------------------------------------------------------------------
2077 #pragma mark Contact list editing
2078 - (void)removeListObjects:(NSArray *)objectArray
2080 NSEnumerator *enumerator = [objectArray objectEnumerator];
2081 AIListObject *listObject;
2083 while(listObject = [enumerator nextObject]) {
2084 if([listObject isKindOfClass:[AIMetaContact class]]) {
2085 NSArray *objectsToRemove = nil;
2087 //If the metaContact only has one listContact, we will remove that contact from all accounts
2088 if([[(AIMetaContact *)listObject listContacts] count] == 1) {
2089 AIListContact *listContact = [[(AIMetaContact *)listObject listContacts] objectAtIndex:0];
2091 objectsToRemove = [self allContactsWithService:[listContact service]
2092 UID:[listContact UID]];
2095 //And actually remove the single contact if applicable
2096 if(objectsToRemove) {
2097 [self removeListObjects:objectsToRemove];
2100 //Now break the metaContact down, taking out all contacts and putting them back in the main list
2101 [self breakdownAndRemoveMetaContact:(AIMetaContact *)listObject];
2103 } else if([listObject isKindOfClass:[AIListGroup class]]) {
2104 AIListObject *containingObject = [listObject containingObject];
2105 NSEnumerator *enumerator;
2108 //If this is a group, delete all the objects within it
2109 [self removeListObjects:[(AIListGroup *)listObject containedObjects]];
2111 //Delete the list off of all active accounts
2112 enumerator = [[[adium accountController] accountArray] objectEnumerator];
2113 while (account = [enumerator nextObject]) {
2114 if ([account online]) {
2115 [account deleteGroup:(AIListGroup *)listObject];
2119 //Then, procede to delete the group
2120 [listObject retain];
2121 [(AIMetaContact *)containingObject removeObject:listObject];
2122 [groupDict removeObjectForKey:[listObject UID]];
2123 [self _listChangedGroup:containingObject object:listObject];
2124 [listObject release];
2127 AIAccount *account = [(AIListContact *)listObject account];
2128 if ([account online]) {
2129 [account removeContacts:[NSArray arrayWithObject:listObject]];
2135 - (void)addContacts:(NSArray *)contactArray toGroup:(AIListGroup *)group
2137 NSEnumerator *enumerator;
2138 AIListContact *listObject;
2140 [self delayListObjectNotifications];
2142 enumerator = [contactArray objectEnumerator];
2143 while(listObject = [enumerator nextObject]) {
2144 [[listObject account] addContacts:[NSArray arrayWithObject:listObject] toGroup:group];
2147 [self endListObjectNotificationsDelay];
2150 - (void)requestAddContactWithUID:(NSString *)contactUID service:(AIService *)inService
2152 NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:contactUID, UID_KEY, inService, @"service",nil];
2153 [[adium notificationCenter] postNotificationName:Contact_AddNewContact
2158 - (void)moveListObjects:(NSArray *)objectArray toGroup:(AIListObject<AIContainingObject> *)group index:(int)index
2160 NSEnumerator *enumerator;
2161 AIListContact *listContact;
2163 [self delayListObjectNotifications];
2165 if([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2166 [(id)group setDelayContainedObjectSorting:YES];
2169 enumerator = [objectArray objectEnumerator];
2170 while(listContact = [enumerator nextObject]) {
2171 [self moveContact:listContact toGroup:group];
2173 //Set the new index / position of the object
2174 [self _positionObject:listContact atIndex:index inGroup:group];
2177 [self endListObjectNotificationsDelay];
2179 if([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2180 [(id)group setDelayContainedObjectSorting:NO];
2184 Resort the entire list if we are moving within or between AIListGroup objects
2185 (other containing objects such as metaContacts will handle their own sorting).
2187 if([group isKindOfClass:[AIListGroup class]]) {
2188 [self sortContactList];
2192 - (void)moveContact:(AIListContact *)listContact toGroup:(AIListObject<AIContainingObject> *)group
2194 //Move the object to the new group if necessary
2195 if(group != [listContact containingObject]) {
2197 if ([group isKindOfClass:[AIListGroup class]]) {
2198 if([listContact isKindOfClass:[AIMetaContact class]]) {
2199 //This is a meta contact, move the objects within it. listContacts will give us a flat array of AIListContacts.
2202 NSEnumerator *metaEnumerator;
2203 AIListContact *aContainedContact;
2205 metaEnumerator = [[(AIMetaContact *)listContact listContacts] objectEnumerator];
2206 while(aContainedContact = [metaEnumerator nextObject]) {
2207 NSEnumerator *allContactsEnumerator;
2208 AIListContact *specificContact;
2210 //Leave no contact behind.
2211 allContactsEnumerator = [[self allContactsWithService:[aContainedContact service]
2212 UID:[aContainedContact UID]] objectEnumerator];
2213 while (specificContact = [allContactsEnumerator nextObject]) {
2214 [self _moveObjectServerside:specificContact toGroup:(AIListGroup *)group];
2219 [self _moveContactLocally:listContact toGroup:(AIListGroup *)group];
2221 } else if([listContact isKindOfClass:[AIListContact class]]) {
2223 [self _moveObjectServerside:listContact toGroup:(AIListGroup *)group];
2226 } else if ([group isKindOfClass:[AIMetaContact class]]) {
2227 //Moving a contact into a meta contact
2228 [self addListObject:listContact toMetaContact:(AIMetaContact *)group];
2233 //Move an object to another group
2234 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group
2236 AIAccount *account = [(AIListContact *)listObject account];
2237 if ([account online]) {
2238 [account moveListObjects:[NSArray arrayWithObject:listObject] toGroup:group];
2243 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName
2245 NSEnumerator *enumerator = [[[adium accountController] accountArray] objectEnumerator];
2248 //Since Adium has no memory of what accounts a group is on, we have to send this message to all available accounts
2249 //The accounts without this group will just ignore it
2250 while(account = [enumerator nextObject]) {
2251 [account renameGroup:listGroup to:newName];
2254 //Remove the old group if it's empty
2255 if([listGroup containedObjectsCount] == 0) {
2256 [self removeListObjects:[NSArray arrayWithObject:listGroup]];
2260 //Position a list object within a group
2261 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inGroup:(AIListObject<AIContainingObject> *)group
2264 //Moved to the top of a group. New index is between 0 and the lowest current index
2265 [listObject setOrderIndex:([group smallestOrder] / 2.0)];
2267 } else if(index >= [group visibleCount]) {
2268 //Moved to the bottom of a group. New index is one higher than the highest current index
2269 [listObject setOrderIndex:([group largestOrder] + 1.0)];
2272 //Moved somewhere in the middle. New index is the average of the next largest and smallest index
2273 AIListObject *previousObject = [group objectAtIndex:index-1];
2274 AIListObject *nextObject = [group objectAtIndex:index];
2275 float nextLowest = [previousObject orderIndex];
2276 float nextHighest = [nextObject orderIndex];
2279 [listObject setOrderIndex:((nextHighest + nextLowest) / 2.0)];
2283 - (float)nextOrderIndex
2285 return nextOrderIndex++;