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 "AISCLViewPlugin.h"
23 #import <Adium/AIAccountControllerProtocol.h>
24 #import <Adium/AIInterfaceControllerProtocol.h>
25 #import <Adium/AILoginControllerProtocol.h>
26 #import <Adium/AIMenuControllerProtocol.h>
27 #import <Adium/AIPreferenceControllerProtocol.h>
28 #import <Adium/AIToolbarControllerProtocol.h>
29 #import <Adium/AIContactAlertsControllerProtocol.h>
31 #import <AIUtilities/AIDictionaryAdditions.h>
32 #import <AIUtilities/AIFileManagerAdditions.h>
33 #import <AIUtilities/AIMenuAdditions.h>
34 #import <AIUtilities/AIToolbarUtilities.h>
35 #import <AIUtilities/AIApplicationAdditions.h>
36 #import <AIUtilities/AIImageAdditions.h>
37 #import <AIUtilities/AIStringAdditions.h>
38 #import <Adium/AIAccount.h>
39 #import <Adium/AIChat.h>
40 #import <Adium/AIContentMessage.h>
41 #import <Adium/AIListContact.h>
42 #import <Adium/AIListGroup.h>
43 #import <Adium/AIListObject.h>
44 #import <Adium/AIMetaContact.h>
45 #import <Adium/AIService.h>
46 #import <Adium/AISortController.h>
47 #import <Adium/AIUserIcons.h>
48 #import <Adium/AIServiceIcons.h>
49 #import <Adium/AIListBookmark.h>
51 #import "AdiumAuthorization.h"
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
56 #define KEY_BOOKMARKS @"Bookmarks"
58 #define OBJECT_STATUS_CACHE @"Object Status Cache"
60 #define UPDATE_CLUMP_INTERVAL 1.0
62 #define TOP_METACONTACT_ID @"TopMetaContactID"
63 #define KEY_IS_METACONTACT @"isMetaContact"
64 #define KEY_OBJECTID @"objectID"
65 #define KEY_METACONTACT_OWNERSHIP @"MetaContact Ownership"
66 #define CONTACT_DEFAULT_PREFS @"ContactPrefs"
68 #define SHOW_GROUPS_MENU_TITLE AILocalizedString(@"Show Groups",nil)
69 #define SHOW_GROUPS_IDENTIFER @"ShowGroups"
71 #define SERVICE_ID_KEY @"ServiceID"
72 #define UID_KEY @"UID"
74 @interface AIContactController (PRIVATE)
75 - (AIListGroup *)processGetGroupNamed:(NSString *)serverGroup;
76 - (void)_performDelayedUpdates:(NSTimer *)timer;
78 - (void)saveContactList;
79 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
80 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject;
81 - (void)prepareContactInfo;
83 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inGroup withTarget:(id)target firstLevel:(BOOL)firstLevel;
84 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level;
86 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects;
87 - (void)_loadGroupsFromArray:(NSArray *)array;
89 - (void)_loadBookmarks;
90 - (NSArray *)allBookmarks;
92 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object;
93 - (void)prepareShowHideGroups;
94 - (void)_performChangeOfUseContactListGroups;
96 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inObject:(AIListObject<AIContainingObject> *)group;
97 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group;
98 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName;
101 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID;
102 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact;
103 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray;
104 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
105 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
106 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact;
107 - (void)_loadMetaContactsFromArray:(NSArray *)array;
108 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict;
109 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact;
110 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact;
112 - (void)_addMenuItemsFromArray:(NSArray *)contactArray toMenu:(NSMenu *)contactMenu target:(id)target offlineContacts:(BOOL)offlineContacts;
116 @implementation AIContactController
121 if ((self = [super init])) {
123 contactObservers = [[NSMutableSet alloc] init];
124 sortControllerArray = [[NSMutableArray alloc] init];
125 activeSortController = nil;
126 delayedStatusChanges = 0;
127 delayedModifiedStatusKeys = [[NSMutableSet alloc] init];
128 delayedAttributeChanges = 0;
129 delayedModifiedAttributeKeys = [[NSMutableSet alloc] init];
130 delayedContactChanges = 0;
131 delayedUpdateRequests = 0;
132 updatesAreDelayed = NO;
135 contactDict = [[NSMutableDictionary alloc] init];
136 groupDict = [[NSMutableDictionary alloc] init];
137 metaContactDict = [[NSMutableDictionary alloc] init];
138 contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
139 detachedContactLists = [[NSMutableArray alloc] init];
146 - (void)controllerDidLoad
148 //Default contact preferences
149 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:CONTACT_DEFAULT_PREFS
150 forClass:[self class]]
151 forGroup:PREF_GROUP_CONTACT_LIST];
153 contactList = [[AIListGroup alloc] initWithUID:ADIUM_ROOT_GROUP_NAME];
154 //Root is always "expanded"
155 [contactList setExpanded:YES];
157 //Show Groups menu item
158 [self prepareShowHideGroups];
160 //Observe content (for preferredContactForContentType:forListContact:)
161 [[adium notificationCenter] addObserver:self
162 selector:@selector(didSendContent:)
163 name:CONTENT_MESSAGE_SENT
166 [self loadContactList];
167 [self sortContactList];
169 adiumAuthorization = [[AdiumAuthorization alloc] init];
171 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST_DISPLAY];
175 - (void)controllerWillClose
177 [self saveContactList];
183 [[adium preferenceController] unregisterPreferenceObserver:self];
185 [contactList release];
186 [contactObservers release]; contactObservers = nil;
188 [sortControllerArray release];
189 [delayedModifiedStatusKeys release];
190 [delayedModifiedAttributeKeys release];
192 [contactDict release];
194 [metaContactDict release];
195 [contactToMetaContactLookupDict release];
196 [detachedContactLists release];
198 [adiumAuthorization release];
203 - (void)clearAllMetaContactData
206 NSDictionary *metaContactDictCopy = [metaContactDict copy];
207 NSEnumerator *enumerator;
208 AIMetaContact *metaContact;
210 if ([metaContactDictCopy count]) {
211 [self delayListObjectNotifications];
213 //Remove all the metaContacts to get any existing objects out of them
214 enumerator = [metaContactDictCopy objectEnumerator];
215 while ((metaContact = [enumerator nextObject])) {
216 [self breakdownAndRemoveMetaContact:metaContact];
219 [self endListObjectNotificationsDelay];
222 [metaContactDict release]; metaContactDict = [[NSMutableDictionary alloc] init];
223 [contactToMetaContactLookupDict release]; contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
225 //Clear the preferences for good measure
226 [[adium preferenceController] setPreference:nil
227 forKey:KEY_FLAT_METACONTACTS
228 group:PREF_GROUP_CONTACT_LIST];
229 [[adium preferenceController] setPreference:nil
230 forKey:KEY_METACONTACT_OWNERSHIP
231 group:PREF_GROUP_CONTACT_LIST];
233 //Clear out old metacontact files
234 path = [[[adium loginController] userDirectory] stringByAppendingPathComponent:OBJECT_PREFS_PATH];
235 [[NSFileManager defaultManager] removeFilesInDirectory:path
236 withPrefix:@"MetaContact"
238 [[NSFileManager defaultManager] removeFilesInDirectory:[adium cachesPath]
239 withPrefix:@"MetaContact"
242 [metaContactDictCopy release];
245 //Local Contact List Storage -------------------------------------------------------------------------------------------
246 #pragma mark Local Contact List Storage
247 //Load the contact list
248 - (void)loadContactList
250 //We must load all the groups before loading contacts for the ordering system to work correctly.
251 [self _loadMetaContactsFromArray:[[adium preferenceController] preferenceForKey:KEY_FLAT_METACONTACTS
252 group:PREF_GROUP_CONTACT_LIST]];
253 [self _loadBookmarks];
256 //Save the contact list
257 - (void)saveContactList
259 NSEnumerator *enumerator = [groupDict objectEnumerator];
260 AIListGroup *listGroup;
262 while ((listGroup = [enumerator nextObject])) {
263 [listGroup setPreference:[NSNumber numberWithBool:[listGroup isExpanded]]
265 group:PREF_GROUP_CONTACT_LIST];
268 NSMutableArray *bookmarks = [NSMutableArray array];
269 AIListObject *listObject;
270 enumerator = [[self allBookmarks] objectEnumerator];
271 while ((listObject = [enumerator nextObject])) {
272 if ([listObject isKindOfClass:[AIListBookmark class]]) {
273 [bookmarks addObject:[NSKeyedArchiver archivedDataWithRootObject:listObject]];
277 [[adium preferenceController] setPreference:bookmarks
279 group:PREF_GROUP_CONTACT_LIST];
282 - (void)_loadBookmarks
284 NSEnumerator *enumerator = [[[adium preferenceController] preferenceForKey:KEY_BOOKMARKS
285 group:PREF_GROUP_CONTACT_LIST] objectEnumerator];
287 while ((data = [enumerator nextObject])) {
288 AIListBookmark *bookmark;
289 //As a bookmark is initialized, it will add itself to the contact list in the right place
290 bookmark = [NSKeyedUnarchiver unarchiveObjectWithData:data];
292 //It's a newly created object, so set its initial attributes
293 [self _updateAllAttributesOfObject:bookmark];
297 - (void)_loadMetaContactsFromArray:(NSArray *)array
299 NSEnumerator *enumerator = [array objectEnumerator];
300 NSString *identifier;
302 while ((identifier = [enumerator nextObject])) {
303 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
304 NSNumber *objectID = [NSNumber numberWithInt:[[[identifier componentsSeparatedByString:@"-"] objectAtIndex:1] intValue]];
305 [self metaContactWithObjectID:objectID];
310 //Flattened array of the contact list content
311 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects
313 NSMutableArray *array = [NSMutableArray array];
314 NSEnumerator *enumerator = [listObjects objectEnumerator];;
315 AIListObject *object;
317 //Create temporary strings outside the loop
318 NSString *Group = @"Group";
319 NSString *Type = @"Type";
320 NSString *Expanded = @"Expanded";
322 while ((object = [enumerator nextObject])) {
323 [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
325 [object UID], UID_KEY,
326 [NSNumber numberWithBool:[(AIListGroup *)object isExpanded]], Expanded,
334 //Status and Display updates -------------------------------------------------------------------------------------------
335 #pragma mark Status and Display updates
336 //These delay Contact_ListChanged, ListObject_AttributesChanged, Contact_OrderChanged notificationsDelays,
337 //sorting and redrawing to prevent redundancy when making a large number of changes
338 //Explicit delay. Call endListObjectNotificationsDelay to end
339 - (void)delayListObjectNotifications
341 delayedUpdateRequests++;
342 updatesAreDelayed = YES;
345 //End an explicit delay
346 - (void)endListObjectNotificationsDelay
348 delayedUpdateRequests--;
349 if (delayedUpdateRequests == 0 && !delayedUpdateTimer) {
350 [self _performDelayedUpdates:nil];
354 //Delay all list object notifications until a period of inactivity occurs. This is useful for accounts that do not
355 //know when they have finished connecting but still want to mute events.
356 - (void)delayListObjectNotificationsUntilInactivity
358 if (!delayedUpdateTimer) {
359 updatesAreDelayed = YES;
360 delayedUpdateTimer = [[NSTimer scheduledTimerWithTimeInterval:UPDATE_CLUMP_INTERVAL
362 selector:@selector(_performDelayedUpdates:)
364 repeats:YES] retain];
367 [delayedUpdateTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:UPDATE_CLUMP_INTERVAL]];
371 //Update the status of a list object. This will update any information that is otherwise too expensive to update
372 //automatically, such as their profile.
373 - (void)updateListContactStatus:(AIListContact *)inContact
375 //If we're handed something that can contain other contacts, update the status of the contacts contained within it
376 if ([inContact conformsToProtocol:@protocol(AIContainingObject)]) {
377 NSEnumerator *enumerator = [[(AIListObject<AIContainingObject> *)inContact listContacts] objectEnumerator];
378 AIListContact *contact;
380 while ((contact = [enumerator nextObject])) {
381 [self updateListContactStatus:contact];
385 AIAccount *account = [inContact account];
386 if (![account online]) {
387 account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
388 toContact:inContact];
391 [account updateContactStatus:inContact];
395 //Called after modifying a contact's status
396 // Silent: Silences all events, notifications, sounds, overlays, etc. that would have been associated with this status change
397 - (void)listObjectStatusChanged:(AIListObject *)inObject modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
399 NSSet *modifiedAttributeKeys;
401 //Let all observers know the contact's status has changed before performing any sorting or further notifications
402 modifiedAttributeKeys = [self _informObserversOfObjectStatusChange:inObject withKeys:inModifiedKeys silent:silent];
404 //Resort the contact list
405 if (updatesAreDelayed) {
406 delayedStatusChanges++;
407 [delayedModifiedStatusKeys unionSet:inModifiedKeys];
409 //We can safely skip sorting if we know the modified attributes will invoke a resort later
410 if (![[self activeSortController] shouldSortForModifiedAttributeKeys:modifiedAttributeKeys] &&
411 [[self activeSortController] shouldSortForModifiedStatusKeys:inModifiedKeys]) {
412 [self sortListObject:inObject];
416 //Post an attributes changed message (if necessary)
417 if ([modifiedAttributeKeys count]) {
418 [self listObjectAttributesChanged:inObject modifiedKeys:modifiedAttributeKeys];
422 //Call after modifying an object's display attributes
423 //(When modifying display attributes in response to a status change, this is not necessary)
424 - (void)listObjectAttributesChanged:(AIListObject *)inObject modifiedKeys:(NSSet *)inModifiedKeys
426 if (updatesAreDelayed) {
427 delayedAttributeChanges++;
428 [delayedModifiedAttributeKeys unionSet:inModifiedKeys];
430 //Resort the contact list if necessary
431 if ([[self activeSortController] shouldSortForModifiedAttributeKeys:inModifiedKeys]) {
432 [self sortListObject:inObject];
435 //Post an attributes changed message
436 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
438 userInfo:(inModifiedKeys ?
439 [NSDictionary dictionaryWithObject:inModifiedKeys
445 //Performs any delayed list object/handle updates
446 - (void)_performDelayedUpdates:(NSTimer *)timer
448 BOOL updatesOccured = (delayedStatusChanges || delayedAttributeChanges || delayedContactChanges);
450 //Send out global attribute & status changed notifications (to cover any delayed updates)
451 if (updatesOccured) {
452 BOOL shouldSort = NO;
454 //Inform observers of any changes
455 if (delayedContactChanges) {
456 delayedContactChanges = 0;
459 if (delayedStatusChanges) {
461 [[self activeSortController] shouldSortForModifiedStatusKeys:delayedModifiedStatusKeys]) {
464 [delayedModifiedStatusKeys removeAllObjects];
465 delayedStatusChanges = 0;
467 if (delayedAttributeChanges) {
469 [[self activeSortController] shouldSortForModifiedAttributeKeys:delayedModifiedAttributeKeys]) {
472 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
474 userInfo:(delayedModifiedAttributeKeys ?
475 [NSDictionary dictionaryWithObject:delayedModifiedAttributeKeys
478 [delayedModifiedAttributeKeys removeAllObjects];
479 delayedAttributeChanges = 0;
482 //Sort only if necessary
484 [self sortContactList];
488 //If no more updates are left to process, disable the update timer
489 //If there are no delayed update requests, remove the hold
490 if (!delayedUpdateTimer || !updatesOccured) {
491 if (delayedUpdateTimer) {
492 [delayedUpdateTimer invalidate];
493 [delayedUpdateTimer release];
494 delayedUpdateTimer = nil;
496 if (delayedUpdateRequests == 0) {
497 updatesAreDelayed = NO;
502 #pragma mark Contact Grouping
503 //Contact Grouping -----------------------------------------------------------------------------------------------------
505 //Redetermine the local grouping of a contact in response to server grouping information or an external change
506 - (void)listObjectRemoteGroupingChanged:(AIListContact *)inContact
508 AIListObject<AIContainingObject> *containingObject;
509 NSString *remoteGroupName = [inContact remoteGroupName];
512 containingObject = [inContact containingObject];
514 if ([containingObject isKindOfClass:[AIMetaContact class]]) {
516 /* If inContact's containingObject is a metaContact, and that metaContact has no containingObject,
517 * use inContact's remote grouping as the metaContact's grouping.
519 if (![containingObject containingObject] && [remoteGroupName length]) {
520 //If no similar objects exist, we add this contact directly to the list
521 //Create a group for the contact even if contact list groups aren't on,
522 //otherwise requests for all the contact list groups will return nothing
523 AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
525 localGroup = (useContactListGroups ?
526 ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
529 [localGroup addObject:containingObject];
531 [self _listChangedGroup:localGroup object:containingObject];
532 [[NSNotificationCenter defaultCenter] postNotificationName:@"Contact_ListChanged"
533 object:[localGroup containingObject]
535 //NSLog(@"listObjectRemoteGroupingChanged: %@ is in %@, which was moved to %@",inContact,containingObject,localGroup);
539 //If we have a remoteGroupName, add the contact locally to the list
540 if (remoteGroupName) {
541 //Create a group for the contact even if contact list groups aren't on,
542 //otherwise requests for all the contact list groups will return nothing
543 AIListGroup *localGroup, *contactGroup = [self groupWithUID:remoteGroupName];
545 localGroup = (useContactListGroups ?
546 ((useOfflineGroup && ![inContact online]) ? [self offlineGroup] : contactGroup) :
549 //NSLog(@"listObjectRemoteGroupingChanged: %@: remoteGroupName %@ --> %@",inContact,remoteGroupName,localGroup);
551 [self _moveContactLocally:inContact
554 if([[localGroup containingObject] isKindOfClass:[AIListGroup class]])
555 [(AIListGroup *)[localGroup containingObject] visibilityOfContainedObject:localGroup changedTo:YES];
558 //If !remoteGroupName, remove the contact from any local groups
559 if (containingObject) {
561 [(AIListGroup *)containingObject removeObject:inContact];
563 [self _listChangedGroup:(AIListGroup *)containingObject object:inContact];
565 //NSLog(@"listObjectRemoteGroupingChanged: %@: -- !remoteGroupName so removed from %@",inContact,containingObject);
570 BOOL isCurrentlyAStranger = [inContact isStranger];
571 if ((isCurrentlyAStranger && (remoteGroupName != nil)) ||
572 (!isCurrentlyAStranger && (remoteGroupName == nil))) {
573 [inContact setStatusObject:(remoteGroupName ? [NSNumber numberWithBool:YES] : nil)
574 forKey:@"NotAStranger"
576 [inContact notifyOfChangedStatusSilently:YES];
582 - (void)_moveContactLocally:(AIListContact *)listContact toGroup:(AIListGroup *)localGroup
584 AIListObject *containingObject;
585 AIListObject *existingObject;
586 BOOL performedGrouping = NO;
588 //Protect with a retain while we are removing and adding the contact to our arrays
589 [listContact retain];
592 // AILog(@"Moving %@ to %@",listContact,localGroup);
594 //Remove this object from any local groups we have it in currently
595 if ((containingObject = [listContact containingObject]) &&
596 ([containingObject isKindOfClass:[AIListGroup class]])) {
598 [(AIListGroup *)containingObject removeObject:listContact];
599 [self _listChangedGroup:(AIListGroup *)containingObject object:listContact];
602 if ([listContact canJoinMetaContacts]) {
603 if ((existingObject = [localGroup objectWithService:[listContact service] UID:[listContact UID]])) {
604 //If an object exists in this group with the same UID and serviceID, create a MetaContact
606 [self groupListContacts:[NSArray arrayWithObjects:listContact,existingObject,nil]];
607 performedGrouping = YES;
610 AIMetaContact *metaContact;
612 //If no object exists in this group which matches, we should check if there is already
613 //a MetaContact holding a matching ListContact, since we should include this contact in it
614 //If we found a metaContact to which we should add, do it.
615 if ((metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]])) {
617 // AILog(@"Found an existing metacontact; adding %@ to %@",listContact,metaContact);
619 [self addListObject:listContact toMetaContact:metaContact];
620 performedGrouping = YES;
625 if (!performedGrouping) {
626 //If no similar objects exist, we add this contact directly to the list
627 [localGroup addObject:listContact];
630 [self _listChangedGroup:localGroup object:listContact];
634 [listContact release];
637 - (AIListGroup *)remoteGroupForContact:(AIListContact *)inContact
641 if ([inContact isKindOfClass:[AIMetaContact class]]) {
642 //For a metaContact, the closest we have to a remote group is the group it is within locally
643 group = [(AIMetaContact *)inContact parentGroup];
646 NSString *remoteGroup = [inContact remoteGroupName];
647 group = (remoteGroup ? [self groupWithUID:remoteGroup] : nil);
653 //Post a list grouping changed notification for the object and group
654 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object
656 if (updatesAreDelayed) {
657 delayedContactChanges++;
659 [[adium notificationCenter] postNotificationName:Contact_ListChanged
661 userInfo:(group ? [NSDictionary dictionaryWithObject:group forKey:@"ContainingGroup"] : nil)];
665 - (BOOL)useContactListGroups
667 return useContactListGroups;
670 - (void)setUseContactListGroups:(BOOL)inFlag
672 if (inFlag != useContactListGroups) {
673 useContactListGroups = inFlag;
675 [self _performChangeOfUseContactListGroups];
679 - (void)_performChangeOfUseContactListGroups
681 NSEnumerator *enumerator;
682 AIListObject *listObject;
684 [self delayListObjectNotifications];
686 //Store the preference
687 [[adium preferenceController] setPreference:[NSNumber numberWithBool:!useContactListGroups]
688 forKey:KEY_HIDE_CONTACT_LIST_GROUPS
689 group:PREF_GROUP_CONTACT_LIST_DISPLAY];
691 //Configure the sort controller to force ignoring of groups as appropriate
692 [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
694 enumerator = [[[[contactList containedObjects] copy] autorelease] objectEnumerator];
696 if (useContactListGroups) { /* We are now using contact list groups, but we weren't before. */
698 //Restore the grouping of all root-level contacts
699 while ((listObject = [enumerator nextObject])) {
700 if ([listObject isKindOfClass:[AIListContact class]]) {
701 [(AIListContact *)listObject restoreGrouping];
705 } else { /* We are no longer using contact list groups, but we were before. */
707 while ((listObject = [enumerator nextObject])) {
708 if ([listObject isKindOfClass:[AIListGroup class]]) {
709 NSArray *containedObjects;
710 NSEnumerator *groupEnumerator;
711 AIListObject *containedListObject;
713 containedObjects = [[(AIListGroup *)listObject containedObjects] copy];
714 groupEnumerator = [containedObjects objectEnumerator];
715 while ((containedListObject = [groupEnumerator nextObject])) {
716 if ([containedListObject isKindOfClass:[AIListContact class]]) {
717 [self _moveContactLocally:(AIListContact *)containedListObject
718 toGroup:contactList];
721 [containedObjects release];
726 //Stop delaying object notifications; this will automatically resort the contact list, so we're done.
727 [self endListObjectNotificationsDelay];
730 - (void)prepareShowHideGroups
732 //Load the preference
733 useContactListGroups = ![[[adium preferenceController] preferenceForKey:KEY_HIDE_CONTACT_LIST_GROUPS
734 group:PREF_GROUP_CONTACT_LIST_DISPLAY] boolValue];
736 //Show offline contacts menu item
737 menuItem_showGroups = [[NSMenuItem alloc] initWithTitle:SHOW_GROUPS_MENU_TITLE
739 action:@selector(toggleShowGroups:)
741 [menuItem_showGroups setState:useContactListGroups];
742 [[adium menuController] addMenuItem:menuItem_showGroups toLocation:LOC_View_Toggles];
745 NSToolbarItem *toolbarItem;
746 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SHOW_GROUPS_IDENTIFER
747 label:AILocalizedString(@"Show Groups",nil)
748 paletteLabel:AILocalizedString(@"Toggle Groups Display",nil)
749 toolTip:AILocalizedString(@"Toggle display of groups",nil)
751 settingSelector:@selector(setImage:)
752 itemContent:[NSImage imageNamed:(useContactListGroups ?
753 @"togglegroups_transparent" :
755 forClass:[self class]
757 action:@selector(toggleShowGroupsToolbar:)
759 [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ContactList"];
762 - (IBAction)toggleShowGroups:(id)sender
765 useContactListGroups = !useContactListGroups;
766 [menuItem_showGroups setState:useContactListGroups];
768 //Update the contact list. Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure.
769 [self performSelector:@selector(_performChangeOfUseContactListGroups)
771 afterDelay:0.000001];
774 - (IBAction)toggleShowGroupsToolbar:(id)sender
776 [self toggleShowGroups:sender];
778 [sender setImage:[NSImage imageNamed:(useContactListGroups ?
779 @"togglegroups_transparent" :
781 forClass:[self class]]];
784 - (BOOL)useOfflineGroup
786 return useOfflineGroup;
789 - (void)setUseOfflineGroup:(BOOL)inFlag
791 if (inFlag != useOfflineGroup) {
792 useOfflineGroup = inFlag;
794 if (useOfflineGroup) {
795 [self registerListObjectObserver:self];
797 [self updateAllListObjectsForObserver:self];
798 [self unregisterListObjectObserver:self];
803 - (AIListGroup *)offlineGroup
805 return [self groupWithUID:AILocalizedString(@"Offline", "Name of offline group")];
808 #pragma mark Meta Contacts
809 //Meta Contacts --------------------------------------------------------------------------------------------------------
811 * @brief Create or load a metaContact
813 * @param inObjectID The objectID of an existing but unloaded metaContact, or nil to create and save a new metaContact
815 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID
817 NSString *metaContactDictKey;
818 AIMetaContact *metaContact;
819 BOOL shouldRestoreContacts = YES;
821 //If no object ID is provided, use the next available object ID
822 //(MetaContacts should always have an individually unique object id)
824 int topID = [[[adium preferenceController] preferenceForKey:TOP_METACONTACT_ID
825 group:PREF_GROUP_CONTACT_LIST] intValue];
826 inObjectID = [NSNumber numberWithInt:topID];
827 [[adium preferenceController] setPreference:[NSNumber numberWithInt:([inObjectID intValue] + 1)]
828 forKey:TOP_METACONTACT_ID
829 group:PREF_GROUP_CONTACT_LIST];
831 //No reason to waste time restoring contacts when none are in the meta contact yet.
832 shouldRestoreContacts = NO;
835 //Look for a metacontact with this object ID. If none is found, create one
836 //and add its contained contacts to it.
837 metaContactDictKey = [AIMetaContact internalObjectIDFromObjectID:inObjectID];
839 metaContact = [metaContactDict objectForKey:metaContactDictKey];
841 metaContact = [[AIMetaContact alloc] initWithObjectID:inObjectID];
843 //Keep track of it in our metaContactDict for retrieval by objectID
844 [metaContactDict setObject:metaContact forKey:metaContactDictKey];
846 //Add it to our more general contactDict, as well
847 [contactDict setObject:metaContact forKey:[metaContact internalUniqueObjectID]];
849 /* We restore contacts (actually, internalIDs for contacts, to be added as necessary later) if the metaContact
850 * existed before this call to metaContactWithObjectID:
852 if (shouldRestoreContacts) {
853 if (![self _restoreContactsToMetaContact:metaContact]) {
855 //If restoring the metacontact did not actually add any contacts, delete it since it is invalid
856 [self breakdownAndRemoveMetaContact:metaContact];
861 /* As with contactWithService:account:UID, update all attributes so observers are initially informed of
862 * this object's existence.
864 [self _updateAllAttributesOfObject:metaContact];
866 [metaContact release];
869 return (metaContact);
873 * @brief Associate the appropriate internal IDs for contained contacts with a metaContact
875 * @result YES if one or more contacts was associated with the metaContact; NO if none were.
877 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact
879 NSDictionary *allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
880 group:PREF_GROUP_CONTACT_LIST];
881 NSArray *containedContactsArray = [allMetaContactsDict objectForKey:[metaContact internalObjectID]];
882 BOOL restoredContacts;
884 if ([containedContactsArray count]) {
885 [self _restoreContactsToMetaContact:metaContact
886 fromContainedContactsArray:containedContactsArray];
888 restoredContacts = YES;
891 restoredContacts = NO;
894 return restoredContacts;
898 * @brief Associate the internal IDs for an array of contacts with a specific metaContact
900 * This does not actually place any AIListContacts within the metaContact. Instead, it updates the contactToMetaContactLookupDict
901 * dictionary to have metaContact associated with the list contacts specified by containedContactsArray. This
902 * allows us to add them lazily to the metaContact (in contactWithService:account:UID:) as necessary.
904 * @param metaContact The metaContact to which contact referneces are added
905 * @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
907 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray
909 NSEnumerator *enumerator = [containedContactsArray objectEnumerator];
910 NSDictionary *containedContactDict;
912 while ((containedContactDict = [enumerator nextObject])) {
913 /* Before Adium 0.80, metaContacts could be created within metaContacts. Simply ignore any attempt to restore
914 * such erroneous data, which will have a YES boolValue for KEY_IS_METACONTACT. */
915 if (![[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) {
916 /* Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject.
918 * As listObjects are loaded/created/requested which match this internalObjectID,
919 * they will be inserted into the metaContact.
921 NSString *internalObjectID = [AIListObject internalObjectIDForServiceID:[containedContactDict objectForKey:SERVICE_ID_KEY]
922 UID:[containedContactDict objectForKey:UID_KEY]];
923 [contactToMetaContactLookupDict setObject:metaContact
924 forKey:internalObjectID];
930 //Add a list object to a meta contact, setting preferences and such
931 //so the association is lasting across program launches.
932 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
934 if (!listObject || [listObject isKindOfClass:[AIListGroup class]]) {
935 //I can't think of why one would want to add an entire group to a metacontact. Let's say you can't.
936 NSLog(@"Warning: addListObject:toMetaContact: Attempted to add %@ to %@",listObject,metaContact);
940 if (listObject == metaContact) return;
942 //If listObject contains other contacts, perform addListObject:toMetaContact: recursively
943 if ([listObject conformsToProtocol:@protocol(AIContainingObject)]) {
944 NSEnumerator *enumerator = [[[[(AIListObject<AIContainingObject> *)listObject containedObjects] copy] autorelease] objectEnumerator];
945 AIListObject *someObject;
947 while ((someObject = [enumerator nextObject]))
948 [self addListObject:someObject toMetaContact:metaContact];
951 //Obtain any metaContact this listObject is currently within, so we can remove it later
952 AIMetaContact *oldMetaContact = [contactToMetaContactLookupDict objectForKey:[listObject internalObjectID]];
954 if ([self _performAddListObject:listObject toMetaContact:metaContact]) {
955 //If this listObject was not in this metaContact in any form before, store the change
956 if (metaContact != oldMetaContact) {
957 //Remove the list object from any other metaContact it is in at present
959 [self removeListObject:listObject fromMetaContact:oldMetaContact];
961 [self _storeListObject:listObject inMetaContact:metaContact];
967 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact
969 //we only allow group->meta->contact, not group->meta->meta->contact
970 NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
972 // AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact);
973 NSDictionary *containedContactDict;
974 NSMutableDictionary *allMetaContactsDict;
975 NSMutableArray *containedContactsArray;
977 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
979 //Get the dictionary of all metaContacts
980 allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
981 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
982 if (!allMetaContactsDict) {
983 allMetaContactsDict = [[NSMutableDictionary alloc] init];
986 //Load the array for the new metaContact
987 containedContactsArray = [[allMetaContactsDict objectForKey:metaContactInternalObjectID] mutableCopy];
988 if (!containedContactsArray) containedContactsArray = [[NSMutableArray alloc] init];
989 containedContactDict = nil;
991 //Create the dictionary describing this list object
992 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
993 [[listObject service] serviceID],SERVICE_ID_KEY,
994 [listObject UID],UID_KEY,nil];
996 //Only add if this dict isn't already in the array
997 if (containedContactDict && ([containedContactsArray indexOfObject:containedContactDict] == NSNotFound)) {
998 [containedContactsArray addObject:containedContactDict];
999 [allMetaContactsDict setObject:containedContactsArray forKey:metaContactInternalObjectID];
1002 [self _saveMetaContacts:allMetaContactsDict];
1004 [[adium contactAlertsController] mergeAndMoveContactAlertsFromListObject:listObject
1005 intoListObject:metaContact];
1008 [allMetaContactsDict release];
1009 [containedContactsArray release];
1012 //Actually adds a list object to a meta contact. No preferences are changed.
1013 //Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict
1014 //for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful.
1015 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
1017 //we only allow group->meta->contact, not group->meta->meta->contact
1018 //FIXME: deal with smack contacts correctly.
1019 //NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
1020 AIListObject<AIContainingObject> *localGroup;
1023 localGroup = [listObject containingObject];
1025 //Remove the object from its previous containing group
1026 if (localGroup && (localGroup != metaContact)) {
1027 [localGroup removeObject:listObject];
1028 [self _listChangedGroup:localGroup object:listObject];
1031 //AIMetaContact will handle reassigning the list object's grouping to being itself
1032 if ((success = [metaContact addObject:listObject])) {
1033 [contactToMetaContactLookupDict setObject:metaContact forKey:[listObject internalObjectID]];
1035 [self _listChangedGroup:metaContact object:listObject];
1036 //If the metaContact isn't in a group yet, use the group of the object we just added
1037 if ((![metaContact containingObject]) && localGroup) {
1038 //Add the new meta contact to our list
1039 [(AIMetaContact *)localGroup addObject:metaContact];
1040 [self _listChangedGroup:localGroup object:metaContact];
1047 - (void)removeAllListObjectsMatching:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1049 NSEnumerator *enumerator;
1050 AIListObject *theObject;
1052 enumerator = [[self allContactsWithService:[listObject service]
1053 UID:[listObject UID]
1054 existingOnly:YES] objectEnumerator];
1056 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1057 [contactToMetaContactLookupDict removeObjectForKey:[listObject internalObjectID]];
1059 [self delayListObjectNotifications];
1060 while ((theObject = [enumerator nextObject])) {
1061 [self removeListObject:theObject fromMetaContact:metaContact];
1063 [self endListObjectNotificationsDelay];
1066 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1068 //we only allow group->meta->contact, not group->meta->meta->contact
1069 NSParameterAssert(![listObject conformsToProtocol:@protocol(AIContainingObject)]);
1071 NSEnumerator *enumerator;
1072 NSArray *containedContactsArray;
1073 NSDictionary *containedContactDict = nil;
1074 NSMutableDictionary *allMetaContactsDict;
1075 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
1077 //Get the dictionary of all metaContacts
1078 allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1079 group:PREF_GROUP_CONTACT_LIST];
1081 //Load the array for the metaContact
1082 containedContactsArray = [allMetaContactsDict objectForKey:metaContactInternalObjectID];
1084 //Enumerate it, looking only for the appropriate type of containedContactDict
1085 enumerator = [containedContactsArray objectEnumerator];
1087 NSString *listObjectUID = [listObject UID];
1088 NSString *listObjectServiceID = [[listObject service] serviceID];
1090 while ((containedContactDict = [enumerator nextObject])) {
1091 if ([[containedContactDict objectForKey:UID_KEY] isEqualToString:listObjectUID] &&
1092 [[containedContactDict objectForKey:SERVICE_ID_KEY] isEqualToString:listObjectServiceID]) {
1097 //If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result
1098 if (containedContactDict) {
1099 NSMutableArray *newContainedContactsArray;
1100 NSMutableDictionary *newAllMetaContactsDict;
1102 newContainedContactsArray = [containedContactsArray mutableCopy];
1103 [newContainedContactsArray removeObjectIdenticalTo:containedContactDict];
1105 newAllMetaContactsDict = [allMetaContactsDict mutableCopy];
1106 [newAllMetaContactsDict setObject:newContainedContactsArray
1107 forKey:metaContactInternalObjectID];
1109 [self _saveMetaContacts:newAllMetaContactsDict];
1111 [newContainedContactsArray release];
1112 [newAllMetaContactsDict release];
1115 //The listObject can be within the metaContact without us finding a containedContactDict if we are removing multiple
1116 //listContacts referring to the same UID & serviceID combination - that is, on multiple accounts on the same service.
1117 //We therefore request removal of the object regardless of the if (containedContactDict) check above.
1118 [metaContact removeObject:listObject];
1123 * @brief Groups UIDs for services into a single metacontact
1125 * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to
1126 * a UID and a service, respectively, which together define a contact which should be included in the grouping.
1128 * Assumption: This is only called after the contact list is finished loading, which occurs via
1129 * -(void)controllerDidLoad above.
1131 * @param UIDsArray NSArray of UIDs
1132 * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
1134 - (AIMetaContact *)groupUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray
1136 NSMutableSet *internalObjectIDs = [[NSMutableSet alloc] init];
1137 AIMetaContact *metaContact = nil;
1138 NSEnumerator *enumerator;
1139 NSString *internalObjectID;
1140 int count = [UIDsArray count];
1143 //Build an array of all contacts matching this description (multiple accounts on the same service listing
1144 //the same UID mean that we can have multiple AIListContact objects with a UID/service combination)
1145 for (i = 0; i < count; i++) {
1146 NSString *serviceID = [servicesArray objectAtIndex:i];
1147 NSString *UID = [UIDsArray objectAtIndex:i];
1149 internalObjectID = [AIListObject internalObjectIDForServiceID:serviceID
1152 metaContact = [contactToMetaContactLookupDict objectForKey:internalObjectID];
1155 [internalObjectIDs addObject:internalObjectID];
1158 //Create a new metaContact is we didn't find one.
1160 metaContact = [self metaContactWithObjectID:nil];
1163 enumerator = [internalObjectIDs objectEnumerator];
1164 while ((internalObjectID = [enumerator nextObject])) {
1165 AIListObject *existingObject;
1166 if ((existingObject = [self existingListObjectWithUniqueID:internalObjectID])) {
1167 /* If there is currently an object (or multiple objects) matching this internalObjectID
1168 * we should add immediately.
1170 [self addListObject:existingObject
1171 toMetaContact:metaContact];
1173 /* If no objects matching this internalObjectID exist, we can simply add to the
1174 * contactToMetaContactLookupDict for use if such an object is created later.
1176 [contactToMetaContactLookupDict setObject:metaContact
1177 forKey:internalObjectID];
1180 [internalObjectIDs release];
1185 /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added.
1187 * This will reuse an existing metacontact (for one of the contacts in the array) if possible.
1188 * @param contactsToGroupArray Contacts to group together
1190 - (AIMetaContact *)groupListContacts:(NSArray *)contactsToGroupArray
1192 NSEnumerator *enumerator;
1193 AIListContact *listContact;
1194 AIMetaContact *metaContact = nil;
1196 //Look for an existing MetaContact we can use. The first one we find is the lucky winner.
1197 enumerator = [contactsToGroupArray objectEnumerator];
1198 while ((listContact = [enumerator nextObject]) && (metaContact == nil)) {
1199 if ([listContact isKindOfClass:[AIMetaContact class]]) {
1200 metaContact = (AIMetaContact *)listContact;
1202 metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]];
1206 //Create a new metaContact is we didn't find one.
1208 metaContact = [self metaContactWithObjectID:nil];
1211 /* Add all these contacts to our MetaContact.
1212 * Some may already be present, but that's fine, as nothing will happen.
1214 enumerator = [contactsToGroupArray objectEnumerator];
1215 while ((listContact = [enumerator nextObject])) {
1216 [self addListObject:listContact toMetaContact:metaContact];
1222 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact
1224 //Remove the objects within it from being inside it
1225 NSArray *containedObjects = [[metaContact containedObjects] copy];
1226 NSEnumerator *metaEnumerator = [containedObjects objectEnumerator];
1227 AIListObject<AIContainingObject> *containingObject = [metaContact containingObject];
1228 AIListObject *object;
1230 NSMutableDictionary *allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1231 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
1233 while ((object = [metaEnumerator nextObject])) {
1235 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1236 [contactToMetaContactLookupDict removeObjectForKey:[object internalObjectID]];
1238 [self removeListObject:object fromMetaContact:metaContact];
1241 //Then, procede to remove the metaContact
1244 [metaContact retain];
1246 //Remove it from its containing group
1247 [containingObject removeObject:metaContact];
1249 NSString *metaContactInternalObjectID = [metaContact internalObjectID];
1251 //Remove our reference to it internally
1252 [metaContactDict removeObjectForKey:metaContactInternalObjectID];
1254 //Remove it from the preferences dictionary
1255 [allMetaContactsDict removeObjectForKey:metaContactInternalObjectID];
1257 //XXX - contactToMetaContactLookupDict
1259 //Post the list changed notification for the old containingObject
1260 [self _listChangedGroup:containingObject object:metaContact];
1262 //Save the updated allMetaContactsDict which no longer lists the metaContact
1263 [self _saveMetaContacts:allMetaContactsDict];
1265 //Protection is overrated.
1266 [metaContact release];
1267 [containedObjects release];
1268 [allMetaContactsDict release];
1271 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict
1273 AILog(@"MetaContacts: Saving!");
1274 [[adium preferenceController] setPreference:allMetaContactsDict
1275 forKey:KEY_METACONTACT_OWNERSHIP
1276 group:PREF_GROUP_CONTACT_LIST];
1277 [[adium preferenceController] setPreference:[allMetaContactsDict allKeys]
1278 forKey:KEY_FLAT_METACONTACTS
1279 group:PREF_GROUP_CONTACT_LIST];
1282 //Sort list objects alphabetically by their display name
1283 int contactDisplayNameSort(AIListObject *objectA, AIListObject *objectB, void *context)
1285 return [[objectA displayName] caseInsensitiveCompare:[objectB displayName]];
1288 //Return either the highest metaContact containing this list object, or the list object itself. Appropriate for when
1289 //preferences should be read from/to the most generalized contact possible.
1290 - (AIListObject *)parentContactForListObject:(AIListObject *)listObject
1292 if ([listObject isKindOfClass:[AIListContact class]]) {
1293 //Find the highest-up metaContact
1294 AIListObject *containingObject;
1295 while ([(containingObject = [listObject containingObject]) isKindOfClass:[AIMetaContact class]]) {
1296 listObject = (AIMetaContact *)containingObject;
1303 #pragma mark Preference observing
1305 * @brief Preferences changed
1307 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
1308 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
1310 [self setUseOfflineGroup:((![[prefDict objectForKey:KEY_HIDE_CONTACTS] boolValue] ||
1311 [[prefDict objectForKey:KEY_SHOW_OFFLINE_CONTACTS] boolValue]) &&
1312 [[prefDict objectForKey:KEY_USE_OFFLINE_GROUP] boolValue])];
1316 * @brief Move contacts to and from the offline group as necessary as their online state changes.
1318 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1320 if (!inModifiedKeys ||
1321 [inModifiedKeys containsObject:@"Online"]) {
1323 if ([inObject isKindOfClass:[AIListContact class]]) {
1324 //If this contact is not its own parent contact, don't bother since we'll get an update for the parent if appropriate
1325 if (inObject == [(AIListContact *)inObject parentContact]) {
1326 if (useOfflineGroup) {
1327 AIListObject *containingObject = [inObject containingObject];
1329 if ([inObject online] &&
1330 (containingObject == [self offlineGroup])) {
1331 [(AIListContact *)inObject restoreGrouping];
1333 } else if (![inObject online] &&
1335 (containingObject != [self offlineGroup])) {
1336 [self _moveContactLocally:(AIListContact *)inObject
1337 toGroup:[self offlineGroup]];
1341 if ([inObject containingObject] == [self offlineGroup]) {
1342 [(AIListContact *)inObject restoreGrouping];
1352 //Contact Sorting --------------------------------------------------------------------------------
1353 #pragma mark Contact Sorting
1354 //Register sorting code
1355 - (void)registerListSortController:(AISortController *)inController
1357 [sortControllerArray addObject:inController];
1359 - (NSArray *)sortControllerArray
1361 return sortControllerArray;
1364 //Set and get the active sort controller
1365 - (void)setActiveSortController:(AISortController *)inController
1367 activeSortController = inController;
1369 [activeSortController didBecomeActive];
1371 //The newly-active sort controller needs to know whether it should be forced to ignore groups
1372 [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
1375 [self sortContactList];
1377 - (AISortController *)activeSortController
1379 return activeSortController;
1382 //Sort the entire contact list
1383 - (void)sortContactList
1386 NSEnumerator *i = [detachedContactLists objectEnumerator];
1388 while((list = [i nextObject])){
1389 [list sortGroupAndSubGroups:YES sortController:activeSortController];
1392 // Main contact list
1393 [contactList sortGroupAndSubGroups:YES sortController:activeSortController];
1394 [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:nil];
1396 - (void)sortAContactList:(AIListGroup *)group
1398 [group sortGroupAndSubGroups:YES sortController:activeSortController];
1399 [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:nil];
1402 //Sort an individual object
1403 - (void)sortListObject:(AIListObject *)inObject
1405 if (updatesAreDelayed) {
1406 delayedContactChanges++;
1408 AIListObject *group = [inObject containingObject];
1410 if ([group isKindOfClass:[AIListGroup class]]) {
1411 //Sort the groups containing this object
1412 [(AIListGroup *)group sortListObject:inObject sortController:activeSortController];
1413 [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:inObject];
1418 //List object observers ------------------------------------------------------------------------------------------------
1419 #pragma mark List object observers
1420 //Registers code to observe handle status changes
1421 - (void)registerListObjectObserver:(id <AIListObjectObserver>)inObserver
1424 [contactObservers addObject:[NSValue valueWithNonretainedObject:inObserver]];
1426 //Let the new observer process all existing objects
1427 [self updateAllListObjectsForObserver:inObserver];
1430 - (void)unregisterListObjectObserver:(id)inObserver
1432 [contactObservers removeObject:[NSValue valueWithNonretainedObject:inObserver]];
1437 * @brief Update all contacts for an observer, notifying the observer of each one in turn
1439 * @param contacts The contacts to update, or nil to update all contacts
1440 * @param inObserver The observer
1442 - (void)updateContacts:(NSSet *)contacts forObserver:(id <AIListObjectObserver>)inObserver
1444 NSEnumerator *enumerator;
1445 AIListObject *listObject;
1447 [self delayListObjectNotifications];
1449 enumerator = (contacts ? [contacts objectEnumerator] : [contactDict objectEnumerator]);
1450 while ((listObject = [enumerator nextObject])) {
1451 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1452 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1453 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1455 //If this contact is within a meta contact, update the meta contact too
1456 AIListObject<AIContainingObject> *containingObject = [listObject containingObject];
1457 if (containingObject && [containingObject isKindOfClass:[AIMetaContact class]]) {
1458 NSSet *attributes = [inObserver updateListObject:containingObject
1461 if (attributes) [self listObjectAttributesChanged:containingObject
1462 modifiedKeys:attributes];
1467 [self endListObjectNotificationsDelay];
1470 //Instructs a controller to update all available list objects
1471 - (void)updateAllListObjectsForObserver:(id <AIListObjectObserver>)inObserver
1473 NSEnumerator *enumerator;
1474 AIListObject *listObject;
1476 [self delayListObjectNotifications];
1479 [self updateContacts:nil forObserver:inObserver];
1482 enumerator = [groupDict objectEnumerator];
1483 while ((listObject = [enumerator nextObject])) {
1484 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1485 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1488 //Reset all accounts
1489 enumerator = [[[adium accountController] accounts] objectEnumerator];
1490 while ((listObject = [enumerator nextObject])) {
1491 NSSet *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1492 if (attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1495 [self endListObjectNotificationsDelay];
1499 //Notify observers of a status change. Returns the modified attribute keys
1500 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
1502 NSMutableSet *attrChange = nil;
1503 NSEnumerator *enumerator;
1504 NSValue *observerValue;
1506 //Let our observers know
1507 enumerator = [contactObservers objectEnumerator];
1508 while ((observerValue = [enumerator nextObject])) {
1509 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1510 id <AIListObjectObserver> observer;
1513 observer = [observerValue nonretainedObjectValue];
1514 if ((newKeys = [observer updateListObject:inObject keys:modifiedKeys silent:silent])) {
1515 if (!attrChange) attrChange = [[NSMutableSet alloc] init];
1516 [attrChange unionSet:newKeys];
1521 //Send out the notification for other observers
1522 [[adium notificationCenter] postNotificationName:ListObject_StatusChanged
1524 userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys
1525 forKey:@"Keys"] : nil)];
1527 return [attrChange autorelease];
1530 //Command all observers to apply their attributes to an object
1531 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject
1533 NSEnumerator *enumerator = [contactObservers objectEnumerator];
1534 NSValue *observerValue;
1536 while ((observerValue = [enumerator nextObject])) {
1537 id <AIListObjectObserver> observer = [observerValue nonretainedObjectValue];
1539 [observer updateListObject:inObject keys:nil silent:YES];
1545 //Contact List Access --------------------------------------------------------------------------------------------------
1546 #pragma mark Contact List Access
1547 //Returns the main contact list group
1548 - (AIListGroup *)contactList
1554 * @brief Return an array of all contact list groups, detached or not
1556 - (NSMutableArray *)allGroups
1558 NSEnumerator *enumerator = [detachedContactLists objectEnumerator];
1559 NSMutableArray *groups = [NSMutableArray array];
1560 AIListGroup *detachedGroup;
1562 // Main contact list
1563 [groups addObjectsFromArray:[contactList containedObjects]];
1566 while ((detachedGroup = [enumerator nextObject])) {
1567 [groups addObjectsFromArray:[detachedGroup containedObjects]];
1573 //Returns a flat array of all contacts (by calling through to -allContactsInObject:recurse:onAccount:)
1574 - (NSMutableArray *)allContacts
1576 return [self allContactsOnAccount:nil];
1579 //Returns a flat array of all contacts on a given account (by calling through to -allContactsInObject:recurse:onAccount:)
1580 - (NSMutableArray *)allContactsOnAccount:(AIAccount *)inAccount
1582 NSMutableArray *result = [self allContactsInObject:contactList recurse:YES onAccount:inAccount];
1584 /** Could be perfected I'm sure */
1585 NSEnumerator *enumerator = [detachedContactLists objectEnumerator];
1586 AIListGroup *detached;
1587 while((detached = [enumerator nextObject])){
1588 [result addObjectsFromArray:[self allContactsInObject:detached recurse:YES onAccount:inAccount]];
1594 //Return a flat array of all the objects in a group on an account (and all subgroups, if desired)
1595 - (NSMutableArray *)allContactsInObject:(AIListObject<AIContainingObject> *)inGroup recurse:(BOOL)recurse onAccount:(AIAccount *)inAccount
1597 NSParameterAssert(inGroup != nil);
1599 NSMutableArray *contactArray = [NSMutableArray array];
1601 NSEnumerator *enumerator = [[inGroup containedObjects] objectEnumerator];
1602 AIListObject *object;
1603 while ((object = [enumerator nextObject])) {
1604 if (recurse && [object conformsToProtocol:@protocol(AIContainingObject)]) {
1605 [contactArray addObjectsFromArray:[self allContactsInObject:(AIListObject<AIContainingObject> *)object
1607 onAccount:inAccount]];
1608 } else if ([object isMemberOfClass:[AIListContact class]] && (!inAccount || ([(AIListContact *)object account] == inAccount)))
1609 [contactArray addObject:object];
1612 return contactArray;
1615 //Return a flat array of all the bookmarks in a group on an account (and all subgroups, if desired)
1616 - (NSMutableArray *)allBookmarksInObject:(AIListObject<AIContainingObject> *)inGroup recurse:(BOOL)recurse onAccount:(AIAccount *)inAccount
1618 NSParameterAssert(inGroup != nil);
1620 NSMutableArray *bookmarkArray = [NSMutableArray array];
1622 NSEnumerator *enumerator = [[inGroup containedObjects] objectEnumerator];
1623 AIListObject *object;
1624 while ((object = [enumerator nextObject])) {
1625 if (recurse && [object conformsToProtocol:@protocol(AIContainingObject)]) {
1626 [bookmarkArray addObjectsFromArray:[self allBookmarksInObject:(AIListObject<AIContainingObject> *)object
1628 onAccount:inAccount]];
1629 } else if ([object isMemberOfClass:[AIListBookmark class]] && (!inAccount || ([(AIListBookmark *)object account] == inAccount))) {
1630 [bookmarkArray addObject:object];
1634 return bookmarkArray;
1637 - (NSArray *)allBookmarks
1639 NSMutableArray *result = [self allBookmarksInObject:contactList recurse:YES onAccount:nil];
1641 /** Could be perfected I'm sure */
1642 NSEnumerator *enumerator = [detachedContactLists objectEnumerator];
1643 AIListGroup *detached;
1644 while((detached = [enumerator nextObject])){
1645 [result addObjectsFromArray:[self allBookmarksInObject:detached recurse:YES onAccount:nil]];
1651 //Contact List Menus- --------------------------------------------------------------------------------------------------
1652 #pragma mark Contact List Menus
1654 //Returns a menu containing all the groups within a group
1655 //- Selector called on group selection is selectGroup:
1656 //- The menu items represented object is the group it represents
1657 - (NSMenu *)menuOfAllGroupsInGroup:(AIListGroup *)inGroup withTarget:(id)target
1659 NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
1661 [menu setAutoenablesItems:NO];
1662 [self _menuOfAllGroups:menu forGroup:inGroup withTarget:target level:0];
1664 return [menu autorelease];
1666 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level
1668 NSMutableArray *fromGroups;
1669 NSEnumerator *detachedEnumerator;
1671 //Passing nil scans the entire contact list
1673 fromGroups = [NSMutableArray arrayWithArray:detachedContactLists];
1674 [fromGroups addObject:contactList];
1676 fromGroups = [NSMutableArray arrayWithObject:group];
1679 detachedEnumerator = [fromGroups objectEnumerator];
1681 while((group = [detachedEnumerator nextObject])){
1682 //Enumerate this group and process all groups we find within it
1683 NSEnumerator *enumerator;
1684 AIListObject *object;
1685 enumerator = [[group containedObjects] objectEnumerator];
1686 while ((object = [enumerator nextObject])) {
1687 if ([object isKindOfClass:[AIListGroup class]] && object != [self offlineGroup]) {
1688 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[object displayName]
1690 action:@selector(selectGroup:)
1692 [menuItem setRepresentedObject:object];
1693 if ([menuItem respondsToSelector:@selector(setIndentationLevel:)]) {
1694 [menuItem setIndentationLevel:level];
1696 [menu addItem:menuItem];
1699 [self _menuOfAllGroups:menu forGroup:(AIListGroup *)object withTarget:target level:level+1];
1706 //Returns a menu containing all the objects in a group on an account
1707 //- Selector called on contact selection is selectContact:
1708 //- The menu item's represented object is the contact it represents
1709 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target{
1710 return [self menuOfAllContactsInContainingObject:inObject withTarget:target firstLevel:YES];
1712 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target firstLevel:(BOOL)firstLevel
1714 NSEnumerator *enumerator;
1715 AIListObject *object;
1718 NSMenu *menu = [[NSMenu alloc] init];
1719 [menu setAutoenablesItems:NO];
1721 //Passing nil scans the entire contact list
1722 if (inObject == nil) inObject = contactList;
1724 //The pull down menu needs an extra item at the top of its root menu to handle the selection.
1725 if (firstLevel) [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
1727 //All menu items for all contained objects
1728 enumerator = [[inObject listContacts] objectEnumerator];
1729 while ((object = [enumerator nextObject])) {
1730 NSImage *menuServiceImage;
1731 NSMenuItem *menuItem;
1732 BOOL needToCreateSubmenu;
1733 BOOL isGroup = [object isKindOfClass:[AIListGroup class]];
1734 BOOL isValidGroup = (isGroup &&
1735 [(AIListGroup *)object containedObjectsCount]);
1737 //We don't want to include empty groups
1738 if (!isGroup || isValidGroup) {
1740 needToCreateSubmenu = (isValidGroup ||
1741 ([object isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)object listContacts] count] > 1)));
1744 menuServiceImage = [AIUserIcons menuUserIconForObject:object];
1746 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(needToCreateSubmenu ?
1747 [object displayName] :
1748 [object formattedUID])
1750 action:@selector(selectContact:)
1753 if (needToCreateSubmenu) {
1754 [menuItem setSubmenu:[self menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)object withTarget:target firstLevel:NO]];
1757 [menuItem setRepresentedObject:object];
1758 [menuItem setImage:menuServiceImage];
1759 [menu addItem:menuItem];
1764 return [menu autorelease];
1767 //Retrieving Specific Contacts -----------------------------------------------------------------------------------------
1768 #pragma mark Retrieving Specific Contacts
1770 - (AIListContact *)contactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1772 return [self contactWithService:inService account:inAccount UID:inUID usingClass:[AIListContact class]];
1775 //Retrieve a contact from the contact list (Creating if necessary)
1776 - (AIListContact *)contactWithService:(AIService *)inService
1777 account:(AIAccount *)inAccount
1778 UID:(NSString *)inUID
1779 usingClass:(Class)ContactClass
1781 if (!(inUID && [inUID length] && inService)) return nil; //Ignore invalid requests
1783 AIListContact *contact = nil;
1784 NSString *key = [ContactClass internalUniqueObjectIDForService:inService
1787 contact = [contactDict objectForKey:key];
1790 contact = [[ContactClass alloc] initWithUID:inUID account:inAccount service:inService];
1792 //Do the update thing
1793 [self _updateAllAttributesOfObject:contact];
1795 //Check to see if we should add to a metaContact
1796 AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:[contact internalObjectID]];
1798 /* We already know to add this object to the metaContact, since we did it before with another object,
1799 but this particular listContact is new and needs to be added directly to the metaContact
1800 (on future launches, the metaContact will obtain it automatically since all contacts matching this UID
1801 and serviceID should be included). */
1802 [self _performAddListObject:contact toMetaContact:metaContact];
1805 //Set the contact as mobile if it is a phone number
1806 if ([inUID characterAtIndex:0] == '+') {
1807 [contact setIsMobile:YES notify:NotifyNever];
1811 [contactDict setObject:contact forKey:key];
1818 - (AIListBookmark *)bookmarkForChat:(AIChat *)inChat
1820 AIListBookmark *bookmark = [[AIListBookmark alloc] initWithChat:inChat];
1822 //Do the update thing
1823 [self _updateAllAttributesOfObject:bookmark];
1825 return [bookmark autorelease];
1828 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID usingClass:(Class)ContactClass
1830 if (inService && [inUID length]) {
1831 return [contactDict objectForKey:[ContactClass internalUniqueObjectIDForService:inService
1839 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1841 return [self existingContactWithService:inService account:inAccount UID:inUID usingClass:[AIListContact class]];
1845 * @brief Return a set of all contacts with a specified UID and service
1847 * @param service The AIService in question
1848 * @param inUID The UID, which should be normalized (lower case, no spaces, etc.) as appropriate for the service
1849 * @param existingOnly If YES, only pre-existing contacts. If NO, an AIListContact is guaranteed to be returned
1850 * on each compatible account, even if one did not previously exist.
1852 - (NSSet *)allContactsWithService:(AIService *)service UID:(NSString *)inUID existingOnly:(BOOL)existingOnly
1854 NSEnumerator *enumerator;
1856 NSMutableSet *returnContactSet = [NSMutableSet set];
1858 enumerator = [[[adium accountController] accountsCompatibleWithService:service] objectEnumerator];
1860 while ((account = [enumerator nextObject])) {
1861 AIListContact *listContact;
1864 listContact = [self existingContactWithService:service
1868 listContact = [self contactWithService:service
1874 [returnContactSet addObject:listContact];
1878 return returnContactSet;
1881 - (AIListObject *)existingListObjectWithUniqueID:(NSString *)uniqueID
1883 NSEnumerator *enumerator;
1884 AIListObject *listObject;
1887 enumerator = [contactDict objectEnumerator];
1888 while ((listObject = [enumerator nextObject])) {
1889 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1893 enumerator = [groupDict objectEnumerator];
1894 while ((listObject = [enumerator nextObject])) {
1895 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1899 enumerator = [metaContactDict objectEnumerator];
1900 while ((listObject = [enumerator nextObject])) {
1901 if ([[listObject internalObjectID] isEqualToString:uniqueID]) return listObject;
1907 - (AIListContact *)preferredContactForContentType:(NSString *)inType forListContact:(AIListContact *)inContact
1909 AIListContact *returnContact = nil;
1912 if ([inContact containsMultipleContacts]) {
1913 AIListObject *preferredContact;
1914 NSString *internalObjectID;
1916 /* If we've messaged this object previously, prefer the last contact we sent to if that
1917 * contact is currently in the most-available status the metacontact can offer
1919 internalObjectID = [inContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1920 group:OBJECT_STATUS_CACHE];
1922 if ((internalObjectID) &&
1923 (preferredContact = [self existingListObjectWithUniqueID:internalObjectID]) &&
1924 ([preferredContact isKindOfClass:[AIListContact class]]) &&
1925 ([preferredContact statusSummary] == [inContact statusSummary]) &&
1926 ([[(AIMetaContact *)inContact containedObjects] containsObject:preferredContact])) {
1927 returnContact = [self preferredContactForContentType:inType
1928 forListContact:(AIListContact *)preferredContact];
1931 /* If the last contact we sent to is not appropriate, use the following algorithm, which differs from -[AIMetaContact preferredContact]
1932 * in that it doesn't "like" mobile contacts.
1934 * 1) Prefer available contacts who are not mobile
1935 * 2) If no available non-mobile contacts, use the first online contact
1936 * 3) If no online contacts, use the metacontact's preferredContact
1938 #warning This is a casting mess... define a protocol for metacontact-like AIListContact subclasses
1939 if (!returnContact) {
1940 //Recurse into metacontacts if necessary
1941 AIListContact *firstAvailableContact = nil;
1942 AIListContact *firstNotOfflineContact = nil;
1944 AIListContact *thisContact;
1945 NSEnumerator *contactsEnum = [[(AIMetaContact *)inContact containedObjects] objectEnumerator];
1946 while ((thisContact = [contactsEnum nextObject])) {
1947 AIStatusType statusSummary = [thisContact statusSummary];
1949 if (statusSummary != AIOfflineStatus) {
1950 if (!firstNotOfflineContact) {
1951 firstNotOfflineContact = thisContact;
1954 if (statusSummary == AIAvailableStatus && ![thisContact isMobile]) {
1955 if (!firstAvailableContact) {
1956 firstAvailableContact = thisContact;
1964 returnContact = (firstAvailableContact ?
1965 firstAvailableContact :
1966 (firstNotOfflineContact ? firstNotOfflineContact : [(AIMetaContact *)inContact preferredContact]));
1970 //This contact doesn't contain multiple contacts... but it might still be a metacontact. Do NOT proceed with a metacontact.
1971 if ([inContact respondsToSelector:@selector(preferredContact)])
1972 inContact = [inContact performSelector:@selector(preferredContact)];
1974 //find the best account for talking to this contact,
1975 //and return an AIListContact on that account
1976 account = [[adium accountController] preferredAccountForSendingContentType:inType
1977 toContact:inContact];
1979 if ([inContact account] == account) {
1980 returnContact = inContact;
1982 returnContact = [self contactWithService:[inContact service]
1984 UID:[inContact UID]];
1989 return returnContact;
1992 //Retrieve a list contact matching the UID and serviceID of the passed contact but on the specified account.
1993 //In many cases this will be the same as inContact.
1994 - (AIListContact *)contactOnAccount:(AIAccount *)account fromListContact:(AIListContact *)inContact
1996 if (account && ([inContact account] != account)) {
1997 return [self contactWithService:[inContact service] account:account UID:[inContact UID]];
2003 //XXX - This is ridiculous.
2004 - (AIListContact *)preferredContactWithUID:(NSString *)inUID andServiceID:(NSString *)inService forSendingContentType:(NSString *)inType
2006 AIService *theService = [[adium accountController] firstServiceWithServiceID:inService];
2007 AIListContact *tempListContact = [[AIListContact alloc] initWithUID:inUID
2008 service:theService];
2009 AIAccount *account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
2010 toContact:tempListContact
2011 includeOffline:YES];
2012 [tempListContact release];
2014 return [self contactWithService:theService account:account UID:inUID];
2019 * @brief Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts
2021 * If the destination contact's parent contact differs from the destination contact itself, the chat is with a metaContact.
2022 * If that metaContact's preferred destination for messaging isn't the same as the contact which was just messaged,
2023 * update the preference so that a new chat with this metaContact would default to the proper contact.
2025 - (void)didSendContent:(NSNotification *)notification
2027 AIChat *chat = [[notification userInfo] objectForKey:@"AIChat"];
2028 AIListContact *destContact = [chat listObject];
2029 AIListContact *metaContact = [destContact parentContact];
2031 //it's not particularly obvious from the name, but -parentContact can return self
2032 if (metaContact == destContact) return;
2034 NSString *destinationInternalObjectID = [destContact internalObjectID];
2035 NSString *currentPreferredDestination = [metaContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
2036 group:OBJECT_STATUS_CACHE];
2038 if (![destinationInternalObjectID isEqualToString:currentPreferredDestination]) {
2039 [metaContact setPreference:destinationInternalObjectID
2040 forKey:KEY_PREFERRED_DESTINATION_CONTACT
2041 group:OBJECT_STATUS_CACHE];
2045 //Retrieving Groups ----------------------------------------------------------------------------------------------------
2046 #pragma mark Retrieving Groups
2048 //Retrieve a group from the contact list (Creating if necessary)
2049 - (AIListGroup *)groupWithUID:(NSString *)groupUID
2051 //Return our root group if it is requested.
2052 //XXX: is this a good idea? it might semi-mask bugs where we accidentally pass nil
2053 if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME])
2054 return [self contactList];
2056 AIListGroup *group = nil;
2057 if (!(group = [groupDict objectForKey:[groupUID lowercaseString]])) {
2059 group = [[AIListGroup alloc] initWithUID:groupUID];
2062 [self _updateAllAttributesOfObject:group];
2063 [groupDict setObject:group forKey:[groupUID lowercaseString]];
2065 //Add to the contact list
2066 [contactList addObject:group];
2067 [self _listChangedGroup:contactList object:group];
2074 - (AIListGroup *)existingGroupWithUID:(NSString *)groupUID
2076 //Return our root group if it is requested
2077 //XXX: is this a good idea? it might semi-mask bugs where we accidentally pass nil
2078 if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME])
2079 return [self contactList];
2081 return [groupDict objectForKey:groupUID];
2084 //Contact list editing -------------------------------------------------------------------------------------------------
2085 #pragma mark Contact list editing
2086 - (void)removeListObjects:(NSArray *)objectArray
2088 NSEnumerator *enumerator = [objectArray objectEnumerator];
2089 AIListObject *listObject;
2091 while ((listObject = [enumerator nextObject])) {
2092 if ([listObject isKindOfClass:[AIMetaContact class]]) {
2093 NSSet *objectsToRemove = nil;
2095 //If the metaContact only has one listContact, we will remove that contact from all accounts
2096 if ([[(AIMetaContact *)listObject listContacts] count] == 1) {
2097 AIListContact *listContact = [[(AIMetaContact *)listObject listContacts] objectAtIndex:0];
2099 objectsToRemove = [self allContactsWithService:[listContact service]
2100 UID:[listContact UID]
2104 //And actually remove the single contact if applicable
2105 if (objectsToRemove) {
2106 [self removeListObjects:[objectsToRemove allObjects]];
2109 //Now break the metaContact down, taking out all contacts and putting them back in the main list
2110 [self breakdownAndRemoveMetaContact:(AIMetaContact *)listObject];
2112 } else if ([listObject isKindOfClass:[AIListGroup class]]) {
2113 AIListObject <AIContainingObject> *containingObject = [listObject containingObject];
2114 NSEnumerator *enumerator;
2117 //If this is a group, delete all the objects within it
2118 [self removeListObjects:[(AIListGroup *)listObject containedObjects]];
2120 //Delete the list off of all active accounts
2121 enumerator = [[[adium accountController] accounts] objectEnumerator];
2122 while ((account = [enumerator nextObject])) {
2123 if ([account online]) {
2124 [account deleteGroup:(AIListGroup *)listObject];
2128 //Then, procede to delete the group
2129 [listObject retain];
2130 [containingObject removeObject:listObject];
2131 [groupDict removeObjectForKey:[[listObject UID] lowercaseString]];
2132 [self _listChangedGroup:containingObject object:listObject];
2133 [listObject release];
2136 AIAccount *account = [(AIListContact *)listObject account];
2137 if ([account online]) {
2138 [account removeContacts:[NSArray arrayWithObject:listObject]];
2144 - (void)addContacts:(NSArray *)contactArray toGroup:(AIListGroup *)group
2146 NSEnumerator *enumerator;
2147 AIListContact *listObject;
2149 [self delayListObjectNotifications];
2151 enumerator = [contactArray objectEnumerator];
2152 while ((listObject = [enumerator nextObject])) {
2153 if(![group containsObject:listObject]) //don't add it if it's already there.
2154 [[listObject account] addContacts:[NSArray arrayWithObject:listObject] toGroup:group];
2157 [self endListObjectNotificationsDelay];
2160 - (void)requestAddContactWithUID:(NSString *)contactUID service:(AIService *)inService account:(AIAccount *)inAccount
2162 NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:contactUID
2164 if (inService) [userInfo setObject:inService forKey:@"AIService"];
2165 if (inAccount) [userInfo setObject:inAccount forKey:@"AIAccount"];
2167 [[adium notificationCenter] postNotificationName:Contact_AddNewContact
2172 - (void)moveListObjects:(NSArray *)objectArray intoObject:(AIListObject<AIContainingObject> *)group index:(int)index
2174 NSEnumerator *enumerator;
2175 AIListContact *listContact;
2177 [self delayListObjectNotifications];
2179 if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2180 [(id)group setDelayContainedObjectSorting:YES];
2183 enumerator = [objectArray objectEnumerator];
2184 while ((listContact = [enumerator nextObject])) {
2185 [self moveContact:listContact intoObject:group];
2187 //Set the new index / position of the object
2188 [self _positionObject:listContact atIndex:index inObject:group];
2191 [self endListObjectNotificationsDelay];
2193 if ([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2194 [(id)group setDelayContainedObjectSorting:NO];
2198 Resort the entire list if we are moving within or between AIListGroup objects
2199 (other containing objects such as metaContacts will handle their own sorting).
2201 if ([group isKindOfClass:[AIListGroup class]]){
2202 [(AIListGroup *)group visibilityOfContainedObject:group changedTo:YES];
2203 [self sortAContactList:(AIListGroup *)group];
2207 - (void)moveContact:(AIListContact *)listContact intoObject:(AIListObject<AIContainingObject> *)group
2209 //Move the object to the new group only if necessary
2210 if (group == [listContact containingObject]) return;
2212 if ([group isKindOfClass:[AIListGroup class]]) {
2213 //Move a contact into a new group
2214 if ([listContact isKindOfClass:[AIListBookmark class]]) {
2215 [self _moveContactLocally:listContact toGroup:(AIListGroup *)group];
2217 } else if ([listContact isKindOfClass:[AIMetaContact class]]) {
2218 //Move the meta contact to this new group
2219 [self _moveContactLocally:listContact toGroup:(AIListGroup *)group];
2221 NSEnumerator *enumerator;
2222 AIListContact *actualListContact;
2224 //This is a meta contact, move the objects within it. listContacts will give us a flat array of AIListContacts.
2225 enumerator = [[(AIMetaContact *)listContact listContacts] objectEnumerator];
2226 while ((actualListContact = [enumerator nextObject])) {
2227 //Only move the contact if it is actually listed on the account in question
2228 if (![actualListContact isStranger]) {
2229 [self _moveObjectServerside:actualListContact toGroup:(AIListGroup *)group];
2232 } else if ([listContact isKindOfClass:[AIListContact class]]) {
2234 [self _moveObjectServerside:listContact toGroup:(AIListGroup *)group];
2235 } else if ([listContact isKindOfClass:[AIListGroup class]]) {
2236 // Move contact from one contact list to another
2237 [(AIListGroup *)listContact moveGroupFrom:[(AIListGroup *)listContact containingObject] to:group];
2239 AILogWithSignature(@"I don't know what to do with %@",listContact);
2241 } else if ([group isKindOfClass:[AIMetaContact class]]) {
2242 //Moving a contact into a meta contact
2243 [self addListObject:listContact toMetaContact:(AIMetaContact *)group];
2247 //Move an object to another group
2248 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group
2250 AIAccount *account = [(AIListContact *)listObject account];
2251 if ([account online]) {
2252 [account moveListObjects:[NSArray arrayWithObject:listObject] toGroup:group];
2257 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName
2259 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
2262 //Since Adium has no memory of what accounts a group is on, we have to send this message to all available accounts
2263 //The accounts without this group will just ignore it
2264 while ((account = [enumerator nextObject])) {
2265 [account renameGroup:listGroup to:newName];
2268 //Remove the old group if it's empty
2269 if ([listGroup containedObjectsCount] == 0) {
2270 [self removeListObjects:[NSArray arrayWithObject:listGroup]];
2274 //Position a list object within a group
2275 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inObject:(AIListObject<AIContainingObject> *)group
2278 //Moved to the top of a group. New index is between 0 and the lowest current index
2279 [listObject setOrderIndex:([group smallestOrder] / 2.0)];
2281 } else if (index >= [group visibleCount]) {
2282 //Moved to the bottom of a group. New index is one higher than the highest current index
2283 [listObject setOrderIndex:([group largestOrder] + 1.0)];
2286 //Moved somewhere in the middle. New index is the average of the next largest and smallest index
2287 AIListObject *previousObject = [group objectAtIndex:index-1];
2288 AIListObject *nextObject = [group objectAtIndex:index];
2289 float nextLowest = [previousObject orderIndex];
2290 float nextHighest = [nextObject orderIndex];
2292 /* XXX - Fixme as per below
2293 * It's possible that nextLowest > nextHighest if ordering is not strictly based on the ordering indexes themselves.
2294 * For example, a group sorted by status then manually could look like (status - ordering index):
2296 * Away Contact - 100
2297 * Away Contact - 120
2298 * Offline Contact - 110
2299 * Offline Contact - 113
2300 * Offline Contact - 125
2302 * Dropping between Away Contact and Offline Contact should make an Away Contact be > 120 but an Offline Contact be < 110.
2303 * Only the sort controller knows the answer as to where this contact should be positioned in the end.
2306 [listObject setOrderIndex:((nextHighest + nextLowest) / 2.0)];
2310 #pragma mark Authorization
2311 - (id)showAuthorizationRequestWithDict:(NSDictionary *)inDict forAccount:(AIAccount *)inAccount
2313 return [adiumAuthorization showAuthorizationRequestWithDict:inDict forAccount:inAccount];
2316 //Detached Contact Lists ----------------------------------------------------------------------------------------------------
2317 #pragma mark Detached Contact Lists
2320 * @returns Empty contact list
2322 - (AIListGroup *)createDetachedContactList{
2323 static int count = 0;
2324 AIListGroup * list = [[AIListGroup alloc] initWithUID:[NSString stringWithFormat:@"Detached%d",count++]];
2325 [detachedContactLists addObject:list];
2331 * @brief Removes detached contact list
2333 - (void)removeDetachedContactList:(AIListGroup *)detachedList{
2334 [detachedContactLists removeObject:detachedList];
2338 * @breif Checks if a particular group is in a detached contact list
2340 - (BOOL)isGroupDetached:(AIListObject *)group{
2341 NSEnumerator *i = [detachedContactLists objectEnumerator];
2342 AIListGroup *currentGroup;
2344 while((currentGroup = [i nextObject])){
2345 if(currentGroup == group)
2352 * @returns Number of contact lists (ie. both main contact list and all detached contact lists)
2354 - (unsigned)contactListCount {
2355 return (contactList!=nil) + [detachedContactLists count];