* Adium is the legal property of its developers, whose names are listed in the copyright file included * with this source distribution. * This program is free software; you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * You should have received a copy of the GNU General Public License along with this program; if not, * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #import "AIContactController.h" #import "AISCLViewPlugin.h" #import <Adium/AIContactHidingController.h> #import <Adium/AIAccountControllerProtocol.h> #import <Adium/AIInterfaceControllerProtocol.h> #import <Adium/AILoginControllerProtocol.h> #import <Adium/AIMenuControllerProtocol.h> #import <Adium/AIToolbarControllerProtocol.h> #import <Adium/AIContactAlertsControllerProtocol.h> #import <AIUtilities/AIArrayAdditions.h> #import <AIUtilities/AIDictionaryAdditions.h> #import <AIUtilities/AIFileManagerAdditions.h> #import <AIUtilities/AIMenuAdditions.h> #import <AIUtilities/AIToolbarUtilities.h> #import <AIUtilities/AIImageAdditions.h> #import <AIUtilities/AIStringAdditions.h> #import <Adium/AIAccount.h> #import <Adium/AIContentMessage.h> #import <Adium/AIListContact.h> #import <Adium/AIListGroup.h> #import <Adium/AIListObject.h> #import <Adium/AIMetaContact.h> #import <Adium/AIService.h> #import <Adium/AISortController.h> #import <Adium/AIUserIcons.h> #import <Adium/AIServiceIcons.h> #import <Adium/AIListBookmark.h> #import <Adium/AIContactList.h> #define KEY_FLAT_GROUPS @"FlatGroups" //Group storage #define KEY_FLAT_CONTACTS @"FlatContacts" //Contact storage #define KEY_FLAT_METACONTACTS @"FlatMetaContacts" //Metacontact objectID storage #define KEY_BOOKMARKS @"Bookmarks" #define TOP_METACONTACT_ID @"TopMetaContactID" #define KEY_IS_METACONTACT @"isMetaContact" #define KEY_OBJECTID @"objectID" #define KEY_METACONTACT_OWNERSHIP @"MetaContact Ownership" #define CONTACT_DEFAULT_PREFS @"ContactPrefs" #define SHOW_GROUPS_MENU_TITLE AILocalizedString(@"Show Groups",nil) #define SHOW_GROUPS_IDENTIFER @"ShowGroups" #define SERVICE_ID_KEY @"ServiceID" //#define CONTACT_MOVEMENT_DEBUG @interface AIListObject () @property ( readwrite , nonatomic ) CGFloat orderIndex ; @interface AIMetaContact () - ( BOOL ) addObject : ( AIListObject * ) inObject ; - ( BOOL ) removeObject: ( AIListObject * ) inObject ; - ( AIListContact * ) preferredContactForContentType: ( NSString * ) inType ; @interface AIListGroup () - ( void ) removeObject : ( AIListObject * ) inObject ; - ( BOOL ) addObject: ( AIListObject * ) inObject ; @interface AIContactList () - ( void ) removeObject : ( AIListObject * ) inObject ; - ( BOOL ) addObject: ( AIListObject * ) inObject ; @interface AIListBookmark () //Freshly minted bookmarks don't know where to restore to, since they have no serverside counterpart. This tells them. - ( void ) setInitialGroup : ( AIListGroup * ) inGroup ; @interface AIContactController () @property ( readwrite , nonatomic ) BOOL useOfflineGroup ; - ( void ) _didChangeContainer: ( id < AIContainingObject > ) inContainingObject object: ( AIListObject * ) object ; - ( void ) prepareShowHideGroups ; - ( void ) _performChangeOfUseContactListGroups ; - ( void ) didSendContent: ( NSNotification * ) notification ; - ( IBAction ) toggleShowGroups: ( id ) sender ; - ( BOOL ) _restoreContactsToMetaContact: ( AIMetaContact * ) metaContact ; - ( void ) _restoreContactsToMetaContact: ( AIMetaContact * ) metaContact fromContainedContactsArray: ( NSArray * ) containedContactsArray ; - ( void ) addContact: ( AIListContact * ) inContact toMetaContact: ( AIMetaContact * ) metaContact ; - ( BOOL ) _performAddContact: ( AIListContact * ) inContact toMetaContact: ( AIMetaContact * ) metaContact ; - ( void ) removeContact: ( AIListContact * ) inContact fromMetaContact: ( AIMetaContact * ) metaContact ; - ( void ) _loadMetaContactsFromArray: ( NSArray * ) array ; - ( void ) _saveMetaContacts: ( NSDictionary * ) allMetaContactsDict ; - ( void ) _storeListObject: ( AIListObject * ) listObject inMetaContact: ( AIMetaContact * ) metaContact ; @implementation AIContactController if (( self = [ super init ])) { contactDict = [[ NSMutableDictionary alloc ] init ]; groupDict = [[ NSMutableDictionary alloc ] init ]; metaContactDict = [[ NSMutableDictionary alloc ] init ]; bookmarkDict = [[ NSMutableDictionary alloc ] init ]; contactToMetaContactLookupDict = [[ NSMutableDictionary alloc ] init ]; contactLists = [[ NSMutableArray alloc ] init ]; contactPropertiesObserverManager = [ AIContactObserverManager sharedManager ]; - ( void ) controllerDidLoad //Default contact preferences [ adium . preferenceController registerDefaults : [ NSDictionary dictionaryNamed : CONTACT_DEFAULT_PREFS forGroup : PREF_GROUP_CONTACT_LIST ]; contactList = [[ AIContactList alloc ] initWithUID : ADIUM_ROOT_GROUP_NAME ]; [ contactLists addObject : contactList ]; //Root is always "expanded" [ contactList setExpanded : YES ]; [ self prepareShowHideGroups ]; //Observe content (for preferredContactForContentType:forListContact:) [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( didSendContent : ) name : CONTENT_MESSAGE_SENT [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( didSendContent : ) name : CONTENT_MESSAGE_SENT_GROUP [ adium . preferenceController registerPreferenceObserver : self forGroup : PREF_GROUP_CONTACT_LIST_DISPLAY ]; - ( void ) controllerWillClose [ adium . preferenceController unregisterPreferenceObserver : self ]; [ metaContactDict release ]; [ contactToMetaContactLookupDict release ]; [ contactPropertiesObserverManager release ]; - ( void ) clearAllMetaContactData if ( metaContactDict . count ) { [ contactPropertiesObserverManager delayListObjectNotifications ]; //Remove all the metaContacts to get any existing objects out of them for ( AIMetaContact * metaContact in [[[ metaContactDict copy ] autorelease ] objectEnumerator ]) { [ self explodeMetaContact : metaContact ]; [ contactPropertiesObserverManager endListObjectNotificationsDelay ]; [ metaContactDict release ]; metaContactDict = [[ NSMutableDictionary alloc ] init ]; [ contactToMetaContactLookupDict release ]; contactToMetaContactLookupDict = [[ NSMutableDictionary alloc ] init ]; //Clear the preferences for good measure [ adium . preferenceController setPreference : nil forKey : KEY_FLAT_METACONTACTS group : PREF_GROUP_CONTACT_LIST ]; [ adium . preferenceController setPreference : nil forKey : KEY_METACONTACT_OWNERSHIP group : PREF_GROUP_CONTACT_LIST ]; //Clear out old metacontact files [[ NSFileManager defaultManager ] removeFilesInDirectory : [[ adium . loginController userDirectory ] stringByAppendingPathComponent : OBJECT_PREFS_PATH ] withPrefix : @"MetaContact" [[ NSFileManager defaultManager ] removeFilesInDirectory : [ adium cachesPath ] withPrefix : @"MetaContact" #pragma mark Local Contact List Storage //We must load all the groups before loading contacts for the ordering system to work correctly. [ self _loadMetaContactsFromArray : [ adium . preferenceController preferenceForKey : KEY_FLAT_METACONTACTS group : PREF_GROUP_CONTACT_LIST ]]; for ( AIListGroup * listGroup in [ groupDict objectEnumerator ]) { [ listGroup setPreference : [ NSNumber numberWithBool : [ listGroup isExpanded ]] group : PREF_GROUP_CONTACT_LIST ]; NSMutableArray * bookmarks = [ NSMutableArray array ]; for ( AIListBookmark * bookmark in self . allBookmarks ) { [ bookmarks addObject : [ NSKeyedArchiver archivedDataWithRootObject : bookmark ]]; [ adium . preferenceController setPreference : bookmarks group : PREF_GROUP_CONTACT_LIST ]; for ( NSData * data in [ adium . preferenceController preferenceForKey : KEY_BOOKMARKS group : PREF_GROUP_CONTACT_LIST ]) { //As a bookmark is initialized, it will add itself to the contact list in the right place AIListBookmark * bookmark = [ NSKeyedUnarchiver unarchiveObjectWithData : data ]; if ([ bookmarkDict objectForKey : bookmark . internalObjectID ]) { // In case we end up with two bookmarks with the same internalObjectID; this should be almost impossible. [ self removeBookmark : [ bookmarkDict objectForKey : bookmark . internalObjectID ]]; [ bookmarkDict setObject : bookmark forKey : bookmark . internalObjectID ]; //It's a newly created object, so set its initial attributes [ contactPropertiesObserverManager _updateAllAttributesOfObject : bookmark ]; - ( void ) _loadMetaContactsFromArray: ( NSArray * ) array for ( NSString * identifier in array ) { NSAutoreleasePool * pool = [[ NSAutoreleasePool alloc ] init ]; NSNumber * objectID = [ NSNumber numberWithInteger : [[[ identifier componentsSeparatedByString : @"-" ] objectAtIndex : 1 ] integerValue ]]; [ self metaContactWithObjectID : objectID ]; #pragma mark Contact Grouping - ( void ) _addContactLocally: ( AIListContact * ) listContact toGroup: ( id < AIContainingObject > ) localGroup BOOL performedGrouping = NO ; //Protect with a retain while we are removing and adding the contact to our arrays if ( listContact . canJoinMetaContacts ) { AIListObject * existingObject = [ localGroup objectWithService : listContact . service UID : listContact . UID ]; if ( existingObject && ( existingObject != listContact )) { /* If an object exists in this group with the same UID and serviceID, create a MetaContact for the two. */ AIMetaContact * metaContact = [ self groupContacts : [ NSArray arrayWithObjects : listContact , existingObject , nil ]]; AILogWithSignature ( @"%@ and %@ match; grouped into %@" , listContact , existingObject , metaContact ); AIMetaContact * metaContact = [ contactToMetaContactLookupDict objectForKey : listContact . internalObjectID ]; /* If no object exists in this group which matches, we should check if there is already * a MetaContact holding a matching ListContact, since we should include this contact in it * If we found a metaContact to which we should add, do it. AILogWithSignature ( @"%@: Add %@" , metaContact , listContact ); [ self addContact : listContact toMetaContact : metaContact ]; if ( ! performedGrouping ) { //If no similar objects exist, we add this contact directly to the list [ localGroup addObject : listContact ]; [ self _didChangeContainer : localGroup object : listContact ]; * @brief Handle a local move * This will remove listContact from our local tracking within each of oldGroups, which must be AIListObject<AIContainingObject> * objects. It will then add them to groups, which must also be AIListObject<AIContainingObject> objects. * This does not make any changes serverside; that should be handled separately. - ( void ) _moveContactLocally: ( AIListContact * ) listContact fromGroups: ( NSSet * ) oldGroups toGroups: ( NSSet * ) groups if ( ! [ oldGroups isEqualToSet : groups ]) { //Protect with a retain while we are removing and adding the contact to our arrays [ contactPropertiesObserverManager delayListObjectNotifications ]; #ifdef CONTACT_MOVEMENT_DEBUG AILogWithSignature ( @"%@: %@ --> %@" , ( oldGroups . count ? oldGroups : nil ), ( groups . count ? ( groups . count == 1 ? [ groups anyObject ] ? groups ) : nil )); //Remove this object from any local groups we have it in currently for ( id < AIContainingObject > group in oldGroups ) { [ group removeObject : listContact ]; [ self _didChangeContainer : group object : listContact ]; for ( id < AIContainingObject > group in groups ) [ self _addContactLocally : listContact toGroup : group ]; [ contactPropertiesObserverManager endListObjectNotificationsDelay ]; //Post a list grouping changed notification for the object and containing object - ( void ) _didChangeContainer: ( id < AIContainingObject > ) inContainingObject object: ( AIListObject * ) object if ([ contactPropertiesObserverManager shouldDelayUpdates ]) { [ contactPropertiesObserverManager noteContactChanged : object ]; [[ NSNotificationCenter defaultCenter ] postNotificationName : Contact_ListChanged object : inContainingObject - ( BOOL ) useContactListGroups return useContactListGroups ; - ( void ) setUseContactListGroups: ( BOOL ) inFlag if ( inFlag != useContactListGroups ) { useContactListGroups = inFlag ; [ self _performChangeOfUseContactListGroups ]; - ( void ) _performChangeOfUseContactListGroups [ contactPropertiesObserverManager delayListObjectNotifications ]; [ adium . preferenceController setPreference : [ NSNumber numberWithBool :! useContactListGroups ] forKey : KEY_HIDE_CONTACT_LIST_GROUPS group : PREF_GROUP_CONTACT_LIST_DISPLAY ]; //Configure the sort controller to force ignoring of groups as appropriate [[ AISortController activeSortController ] forceIgnoringOfGroups : ( useContactListGroups ? NO : YES )]; //Restore the grouping of all root-level contacts for ( AIListContact * contact in [ self contactEnumerator ]) { [ contact restoreGrouping ]; //Restore the grouping of all list bookmarks for ( AIListBookmark * bookmark in [ self bookmarkEnumerator ]) { [ bookmark restoreGrouping ]; //Stop delaying object notifications; this will automatically resort the contact list, so we're done. [ contactPropertiesObserverManager endListObjectNotificationsDelay ]; - ( void ) prepareShowHideGroups useContactListGroups = ! [[ adium . preferenceController preferenceForKey : KEY_HIDE_CONTACT_LIST_GROUPS group : PREF_GROUP_CONTACT_LIST_DISPLAY ] boolValue ]; //Show offline contacts menu item menuItem_showGroups = [[ NSMenuItem alloc ] initWithTitle : SHOW_GROUPS_MENU_TITLE action : @selector ( toggleShowGroups : ) [ menuItem_showGroups setState : useContactListGroups ]; [ adium . menuController addMenuItem : menuItem_showGroups toLocation : LOC_View_Toggles ]; - ( IBAction ) toggleShowGroups: ( id ) sender useContactListGroups = ! useContactListGroups ; [ menuItem_showGroups setState : useContactListGroups ]; //Update the contact list. Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure. [ self performSelector : @selector ( _performChangeOfUseContactListGroups ) @synthesize useOfflineGroup ; - ( AIListGroup * ) offlineGroup return [ self groupWithUID : AILocalizedString ( @"Offline" , "Name of offline group" )]; #pragma mark Meta Contacts * @brief Create or load a metaContact * @param inObjectID The objectID of an existing but unloaded metaContact, or nil to create and save a new metaContact - ( AIMetaContact * ) metaContactWithObjectID: ( NSNumber * ) inObjectID BOOL shouldRestoreContacts = YES ; //If no object ID is provided, use the next available object ID //(MetaContacts should always have an individually unique object id) NSInteger topID = [[ adium . preferenceController preferenceForKey : TOP_METACONTACT_ID group : PREF_GROUP_CONTACT_LIST ] integerValue ]; inObjectID = [ NSNumber numberWithInteger : topID ]; [ adium . preferenceController setPreference : [ NSNumber numberWithInteger : ([ inObjectID integerValue ] + 1 )] forKey : TOP_METACONTACT_ID group : PREF_GROUP_CONTACT_LIST ]; //No reason to waste time restoring contacts when none are in the meta contact yet. shouldRestoreContacts = NO ; //Look for a metacontact with this object ID. If none is found, create one //and add its contained contacts to it. NSString * metaContactDictKey = [ AIMetaContact internalObjectIDFromObjectID : inObjectID ]; AIMetaContact * metaContact = [ metaContactDict objectForKey : metaContactDictKey ]; metaContact = [( AIMetaContact * )[ AIMetaContact alloc ] initWithObjectID : inObjectID ]; //Keep track of it in our metaContactDict for retrieval by objectID [ metaContactDict setObject : metaContact forKey : metaContactDictKey ]; //Add it to our more general contactDict, as well [ contactDict setObject : metaContact forKey : [ metaContact internalUniqueObjectID ]]; /* We restore contacts (actually, internalIDs for contacts, to be added as necessary later) if the metaContact * existed before this call to metaContactWithObjectID: if ( shouldRestoreContacts ) [ self _restoreContactsToMetaContact : metaContact ]; /* As with contactWithService:account:UID, update all attributes so observers are initially informed of * this object's existence. [ contactPropertiesObserverManager _updateAllAttributesOfObject : metaContact ]; * @brief Associate the appropriate internal IDs for contained contacts with a metaContact * @result YES if one or more contacts was associated with the metaContact; NO if none were. - ( BOOL ) _restoreContactsToMetaContact: ( AIMetaContact * ) metaContact NSDictionary * allMetaContactsDict = [ adium . preferenceController preferenceForKey : KEY_METACONTACT_OWNERSHIP group : PREF_GROUP_CONTACT_LIST ]; NSArray * containedContactsArray = [ allMetaContactsDict objectForKey : metaContact . internalObjectID ]; if ( containedContactsArray . count ) { [ self _restoreContactsToMetaContact : metaContact fromContainedContactsArray : containedContactsArray ]; * @brief Associate the internal IDs for an array of contacts with a specific metaContact * This does not actually place any AIListContacts within the metaContact. Instead, it updates the contactToMetaContactLookupDict * dictionary to have metaContact associated with the list contacts specified by containedContactsArray. This * allows us to add them lazily to the metaContact (in contactWithService:account:UID:) as necessary. * @param metaContact The metaContact to which contact referneces are added * @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 - ( void ) _restoreContactsToMetaContact: ( AIMetaContact * ) metaContact fromContainedContactsArray: ( NSArray * ) containedContactsArray for ( NSDictionary * containedContactDict in containedContactsArray ) { /* Before Adium 0.80, metaContacts could be created within metaContacts. Simply ignore any attempt to restore * such erroneous data, which will have a YES boolValue for KEY_IS_METACONTACT. */ if ( ! [[ containedContactDict objectForKey : KEY_IS_METACONTACT ] boolValue ]) { /* Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject. * As listObjects are loaded/created/requested which match this internalObjectID, * they will be inserted into the metaContact. NSString * internalObjectID = [ AIListObject internalObjectIDForServiceID : [ containedContactDict objectForKey : SERVICE_ID_KEY ] UID :[ containedContactDict objectForKey : UID_KEY ]]; [ contactToMetaContactLookupDict setObject : metaContact forKey : internalObjectID ]; //Add a list object to a meta contact, setting preferences and such //so the association is lasting across program launches. - ( void ) addContact: ( AIListContact * ) inContact toMetaContact: ( AIMetaContact * ) metaContact //I can't think of why one would want to add an entire group to a metacontact. Let's say you can't. NSLog ( @"Warning: addContact:toMetaContact: Attempted to add %@ to %@" , inContact , metaContact ); if ( inContact == metaContact ) return ; AILogWithSignature ( @"%@ will add %@" , metaContact , inContact ); //If listObject contains other contacts, perform addContact:toMetaContact: recursively if ([ inContact conformsToProtocol : @ protocol ( AIContainingObject )]) { AILogWithSignature ( @"Adding recursively (%@)" , (( id < AIContainingObject > ) inContact ). containedObjects ); for ( AIListContact * someObject in (( id < AIContainingObject > ) inContact ). containedObjects ) { [ self addContact : someObject toMetaContact : metaContact ]; //Obtain any metaContact this listObject is currently within, so we can remove it later AIMetaContact * oldMetaContact ; /* First, look for a metaContact which is -properly- associated with inContact, already. * That is, a metaContact that has current ownership but which should be disassociated, in faovr of the new oldMetaContact = [ contactToMetaContactLookupDict objectForKey : [ inContact internalObjectID ]]; if ( metaContact == oldMetaContact ) { /* According to contactToMetaContactLookupDict, inContact is within metaContact already. * However, a single UID/serviceID pair can have multiple AIListContacts (for multiple accounts). * If Account A and Account B both have a contact like inContact, and we are looking at Account B's * contact now, we'll have already made a reassignment. This contact does still need to be removed from if (( inContact . parentContact != inContact ) && ([ inContact . parentContact isKindOfClass : [ AIMetaContact class ]])) { oldMetaContact = ( AIMetaContact * )( inContact . parentContact ); if ([ self _performAddContact : inContact toMetaContact : metaContact ]) { if ( metaContact != oldMetaContact ) { AILogWithSignature ( @"oldMetaContact for %@ is %@" , inContact . internalObjectID , oldMetaContact ); //If this listObject was not in this metaContact in any form before, store the change //Remove the list object from any other metaContact it is in at present [ self removeContact : inContact fromMetaContact : oldMetaContact ]; [ self _storeListObject : inContact inMetaContact : metaContact ]; [ contactPropertiesObserverManager _updateAllAttributesOfObject : metaContact ]; AILogWithSignature ( @"The old metacontact was the same as the new metacontact" ); AILogWithSignature ( @"Failed to add %@ to %@" , inContact , metaContact ); - ( void ) _storeListObject: ( AIListObject * ) listObject inMetaContact: ( AIMetaContact * ) metaContact //we only allow group->meta->contact, not group->meta->meta->contact NSParameterAssert ( ! [ listObject conformsToProtocol : @ protocol ( AIContainingObject )]); // AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact); NSDictionary * containedContactDict ; NSMutableDictionary * allMetaContactsDict ; NSMutableArray * containedContactsArray ; NSString * metaContactInternalObjectID = [ metaContact internalObjectID ]; //Get the dictionary of all metaContacts allMetaContactsDict = [[ adium . preferenceController preferenceForKey : KEY_METACONTACT_OWNERSHIP group : PREF_GROUP_CONTACT_LIST ] mutableCopy ]; if ( ! allMetaContactsDict ) { allMetaContactsDict = [[ NSMutableDictionary alloc ] init ]; //Load the array for the new metaContact containedContactsArray = [[ allMetaContactsDict objectForKey : metaContactInternalObjectID ] mutableCopy ]; if ( ! containedContactsArray ) containedContactsArray = [[ NSMutableArray alloc ] init ]; containedContactDict = nil ; //Create the dictionary describing this list object containedContactDict = [ NSDictionary dictionaryWithObjectsAndKeys : listObject . service . serviceID , SERVICE_ID_KEY , listObject . UID , UID_KEY , nil ]; //Only add if this dict isn't already in the array if ( containedContactDict && ([ containedContactsArray indexOfObject : containedContactDict ] == NSNotFound )) { [ containedContactsArray addObject : containedContactDict ]; [ allMetaContactsDict setObject : containedContactsArray forKey : metaContactInternalObjectID ]; [ self _saveMetaContacts : allMetaContactsDict ]; [ adium . contactAlertsController mergeAndMoveContactAlertsFromListObject : listObject intoListObject : metaContact ]; AILogWithSignature ( @"Updated %@'s containedContactsArray: %@" , metaContact , containedContactsArray ); [ allMetaContactsDict release ]; [ containedContactsArray release ]; * @brief Makes the associations between an AIListContact and an AIMetaContact within memory * No preferences are changed; this is an internal step during the process of moving a contact into a metacontact. * Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict * for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful. * @result YES if a change was made; NO if inContact is already contained by metaContact - ( BOOL ) _performAddContact: ( AIListContact * ) inContact toMetaContact: ( AIMetaContact * ) metaContact if ( inContact . metaContact == metaContact ) { AILogWithSignature ( @"%@'s metaContact is already %@" , inContact , metaContact ); //we only allow group->meta->contact, not group->meta->meta->contact NSParameterAssert ([ metaContact canContainObject : inContact ]); //Remove the object from its previous containing groups if ( inContact . groups . count ) [ self _moveContactLocally : inContact fromGroups : inContact . groups toGroups : [ NSSet set ]]; //AIMetaContact will handle reassigning the list object's grouping to being itself if (( success = [ metaContact addObject : inContact ])) { [ contactToMetaContactLookupDict setObject : metaContact forKey : [ inContact internalObjectID ]]; [ self _didChangeContainer : metaContact object : inContact ]; //Ensure the metacontact ends up in the appropriate groups [ metaContact restoreGrouping ]; - ( void ) removeContact: ( AIListContact * ) inContact fromMetaContact: ( AIMetaContact * ) metaContact AILogWithSignature ( @"%@: Remove %@" , metaContact , inContact ); //we only allow group->meta->contact, not group->meta->meta->contact NSParameterAssert ( ! [ inContact conformsToProtocol : @ protocol ( AIContainingObject )]); NSString * metaContactInternalObjectID = [ metaContact internalObjectID ]; //Get the dictionary of all metaContacts NSMutableDictionary * allMetaContactsDict = [ adium . preferenceController preferenceForKey : KEY_METACONTACT_OWNERSHIP group : PREF_GROUP_CONTACT_LIST ]; //Load the array for the metaContact NSArray * containedContactsArray = [ allMetaContactsDict objectForKey : metaContactInternalObjectID ]; //Enumerate it, looking only for the appropriate type of containedContactDict NSString * listObjectUID = inContact . UID ; NSString * listObjectServiceID = inContact . service . serviceID ; NSDictionary * containedContactDict = nil ; for ( containedContactDict in containedContactsArray ) { if ([[ containedContactDict objectForKey : UID_KEY ] isEqualToString : listObjectUID ] && [[ containedContactDict objectForKey : SERVICE_ID_KEY ] isEqualToString : listObjectServiceID ]) { //If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result if ( containedContactDict ) { NSMutableArray * newContainedContactsArray ; NSMutableDictionary * newAllMetaContactsDict ; newContainedContactsArray = [ containedContactsArray mutableCopy ]; [ newContainedContactsArray removeObjectIdenticalTo : containedContactDict ]; newAllMetaContactsDict = [ allMetaContactsDict mutableCopy ]; [ newAllMetaContactsDict setObject : newContainedContactsArray forKey : metaContactInternalObjectID ]; [ self _saveMetaContacts : newAllMetaContactsDict ]; [ newContainedContactsArray release ]; [ newAllMetaContactsDict release ]; /* Remove all contacts matching this service/UID from the metacontact */ [ contactPropertiesObserverManager delayListObjectNotifications ]; for ( AIListContact * matchingContact in [ self allContactsWithService : inContact . service UID : inContact . UID ]) { if ([ metaContact removeObject : matchingContact ]) { [ contactPropertiesObserverManager _updateAllAttributesOfObject : inContact ]; [ self _didChangeContainer : metaContact object : inContact ]; [ contactPropertiesObserverManager _updateAllAttributesOfObject : metaContact ]; [ contactPropertiesObserverManager endListObjectNotificationsDelay ]; * @brief Determine the existing metacontact into which a grouping of UIDs and services would be placed * @param UIDsArray NSArray of UIDs * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray * @result Either the existing AIMetaContact -[self groupUIDs:forServices:usingMetaContactHint:] would return if passed a nil metaContactHint, * or nil (if no existing metacontact would be used). - ( AIMetaContact * ) knownMetaContactForGroupingUIDs: ( NSArray * ) UIDsArray forServices: ( NSArray * ) servicesArray AIMetaContact * metaContact = nil ; NSInteger count = [ UIDsArray count ]; for ( NSInteger i = 0 ; i < count ; i ++ ) { if (( metaContact = [ contactToMetaContactLookupDict objectForKey : [ AIListObject internalObjectIDForServiceID : [ servicesArray objectAtIndex : i ] UID :[ UIDsArray objectAtIndex : i ]]])) { * @brief Groups UIDs for services into a single metacontact * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to * a UID and a service, respectively, which together define a contact which should be included in the grouping. * Assumption: This is only called after the contact list is finished loading, which occurs via * -(void)controllerDidLoad above. * @param UIDsArray NSArray of UIDs * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray * @param metaContactHint If passed, an AIMetaContact to use for the grouping if an existing one isn't found. If nil, a new metacontact will be craeted in that case. - ( AIMetaContact * ) groupUIDs: ( NSArray * ) UIDsArray forServices: ( NSArray * ) servicesArray usingMetaContactHint: ( AIMetaContact * ) metaContactHint NSMutableSet * internalObjectIDs = [[ NSMutableSet alloc ] init ]; AIMetaContact * metaContact = nil ; NSString * internalObjectID ; NSInteger count = [ UIDsArray count ]; /* Build an array of all contacts matching this description (multiple accounts on the same service listing * the same UID mean that we can have multiple AIListContact objects with a UID/service combination) for ( NSUInteger i = 0 ; i < count ; i ++ ) { NSString * serviceID = [ servicesArray objectAtIndex : i ]; NSString * UID = [ UIDsArray objectAtIndex : i ]; internalObjectID = [ AIListObject internalObjectIDForServiceID : serviceID metaContact = [ contactToMetaContactLookupDict objectForKey : internalObjectID ]; [ internalObjectIDs addObject : internalObjectID ]; if ([ internalObjectIDs count ] > 1 ) { //Create a new metaContact is we didn't find one and weren't supplied a hint if ( ! metaContact && ! ( metaContact = metaContactHint )) { AILogWithSignature ( @"New metacontact to group %@ on %@" , UIDsArray , servicesArray ); metaContact = [ self metaContactWithObjectID : nil ]; for ( internalObjectID in internalObjectIDs ) { AIListObject * existingObject ; if (( existingObject = [ self existingListObjectWithUniqueID : internalObjectID ])) { /* If there is currently an object (or multiple objects) matching this internalObjectID * we should add immediately. NSAssert ([ metaContact canContainObject : existingObject ], @"Attempting to add something metacontacts can't hold to a metacontact" ); [ self addContact : ( id ) existingObject toMetaContact : metaContact ]; /* If no objects matching this internalObjectID exist, we can simply add to the * contactToMetaContactLookupDict for use if such an object is created later. [ contactToMetaContactLookupDict setObject : metaContact forKey : internalObjectID ]; [ internalObjectIDs release ]; /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added. * This will reuse an existing metacontact (for one of the contacts in the array) if possible. * @param contactsToGroupArray Contacts to group together - ( AIMetaContact * ) groupContacts: ( NSArray * ) contactsToGroupArray AIMetaContact * metaContact = nil ; //Look for a metacontact we were passed directly for ( AIListContact * listContact in contactsToGroupArray ) { if ([ listContact isKindOfClass : [ AIMetaContact class ]]) { metaContact = ( AIMetaContact * ) listContact ; //If we weren't passed a metacontact, look for an existing metacontact associated with a passed contact for ( AIListContact * listContact in contactsToGroupArray ) { if ( ! [ listContact isKindOfClass : [ AIMetaContact class ]] && ( metaContact = [ contactToMetaContactLookupDict objectForKey : [ listContact internalObjectID ]])) { //Create a new metaContact is we didn't find one. metaContact = [ self metaContactWithObjectID : nil ]; AILogWithSignature ( @"Created new metacontact %@ for grouping %@" , metaContact , contactsToGroupArray ); AILogWithSignature ( @"Existing metacontact %@ will now gain %@" , metaContact , contactsToGroupArray ); /* Add all these contacts to our MetaContact. * Some may already be present, but that's fine, as nothing will happen. for ( AIListContact * listContact in contactsToGroupArray ) { [ self addContact : listContact toMetaContact : metaContact ]; if ([ listContact isKindOfClass : [ AIMetaContact class ]] && listContact != metaContact ) { /* If adding a metacontact to another metacontact, its contents will be recursively added. * The original metacontact must then be destroyed lest it give rise to a zombie metacontact apocalypse. [ self explodeMetaContact : ( AIMetaContact * ) listContact ]; AILogWithSignature ( @"Completed groupContacts; metaContact is %@" , metaContact ); - ( void ) explodeMetaContact: ( AIMetaContact * ) metaContact AILogWithSignature ( @"Exploding %@" , metaContact ); //Remove the objects within it from being inside it [ contactPropertiesObserverManager delayListObjectNotifications ]; NSArray * containedObjects = metaContact . containedObjects ; NSMutableDictionary * allMetaContactsDict = [[ adium . preferenceController preferenceForKey : KEY_METACONTACT_OWNERSHIP group : PREF_GROUP_CONTACT_LIST ] mutableCopy ]; for ( AIListContact * object in containedObjects ) { //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact [ contactToMetaContactLookupDict removeObjectForKey : [ object internalObjectID ]]; [ self removeContact : object fromMetaContact : metaContact ]; //Then, procede to remove the metaContact //Remove it from its containing groups (no contained contacts == present in no groups) [ metaContact restoreGrouping ]; NSString * metaContactInternalObjectID = [ metaContact internalObjectID ]; //Remove our reference to it internally [ metaContactDict removeObjectForKey : metaContactInternalObjectID ]; //Remove it from the preferences dictionary [ allMetaContactsDict removeObjectForKey : metaContactInternalObjectID ]; //Save the updated allMetaContactsDict which no longer lists the metaContact [ self _saveMetaContacts : allMetaContactsDict ]; [ contactPropertiesObserverManager endListObjectNotificationsDelay ]; //Protection is overrated. [ allMetaContactsDict release ]; - ( void ) _saveMetaContacts: ( NSDictionary * ) allMetaContactsDict AILog ( @"MetaContacts: Saving!" ); [ adium . preferenceController setPreference : allMetaContactsDict forKey : KEY_METACONTACT_OWNERSHIP group : PREF_GROUP_CONTACT_LIST ]; [ adium . preferenceController setPreference : [ allMetaContactsDict allKeys ] forKey : KEY_FLAT_METACONTACTS group : PREF_GROUP_CONTACT_LIST ]; //Sort list objects alphabetically by their display name NSInteger contactDisplayNameSort ( AIListObject * objectA , AIListObject * objectB , void * context ) return [ objectA . displayName caseInsensitiveCompare : objectB . displayName ]; #pragma mark Preference observing * @brief Preferences changed - ( void ) preferencesChangedForGroup: ( NSString * ) group key: ( NSString * ) key object :( AIListObject * ) object preferenceDict: ( NSDictionary * ) prefDict firstTime: ( BOOL ) firstTime ! [ key isEqualToString : KEY_HIDE_CONTACTS ] && ! [ key isEqualToString : KEY_SHOW_OFFLINE_CONTACTS ] && ! [ key isEqualToString : KEY_USE_OFFLINE_GROUP ] && ! [ key isEqualToString : KEY_HIDE_CONTACT_LIST_GROUPS ]) { BOOL shouldUseOfflineGroup = (( ! [[ prefDict objectForKey : KEY_HIDE_CONTACTS ] boolValue ] || [[ prefDict objectForKey : KEY_SHOW_OFFLINE_CONTACTS ] boolValue ]) && [[ prefDict objectForKey : KEY_USE_OFFLINE_GROUP ] boolValue ]); if ( shouldUseOfflineGroup != self . useOfflineGroup || ! key ) { self . useOfflineGroup = shouldUseOfflineGroup ; if ( self . useOfflineGroup ) [ contactPropertiesObserverManager registerListObjectObserver : self ]; [ contactPropertiesObserverManager updateAllListObjectsForObserver : self ]; if ( ! self . useOfflineGroup ) [ contactPropertiesObserverManager unregisterListObjectObserver : self ]; * @brief Move contacts to and from the offline group as necessary as their online state changes. - ( NSSet * ) updateListObject: ( AIListObject * ) inObject keys: ( NSSet * ) inModifiedKeys silent: ( BOOL ) silent if (( ! inModifiedKeys || [ inModifiedKeys containsObject : @"isOnline" ]) && [ inObject isKindOfClass : [ AIListContact class ]]) { [(( AIListContact * ) inObject ). parentContact restoreGrouping ]; #pragma mark Contact Sorting //Sort the entire contact list [ self sortContactLists : contactLists ]; - ( void ) sortContactLists: ( NSArray * ) lists for ( AIContactList * list in lists ) { [[ NSNotificationCenter defaultCenter ] postNotificationName : Contact_OrderChanged object : nil ]; //Sort an individual object - ( void ) sortListObject: ( AIListObject * ) inObject if ([ contactPropertiesObserverManager shouldDelayUpdates ]) { [ contactPropertiesObserverManager noteContactChanged : inObject ]; for ( AIListGroup * group in inObject . groups ) { //Sort the groups containing this object [ group sortListObject : inObject ]; [[ NSNotificationCenter defaultCenter ] postNotificationName : Contact_OrderChanged object : group ]; #pragma mark Contact List Access * @brief Return an array of all contact list groups return [ groupDict allValues ]; * @brief Returns a flat array of all contacts * This does not include metacontacts NSMutableArray * result = [[[ NSMutableArray alloc ] init ] autorelease ]; for ( AIListContact * contact in self . contactEnumerator ) { /* We want only contacts, not metacontacts. For a given contact, -[contact parentContact] could be used to access the meta. */ if ( ! [ contact conformsToProtocol : @ protocol ( AIContainingObject )]) [ result addObject : contact ]; * @brief Returns a flat array of all bookmarks. - ( NSArray * ) allBookmarks return [[[ bookmarkDict allValues ] copy ] autorelease ]; * @brief Returns a flat array of all metacontacts - ( NSArray * ) allMetaContacts return [ metaContactDict allValues ]; //Return a flat array of all the objects in a group on an account (and all subgroups, if desired) - ( NSArray * ) allContactsInObject: ( id < AIContainingObject > ) inGroup onAccount: ( AIAccount * ) inAccount NSParameterAssert ( inGroup != nil ); NSMutableArray * contactArray = [ NSMutableArray array ]; for ( AIListObject * object in inGroup ) { if ([ object conformsToProtocol : @ protocol ( AIContainingObject )]) { [ contactArray addObjectsFromArray : [ self allContactsInObject : ( id < AIContainingObject > ) object } else if ([ object isMemberOfClass : [ AIListContact class ]] && ( ! inAccount || ([( AIListContact * ) object account ] == inAccount ))) [ contactArray addObject : object ]; #pragma mark Contact List Menus //Returns a menu containing all the groups within a group //- Selector called on group selection is selectGroup: //- The menu items represented object is the group it represents - ( NSMenu * ) groupMenuWithTarget: ( id ) target NSMenu * menu = [[ NSMenu alloc ] initWithTitle : @"" ]; [ menu setAutoenablesItems : NO ]; // Separate groups by the contact list they're on. NSMutableDictionary * groupsByList = [ NSMutableDictionary dictionaryWithCapacity : contactLists . count ]; for ( AIListGroup * group in self . allGroups ) { if ( group != self . offlineGroup ) { NSMutableArray * groups = [ groupsByList objectForKey : group . contactList . UID ]; groups = [ NSMutableArray array ]; [ groupsByList setObject : groups forKey : group . contactList . UID ]; [ groups addObject : group ]; // Now traverse the contactLists array in order and build a menu showing the groups in each list with separators in between. for ( AIContactList * list in contactLists ) { NSMutableArray * groups = [ groupsByList objectForKey : list . UID ]; [ groups sortUsingActiveSortControllerInContainer : list ]; for ( AIListGroup * group in groups ) { NSMenuItem * menuItem = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : group . displayName action : @selector ( selectGroup : ) [ menuItem setRepresentedObject : group ]; // Add the separator unless this is the last list. if ( i < contactLists . count ) { [ menu addItem : [ NSMenuItem separatorItem ]]; return [ menu autorelease ]; #pragma mark Retrieving Specific Contacts * @brief Change the UID for a contact * @param UID The new UID to set * @param contact The contact whose UID is going to be changed * Preserves our reference to the contact as the UID changes, allowing us to continue * returning it when asked about the new UID in the future. - ( void ) setUID: ( NSString * ) UID forContact: ( AIListContact * ) contact // Remove the old value, its internal ID is going to change. [ contactDict removeObjectForKey : contact . internalUniqueObjectID ]; // Add it back int othe dict. [ contactDict setObject : contact forKey : contact . internalUniqueObjectID ]; - ( AIListContact * ) contactWithService: ( AIService * ) inService account: ( AIAccount * ) inAccount UID: ( NSString * ) inUID if ( ! ( inUID && [ inUID length ] && inService )) return nil ; //Ignore invalid requests AIListContact * contact = nil ; NSString * key = [ AIListContact internalUniqueObjectIDForService : inService contact = [ contactDict objectForKey : key ]; contact = [[ AIListContact alloc ] initWithUID : inUID account : inAccount service : inService ]; //Check to see if we should add to a metaContact AIMetaContact * metaContact = [ contactToMetaContactLookupDict objectForKey : [ contact internalObjectID ]]; /* We already know to add this object to the metaContact, since we did it before with another object, but this particular listContact is new and needs to be added directly to the metaContact (on future launches, the metaContact will obtain it automatically since all contacts matching this UID and serviceID should be included). */ [ self _performAddContact : contact toMetaContact : metaContact ]; //Set the contact as mobile if it is a phone number if ([ inUID hasPrefix : @"+" ]) { [ contact setIsMobile : YES notify : NotifyNever ]; [ contactDict setObject : contact forKey : key ]; [ contactPropertiesObserverManager _updateAllAttributesOfObject : contact ]; - ( void ) accountDidStopTrackingContact: ( AIListContact * ) inContact [[ inContact retain ] autorelease ]; for ( id < AIContainingObject > container in inContact . containingObjects ) { [ container removeObjectAfterAccountStopsTracking : inContact ]; [ contactDict removeObjectForKey : inContact . internalUniqueObjectID ]; * @brief Find an existing bookmark * Finds an existing bookmark for a given AIChat - ( AIListBookmark * ) existingBookmarkForChat: ( AIChat * ) inChat return [ self existingBookmarkForChatName : inChat . name chatCreationInfo : inChat . chatCreationDictionary ]; * @brief Find an existing bookmark * Finds an existing bookmark for given information. - ( AIListBookmark * ) existingBookmarkForChatName: ( NSString * ) inName onAccount :( AIAccount * ) inAccount chatCreationInfo :( NSDictionary * ) inCreationInfo AIListBookmark * existingBookmark = nil ; for ( AIListBookmark * listBookmark in self . allBookmarks ) { if ([ listBookmark . name isEqualToString : [ inAccount . service normalizeChatName : inName ]] && listBookmark . account == inAccount && (( ! listBookmark . chatCreationDictionary && ! inCreationInfo ) || ([ listBookmark . chatCreationDictionary isEqualToDictionary : inCreationInfo ]))) { existingBookmark = listBookmark ; * @brief Find or create a bookmark for a chat - ( AIListBookmark * ) bookmarkForChat: ( AIChat * ) inChat inGroup: ( AIListGroup * ) group AIListBookmark * bookmark = [ self existingBookmarkForChat : inChat ]; bookmark = [[[ AIListBookmark alloc ] initWithChat : inChat ] autorelease ]; if ([ bookmarkDict objectForKey : bookmark . internalObjectID ]) { // In case we end up with two bookmarks with the same internalObjectID; this should be almost impossible. [ self removeBookmark : [ bookmarkDict objectForKey : bookmark . internalObjectID ]]; [ bookmarkDict setObject : bookmark forKey : bookmark . internalObjectID ]; [ bookmark setInitialGroup : group ]; [ bookmark restoreGrouping ]; [ contactPropertiesObserverManager _updateAllAttributesOfObject : bookmark ]; * @brief Remove a bookmark - ( void ) removeBookmark: ( AIListBookmark * ) listBookmark [ self moveContact : listBookmark fromGroups : listBookmark . groups intoGroups : [ NSSet set ]]; [ bookmarkDict removeObjectForKey : listBookmark . internalObjectID ]; - ( AIListContact * ) existingContactWithService: ( AIService * ) inService account: ( AIAccount * ) inAccount UID: ( NSString * ) inUID if ( inService && [ inUID length ]) { return [ contactDict objectForKey : [ AIListContact internalUniqueObjectIDForService : inService * @brief Return a set of all contacts with a specified UID and service * @param service The AIService in question * @param inUID The UID, which should be normalized (lower case, no spaces, etc.) as appropriate for the service - ( NSSet * ) allContactsWithService: ( AIService * ) service UID: ( NSString * ) inUID NSMutableSet * returnContactSet = [ NSMutableSet set ]; for ( AIAccount * account in [ adium . accountController accountsCompatibleWithService : service ]) { AIListContact * listContact = [ self existingContactWithService : service [ returnContactSet addObject : listContact ]; - ( AIListObject * ) existingListObjectWithUniqueID: ( NSString * ) uniqueID for ( AIListObject * listObject in contactDict . objectEnumerator ) { if ([ listObject . internalObjectID isEqualToString : uniqueID ]) return listObject ; for ( AIListGroup * listObject in groupDict . objectEnumerator ) { if ([ listObject . internalObjectID isEqualToString : uniqueID ]) return listObject ; for ( AIMetaContact * listObject in metaContactDict . objectEnumerator ) { if ([ listObject . internalObjectID isEqualToString : uniqueID ]) return listObject ; * @brief Get the best AIListContact to send a given content type to a contat * The resulting AIListContact will be the most available individual contact (not metacontact) on the best account to * receive the specified content type. * @result The contact, or nil if it is impossible to send inType to inContact - ( AIListContact * ) preferredContactForContentType: ( NSString * ) inType forListContact: ( AIListContact * ) inContact if ([ inContact isKindOfClass : [ AIMetaContact class ]]) inContact = [( AIMetaContact * ) inContact preferredContactForContentType : inType ]; /* Find the best account for talking to this contact, and return an AIListContact on that account. * We'll get nil if no account can send inType to inContact. AIAccount * account = [ adium . accountController preferredAccountForSendingContentType : inType toContact : inContact ]; return [ self contactWithService : inContact . service account : account UID : inContact . UID ]; //XXX - This is ridiculous. - ( AIListContact * ) preferredContactWithUID: ( NSString * ) inUID andServiceID: ( NSString * ) inService forSendingContentType: ( NSString * ) inType AIService * theService = [ adium . accountController firstServiceWithServiceID : inService ]; AIListContact * tempListContact = [[ AIListContact alloc ] initWithUID : inUID AIAccount * account = [ adium . accountController preferredAccountForSendingContentType : CONTENT_MESSAGE_TYPE toContact : tempListContact ]; [ tempListContact release ]; return [ self contactWithService : theService account : account UID : inUID ]; * @brief Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts * If the destination contact's parent contact differs from the destination contact itself, the chat is with a metaContact. * If that metaContact's preferred destination for messaging isn't the same as the contact which was just messaged, * update the preference so that a new chat with this metaContact would default to the proper contact. - ( void ) didSendContent: ( NSNotification * ) notification AIChat * chat = [[ notification userInfo ] objectForKey : @"AIChat" ]; AIListContact * destContact = chat . listObject ; AIListContact * metaContact = destContact . metaContact ; NSString * destinationInternalObjectID = destContact . internalObjectID ; NSString * currentPreferredDestination = [ metaContact preferenceForKey : KEY_PREFERRED_DESTINATION_CONTACT group : PREF_GROUP_OBJECT_STATUS_CACHE ]; if ( ! [ destinationInternalObjectID isEqualToString : currentPreferredDestination ]) { [ metaContact setPreference : destinationInternalObjectID forKey : KEY_PREFERRED_DESTINATION_CONTACT group : PREF_GROUP_OBJECT_STATUS_CACHE ]; #pragma mark Retrieving Groups //Retrieve a group from the contact list (Creating if necessary) - ( AIListGroup * ) groupWithUID: ( NSString * ) groupUID NSParameterAssert ( groupUID != nil ); //Return our root group if it is requested. if ([ groupUID isEqualToString : ADIUM_ROOT_GROUP_NAME ]) return [ self contactList ]; AIListGroup * group = nil ; if ( ! ( group = [ groupDict objectForKey : [ groupUID lowercaseString ]])) { group = [[ AIListGroup alloc ] initWithUID : groupUID ]; //Update afterwards, in case it's being called inside updateListObject already. [ contactPropertiesObserverManager performSelector : @selector ( _updateAllAttributesOfObject : ) withObject : group afterDelay : 0.0 ]; [ groupDict setObject : group forKey : [ groupUID lowercaseString ]]; //Add to the contact list [ contactList addObject : group ]; [ self _didChangeContainer : contactList object : group ]; #pragma mark Contact list editing - ( void ) removeListGroup: ( AIListGroup * ) group NSMutableSet * accountsToIgnore = nil ; for ( AIAccount * account in adium . accountController . accounts ) { if ([ account willDeleteGroup : group ] == AIAccountGroupDeletionShouldIgnoreContacts ) { accountsToIgnore = [ NSMutableSet set ]; [ accountsToIgnore addObject : account ]; AIContactList * containingObject = group . contactList ; //Remove all the contacts from this group for ( AIListContact * contact in group . containedObjects ) { if ([ contact isKindOfClass : [ AIMetaContact class ]]) { /* Look at each contact within the metaContact */ for ( AIListContact * containedContact in ( AIMetaContact * ) contact ) { /* Remove it from the group if that contact's account isn't within accountstoIgnore */ if ( ! [ accountsToIgnore containsObject : containedContact . account ]) [ containedContact removeFromGroup : group ]; /* Remove it from the group if the contact's account isn't within accountstoIgnore */ if ( ! [ accountsToIgnore containsObject : contact . account ]) [ contact removeFromGroup : group ]; //Delete the group from all active accounts for ( AIAccount * account in adium . accountController . accounts ) { [ account deleteGroup : group ]; //Then, procede to delete the group [ containingObject removeObject : group ]; [ groupDict removeObjectForKey : [ group . UID lowercaseString ]]; [ self _didChangeContainer : containingObject object : group ]; - ( void ) requestAddContactWithUID: ( NSString * ) contactUID service: ( AIService * ) inService account: ( AIAccount * ) inAccount NSMutableDictionary * userInfo = [ NSMutableDictionary dictionaryWithObject : contactUID if ( inService ) [ userInfo setObject : inService forKey : @"AIService" ]; if ( inAccount ) [ userInfo setObject : inAccount forKey : @"AIAccount" ]; [[ NSNotificationCenter defaultCenter ] postNotificationName : Contact_AddNewContact - ( void ) moveContact: ( AIListContact * ) contact fromGroups: ( NSSet * ) oldGroups intoGroups: ( NSSet * ) groups AILogWithSignature ( @"moveContact %@ [meta=%p; contained by %@; %@] from %@ into %@" , contact . metaContact , contact . containingObjects , ( contact . existsServerside ? @"exists serverside" : @"does NOT exist serverside" ), [ contactPropertiesObserverManager delayListObjectNotifications ]; if ( contact . metaContact ) { AIMetaContact * meta = contact . metaContact ; //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact [ contactToMetaContactLookupDict removeObjectForKey : contact . internalObjectID ]; [ self removeContact : contact fromMetaContact : meta ]; if ( contact . existsServerside ) { /* This is an AIListContact for an online account; just perform the serverside move */ if ( contact . account . online ) [ contact . account moveListObjects : [ NSArray arrayWithObject : contact ] fromGroups : oldGroups toGroups : groups ]; [ self _moveContactLocally : contact fromGroups : oldGroups toGroups : groups ]; if ([ contact conformsToProtocol : @ protocol ( AIContainingObject )]) { //This is a meta contact, move the objects within it. for ( AIListContact * child in ( id < AIContainingObject > ) contact ) { //Only move the contact if it is actually listed on the account in question if ( child . account . online && ! child . isStranger ) [ child . account moveListObjects : [ NSArray arrayWithObject : child ] fromGroups : oldGroups toGroups : groups ]; [ contactPropertiesObserverManager endListObjectNotificationsDelay ]; #pragma mark Detached Contact Lists - ( void ) moveGroup: ( AIListGroup * ) group fromContactList: ( AIContactList * ) oldContactList toContactList: ( AIContactList * ) newContactList if ( ! [ oldContactList containsObject : group ] || [ newContactList containsObject : group ]) { [ oldContactList removeObject : group ]; [ newContactList addObject : group ]; [[ NSNotificationCenter defaultCenter ] postNotificationName : Contact_ListChanged if ( ! oldContactList . containedObjects . count ) { [[ NSNotificationCenter defaultCenter ] postNotificationName : DetachedContactListIsEmpty [[ NSNotificationCenter defaultCenter ] postNotificationName : Contact_ListChanged * @returns Empty contact list - ( AIContactList * ) createDetachedContactList static NSInteger count = 0 ; AIContactList * list = [[ AIContactList alloc ] initWithUID : [ NSString stringWithFormat : @"Detached%ld" , count ++ ]]; [ contactLists addObject : list ]; * @brief Removes detached contact list - ( void ) removeDetachedContactList: ( AIContactList * ) detachedList [ contactLists removeObject : detachedList ]; @implementation AIContactController (ContactControllerHelperAccess) - ( NSEnumerator * ) contactEnumerator return [ contactDict objectEnumerator ]; - ( NSEnumerator * ) groupEnumerator return [ groupDict objectEnumerator ]; - ( NSEnumerator * ) bookmarkEnumerator return [ bookmarkDict objectEnumerator ];