Frameworks/Adium Framework/Source/AIListContact.m
author Zachary West <zacw@adium.im>
Fri Nov 27 15:50:57 2009 -0500 (2009-11-27)
changeset 2835 1e8c89f99dfe
parent 2834 e7db526620d1
child 3033 0b981d5c2c76
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/AIContactControllerProtocol.h>
    18 #import <Adium/AIChatControllerProtocol.h>
    19 #import <Adium/AIContentControllerProtocol.h>
    20 #import <Adium/AIStatusControllerProtocol.h>
    21 #import <Adium/AIContentMessage.h>
    22 #import <Adium/AIListContact.h>
    23 #import <Adium/AIContactList.h>
    24 #import <Adium/AIListGroup.h>
    25 #import <Adium/AIMetaContact.h>
    26 #import <Adium/AIService.h>
    27 #import <Adium/AIUserIcons.h>
    28 #import <Adium/ESFileTransfer.h>
    29 #import <Adium/AIStatus.h>
    30 #import <Adium/AIHTMLDecoder.h>
    31 
    32 #import <AIUtilities/AIMutableOwnerArray.h>
    33 #import <AIUtilities/AIMutableStringAdditions.h>
    34 
    35 #import <AvailabilityMacros.h>
    36 
    37 #import "AIAddressBookController.h"
    38 
    39 #define KEY_BASE_WRITING_DIRECTION		@"Base Writing Direction"
    40 #define PREF_GROUP_WRITING_DIRECTION	@"Writing Direction"
    41 
    42 #define CONTACT_SIGN_ON_OR_OFF_PERSISTENCE_DELAY 15
    43 
    44 @interface AIListObject ()
    45 - (void)setContainingObject:(AIListObject <AIContainingObject> *)inGroup;
    46 @end
    47 
    48 @interface AIListContact ()
    49 @property (readwrite, nonatomic, assign) AIMetaContact *metaContact;
    50 - (void) remoteGroupingChanged;
    51 @end
    52 
    53 @implementation AIListContact
    54 
    55 //Init with an account
    56 - (id)initWithUID:(NSString *)inUID account:(AIAccount *)inAccount service:(AIService *)inService
    57 {
    58 	if ((self = [self initWithUID:inUID service:inService])) {
    59 		account = [inAccount retain];
    60 	}
    61 	
    62 	return self;
    63 }
    64 
    65 //Standard init
    66 - (id)initWithUID:(NSString *)inUID service:(AIService *)inService
    67 {
    68 	if ((self = [super initWithUID:inUID service:inService])) {
    69 		account = nil;
    70 		m_remoteGroupNames = [[NSMutableSet alloc] initWithCapacity:1];
    71 		internalUniqueObjectID = nil;
    72 	}
    73 
    74 	return self;
    75 }
    76 
    77 - (void)dealloc
    78 {
    79 	[account release]; account = nil;
    80 	[m_remoteGroupNames release]; m_remoteGroupNames = nil;
    81 	[internalUniqueObjectID release]; internalUniqueObjectID = nil;
    82 	
    83 	[super dealloc];
    84 }
    85 
    86 //The account that owns this contact
    87 @synthesize account;
    88 
    89 /*!
    90  * @brief Set the UID of this contact
    91  *
    92  * The UID for an AIListContact generally shouldn't change... if the contact is actually renamed serverside, however,
    93  * it is useful to change the UID without having to change everything else associated with it.
    94  */
    95 - (void)setUID:(NSString *)inUID
    96 {
    97 	if (UID != inUID) {
    98 		[UID release]; UID = [inUID retain];
    99 		[internalObjectID release]; internalObjectID = nil;
   100 		[internalUniqueObjectID release]; internalUniqueObjectID = nil;		
   101 	}
   102 }
   103 
   104 //An object ID generated by Adium that is completely unique to this contact.  This ID is generated from the service ID, 
   105 //UID, and account UID.  Adium will not allow multiple contacts with the same internalUniqueObjectID to be created.
   106 - (NSString *)internalUniqueObjectID
   107 {
   108 	if (!internalUniqueObjectID) {
   109 		internalUniqueObjectID = [[AIListContact internalUniqueObjectIDForService:self.service
   110 																		  account:self.account
   111 																			  UID:self.UID] retain];
   112 	}
   113 	return internalUniqueObjectID;
   114 }
   115 
   116 //Generate a unique object ID for the passed object
   117 + (NSString *)internalUniqueObjectIDForService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
   118 {
   119 	return [NSString stringWithFormat:@"%@.%@.%@", inService.serviceClass, inAccount.UID, inUID];
   120 }
   121 
   122 
   123 //Remote Grouping ------------------------------------------------------------------------------------------------------
   124 #pragma mark Remote Grouping
   125 
   126 - (NSSet *) remoteGroupNames
   127 {
   128 	return [[m_remoteGroupNames copy] autorelease];
   129 }
   130 
   131 - (void) setRemoteGroupNames:(NSSet *)inGroupNames
   132 {
   133 	NSParameterAssert(inGroupNames != nil);
   134 	[m_remoteGroupNames setSet:inGroupNames];
   135 	[self remoteGroupingChanged];
   136 }
   137 
   138 - (void) addRemoteGroupName:(NSString *)inName
   139 {
   140 	NSParameterAssert(inName != nil);
   141 	if ([m_remoteGroupNames containsObject:inName])
   142 		return;
   143 	
   144 	[m_remoteGroupNames addObject:inName];
   145 	[self remoteGroupingChanged];
   146 }
   147 
   148 - (void) removeRemoteGroupName:(NSString *)inName
   149 {
   150 	NSParameterAssert(inName != nil);
   151 	if (![m_remoteGroupNames containsObject:inName])
   152 		return;
   153 	
   154 	[m_remoteGroupNames removeObject:inName];
   155 	[self remoteGroupingChanged];
   156 }
   157 
   158 - (NSUInteger) countOfRemoteGroupNames
   159 {
   160 	return m_remoteGroupNames.count;
   161 }
   162 
   163 - (NSSet *)remoteGroups
   164 {
   165 	NSMutableSet *groups = [NSMutableSet set];
   166 	for (NSString *remoteGroup in m_remoteGroupNames) {
   167 		[groups addObject:[adium.contactController groupWithUID:remoteGroup]];
   168 	}
   169 	return groups;
   170 }
   171 
   172 - (void) remoteGroupingChanged
   173 {
   174 	NSUInteger remoteGroupCount = m_remoteGroupNames.count;
   175 	if (remoteGroupCount == 0)
   176 		[AIUserIcons flushCacheForObject:self];
   177 	
   178 	[self restoreGrouping];
   179 	
   180 	if (self.isStranger != (remoteGroupCount == 0)) {
   181 		[self setValue:[NSNumber numberWithBool:remoteGroupCount > 0]
   182 		   forProperty:@"NotAStranger"
   183 				notify:NotifyLater];
   184 		[self notifyOfChangedPropertiesSilently:YES];
   185 	}
   186 }
   187 
   188 //An AIListContact normally groups based on its remoteGroupNames (if it is not within a metaContact). 
   189 //Restore this grouping.
   190 - (void)restoreGrouping
   191 {
   192 	if (self.metaContact) {
   193 		[self.metaContact updateRemoteGroupingOfContact:self];		
   194 		return;
   195 	}
   196 	
   197 	//Create a group for the contact even if contact list groups aren't on,
   198 	//otherwise requests for all the contact list groups will return nothing
   199 	NSMutableSet *groups = [NSMutableSet set];
   200 	for (NSString *remoteGroupName in m_remoteGroupNames) {
   201 		AIListGroup *localGroup = [adium.contactController groupWithUID:remoteGroupName];
   202 		
   203 		if (!adium.contactController.useContactListGroups)
   204 			localGroup = adium.contactController.contactList;
   205 		else if (adium.contactController.useOfflineGroup && !self.online && !self.alwaysVisible)
   206 			localGroup = adium.contactController.offlineGroup;
   207 		
   208 		[groups addObject:localGroup];
   209 	}
   210 	[adium.contactController _moveContactLocally:self fromGroups:self.groups toGroups:groups];
   211 }
   212 
   213 #pragma mark Names
   214 /*!
   215  * @brief Display name
   216  *
   217  * Display name, drawing first from any externally-provided display name, then falling back to 
   218  * the formatted UID.
   219  *
   220  * A listContact attempts to have the same displayName as its containing contact (potentially its metaContact).
   221  * If it is not in a metaContact, its display name is returned by super.displayName
   222  */
   223 - (NSString *)displayName
   224 {
   225 	AIMetaContact	*meta = self.metaContact;
   226 
   227 	NSString *displayName = meta ? meta.displayName : super.displayName;
   228 
   229 	//If a display name was found, return it; otherwise, return the formattedUID  
   230 	return displayName ? displayName : self.formattedUID;
   231 }
   232 
   233 /*!
   234  * @brief Own display name
   235  *
   236  * Returns the display name without trying to account for a metaContact. Exists for use by AIMetaContact to avoid
   237  * infinite recursion by its displayName calling our displayName calling its displayName and so on.
   238  */
   239 - (NSString *)ownDisplayName
   240 {
   241 	return super.displayName;
   242 }
   243 
   244 /*!
   245  * @brief This contact's serverside display name, which is generally specificed by the contact remotely
   246  *
   247  * @result The serverside display name, or nil if none is set
   248  */
   249 - (NSString *)serversideDisplayName
   250 {
   251 	return [self valueForProperty:@"Server Display Name"];	
   252 }
   253 
   254 - (void)setServersideAlias:(NSString *)alias 
   255 				  silently:(BOOL)silent
   256 {
   257 	BOOL changes = NO;
   258 	BOOL displayNameChanges = NO;
   259 	
   260 	AILogWithSignature(@"%@ received alias %@", self, alias);
   261 	
   262 	//This is the server display name.  Set it as such.
   263 	if (![alias isEqualToString:[self valueForProperty:@"Server Display Name"]]) {
   264 		//Set the server display name property as the full display name
   265 		[self setValue:alias
   266 					   forProperty:@"Server Display Name"
   267 					   notify:NotifyLater];
   268 		
   269 		changes = YES;
   270 	}
   271 
   272 	NSMutableString *cleanedAlias;
   273 	
   274 	//Remove any newlines, since we won't want them anywhere below
   275 	cleanedAlias = [alias mutableCopy];
   276 	[cleanedAlias convertNewlinesToSlashes];
   277 
   278 	AIMutableOwnerArray	*displayNameArray = [self displayArrayForKey:@"Display Name"];
   279 	NSString			*oldDisplayName = [displayNameArray objectValue];
   280 	
   281 	//If the mutableOwnerArray's current value isn't identical to this alias, we should set it
   282 	if (![[displayNameArray objectWithOwner:self.account] isEqualToString:cleanedAlias]) {
   283 		[displayNameArray setObject:cleanedAlias
   284 						  withOwner:self.account
   285 					  priorityLevel:Low_Priority];
   286 		
   287 		//If this causes the object value to change, we need to request a manual update of the display name
   288 		if (oldDisplayName != [displayNameArray objectValue]) {
   289 			displayNameChanges = YES;
   290 		}
   291 	}
   292 	
   293 	if (changes) {
   294 		//Apply any changes
   295 		[self notifyOfChangedPropertiesSilently:silent];
   296 	}
   297 	
   298 	if (displayNameChanges) {
   299 		//Request an alias change
   300 		[[NSNotificationCenter defaultCenter] postNotificationName:Contact_ApplyDisplayName
   301 												  object:self
   302 												userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
   303 																					 forKey:@"Notify"]];
   304 	}
   305 	
   306 	[cleanedAlias release];
   307 }
   308 
   309 /*!
   310  * @brief The way this object's name should be spoken
   311  *
   312  * If not found, the display name is returned.
   313  */
   314 - (NSString *)phoneticName
   315 {
   316 	AIMetaContact *meta = self.metaContact;
   317 	NSString		*phoneticName;
   318 
   319 	phoneticName = meta ? meta.phoneticName : super.phoneticName;;
   320 	
   321 	//If a display name was found, return it; otherwise, return the formattedUID
   322 	return phoneticName ? phoneticName : self.displayName;
   323 }
   324 
   325 /*!
   326  * @brief Own phonetic name
   327  *
   328  * Returns the phonetic name without trying to account for a metaContact. Exists for use by AIMetaContact to avoid
   329  * infinite recursion by its phoneticName calling our phoneticName calling its phoneticName and so on.
   330  */
   331 - (NSString *)ownPhoneticName
   332 {
   333 	return super.phoneticName;
   334 }
   335 
   336 #pragma mark Properties
   337 
   338 /*!
   339  * @brief Set online
   340  */
   341 - (void)setOnline:(BOOL)online notify:(NotifyTiming)notify silently:(BOOL)silent
   342 {
   343 	if (online != self.online) {
   344 		[self setValue:[NSNumber numberWithBool:online]
   345 					   forProperty:@"Online"
   346 					   notify:notify];
   347 		
   348 		if (!silent) {
   349 			[self setValue:[NSNumber numberWithBool:YES] 
   350 						   forProperty:(online ? @"Signed On" : @"Signed Off")
   351 						   notify:notify];
   352 			[self setValue:nil 
   353 						   forProperty:(online ? @"Signed Off" : @"Signed On")
   354 						   notify:notify];
   355 			[self setValue:nil
   356 						   forProperty:(online ? @"Signed On" : @"Signed Off")
   357 					   afterDelay:CONTACT_SIGN_ON_OR_OFF_PERSISTENCE_DELAY];
   358 		}
   359 		
   360 		if (online) {
   361 			if (notify == NotifyNow) {
   362 				[self notifyOfChangedPropertiesSilently:silent];
   363 			}
   364 			
   365 		} else {
   366 			//Will always notify
   367 			[self.account removePropertyValuesFromContact:self
   368 												  silently:silent];	
   369 		}
   370 	}
   371 }
   372 
   373 /*!
   374  * @brief Set the sign on date
   375  */
   376 - (void)setSignonDate:(NSDate *)signonDate notify:(NotifyTiming)notify
   377 {
   378 	[self setValue:signonDate
   379 				   forProperty:@"Signon Date"
   380 				   notify:notify];
   381 }
   382 /*!
   383  * @brief Date this contact signed on, if available
   384  */
   385 - (NSDate *)signonDate
   386 {
   387 	return [self valueForProperty:@"Signon Date"];
   388 }
   389 
   390 /*!
   391  * @brief Set the idle state
   392  *
   393  * @param isIdle YES if the contact is idle
   394  * @param idleSinceDate The date this contact went idle. Only relevant if isIdle is YES
   395  * @param notify The NotifyTiming
   396  */
   397 - (void)setIdle:(BOOL)isIdle sinceDate:(NSDate *)idleSinceDate notify:(NotifyTiming)notify
   398 {
   399 	if (isIdle) {
   400 		if (idleSinceDate) {
   401 			[self setValue:idleSinceDate
   402 						   forProperty:@"IdleSince"
   403 						   notify:NotifyLater];
   404 		} else {
   405 			//No idleSinceDate means we are Idle but don't know how long, so set to -1
   406 			[self setValue:[NSNumber numberWithInt:-1]
   407 						   forProperty:@"Idle"
   408 						   notify:NotifyLater];
   409 		}
   410 	} else {
   411 		[self setValue:nil
   412 					   forProperty:@"IdleSince"
   413 					   notify:NotifyLater];
   414 		[self setValue:nil
   415 					   forProperty:@"Idle"
   416 					   notify:NotifyLater];
   417 	}
   418 	
   419 	/* @"Idle", for a contact with an IdleSince date, will be changing every minute.  @"IsIdle" provides observers a way
   420 	* to perform an action when the contact becomes/comes back from idle, regardless of whether an IdleSince is available,
   421 	* without having to do that action every minute for other contacts.
   422 	*/
   423 	[self setValue:[NSNumber numberWithBool:isIdle]
   424 				   forProperty:@"IsIdle"
   425 				   notify:NotifyLater];
   426 	
   427 	//Apply any changes
   428 	if (notify == NotifyNow) {
   429 		[self notifyOfChangedPropertiesSilently:NO];
   430 	}
   431 }
   432 
   433 - (void)setServersideIconData:(NSData *)iconData notify:(NotifyTiming)notify
   434 {
   435 	[AIUserIcons setServersideIconData:iconData forObject:self notify:notify];
   436 }
   437 
   438 /*!
   439  * @brief Set the warning level
   440  *
   441  * @param warningLevel The warning level, an integer between 0 and 100
   442  * @param notify The NotifyTiming
   443  */
   444 - (void)setWarningLevel:(int)warningLevel notify:(NotifyTiming)notify
   445 {
   446 	if (warningLevel != self.warningLevel) {
   447 		[self setValue:[NSNumber numberWithInt:warningLevel]
   448 					   forProperty:@"Warning"
   449 					   notify:notify];
   450 	}
   451 }
   452 
   453 /*!
   454  * @brief Warning level
   455  *
   456  * @result The warning level, an integer between 0 and 100
   457  */
   458 - (NSInteger)warningLevel
   459 {
   460 	return [self integerValueForProperty:@"Warning"];
   461 }
   462 
   463 /*!
   464  * @brief Set the profile array
   465  */
   466 - (void)setProfileArray:(NSArray *)array notify:(NotifyTiming)notify
   467 {
   468 	[self setValue:array
   469 	   forProperty:@"ProfileArray"
   470 			notify:notify];
   471 }
   472 
   473 /*!
   474  * @brief The profile array
   475  */
   476 - (NSArray *)profileArray
   477 {
   478 	return [self valueForProperty:@"ProfileArray"];	
   479 }
   480 
   481 /*!
   482  * @brief Set the profile
   483  */
   484 - (void)setProfile:(NSAttributedString *)profile notify:(NotifyTiming)notify
   485 {
   486 	[self setValue:profile
   487 				   forProperty:@"TextProfile" 
   488 				   notify:notify];
   489 }
   490 
   491 /*!
   492  * @brief Profile
   493  */
   494 - (NSAttributedString *)profile
   495 {
   496 	return [self valueForProperty:@"TextProfile"];
   497 }
   498 
   499 /*!
   500  * @brief Is this contact a stranger?
   501  * 
   502  * A listContact is a stranger if it has a nil remoteGroupName
   503  */
   504 - (BOOL)isStranger
   505 {
   506 	return ![self boolValueForProperty:@"NotAStranger"];
   507 }
   508 
   509 /*!
   510  * @brief If this contact intentionally on the contact list?
   511  */
   512 - (BOOL)isIntentionallyNotAStranger
   513 {
   514 	return !self.isStranger && [self.account isContactIntentionallyListed:self];
   515 }
   516 
   517 /*!
   518  * @brief Is this object connected via a mobile device?
   519  */
   520 - (BOOL)isMobile
   521 {
   522 	return [self boolValueForProperty:@"IsMobile"];
   523 }
   524 
   525 /*!
   526  * @brief Set if this contact is mobile
   527  */
   528 - (void)setIsMobile:(BOOL)isMobile notify:(NotifyTiming)notify
   529 {
   530 	[self setValue:[NSNumber numberWithBool:isMobile]
   531 				   forProperty:@"IsMobile"
   532 				   notify:notify];
   533 }
   534 
   535 /*!
   536  * @brief Is this contact blocked?
   537  *
   538  * @result A boolean indicating if the contact is blocked or not
   539  */
   540 - (BOOL)isBlocked
   541 {
   542 	return [self boolValueForProperty:KEY_IS_BLOCKED];
   543 }
   544 
   545 - (void)setIsBlocked:(BOOL)yesOrNo updateList:(BOOL)addToPrivacyLists
   546 {
   547 	[self setIsOnPrivacyList:yesOrNo updateList:addToPrivacyLists privacyType:AIPrivacyTypeDeny];
   548 }
   549 
   550 - (void)setIsAllowed:(BOOL)yesOrNo updateList:(BOOL)addToPrivacyLists
   551 {
   552 	[self setIsOnPrivacyList:yesOrNo updateList:addToPrivacyLists privacyType:AIPrivacyTypePermit];
   553 }
   554 
   555 /*!
   556  * @brief Set if this contact is on the privacy list
   557  */
   558 - (void)setIsOnPrivacyList:(BOOL)shouldBeBlocked updateList:(BOOL)addToPrivacyLists privacyType:(AIPrivacyType)privType
   559 {
   560 	if (addToPrivacyLists) {		//caller of this method wants to actually block or unblock the contact, rather than just update the property
   561 		
   562 		if (![self.account conformsToProtocol:@protocol(AIAccount_Privacy)]) {
   563 			NSLog(@"Privacy is not supported on contacts for the account: %@", self.account);
   564 			return;
   565 		}
   566 		
   567 		id<AIAccount_Privacy> contactAccount = (id<AIAccount_Privacy>)self.account;
   568 		
   569 		BOOL isBlocked = [[contactAccount listObjectsOnPrivacyList:privType] containsObject:self];
   570 		
   571 		if (shouldBeBlocked == isBlocked)
   572 			return;
   573 		
   574 		BOOL	result = NO;
   575 
   576 		if (shouldBeBlocked)
   577 			result = [contactAccount addListObject:self toPrivacyList:privType];
   578 		else
   579 			result = [contactAccount removeListObject:self fromPrivacyList:privType];
   580 		
   581 		//Don't update the property if we didn't change anything
   582 		if (!result)
   583 			return;
   584 	} 
   585 
   586 	[self setValue:[NSNumber numberWithBool:((privType == AIPrivacyTypeDeny) == shouldBeBlocked)]
   587 				   forProperty:KEY_IS_BLOCKED
   588 				   notify:NotifyNow];
   589 }
   590 
   591 - (AIEncryptedChatPreference)encryptedChatPreferences {
   592 	AIEncryptedChatPreference	pref = EncryptedChat_Default;
   593 	
   594 	//Get the contact's preference (or metacontact's)
   595 	NSNumber *prefNumber = [self.parentContact preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE group:GROUP_ENCRYPTION];
   596 	
   597 	//If that turned up nothing, check all the groups it's in
   598 	if (!prefNumber || [prefNumber integerValue] == EncryptedChat_Default) {
   599 		for (AIListGroup *group in self.parentContact.groups)
   600 		{
   601 			if ((prefNumber = [group preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE group:GROUP_ENCRYPTION]))
   602 				break;
   603 		}	
   604 	}
   605 	
   606 	//If that turned up nothing, check global prefs
   607 	if (!prefNumber)
   608 		prefNumber = [adium.preferenceController preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE group:GROUP_ENCRYPTION];
   609 	
   610 	//If no contact preference or the contact is set to use the default, use the account preference
   611 	if (!prefNumber || ([prefNumber integerValue] == EncryptedChat_Default)) {
   612 		prefNumber = [self.account preferenceForKey:KEY_ENCRYPTED_CHAT_PREFERENCE
   613 					  group:GROUP_ENCRYPTION];		
   614 	}
   615 	
   616 	if (prefNumber)
   617 		pref = [prefNumber integerValue];
   618 	
   619 	return pref;
   620 }
   621 
   622 - (void)setAlwaysVisible:(BOOL)inVisible
   623 {
   624 	[super setAlwaysVisible:inVisible];
   625 	
   626 	[self restoreGrouping];
   627 }
   628 
   629 - (BOOL)alwaysVisible
   630 {
   631 	if (self.metaContact) {
   632 		return self.metaContact.alwaysVisible;
   633 	}
   634 	
   635 	return [super alwaysVisible];
   636 }
   637 
   638 #pragma mark Status
   639 
   640 /*!
   641 * @brief Determine the status message to be displayed in the contact list
   642  *
   643  * Look at the contact's status message.
   644  * Failing that, look for a statusName, which might be something like "DND" or "Free for Chat"
   645  * and look up the localized description of it.
   646  */
   647 - (NSAttributedString *)contactListStatusMessage
   648 {
   649 	NSAttributedString	*contactListStatusMessage = self.statusMessage;
   650 
   651 	if (!contactListStatusMessage) {
   652 		NSString			*statusName = self.statusName;
   653 		
   654 		if (statusName) {
   655 			NSString *descriptionOfStatus = [adium.statusController localizedDescriptionForStatusName:statusName
   656 											 statusType:self.statusType];
   657 			
   658 			if (descriptionOfStatus)
   659 				contactListStatusMessage = [[[NSAttributedString alloc] initWithString:descriptionOfStatus] autorelease];			
   660 		}
   661 	}
   662 
   663 	return contactListStatusMessage;	
   664 }
   665 
   666 /*!
   667  * @brief Are sounds for this contact muted?
   668  */
   669 - (BOOL)soundsAreMuted
   670 {
   671 	return [self.account.statusState mutesSound];
   672 }
   673 
   674 #pragma mark Parents
   675 
   676 /*!
   677  * @brief This object's parent AIListContact
   678  *
   679  * The parent AIListContact is the appropriate place to apply preferences specific to this contact so that such
   680  * preferences are also applied to other AIListContacts in the same meta contact, if necessary.
   681  *
   682  * @result Either this contact or some more-encompassing contact which ultimately contains it.
   683  */
   684 - (AIListContact *)parentContact
   685 {
   686 	return self.metaContact ?: self;
   687 }
   688 
   689 - (BOOL)containsObject:(AIListObject*)object
   690 {
   691     return NO;
   692 }
   693 
   694 - (NSSet *) containingObjects {
   695 	if (metaContact)
   696 		return [NSSet setWithObject:metaContact];
   697 	return super.containingObjects;
   698 }
   699 
   700 /*!
   701  * @brief Can this object be part of a metacontact?
   702  */
   703 - (BOOL)canJoinMetaContacts
   704 {
   705 	return YES;
   706 }
   707 
   708 - (AIMetaContact *)metaContact
   709 {
   710 	return metaContact;
   711 }
   712 
   713 - (void) setMetaContact:(AIMetaContact *)meta
   714 {
   715 	metaContact = meta;
   716 	[m_groups removeAllObjects];
   717 }
   718 
   719 - (BOOL) existsServerside
   720 {
   721 	return YES;
   722 }
   723 
   724 - (void)removeFromGroup:(AIListObject <AIContainingObject> *)group
   725 {
   726 	if (self.account.online) {
   727 		if (group == adium.contactController.contactList
   728 			|| group == adium.contactController.offlineGroup) {
   729 			[self.account removeContacts:[NSArray arrayWithObject:self]
   730 							  fromGroups:[self.remoteGroups allObjects]];	
   731 		} else {			
   732 			[self.account removeContacts:[NSArray arrayWithObject:self]
   733 							  fromGroups:[NSArray arrayWithObject:group]];	
   734 		}
   735 	}
   736 }
   737 
   738 #pragma mark Equality
   739 /*
   740 - (BOOL)isEqual:(id)anObject
   741 {
   742 	return ([anObject isMemberOfClass:[self class]] &&
   743 			[[(AIListContact *)anObject internalUniqueObjectID] isEqualToString:[self internalUniqueObjectID]]);
   744 }
   745 */
   746 //AppleScript ----------------------------------------------------------------------------------------------------------
   747 #pragma mark AppleScript
   748 
   749 - (id)sendScriptCommand:(NSScriptCommand *)command {
   750 	NSDictionary	*evaluatedArguments = [command evaluatedArguments];
   751 	NSString			*message = [evaluatedArguments objectForKey:@"message"];
   752 	AIAccount		*targetAccount = [evaluatedArguments objectForKey:@"account"];
   753 	NSString			*filePath = [evaluatedArguments objectForKey:@"filePath"];
   754 	
   755 	AIListContact   *targetMessagingContact = self;
   756 	AIListContact   *targetFileTransferContact = nil;
   757 
   758 	if (targetAccount) {
   759 		if (self.account != account)
   760 			targetMessagingContact = [adium.contactController contactWithService:self.service account:account UID:self.UID];
   761 
   762 		targetFileTransferContact = targetMessagingContact;
   763 	}
   764 	
   765 	//Send any message we were told to send
   766 	if (message && [message length]) {
   767 		AIChat			*chat;
   768 		BOOL			autoreply = [[evaluatedArguments objectForKey:@"autoreply"] boolValue];
   769 		
   770 		//Make sure we know where we are sending the message - if we don't have a target yet, find the best contact for
   771 		//sending CONTENT_MESSAGE_TYPE.
   772 		if (!targetMessagingContact) {
   773 			//Get the target contact.  This could be the same contact, an identical contact on another account, 
   774 			//or a subcontact (if we're talking about a metaContact, for example)
   775 			targetMessagingContact = [adium.contactController preferredContactForContentType:CONTENT_MESSAGE_TYPE
   776 																				forListContact:self];
   777 			targetAccount = targetMessagingContact.account;	
   778 		}
   779 		
   780 		if (targetMessagingContact) {
   781 			chat = [adium.chatController openChatWithContact:targetMessagingContact
   782 											onPreferredAccount:NO];
   783 			
   784 			//Take the string and turn it into an attributed string (in case we were passed HTML)
   785 			NSAttributedString  *attributedMessage = [AIHTMLDecoder decodeHTML:message];
   786 			AIContentMessage	*messageContent;
   787 			messageContent = [AIContentMessage messageInChat:chat
   788 												  withSource:targetAccount
   789 												 destination:targetMessagingContact
   790 														date:nil
   791 													 message:attributedMessage
   792 												   autoreply:autoreply];
   793 			
   794 			[adium.contentController sendContentObject:messageContent];
   795 		} else {
   796 			AILogWithSignature(@"No contact available to receive a message to %@", self);
   797 		}
   798 	}
   799 	
   800 	//Send any file we were told to send
   801 	if (filePath && [filePath length]) {
   802 		//Make sure we know where we are sending the file - if we don't have a target yet, find the best contact for
   803 		//sending CONTENT_FILE_TRANSFER_TYPE.
   804 		if (!targetFileTransferContact) {
   805 			//Get the target contact.  This could be the same contact, an identical contact on another account, 
   806 			//or a subcontact (if we're talking about a metaContact, for example)
   807 			targetFileTransferContact = [adium.contactController preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
   808 																				   forListContact:self];
   809 		}
   810 		
   811 		if (targetFileTransferContact) {
   812 			[adium.fileTransferController sendFile:filePath toListContact:targetFileTransferContact];
   813 		} else {
   814 			AILogWithSignature(@"No contact available to receive files to %@", self);
   815 			NSBeep();
   816 		}
   817 	}
   818 		
   819 	return nil;
   820 }
   821 
   822 //Writing Direction ----------------------------------------------------------------------------------------------------------
   823 #pragma mark Writing Direction
   824 
   825 - (NSWritingDirection)defaultBaseWritingDirection
   826 {
   827 	static NSWritingDirection defaultBaseWritingDirection;
   828 	static BOOL determinedDefaultBaseWritingDirection = NO;
   829 	
   830 	if (!determinedDefaultBaseWritingDirection) {
   831 		/* Use  the default writing direction of the language of the user's locale (and not the language
   832 		 * of the active localization). By that, we assume most users are mostly talking to their local friends.
   833 		 */
   834 		NSString	*lang = [[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode];		
   835 		defaultBaseWritingDirection = [NSParagraphStyle defaultWritingDirectionForLanguage:lang];
   836 		determinedDefaultBaseWritingDirection = YES;
   837 	}
   838 	
   839 	return defaultBaseWritingDirection;
   840 }
   841 
   842 - (NSWritingDirection)baseWritingDirection {
   843 	NSNumber	*dir = [self preferenceForKey:KEY_BASE_WRITING_DIRECTION group:PREF_GROUP_WRITING_DIRECTION];
   844 
   845 	return (dir ? [dir intValue] : [self defaultBaseWritingDirection]);
   846 }
   847 
   848 - (void)setBaseWritingDirection:(NSWritingDirection)direction {
   849 	[self setPreference:[NSNumber numberWithInt:direction]
   850 				 forKey:KEY_BASE_WRITING_DIRECTION
   851 				  group:PREF_GROUP_WRITING_DIRECTION];
   852 }
   853 
   854 #pragma mark Address Book
   855 - (ABPerson *)addressBookPerson
   856 {
   857 	return [AIAddressBookController personForListObject:self.parentContact];	
   858 }
   859 - (void)setAddressBookPerson:(ABPerson *)inPerson
   860 {
   861 	[self.parentContact setPreference:[inPerson uniqueId]
   862 								 forKey:KEY_AB_UNIQUE_ID
   863 								  group:PREF_GROUP_ADDRESSBOOK];
   864 }
   865 
   866 #pragma mark Applescript
   867 
   868 - (NSScriptObjectSpecifier *)objectSpecifier
   869 {
   870 	NSScriptObjectSpecifier *containerRef = self.account.objectSpecifier;
   871 	return [[[NSNameSpecifier allocWithZone:[self zone]]
   872 		initWithContainerClassDescription:[containerRef keyClassDescription]
   873 		containerSpecifier:containerRef key:@"contacts" name:self.UID] autorelease];
   874 }
   875 
   876 
   877 - (NSArray *)groupsAsArray
   878 {
   879 	return self.groups.allObjects;
   880 }
   881 
   882 - (BOOL)scriptingBlocked
   883 {
   884 	return [self isBlocked];
   885 }
   886 - (void)setScriptingBlocked:(BOOL)b
   887 {
   888 	[self setIsBlocked:b updateList:YES];
   889 }
   890 
   891 @dynamic containingObject;
   892 
   893 @end