Frameworks/Adium Framework/Source/AIListObject.m
author Zachary West <zacw@adium.im>
Fri Nov 27 15:50:57 2009 -0500 (2009-11-27)
changeset 2835 1e8c89f99dfe
parent 2828 bd512b533b98
child 3042 c6ef8efaf14f
permissions -rw-r--r--
Simplify the "status message" contact/account property into one getter method. Correct error -1728 from AS on contact's status messages. Fixes #13460.

The totally undocumented -1728 error appears to be caused by runtime type disagreeing with event type. "status message" is apparently assumed to only have 1 code, so specifying one for contacts and one for accounts was confusing it when it was going to fetch it.
     1 /* 
     2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
     3  * with this source distribution.
     4  * 
     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.
     8  * 
     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.
    12  * 
    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.
    15  */
    16 
    17 #import <Adium/AIListObject.h>
    18 #import <Adium/AIContactControllerProtocol.h>
    19 #import <Adium/AIListContact.h>
    20 #import <Adium/AIListGroup.h>
    21 #import <Adium/AIService.h>
    22 #import <Adium/AIUserIcons.h>
    23 #import <AIUtilities/AIMutableOwnerArray.h>
    24 #import <AIUtilities/AIImageAdditions.h>
    25 #import <Adium/AIContactObserverManager.h>
    26 #import <Adium/AIContactHidingController.h>
    27 #import <Adium/AIStatus.h>
    28 
    29 #define ObjectStatusCache	@"Object Status Cache"
    30 #define DisplayName			@"Display Name"
    31 #define LongDisplayName		@"Long Display Name"
    32 #define Key					@"Key"
    33 #define Group				@"Group"
    34 #define DisplayServiceID	@"DisplayServiceID"
    35 #define FormattedUID		@"FormattedUID"
    36 #define AlwaysVisible		@"AlwaysVisible"
    37 
    38 @interface AIListObject ()
    39 - (void)setContainingGroup:(AIListGroup *)inGroup;
    40 - (void)setupObservedValues;
    41 - (void)updateOrderCache;
    42 @end
    43 
    44 /*!
    45  * @class AIListObject
    46  * @brief Base class for all contacts, groups, and accounts
    47  */
    48 @implementation AIListObject
    49 
    50 /*!
    51  * @brief Initialize
    52  *
    53  * Designated initializer for AIListObject
    54  */
    55 - (id)initWithUID:(NSString *)inUID service:(AIService *)inService
    56 {
    57 	if ((self = [super init])) {
    58 		m_groups = [[NSMutableSet alloc] initWithCapacity:1];
    59 
    60 		UID = [inUID retain];	
    61 		service = inService;
    62 		
    63 		// Delay until the next run loop so bookmarks can instantiate their values first.
    64 		[self performSelector:@selector(setupObservedValues) withObject:nil afterDelay:0.0];
    65 	}
    66 
    67 	return self;
    68 }
    69 
    70 /*!
    71  * @brief Deallocate
    72  */
    73 - (void)dealloc
    74 {
    75 	[UID release]; UID = nil;
    76 	[internalObjectID release]; internalObjectID = nil;
    77 	[m_groups release]; m_groups = nil;
    78 
    79 	[super dealloc];
    80 }
    81 
    82 - (void)setupObservedValues
    83 {
    84 	[self setValue:[self preferenceForKey:@"Visible" group:PREF_GROUP_ALWAYS_VISIBLE]
    85 	   forProperty:AlwaysVisible
    86 			notify:NotifyNow];
    87 }
    88 
    89 //Identification -------------------------------------------------------------------------------------------------------
    90 #pragma mark Identification
    91 
    92 /*!
    93  * @brief UID for this object
    94  *
    95  * The UID is the name of the object.  If the object's name is not case sensitive, it is normalized.  If the object's
    96  * name should be compared ignoring spaces, it has no spaces.  For an account, this is the account name.  For a contact,
    97  * this is the screen name, buddy name, etc.
    98  */
    99 @synthesize UID;
   100 
   101 /*!
   102  * @brief Service of this object
   103  */
   104 @synthesize service;
   105 
   106 /*!
   107  * @brief Internal ID for this object
   108  *
   109  * An object ID generated by Adium that is shared by all objects which are, to most intents and purposes, identical to
   110  * this object.  Ths ID is composed of the service ID and UID, so any object with identical services and object IDs
   111  * will have the same value here.
   112  */
   113 - (NSString *)internalObjectID
   114 {
   115 	if (!internalObjectID) {
   116 		internalObjectID = [[AIListObject internalObjectIDForServiceID:self.service.serviceID UID:self.UID] retain];
   117 	}
   118 	return internalObjectID;
   119 }
   120 
   121 /*!
   122  * @brief Generate an internal object ID
   123  *
   124  * @result The internalObjectID for an object with the specified serviceID and UID
   125  */
   126 + (NSString *)internalObjectIDForServiceID:(NSString *)inServiceID UID:(NSString *)inUID
   127 {
   128 	return [NSString stringWithFormat:@"%@.%@",inServiceID, inUID];
   129 }
   130 
   131 //Visibility -----------------------------------------------------------------------------------------------------------
   132 #pragma mark Visibility
   133 
   134 /*!
   135  * @brief Sets if list object should always be visible
   136  */
   137 - (void)setAlwaysVisible:(BOOL)inVisible
   138 {
   139 	[self setPreference:[NSNumber numberWithBool:inVisible] 
   140 				 forKey:@"Visible" 
   141 				  group:PREF_GROUP_ALWAYS_VISIBLE];
   142 	
   143 	// This causes our container to update our visibility.
   144 	[self setValue:[NSNumber numberWithBool:inVisible]
   145 				   forProperty:AlwaysVisible
   146 				   notify:NotifyNow];
   147 }
   148 
   149 /*!
   150  * @brief Should this object ignore visibility settings?
   151  *
   152  * @returns If object should always be visible
   153  */
   154 - (BOOL)alwaysVisible
   155 {
   156 	return [self boolValueForProperty:AlwaysVisible];
   157 }
   158 
   159 //Grouping / Ownership -------------------------------------------------------------------------------------------------
   160 #pragma mark Grouping / Ownership
   161 
   162 - (NSSet *) groups
   163 {
   164 	return [[m_groups copy] autorelease];
   165 }
   166 
   167 - (void) addContainingGroup:(AIListGroup *)inGroup
   168 {
   169 	NSParameterAssert(inGroup && [inGroup canContainObject:self]);
   170 	if (![self.groups containsObject:inGroup]) {
   171 		
   172 		if (inGroup)
   173 			[m_groups addObject:inGroup];
   174 	}
   175 }
   176 
   177 - (void) removeContainingGroup:(AIListGroup *)group
   178 {
   179 	NSParameterAssert(group != nil && [m_groups containsObject:group]);
   180 	[m_groups removeObject:group];
   181 }
   182 
   183 - (NSSet *)containingObjects
   184 {
   185 	return self.groups;
   186 }
   187 
   188 - (void)removeFromGroup:(AIListObject <AIContainingObject> *)group
   189 {
   190 	NSString *error = [NSString stringWithFormat:@"%@ needs an implementation of -removeFromGroup:", NSStringFromClass([self class])];
   191 	NSAssert(NO, error);
   192 }
   193 
   194 /*!
   195  * @brief Set the local grouping for this object
   196  *
   197  * PRIVATE: This is only for use by AIListObjects conforming to the AIContainingObject protocol.
   198  */
   199 - (void)setContainingGroup:(AIListGroup *)inGroup
   200 {
   201 	[m_groups removeAllObjects];
   202 	if(inGroup)
   203 		[self addContainingGroup:inGroup];
   204 }
   205 
   206 - (void) moveContainedObject:(AIListObject *)listObject toIndex:(NSInteger)index
   207 {
   208 	AIListObject<AIContainingObject> *container = (AIListObject<AIContainingObject> *)self;
   209 	
   210 	// We can't enforce this, since we're asked to set it for objects we don't yet *officially* contain.
   211 	//NSAssert([container.containedObjects containsObject:listObject], @"Asked to set an index for an object which doesn't exist.");
   212 	
   213 	if (index == 0) {
   214 		//Moved to the top of a group.  New index is between 0 and the lowest current index
   215 		[container listObject:listObject didSetOrderIndex: self.smallestOrder / 2.0];
   216 		
   217 	} else if (index >= container.visibleCount) {
   218 		//Moved to the bottom of a group.  New index is one higher than the highest current index
   219 		[container listObject:listObject didSetOrderIndex: self.largestOrder + 1.0];
   220 		
   221 	} else {
   222 		//Moved somewhere in the middle.  New index is the average of the next largest and smallest index
   223 		AIListObject	*previousObject = [container.visibleContainedObjects objectAtIndex:index-1];
   224 		AIListObject	*nextObject = [container.visibleContainedObjects objectAtIndex:index];
   225 		CGFloat nextLowest = [container orderIndexForObject:previousObject];
   226 		CGFloat nextHighest = [container orderIndexForObject:nextObject];
   227 		
   228 		/* XXX - Fixme as per below
   229 		 * It's possible that nextLowest > nextHighest if ordering is not strictly based on the ordering indexes themselves.
   230 		 * For example, a group sorted by status then manually could look like (status - ordering index):
   231 		 *
   232 		 * Away Contact - 100
   233 		 * Away Contact - 120
   234 		 * Offline Contact - 110
   235 		 * Offline Contact - 113
   236 		 * Offline Contact - 125
   237 		 * 
   238 		 * Dropping between Away Contact and Offline Contact should make an Away Contact be > 120 but an Offline Contact be < 110.
   239 		 * Only the sort controller knows the answer as to where this contact should be positioned in the end.
   240 		 */
   241 		
   242 		[container listObject: listObject didSetOrderIndex: (nextHighest + nextLowest) / 2.0];
   243 	}	
   244 }
   245 
   246 //Properties ------------------------------------------------------------------------------------------------------
   247 #pragma mark Properties
   248 /*!
   249  * @brief Called after properties have been modified; informs the contact controller.
   250  *
   251  * @param keys The properties
   252  * @param silent YES indicates that this should not trigger 'noisy' notifications - it is appropriate for notifications as an account signs on and notes tons of contacts.
   253  */
   254 - (void)didModifyProperties:(NSSet *)keys silent:(BOOL)silent
   255 {
   256 	[[AIContactObserverManager sharedManager] listObjectStatusChanged:self
   257 									modifiedStatusKeys:keys
   258 												silent:silent];
   259 }
   260 /*!
   261  * @brief Called after status changes have been modified and notifications posted
   262  *
   263  * When we notify of queued status changes, our containing group should notify as well so it can stay in sync with
   264  * any changes it may have made in object:didChangeValueForProperty:notify:
   265  *
   266  * @param silent YES indicates that this should not trigger 'noisy' notifications - it is appropriate for notifications as an account signs on and notes tons of contacts.
   267  */
   268 - (void)didNotifyOfChangedPropertiesSilently:(BOOL)silent
   269 {
   270 	//Let our containing objects know about the notification request
   271 	for (AIListContact<AIContainingObject> *container in self.containingObjects)
   272 		[container notifyOfChangedPropertiesSilently:silent];
   273 }
   274 
   275 /*!
   276  * @brief Notification of changed properties
   277  *
   278  * Subclasses may wish to override these - they must be sure to call super's implementation, too!
   279  */
   280 - (void)object:(id)inObject didChangeValueForProperty:(NSString *)key notify:(NotifyTiming)notify
   281 {				
   282 	//Inform our containing groups about the new property value
   283 	for (AIListContact<AIContainingObject> *container in self.containingObjects)
   284 		[container object:self didChangeValueForProperty:key notify:notify];
   285 	
   286 	[super object:inObject didChangeValueForProperty:key notify:notify];
   287 }
   288 
   289 //AIMutableOwnerArray delegate ------------------------------------------------------------------------------------------
   290 #pragma mark AIMutableOwnerArray delegate
   291 
   292 /*!
   293  * @brief One of our mutable owners set an object
   294  *
   295  * A mutable owner array (one of our displayArrays) set an object
   296  */
   297 - (void)mutableOwnerArray:(AIMutableOwnerArray *)inArray didSetObject:(id)anObject withOwner:(id)inOwner priorityLevel:(float)priority
   298 {
   299 	for (AIListContact<AIContainingObject> *container in self.containingObjects)
   300 		[container listObject:self mutableOwnerArray:inArray didSetObject:anObject withOwner:inOwner priorityLevel:priority];
   301 }
   302 
   303 /*!
   304  * @brief Another object changed one of our mutable owner arrays
   305  *
   306  * Empty implementation by default - we do not need to take any action when a mutable owner array changes
   307  */
   308 - (void)listObject:(AIListObject *)listObject mutableOwnerArray:(AIMutableOwnerArray *)inArray didSetObject:(id)anObject withOwner:(AIListObject *)inOwner priorityLevel:(float)priority
   309 {
   310 
   311 }
   312 
   313 //Object specific preferences ------------------------------------------------------------------------------------------
   314 #pragma mark Object specific preferences
   315 /*!
   316  * @brief Set a preference value
   317  */
   318 - (void)setPreference:(id)value forKey:(NSString *)key group:(NSString *)group
   319 {   
   320 	[adium.preferenceController setPreference:value forKey:key group:group object:self];
   321 }
   322 - (void)setPreferences:(NSDictionary *)prefs inGroup:(NSString *)group
   323 {
   324 	[adium.preferenceController setPreferences:prefs inGroup:group object:self];	
   325 }
   326 
   327 - (void)setFormattedUID:(NSString *)inFormattedUID notify:(NotifyTiming)notify
   328 {
   329 	[self setValue:inFormattedUID
   330 				   forProperty:FormattedUID
   331 				   notify:notify];
   332 }
   333 
   334 /*!
   335  * @brief Retrieve a preference value
   336  */
   337 - (id)preferenceForKey:(NSString *)key group:(NSString *)group
   338 {
   339 		return [adium.preferenceController preferenceForKey:key group:group objectIgnoringInheritance:self];
   340 }
   341 
   342 /*!
   343  * @brief Path for storing our reference file
   344  */
   345 - (NSString *)pathToPreferences
   346 {
   347     return OBJECT_PREFS_PATH;
   348 }
   349 
   350 //Display Name  -------------------------------------------------------------------------------------
   351 #pragma mark Display Name 
   352 /*
   353  * A list object basically has 4 different variations of display.
   354  *
   355  * - UID, the base UID of the contact "aiser123"
   356  * - formattedUID, formating or alteration of the UID provided by the account code "AIser 123"
   357  * - DisplayName, short formatted name provided by plugins "Adam Iser"
   358  * - LongDisplayName, long formatted name provided by plugins "Adam Iser (AIser 123)"
   359  *
   360  * A value will always be returned by these methods, so if there is no long display name present it will fall back to
   361  * display name, formattedUID, and finally UID (which is guaranteed to be present).  Use whichever one seems best
   362  * suited for what is being displayed.
   363  */
   364 
   365 /*!
   366  * @brief Server-formatted UID
   367  *
   368  * @result NSString of the server-formatted UID if present; otherwise the same as the UID
   369  */
   370 - (NSString *)formattedUID
   371 {
   372 	NSString  *outName = [self valueForProperty:FormattedUID];
   373 	return outName ? outName : UID;	
   374 }
   375 
   376 /*!
   377  * @brief Long display name
   378  *
   379  * Though in many cases the same as the display name, a long display name allows additional information about the object
   380  * to be displayed.  One preference, for example, sets a long display names formatted as "Alias (Username)".
   381  */
   382 - (NSString *)longDisplayName
   383 {
   384     NSString	*outName = [self displayArrayObjectForKey:LongDisplayName];
   385 	
   386     return outName ? outName : self.displayName;
   387 }
   388 
   389 /*!
   390 * @brief Display name
   391  *
   392  * Display name, drawing first from any externally-provided display name, then falling back to 
   393  * the formatted UID.
   394  */
   395 - (NSString *)displayName
   396 {
   397     NSString	*displayName = [self displayArrayObjectForKey:DisplayName];
   398     return displayName ? displayName : self.formattedUID;
   399 }
   400 
   401 /*!
   402 * @brief The way this object's name should be spoken
   403  *
   404  * If not found, the display name is returned.
   405  */
   406 - (NSString *)phoneticName
   407 {
   408 	NSString	*phoneticName = [self displayArrayObjectForKey:@"Phonetic Name"];
   409     return phoneticName ? phoneticName : self.displayName;
   410 }
   411 
   412 //Apply an alias
   413 - (void)setDisplayName:(NSString *)alias
   414 {
   415 	if ([alias length] == 0) alias = nil; 
   416 	
   417 	NSString	*oldAlias = [self preferenceForKey:@"Alias" group:PREF_GROUP_ALIASES];
   418 	
   419 	if ((!alias && oldAlias) ||
   420 		(alias && !([alias isEqualToString:oldAlias]))) {
   421 		//Save the alias
   422 		AILogWithSignature(@"%@: %@", self, alias);
   423 		[self setPreference:alias forKey:@"Alias" group:PREF_GROUP_ALIASES];
   424 		
   425 		//XXX - There must be a cleaner way to do this alias stuff!  This works for now :)
   426 		[[NSNotificationCenter defaultCenter] postNotificationName:Contact_ApplyDisplayName
   427 												  object:self
   428 												userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
   429 																					 forKey:@"Notify"]];
   430 	}
   431 }
   432 
   433 #pragma mark Key-Value Pairing
   434 - (NSImage *)userIcon
   435 {
   436 	return [self internalUserIcon];
   437 }
   438 - (NSImage *)internalUserIcon
   439 {
   440 	return [AIUserIcons userIconForObject:self];
   441 }
   442 
   443 - (NSData *)userIconData
   444 {
   445 	NSImage *userIcon = [self userIcon];
   446 	return ([userIcon PNGRepresentation]);
   447 }
   448 - (void)setUserIconData:(NSData *)inData
   449 {
   450 	[AIUserIcons setManuallySetUserIconData:inData forObject:self];
   451 }
   452 
   453 - (NSInteger)idleTime
   454 {
   455 	return [self integerValueForProperty:@"Idle"];
   456 }
   457 
   458 //A standard listObject is never a stranger
   459 - (BOOL)isStranger{
   460 	return NO;
   461 }
   462 
   463 - (NSString *)notes
   464 {
   465 	NSString *notes;
   466 	
   467     notes = [self preferenceForKey:@"Notes" group:PREF_GROUP_NOTES];
   468 	if (!notes) notes = [self valueForProperty:@"Notes"];
   469 	
   470 	return notes;
   471 }
   472 - (void)setNotes:(NSString *)notes
   473 {
   474 	if ([notes length] == 0) notes = nil; 
   475 
   476 	NSString	*oldNotes = [self preferenceForKey:@"Notes" group:PREF_GROUP_NOTES];
   477 	if ((!notes && oldNotes) ||
   478 		(notes && (![notes isEqualToString:oldNotes]))) {
   479 		//Save the note
   480 		[self setPreference:notes forKey:@"Notes" group:PREF_GROUP_NOTES];
   481 	}
   482 }
   483 
   484 #pragma mark Status states
   485 
   486 /*!
   487  * @brief The name for the specific status of this object
   488  *
   489  * The statusName provides further detail after the statusType.  It may be a string such as @"Busy" or @"BRB".
   490  * Possible values are determined by installed services; many default possibilities are listed in AIStatusController.h.
   491  *
   492  * The statusName may be nil if no additional status information is available for the contact. For example, an AIM
   493  * contact will never have a statusName value, as the possibilities enumerated by AIStatusType -- and therefore returned
   494  * by -AIListObject.statusType -- cover all possibilities.  An ICQ contact, on the other hand, might have a statusType
   495  * of AIAwayStatusType and then a statusName of @"Not Available" or @"DND".
   496  *
   497  * @result The statusName, or nil none exists
   498  */
   499 - (NSString *)statusName
   500 {
   501 	return [self valueForProperty:@"StatusName"];
   502 }
   503 
   504 /*!
   505  * @brief The general type of this object's status
   506  *
   507  * @result The AIStatusType for this object, indicating if it is available, away, invisible, offline, etc.
   508  */
   509 - (AIStatusType)statusType
   510 {
   511 	if (self.online) {
   512 		NSNumber *statusTypeNumber = [self valueForProperty:@"StatusType"];
   513 		if (statusTypeNumber)
   514 			return [statusTypeNumber intValue];
   515 		return AIAvailableStatusType;
   516 	}
   517 	return AIOfflineStatusType;
   518 }
   519 
   520 /*!
   521  * @brief Store the status name and type for this object
   522  *
   523  * This is used by account code to let the object know its name and status type
   524  * @param statusName The statusName, which further specifies the statusType, or nil if none is available
   525  * @param statusType The AIStatusType describing this object's status
   526  * @param notify The NotifyTiming for this operation
   527  */
   528 - (void)setStatusWithName:(NSString *)statusName statusType:(AIStatusType)statusType notify:(NotifyTiming)notify
   529 {
   530 	AIStatusType	currentStatusType = self.statusType;
   531 	NSString		*oldStatusName = self.statusName;
   532 	
   533 	if (currentStatusType != statusType) {
   534 		[self setValue:[NSNumber numberWithInt:statusType] forProperty:@"StatusType" notify:NotifyLater];
   535 	}
   536 	
   537 	if ((!statusName && oldStatusName) || (statusName && ![statusName isEqualToString:oldStatusName])) {
   538 		[self setValue:statusName forProperty:@"StatusName" notify:NotifyLater];
   539 	}
   540 	
   541 	if (notify) [self notifyOfChangedPropertiesSilently:NO];
   542 }
   543 
   544 /*!
   545  * @brief Return the status message for this object
   546  *
   547  * The statusMessage may supplement the statusType and statusName with a message describing the object's status; in AIM,
   548  * for example, both available and away statuses can have an associated, user-set message.
   549  *
   550  * @result The NSAttributedString statusMessagae, or nil if none is set
   551  */
   552 - (NSAttributedString *)statusMessage
   553 {
   554 	return [self valueForProperty:@"StatusMessage"];
   555 }
   556 
   557 /*!
   558  * @brief Return the status message for this object as NSString
   559  *
   560  * The statusMessageString may supplement the statusType and statusName with a message describing the object's status; in AIM,
   561  * for example, both available and away statuses can have an associated, user-set message.
   562  *
   563  * @result The NSString statusMessage, or nil if none is set
   564  */
   565 - (NSString *)statusMessageString;
   566 {
   567 	return [[self valueForProperty:@"StatusMessage"] string];
   568 }
   569 
   570 /*!
   571  * @brief Is this object connected via a mobile device?
   572  *
   573  * The default implementation simply returns NO.  Only an AIListContact can be mobile... but a base implementation here
   574  * makes code elsewhere much simpler.
   575  */
   576 - (BOOL)isMobile
   577 {
   578 	return NO;
   579 }
   580 
   581 /*!
   582  * @brief Is this contact blocked?
   583  *
   584  * @result A boolean indicating if the object is blocked
   585  */
   586 - (BOOL)isBlocked
   587 {
   588 	return NO;
   589 }
   590 
   591 /*!
   592  * @brief Set the current status message
   593  *
   594  * @param statusMessage Status message. May be nil.
   595  * @param notify How to notify of the change. See -[ESObjectWithProperties setValue:forProperty:notify:].
   596  */
   597 - (void)setStatusMessage:(NSAttributedString *)statusMessage notify:(NotifyTiming)notify
   598 {
   599 	if (!statusMessage ||
   600 	   ![[self valueForProperty:@"StatusMessage"] isEqualToAttributedString:statusMessage]) {
   601 		[self setValue:statusMessage forProperty:@"StatusMessage" notify:notify];
   602 	}
   603 }
   604 
   605 - (void)setBaseAvailableStatusAndNotify:(NotifyTiming)notify
   606 {
   607 	[self setStatusWithName:nil
   608 				 statusType:AIAvailableStatusType
   609 					 notify:NotifyLater];
   610 	[self setStatusMessage:nil
   611 					 notify:NotifyLater];
   612 
   613 	if (notify) [self notifyOfChangedPropertiesSilently:NO];
   614 }
   615 
   616 - (BOOL)online
   617 {
   618 	return [self boolValueForProperty:@"Online"];
   619 }
   620 
   621 - (AIStatusSummary)statusSummary
   622 {
   623 	if (self.online) {		
   624 		if (self.statusType == AIAwayStatusType || self.statusType == AIInvisibleStatusType)
   625 			return [self boolValueForProperty:@"IsIdle"] ? AIAwayAndIdleStatus : AIAwayStatus;
   626 		
   627 		if ([self boolValueForProperty:@"IsIdle"])
   628 			return AIIdleStatus;
   629 		
   630 		return AIAvailableStatus;
   631 	} 
   632 	
   633 	//We don't know the status of an stranger who isn't showing up as online
   634 	return self.isStranger ? AIUnknownStatus : AIOfflineStatus;
   635 }
   636 
   637 - (void)notifyOfChangedPropertiesSilently:(BOOL)silent
   638 {
   639 	[super notifyOfChangedPropertiesSilently:silent];
   640 }
   641 
   642 /*!
   643  * @brief Are sounds for this object muted?
   644  */
   645 - (BOOL)soundsAreMuted
   646 {
   647 	return NO;
   648 }
   649 
   650 #pragma mark Methods for AIContainingObject-compliant classes to inherit
   651 - (void)listObject:(AIListObject *)listObject didSetOrderIndex:(float)orderIndexForObject
   652 {
   653 	NSDictionary		*dict = [self preferenceForKey:@"OrderIndexDictionary"
   654 												 group:ObjectStatusCache];
   655 	NSMutableDictionary *newDict = (dict ? [[dict mutableCopy] autorelease] : [NSMutableDictionary dictionary]);
   656 	
   657 	// Sanity check - are we trying to assign infinity?
   658 	if (orderIndexForObject == INFINITY) {
   659 		AILogWithSignature(@"Correcting for INFINITY index, inObj=%@ allObj=%@", listObject, [newDict allKeysForObject:[NSNumber numberWithFloat:INFINITY]]);
   660 		
   661 		// Remove any objects that currently are currently set to INFINITY, they'll regenerate their position to the last place.
   662 		for (NSString *key in [newDict allKeysForObject:[NSNumber numberWithFloat:INFINITY]]) {
   663 			[newDict removeObjectForKey:key];
   664 		}
   665 		
   666 		// Update the preference.
   667 		[self setPreference:newDict
   668 					 forKey:@"OrderIndexDictionary"
   669 					  group:ObjectStatusCache];
   670 		
   671 		// Update our largest cache.
   672 		[self updateOrderCache];
   673 		
   674 		// Assume an index of largest+1
   675 		orderIndexForObject = self.largestOrder + 1;
   676 	}
   677 	
   678 	NSNumber *orderIndexForObjectNumber = [NSNumber numberWithFloat:orderIndexForObject];
   679 	
   680 	//Prevent setting an order index which we already have
   681 	NSArray *existingKeys = [dict allKeysForObject:orderIndexForObjectNumber];
   682 	while (existingKeys.count && ![existingKeys isEqualToArray:[NSArray arrayWithObject:listObject.internalObjectID]]) {
   683 		if (existingKeys.count == 1) {
   684 			AILogWithSignature(@"*** Warning: %@ had order index %f, but %@ already had an object with that order index. Setting to %f instead. Incrementing.",
   685 							   listObject, orderIndexForObject, self, orderIndexForObject+1);
   686 
   687 			orderIndexForObject++;
   688 			orderIndexForObjectNumber = [NSNumber numberWithFloat:orderIndexForObject];
   689 			existingKeys = [dict allKeysForObject:orderIndexForObjectNumber];
   690 			
   691 		} else {
   692 			/* How could this happen? -evands */
   693 			AILogWithSignature(@"More than one object has %f! We'll grant it to %@", orderIndexForObject, listObject);
   694 
   695 			for (NSString *key in [existingKeys objectEnumerator]) {
   696 				[newDict removeObjectForKey:key];
   697 			}
   698 
   699 			existingKeys = nil;
   700 		}		
   701 	}
   702 
   703 	[newDict setObject:orderIndexForObjectNumber
   704 				forKey:listObject.internalObjectID];
   705 	
   706 	[self setPreference:newDict
   707 				 forKey:@"OrderIndexDictionary"
   708 				  group:ObjectStatusCache];
   709 	
   710 	[self updateOrderCache];
   711 }
   712 
   713 //Order index
   714 - (float)orderIndexForObject:(AIListObject *)listObject
   715 {
   716 	NSDictionary *dict = [self preferenceForKey:@"OrderIndexDictionary"
   717 										  group:ObjectStatusCache 
   718 						 ];
   719 	NSNumber *orderIndexForObjectNumber = [dict objectForKey:listObject.internalObjectID];
   720 	float orderIndexForObject = (orderIndexForObjectNumber ? [orderIndexForObjectNumber floatValue] : 0);
   721 	
   722 	//Evan: I don't know how we got up to infinity.. perhaps pref corruption in a previous version?
   723 	//In any case, check against it; if we stored it, reset to a reasonable number.
   724 	//XXX is this still needed?
   725 	if  (!(orderIndexForObject < INFINITY)) orderIndexForObject = 0;
   726 
   727 	if (!orderIndexForObject) {
   728 		orderIndexForObject = self.largestOrder + 1;
   729 		[(AIListObject<AIContainingObject> *)self listObject:listObject didSetOrderIndex: orderIndexForObject];
   730 	}
   731 	
   732 	return orderIndexForObject;
   733 }
   734 
   735 - (CGFloat)smallestOrder
   736 {
   737 	if (!cachedSmallestOrder) {
   738 		[self updateOrderCache];
   739 	}
   740 
   741 	return cachedSmallestOrder;
   742 }
   743 
   744 - (CGFloat)largestOrder
   745 {
   746 	if (!cachedLargestOrder) {
   747 		[self updateOrderCache];
   748 	}
   749 	
   750 	return cachedLargestOrder;	
   751 }
   752 
   753 - (void)updateOrderCache
   754 {
   755 	CGFloat smallest = INFINITY, largest = 0;
   756 	
   757 	NSDictionary *orderIndex = [self preferenceForKey:@"OrderIndexDictionary" group:ObjectStatusCache];
   758 	
   759 	for (NSNumber *index in orderIndex.allValues) {
   760 		smallest = MIN(smallest, index.floatValue);
   761 		largest = MAX(largest, index.floatValue);
   762 	}
   763 	
   764 	cachedSmallestOrder = (smallest == INFINITY ? 1 : smallest);
   765 	cachedLargestOrder = largest;
   766 }
   767 
   768 #pragma mark Comparison
   769 /*
   770 - (BOOL)isEqual:(id)anObject
   771 {
   772 	return ([anObject isMemberOfClass:[self class]] &&
   773 			[[(AIListObject *)anObject internalObjectID] isEqualToString:self.internalObjectID]);
   774 }
   775 */
   776 
   777 - (NSComparisonResult)compare:(AIListObject *)other {
   778 	NSParameterAssert([other isKindOfClass:[AIListObject class]]);
   779 	return [self.internalObjectID caseInsensitiveCompare:other.internalObjectID];
   780 }
   781 
   782 #pragma mark Icons
   783 - (NSImage *)menuIcon
   784 {
   785 	return [AIUserIcons menuUserIconForObject:self];
   786 }
   787 
   788 - (NSImage *)statusIcon
   789 {
   790 	NSImage *statusIcon = [self valueForProperty:@"List State Icon"];
   791 	if (!statusIcon) statusIcon = [self valueForProperty:@"List Status Icon"];
   792 	if (!statusIcon) statusIcon = [AIStatusIcons statusIconForUnknownStatusWithIconType:AIStatusIconList
   793 																			 direction:AIIconNormal];
   794 	return statusIcon;
   795 }
   796 
   797 #pragma mark Debugging
   798 - (NSString *)description
   799 {
   800 	return [NSString stringWithFormat:@"<%@:%x %@>",NSStringFromClass([self class]), self, self.internalObjectID];
   801 }
   802 
   803 #pragma mark Applescript
   804 - (int)scriptingStatusType
   805 {
   806 	AIStatusType statusType = self.statusType;
   807 	switch (statusType) {
   808 		case AIAvailableStatusType:
   809 			return AIAvailableStatusTypeAS;
   810 		case AIOfflineStatusType:
   811 			return AIOfflineStatusTypeAS;
   812 		case AIAwayStatusType:
   813 			return AIAwayStatusTypeAS;
   814 		case AIInvisibleStatusType:
   815 			return AIInvisibleStatusTypeAS;
   816 	}
   817 	return 0;
   818 }
   819 
   820 /**
   821  * @brief Returns the current status message as rich text
   822  */
   823 - (NSTextStorage *)scriptingStatusMessage
   824 {
   825 	return [[[NSTextStorage alloc] initWithAttributedString:self.statusMessage] autorelease];
   826 }
   827 
   828 @end
   829 
   830 /*
   831  * Trivial plugin compatibility; these methods were removed from the public API
   832  * but have trivial new-API implementations
   833  */
   834 @implementation AIListObject (PluginCompatibility)
   835 - (NSString *)serviceID
   836 {
   837 	return self.service.serviceID;
   838 }
   839 @end