Frameworks/Adium Framework/Source/AIChat.m
author Evan Schoenberg
Sat Nov 21 20:07:58 2009 -0600 (2009-11-21)
changeset 2929 5616a54b1173
parent 2777 a55a449072d4
child 2936 366008549f0c
permissions -rw-r--r--
A manually specified display name must override any server-provided one. A chat can get a display name from the account's own information via -[CBPurpleAccount updateTitle:forChat:]. If an alias is specified for a chat, ensure that it is displayed by setting it at highest priority in the AIMutableOwnerArray.

Fixes #12771, including the comment within that ticket that changes to the alias via the Get Info window previously didn't live-update.
     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 #import <Adium/AIAccount.h>
    17 #import <Adium/AIChat.h>
    18 #import <Adium/AIContentMessage.h>
    19 #import <Adium/AIContentTopic.h>
    20 #import <Adium/AIListContact.h>
    21 #import <Adium/AIService.h>
    22 #import <Adium/ESFileTransfer.h>
    23 #import <Adium/AIHTMLDecoder.h>
    24 #import <Adium/AIServiceIcons.h>
    25 #import <Adium/AIUserIcons.h>
    26 #import <Adium/AIContactHidingController.h>
    27 #import <Adium/AIContactControllerProtocol.h>
    28 #import <Adium/AIContentControllerProtocol.h>
    29 #import <Adium/AIChatControllerProtocol.h>
    30 #import <Adium/AIInterfaceControllerProtocol.h>
    31 #import <Adium/AISortController.h>
    32 
    33 #import <AIUtilities/AIArrayAdditions.h>
    34 #import <AIUtilities/AIMutableOwnerArray.h>
    35 #import <AIUtilities/AIAttributedStringAdditions.h>
    36 
    37 #import "AIMessageWindowController.h"
    38 #import "AIMessageWindow.h"
    39 #import "AIInterfaceControllerProtocol.h"
    40 #import "AIWebKitMessageViewController.h"
    41 
    42 
    43 @interface AIChat ()
    44 - (id)initForAccount:(AIAccount *)inAccount;
    45 - (void)clearUniqueChatID;
    46 - (void)clearListObjectStatuses;
    47 @end
    48 
    49 @implementation AIChat
    50 
    51 static int nextChatNumber = 0;
    52 
    53 + (id)chatForAccount:(AIAccount *)inAccount
    54 {
    55     return [[[self alloc] initForAccount:inAccount] autorelease];
    56 }
    57 
    58 - (id)initForAccount:(AIAccount *)inAccount
    59 {
    60     if ((self = [super init])) {
    61 		name = nil;
    62 		account = [inAccount retain];
    63 		participatingContacts = [[NSMutableArray alloc] init];
    64 		participatingContactsFlags = [[NSMutableDictionary alloc] init];
    65 		participatingContactsAliases = [[NSMutableDictionary alloc] init];
    66 		dateOpened = [[NSDate date] retain];
    67 		uniqueChatID = nil;
    68 		ignoredListContacts = nil;
    69 		isOpen = NO;
    70 		isGroupChat = NO;
    71 		expanded = YES;
    72 		customEmoticons = nil;
    73 		hasSentOrReceivedContent = NO;
    74 		showJoinLeave = YES;
    75 		pendingOutgoingContentObjects = [[NSMutableArray alloc] init];
    76 
    77 		AILog(@"[AIChat: %x initForAccount]",self);
    78 	}
    79 
    80     return self;
    81 }
    82 
    83 /*!
    84  * @brief Deallocate
    85  */
    86 - (void)dealloc
    87 {
    88 	AILog(@"[%@ dealloc]",self);
    89 
    90 	[account release];
    91 	[self removeAllParticipatingContactsSilently];
    92 	[participatingContacts release];
    93 	[participatingContactsFlags release];
    94 	[participatingContactsAliases release];
    95 	[dateOpened release];
    96 	[ignoredListContacts release];
    97 	[pendingOutgoingContentObjects release];
    98 	[uniqueChatID release]; uniqueChatID = nil;
    99 	[customEmoticons release]; customEmoticons = nil;
   100 	[topic release]; [topicSetter release];
   101 	
   102 	[super dealloc];
   103 }
   104 
   105 //Big image
   106 - (NSImage *)chatImage
   107 {
   108 	AIListContact 	*listObject = self.listObject;
   109 	NSImage			*image = nil;
   110 
   111 	if (listObject) {
   112 		image = listObject.parentContact.userIcon;
   113 		if (!image) image = [AIServiceIcons serviceIconForObject:listObject type:AIServiceIconLarge direction:AIIconNormal];
   114 	} else {
   115 		image = [AIServiceIcons serviceIconForObject:self.account type:AIServiceIconLarge direction:AIIconNormal];
   116 	}
   117 
   118 	return image;
   119 }
   120 
   121 //lil image
   122 - (NSImage *)chatMenuImage
   123 {
   124 	AIListObject 	*listObject = self.listObject;
   125 	NSImage			*chatMenuImage = nil;
   126 	
   127 	if (listObject) {
   128 		chatMenuImage = [AIUserIcons menuUserIconForObject:listObject];
   129 	} else {
   130 		chatMenuImage = [AIServiceIcons serviceIconForObject:account
   131 														type:AIServiceIconSmall
   132 												   direction:AIIconNormal];
   133 	}
   134 
   135 	return chatMenuImage;
   136 }
   137 
   138 
   139 //Associated Account ---------------------------------------------------------------------------------------------------
   140 #pragma mark Associated Account
   141 - (AIAccount *)account
   142 {
   143     return account;
   144 }
   145 
   146 - (void)setAccount:(AIAccount *)inAccount
   147 {
   148 	if (inAccount != account) {
   149 		[account release];
   150 		account = [inAccount retain];
   151 		
   152 		//The uniqueChatID may depend upon the account, so clear it
   153 		[self clearUniqueChatID];
   154 		[[NSNotificationCenter defaultCenter] postNotificationName:Chat_SourceChanged object:self]; //Notify
   155 	}
   156 }
   157 
   158 /*@brief: holds information passed upon the creation of the chat:
   159  * handle, server, etc.
   160  */
   161 - (NSDictionary *)chatCreationDictionary
   162 {
   163 	return [self valueForProperty:@"ChatCreationInfo"];
   164 }
   165 
   166 - (void)setChatCreationDictionary:(NSDictionary *)inDict
   167 {
   168 	[self setValue:inDict
   169 				   forProperty:@"ChatCreationInfo"
   170 				   notify:NotifyNever];
   171 }
   172 
   173 @synthesize hasSentOrReceivedContent, isOpen, dateOpened;
   174 
   175 //Status ---------------------------------------------------------------------------------------------------------------
   176 #pragma mark Status
   177 //Status
   178 - (void)didModifyProperties:(NSSet *)keys silent:(BOOL)silent
   179 {
   180 	[adium.chatController chatStatusChanged:self
   181 						   modifiedStatusKeys:keys
   182 									   silent:silent];	
   183 }
   184 
   185 - (void)object:(id)inObject didChangeValueForProperty:(NSString *)key notify:(NotifyTiming)notify
   186 {
   187 	//If our unviewed content changes or typing status changes, and we have a single list object, 
   188 	//apply the change to that object as well so it can be cleanly reflected in the contact list.
   189 	if ([key isEqualToString:KEY_UNVIEWED_CONTENT] ||
   190 		[key isEqualToString:KEY_TYPING]) {
   191 		AIListObject	*listObject = nil;
   192 		
   193 		if (self.isGroupChat) {
   194 			listObject = (AIListContact *)[adium.contactController existingBookmarkForChat:self];
   195 		} else {
   196 			listObject = self.listObject;
   197 		}
   198 		
   199 		if (listObject) [listObject setValue:[self valueForProperty:key] forProperty:key notify:notify];
   200 	}
   201 	
   202 	[super object:inObject didChangeValueForProperty:key notify:notify];
   203 }
   204 
   205 - (void)clearListObjectStatuses
   206 {
   207 	AIListObject	*listObject = self.listObject;
   208 	
   209 	if (listObject) {
   210 		[listObject setValue:nil forProperty:KEY_UNVIEWED_CONTENT notify:NotifyLater];
   211 		[listObject setValue:nil forProperty:KEY_TYPING notify:NotifyLater];
   212 	
   213 		[listObject notifyOfChangedPropertiesSilently:NO];
   214 	}
   215 	
   216 }
   217 //Secure chatting ------------------------------------------------------------------------------------------------------
   218 - (void)setSecurityDetails:(NSDictionary *)securityDetails
   219 {
   220 	[self setValue:securityDetails
   221 				   forProperty:@"SecurityDetails"
   222 				   notify:NotifyNow];
   223 }
   224 - (NSDictionary *)securityDetails
   225 {
   226 	return [self valueForProperty:@"SecurityDetails"];
   227 }
   228 
   229 - (BOOL)isSecure
   230 {	
   231 	return self.encryptionStatus != EncryptionStatus_None;
   232 }
   233 
   234 - (AIEncryptionStatus)encryptionStatus
   235 {
   236 	AIEncryptionStatus	encryptionStatus = EncryptionStatus_None;
   237 
   238 	NSDictionary		*securityDetails = self.securityDetails;
   239 	if (securityDetails) {
   240 		NSNumber *detailsStatus;
   241 		if ((detailsStatus = [securityDetails objectForKey:@"EncryptionStatus"])) {
   242 			encryptionStatus = [detailsStatus intValue];
   243 			
   244 		} else {
   245 			/* If we don't have a specific encryption status, but do have security details, assume
   246 			 * encrypted and verified.
   247 			 */
   248 			encryptionStatus = EncryptionStatus_Verified;
   249 		}
   250 	}
   251 
   252 	return encryptionStatus;
   253 }
   254 
   255 - (BOOL)supportsSecureMessagingToggling
   256 {
   257 	return [account allowSecureMessagingTogglingForChat:self];
   258 }
   259 
   260 //Name  ----------------------------------------------------------------------------------------------------------------
   261 #pragma mark Name
   262 
   263 @synthesize name;
   264 
   265 /*!
   266  * @brief An identifier which can be used to look up this chat later
   267  *
   268  * Use uniqueChatID as a unique identifier for a contact-service combination.
   269  * Only an account which created a chat should specify the identifier; it has no useful meaning outside that context.
   270  */
   271 @synthesize identifier;
   272 
   273 - (NSString *)displayName
   274 {
   275     NSString	*outName = [self displayArrayObjectForKey:@"Display Name"];
   276     return outName ? outName : (name ? name : self.listObject.displayName);
   277 }
   278 
   279 - (void)setDisplayName:(NSString *)inDisplayName
   280 {
   281 	[[self displayArrayForKey:@"Display Name"] setObject:inDisplayName
   282 											   withOwner:self
   283 										   priorityLevel:Highest_Priority];
   284 
   285 	//The display array doesn't cause an attribute update; fake it.
   286 	[adium.chatController chatStatusChanged:self
   287 						 modifiedStatusKeys:[NSSet setWithObject:@"Display Name"]
   288 									 silent:NO];
   289 }
   290 
   291 //Participating ListObjects --------------------------------------------------------------------------------------------
   292 #pragma mark Participating ListObjects
   293 
   294 /*!
   295  * @brief The display name for the contact in this chat.
   296  *
   297  * @param contact The AIListObject whose display name should be created
   298  *
   299  * If the user has an alias set, the alias is used, otherwise the display name.
   300  *
   301  * @returns Display name
   302  */
   303 - (NSString *)displayNameForContact:(AIListObject *)contact
   304 {
   305 	return [self aliasForContact:contact] ?: contact.displayName;
   306 }
   307 
   308 /*!
   309  * @brief The flags for a given contact.
   310  */
   311 - (AIGroupChatFlags)flagsForContact:(AIListObject *)contact
   312 {
   313 	return [[participatingContactsFlags objectForKey:contact.UID] integerValue];
   314 }
   315 
   316 /*!
   317  * @brief The alias for a given contact
   318  */
   319 - (NSString *)aliasForContact:(AIListObject *)contact
   320 {
   321 	NSString *alias = [participatingContactsAliases objectForKey:contact.UID];
   322 	
   323 	if (!alias && self.isGroupChat) {
   324 		alias = [self.account fallbackAliasForContact:(AIListContact *)contact inChat:self];
   325 	}
   326 	
   327 	return alias;
   328 }
   329 
   330 /*!
   331  * @brief Set the flags for a contact
   332  *
   333  * Note that this doesn't set the bitwise or; this directly sets the value passed.
   334  */
   335 - (void)setFlags:(AIGroupChatFlags)flags forContact:(AIListObject *)contact
   336 {
   337 	[participatingContactsFlags setObject:[NSNumber numberWithInteger:flags]
   338 								   forKey:contact.UID];
   339 }
   340 
   341 /*!
   342  * @brief Set the alias for a contact.
   343  */
   344 - (void)setAlias:(NSString *)alias forContact:(AIListObject *)contact
   345 {
   346 	[participatingContactsAliases setObject:alias
   347 									 forKey:contact.UID];
   348 }
   349 
   350 AIGroupChatFlags highestFlag(AIGroupChatFlags flags)
   351 {
   352 	if ((flags & AIGroupChatFounder) == AIGroupChatFounder)
   353 		return AIGroupChatFounder;
   354 	
   355 	if ((flags & AIGroupChatOp) == AIGroupChatOp)
   356 		return AIGroupChatOp;
   357 	
   358 	if ((flags & AIGroupChatHalfOp) == AIGroupChatHalfOp)
   359 		return AIGroupChatHalfOp;
   360 	
   361 	if ((flags & AIGroupChatVoice) == AIGroupChatVoice)
   362 		return AIGroupChatVoice;
   363 	
   364 	return AIGroupChatNone;
   365 }
   366 
   367 NSComparisonResult userListSort (id objectA, id objectB, void *context)
   368 {
   369 	AIChat *chat = (AIChat *)context;
   370 	
   371 	AIGroupChatFlags flagA = highestFlag([chat flagsForContact:objectA]), flagB = highestFlag([chat flagsForContact:objectB]);
   372 	
   373 	if(flagA > flagB) {
   374 		return NSOrderedAscending;
   375 	} else if (flagA < flagB) {
   376 		return NSOrderedDescending;
   377 	} else {
   378 		return [[chat displayNameForContact:objectA] caseInsensitiveCompare:[chat displayNameForContact:objectB]];
   379 	}
   380 }
   381 
   382 /*!
   383  * @brief Resorts our participants
   384  *
   385  * This is called when our list objects change.
   386  */
   387 - (void)resortParticipants
   388 {
   389 	[participatingContacts sortUsingFunction:userListSort context:self];
   390 }
   391 
   392 /*!
   393  * @brief Remove the saved values for a contact
   394  *
   395  * Removes any values which are dependent upon the contact, such as
   396  * its flags or alias.
   397 */
   398 - (void)removeSavedValuesForContactUID:(NSString *)contactUID
   399 {
   400 	[participatingContactsFlags removeObjectForKey:contactUID];
   401 	[participatingContactsAliases removeObjectForKey:contactUID];
   402 }
   403 
   404 - (void)addParticipatingListObject:(AIListContact *)inObject notify:(BOOL)notify
   405 {
   406 	[self addParticipatingListObjects:[NSArray arrayWithObject:inObject] notify:notify];
   407 }
   408 
   409 - (void)addParticipatingListObjects:(NSArray *)inObjects notify:(BOOL)notify
   410 {
   411 	NSMutableArray *contacts = [[inObjects mutableCopy] autorelease];
   412 
   413 	for (AIListObject *obj in inObjects) {
   414 		if ([self containsObject:obj] || ![self canContainObject:obj])
   415 			[contacts removeObject:obj];
   416 	}
   417 	
   418 	[participatingContacts addObjectsFromArray:contacts];
   419 	[adium.chatController chat:self addedListContacts:contacts notify:notify];
   420 }
   421 
   422 // Invite a list object to join the chat. Returns YES if the chat joins, NO otherwise
   423 - (BOOL)inviteListContact:(AIListContact *)inContact withMessage:(NSString *)inviteMessage
   424 {
   425 	return ([self.account inviteContact:inContact toChat:self withMessage:inviteMessage]);
   426 }
   427 
   428 @synthesize preferredListObject = preferredContact;
   429 
   430 //If this chat only has one participating list object, it is returned.  Otherwise, nil is returned
   431 - (AIListContact *)listObject
   432 {
   433 	if (self.countOfContainedObjects == 1 && !self.isGroupChat) {
   434 		return [self.containedObjects objectAtIndex:0];
   435 	}
   436 
   437 	return nil;
   438 }
   439 
   440 - (void)setListObject:(AIListContact *)inListObject
   441 {
   442 	if (inListObject != self.listObject) {
   443 		if (self.countOfContainedObjects) {
   444 			[participatingContacts removeObjectAtIndex:0];
   445 		}
   446 		[self addParticipatingListObject:inListObject notify:YES];
   447 
   448 		//Clear any local caches relying on the list object
   449 		[self clearListObjectStatuses];
   450 		[self clearUniqueChatID];
   451 
   452 		//Notify once the destination has been changed
   453 		[[NSNotificationCenter defaultCenter] postNotificationName:Chat_DestinationChanged object:self];
   454 	}
   455 }
   456 
   457 - (NSString *)uniqueChatID
   458 {
   459 	if (!uniqueChatID) {
   460 		if (self.isGroupChat) {
   461 			uniqueChatID = [[NSString alloc] initWithFormat:@"%@.%i", self.name, nextChatNumber++];
   462 		} else {			
   463 			uniqueChatID = [self.listObject.internalObjectID retain];
   464 		}
   465 
   466 		if (!uniqueChatID) {
   467 			uniqueChatID = [[NSString alloc] initWithFormat:@"UnknownChat.%i", nextChatNumber++];
   468 			NSLog(@"Warning: Unknown chat %p",self);
   469 		}
   470 	}
   471 
   472 	return uniqueChatID;
   473 }
   474 
   475 - (void)clearUniqueChatID
   476 {
   477 	[uniqueChatID release]; uniqueChatID = nil;
   478 }
   479 
   480 - (NSString *)internalObjectID
   481 {
   482 	return self.uniqueChatID;
   483 }
   484 
   485 
   486 //Content --------------------------------------------------------------------------------------------------------------
   487 #pragma mark Content
   488 
   489 /*!
   490  * @brief Informs the chat that the core and the account are ready to begin filtering and sending a content object
   491  *
   492  * If there is only one object in pendingOutgoingContentObjects after adding inObject, we should send immedaitely.
   493  * However, if other objects are in it, we should wait for them to be removed, as they are chronologically first.
   494  * If we are asked if we should begin sending the earliest object in pendingOutgoingContentObjects, the answer is YES.
   495  *
   496  * @param inObject The object being sent
   497  * @result YES if the object should be sent immediately; NO if another object is in process so we should wait
   498  */
   499 - (BOOL)shouldBeginSendingContentObject:(AIContentObject *)inObject
   500 {
   501 	NSInteger	currentIndex = [pendingOutgoingContentObjects indexOfObjectIdenticalTo:inObject];
   502 
   503 	//Don't add the object twice when we are called from -[AIChat finishedSendingContentObject]
   504 	if (currentIndex == NSNotFound) {
   505 		[pendingOutgoingContentObjects addObject:inObject];		
   506 	}
   507 
   508 	return pendingOutgoingContentObjects.count == 1 || currentIndex == 0;
   509 }
   510 
   511 /*!
   512  * @brief Informs the chat that an outgoing content object was sent and dispalyed.
   513  *
   514  * It is no longer pending, so we remove it from that array.
   515  * If there are more pending objects, trigger sending the next.
   516  *
   517  * @param inObject The object with which we are finished
   518  */
   519 - (void)finishedSendingContentObject:(AIContentObject *)inObject
   520 {
   521 	[pendingOutgoingContentObjects removeObjectIdenticalTo:inObject];
   522 	
   523 	if (pendingOutgoingContentObjects.count) {
   524 		[adium.contentController sendContentObject:[pendingOutgoingContentObjects objectAtIndex:0]];
   525 	}
   526 }
   527 
   528 - (AIChatSendingAbilityType)messageSendingAbility
   529 {
   530 	AIChatSendingAbilityType sendingAbilityType;
   531 
   532 	if (self.isGroupChat) {
   533 		if (self.account.online) {
   534 			//XXX Liar!
   535 			sendingAbilityType = AIChatCanSendMessageNow;
   536 		} else {
   537 			sendingAbilityType = AIChatCanNotSendMessage;
   538 		}
   539 
   540 	} else {
   541 		if (self.account.online) {
   542 			AIListContact *listObject = self.listObject;
   543 			
   544 			if (listObject.online || listObject.isStranger) {
   545 				sendingAbilityType = AIChatCanSendMessageNow;
   546 			} else if ([self.account canSendOfflineMessageToContact:listObject]) {
   547 				sendingAbilityType = AIChatCanSendViaServersideOfflineMessage;				
   548 			} else if ([self.account maySendMessageToInvisibleContact:listObject]) {
   549 				sendingAbilityType = AIChatMayNotBeAbleToSendMessage;	
   550 			} else {
   551 				sendingAbilityType = AIChatCanNotSendMessage;
   552 			}
   553 
   554 		} else {
   555 			sendingAbilityType = AIChatCanNotSendMessage;
   556 		}		
   557 	}
   558 	
   559 	return sendingAbilityType;
   560 }
   561 
   562 - (BOOL)canSendImages
   563 {
   564 	return [self.account canSendImagesForChat:self];
   565 }
   566 
   567 - (NSUInteger)unviewedContentCount
   568 {
   569 	return [self integerValueForProperty:KEY_UNVIEWED_CONTENT];
   570 }
   571 
   572 - (NSUInteger)unviewedMentionCount
   573 {
   574 	return [self integerValueForProperty:KEY_UNVIEWED_MENTION];	
   575 }
   576 
   577 - (void)incrementUnviewedContentCount
   578 {
   579 	int currentUnviewed = [self integerValueForProperty:KEY_UNVIEWED_CONTENT];
   580 	[self setValue:[NSNumber numberWithInt:(currentUnviewed+1)]
   581 					 forProperty:KEY_UNVIEWED_CONTENT
   582 					 notify:NotifyNow];
   583 }
   584 
   585 - (void)incrementUnviewedMentionCount
   586 {
   587 	int currentUnviewed = [self integerValueForProperty:KEY_UNVIEWED_MENTION];
   588 	[self setValue:[NSNumber numberWithInt:(currentUnviewed+1)]
   589 	   forProperty:KEY_UNVIEWED_MENTION
   590 			notify:NotifyNow];
   591 }
   592 
   593 - (void)clearUnviewedContentCount
   594 {
   595 	// We also want to clear mention for the same situations we clear normal content.
   596 	[self setValue:nil forProperty:KEY_UNVIEWED_MENTION notify:NotifyNow];
   597 	[self setValue:nil forProperty:KEY_UNVIEWED_CONTENT notify:NotifyNow];
   598 }
   599 
   600 #pragma mark Logging
   601 
   602 - (BOOL)shouldLog
   603 {
   604 	return [self.account shouldLogChat:self];
   605 }
   606 
   607 #pragma mark AIContainingObject protocol
   608 //AIContainingObject protocol
   609 - (NSArray *)visibleContainedObjects
   610 {
   611 	return self.containedObjects;
   612 }
   613 - (NSArray *)containedObjects
   614 {
   615 	return [[participatingContacts copy] autorelease];
   616 }
   617 - (NSUInteger)countOfContainedObjects
   618 {
   619 	return [participatingContacts count];
   620 }
   621 
   622 - (BOOL)containsObject:(AIListObject *)inObject
   623 {
   624 	return [participatingContacts containsObjectIdenticalTo:inObject];
   625 }
   626 
   627 - (id)visibleObjectAtIndex:(NSUInteger)index
   628 {
   629 	return [participatingContacts objectAtIndex:index];
   630 }
   631 
   632 - (NSUInteger)visibleIndexOfObject:(AIListObject *)obj
   633 {
   634 	if(![[AIContactHidingController sharedController] visibilityOfListObject:obj inContainer:self])
   635 		return NSNotFound;
   636 	return [participatingContacts indexOfObject:obj];
   637 }
   638 
   639 //Retrieve a specific object by service and UID
   640 - (AIListObject *)objectWithService:(AIService *)inService UID:(NSString *)inUID
   641 {
   642 	for (AIListContact *object in self) {
   643 		if ([inUID isEqualToString:object.UID] && object.service == inService)
   644 			return object;
   645 	}
   646 	
   647 	return nil;
   648 }
   649 
   650 - (NSArray *)uniqueContainedObjects
   651 {
   652 	return self.containedObjects;
   653 }
   654 
   655 - (void)removeObject:(AIListObject *)inObject
   656 {
   657 	if ([self containsObject:inObject]) {
   658 		AIListContact *contact = (AIListContact *)inObject; //if we contain it, it has to be an AIListContact
   659 		
   660 		//make sure removing it from the array doesn't deallocate it immediately, since we need it for -chat:removedListContact:
   661 		[inObject retain];
   662 		
   663 		[participatingContacts removeObject:inObject];
   664 		
   665 		[self removeSavedValuesForContactUID:inObject.UID];
   666 
   667 		[adium.chatController chat:self removedListContact:contact];
   668 
   669 		if (contact.isStranger &&
   670 			![adium.chatController allGroupChatsContainingContact:contact.parentContact].count &&
   671 			![adium.chatController existingChatWithContact:contact.parentContact]) {
   672 			[adium.contactController accountDidStopTrackingContact:contact];
   673 		}
   674 		
   675 		[inObject release];
   676 	}
   677 }
   678 
   679 - (void)removeObjectAfterAccountStopsTracking:(AIListObject *)object
   680 {
   681 	[self removeObject:object]; //does nothing if we've already removed it
   682 }
   683 
   684 - (void)removeAllParticipatingContactsSilently
   685 {
   686 	for (AIListContact *listContact in self) {
   687 		if (listContact.isStranger &&
   688 			![adium.chatController existingChatWithContact:listContact.parentContact] &&
   689 			![adium.chatController allGroupChatsContainingContact:listContact.parentContact].count) {
   690 			[adium.contactController accountDidStopTrackingContact:listContact];
   691 		}
   692 	}
   693 
   694 	[participatingContacts removeAllObjects];
   695 	[participatingContactsFlags removeAllObjects];
   696 	[participatingContactsAliases removeAllObjects];
   697 
   698 	[[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
   699 											  object:self];
   700 }
   701 
   702 @synthesize expanded;
   703 
   704 - (BOOL)isExpandable
   705 {
   706 	return NO;
   707 }
   708 
   709 - (NSUInteger)visibleCount
   710 {
   711 	return self.countOfContainedObjects;
   712 }
   713 
   714 - (NSString *)contentsBasedIdentifier
   715 {
   716 	return [NSString stringWithFormat:@"%@-%@.%@",self.name, self.account.service.serviceID, self.account.UID];
   717 }
   718 
   719 //Not used
   720 - (float)smallestOrder { return 0; }
   721 - (float)largestOrder { return 1E10; }
   722 - (float)orderIndexForObject:(AIListObject *)listObject { return 0; }
   723 - (void)listObject:(AIListObject *)listObject didSetOrderIndex:(float)inOrderIndex {};
   724 
   725 
   726 #pragma mark Ignoring
   727 /*!
   728  * @brief Set the ignored state of a contact
   729  *
   730  * @param inContact The contact whose state is to be changed
   731  * @param isIgnored YES to ignore the contact; NO to not ignore the contact
   732  */
   733 - (void)setListContact:(AIListContact *)inContact isIgnored:(BOOL)isIgnored
   734 {
   735 	if (self.account.accountManagesGroupChatIgnore) {
   736 		[self.account setContact:inContact ignored:isIgnored inChat:self];
   737 	} else {
   738 		//Create ignoredListContacts if needed
   739 		if (isIgnored && !ignoredListContacts) {
   740 			ignoredListContacts = [[NSMutableSet alloc] init];	
   741 		}
   742 
   743 		if (isIgnored) {
   744 			[ignoredListContacts addObject:inContact];
   745 		} else {
   746 			[ignoredListContacts removeObject:inContact];		
   747 		}	
   748 	}
   749 }
   750 
   751 /*!
   752  * @brief Is the passed object ignored?
   753  *
   754  * @param inContact The contact to check
   755  * @result YES if the contact is ignored; NO if it is not
   756  */
   757 - (BOOL)isListContactIgnored:(AIListObject *)inContact
   758 {
   759 	if (self.account.accountManagesGroupChatIgnore) {
   760 		return [self.account contact:(AIListContact *)inContact isIgnoredInChat:self];
   761 	} else {
   762 		return [ignoredListContacts containsObject:inContact];
   763 	}
   764 }
   765 
   766 #pragma mark Comparison
   767 - (BOOL)isEqual:(id)inChat
   768 {
   769 	return (inChat == self);
   770 }
   771 
   772 #pragma mark Debugging
   773 - (NSString *)description
   774 {
   775 	return [NSString stringWithFormat:@"%@:%@",
   776 		[super description],
   777 		(uniqueChatID ? uniqueChatID : @"<new>")];
   778 }
   779 
   780 #pragma mark Group Chats
   781 
   782 @synthesize isGroupChat, showJoinLeave, hideUserIconAndStatus, topic, topicSetter;
   783 
   784 /*!
   785  * @brief Does this chat support topics?
   786  */
   787 - (BOOL)supportsTopic
   788 {
   789 	return account.groupChatsSupportTopic;
   790 }
   791 
   792 /*!
   793  * @brief Update the topic.
   794  */
   795 - (void)updateTopic:(NSString *)inTopic withSource:(AIListContact *)contact
   796 {
   797 	[topic release];
   798 	topic = [inTopic retain];
   799 	
   800 	self.topicSetter = contact;
   801 	
   802 	// Apply the new topic to the message view
   803 	AIContentTopic *contentTopic = [AIContentTopic topicInChat:self
   804 													withSource:contact
   805 												   destination:nil
   806 														  date:[NSDate date]
   807 													   message:[NSAttributedString stringWithString:topic ?: @""]];
   808 	
   809 	// The content controller has huge problems with blank messages being let through.
   810 	if (!topic.length) {
   811 		contentTopic.message = CONTENT_TOPIC_MESSAGE_ACTUALLY_EMPTY;
   812 		contentTopic.actuallyBlank = YES;
   813 	}
   814 	
   815 	[adium.contentController receiveContentObject:contentTopic];
   816 }
   817 
   818 /*!
   819  * @brief Set the chat's topic, telling the account to update it.
   820  */
   821 - (void)setTopic:(NSString *)inTopic
   822 {
   823 	if (self.supportsTopic) {
   824 		// We mess with the topic, replacing nbsp with spaces; make sure we're not setting an identical one other than this.
   825 		NSString *tempTopic = [topic stringByReplacingOccurrencesOfString:@"\u00A0" withString:@" "];
   826 		if ([tempTopic isEqualToString:inTopic]) {
   827 			AILogWithSignature(@"Not setting topic for %@, already the same.", self);
   828 		} else {
   829 			AILogWithSignature(@"Setting %@ topic to: %@", self, topic);
   830 			[account setTopic:inTopic forChat:self];
   831 		}
   832 	} else {
   833 		AILogWithSignature(@"Attempt to set %@ topic when account doesn't support it.");
   834 	}
   835 }
   836 
   837 #pragma mark Custom emoticons
   838 
   839 - (void)addCustomEmoticon:(AIEmoticon *)inEmoticon
   840 {
   841 	if (!customEmoticons) customEmoticons = [[NSMutableSet alloc] init];
   842 	[customEmoticons addObject:inEmoticon];
   843 }
   844 
   845 @synthesize customEmoticons;
   846 
   847 #pragma mark Errors
   848 
   849 /*!
   850  * @brief Inform the chat that an error occurred
   851  *
   852  * @param type An NSNumber containing an AIChatErrorType
   853  */
   854 - (void)receivedError:(NSNumber *)type
   855 {
   856 	//Notify observers
   857 	[self setValue:type forProperty:KEY_CHAT_ERROR notify:NotifyNow];
   858 
   859 	//No need to continue to store the NSNumber
   860 	[self setValue:nil forProperty:KEY_CHAT_ERROR notify:NotifyNever];
   861 }
   862 
   863 #pragma mark Room commands
   864 - (NSMenu *)actionMenu
   865 {	
   866 	return [self.account actionMenuForChat:self];
   867 }
   868 - (void)setActionMenu:(NSMenu *)inMenu {};
   869 
   870 #pragma mark Applescript
   871 
   872 - (NSScriptObjectSpecifier *)objectSpecifier
   873 {
   874 	//the chat may not be in a window! Just reference it from the application...
   875 	//get my window
   876 	NSScriptClassDescription *containerClassDesc = (NSScriptClassDescription *)[NSScriptClassDescription classDescriptionForClass:[NSApp class]];
   877 	return [[[NSUniqueIDSpecifier allocWithZone:[self zone]]
   878 		initWithContainerClassDescription:containerClassDesc
   879 		containerSpecifier:nil key:@"chats" uniqueID:[self uniqueChatID]] autorelease];
   880 }
   881 
   882 - (unsigned int)index
   883 {
   884 	//what we're going to do is find this tab in the tab view's hierarchy, so as to get its index
   885 	AIMessageWindowController *windowController = self.chatContainer.windowController;
   886 
   887 	NSArray *chats = [windowController containedChats];
   888 	for (unsigned int i=0;i<[chats count];i++) {
   889 		if ([chats objectAtIndex:i] == self)
   890 			return i+1; //one based
   891 	}
   892 	NSAssert(NO, @"This chat is weird.");
   893 	return 0;
   894 }
   895 /*- (void)setIndex:(unsigned int)index
   896 {
   897 	AIMessageWindowController *windowController = self.chatContainer.windowController;
   898 	NSArray *chats = [windowController containedChats];
   899 	NSAssert (index-1 < [chats count], @"Don't let index be bigger than the count!");
   900 	NSLog(@"Trying to move %@ in %@ to %u",messageTab,window,index-1);
   901 	[windowController moveTabViewItem:messageTab toIndex:index-1]; //This is bad bad bad. Why?
   902 	
   903 }*/
   904 
   905 - (NSString *)scriptingName
   906 {
   907 	NSString *aName = self.name;
   908 	if (!aName)
   909 		aName = self.listObject.UID;
   910 	return aName;
   911 }
   912 
   913 - (id <AIChatContainer>)chatContainer
   914 {
   915 	return [self valueForProperty:@"MessageTabViewItem"];
   916 }
   917 
   918 - (id)handleCloseScriptCommand:(NSCloseCommand *)closeCommand
   919 {
   920 	[adium.interfaceController closeChat:self];
   921 	return nil;
   922 }
   923 
   924 - (void)setUniqueChatID:(NSString *)str
   925 {
   926 	[[NSScriptCommand currentCommand] setScriptErrorNumber:errOSACantAssign];
   927 }
   928 
   929 - (AIAccount *)scriptingAccount
   930 {
   931 	return self.account;
   932 }
   933 
   934 - (void)setScriptingAccount:(AIAccount *)a
   935 {
   936 	[[NSScriptCommand currentCommand] setScriptErrorNumber:errOSACantAssign];
   937 	[[NSScriptCommand currentCommand] setScriptErrorString:@"Can't set the account of a chat."];
   938 }
   939 
   940 - (NSString *)content
   941 {
   942 	/*AITranscriptLogEnumerator *e = [[[AITranscriptLogReader alloc] initWithChat:self] autorelease];
   943 	AIContentMessage *m;
   944 	NSMutableString *result = [[[NSMutableString alloc] init] autorelease];
   945 	while ((m = [e nextObject])) {
   946 		[result appendFormat:@"%@\n",[m messageString]];
   947 	}
   948 	return result;*/
   949 	[[NSScriptCommand currentCommand] setScriptErrorNumber:errOSACantAssign];
   950 	[[NSScriptCommand currentCommand] setScriptErrorString:@"Still unsupported."];
   951 	return nil;
   952 }
   953 
   954 /*!
   955  * @brief Applescript command to send a message in this chat
   956  */
   957 - (id)sendScriptCommand:(NSScriptCommand *)command {
   958 	NSDictionary	*evaluatedArguments = [command evaluatedArguments];
   959 	NSString		*message = [evaluatedArguments objectForKey:@"message"];
   960 	NSURL			*fileURL = [evaluatedArguments objectForKey:@"withFile"];
   961 	
   962 	//Send any message we were told to send
   963 	if (message && [message length]) {
   964 		//Take the string and turn it into an attributed string (in case we were passed HTML)
   965 		NSAttributedString  *attributedMessage = [AIHTMLDecoder decodeHTML:message];
   966 		AIContentMessage	*messageContent;
   967 		messageContent = [AIContentMessage messageInChat:self
   968 											  withSource:self.account
   969 											 destination:self.listObject
   970 													date:nil
   971 												 message:attributedMessage
   972 											   autoreply:NO];
   973 		
   974 		[adium.contentController sendContentObject:messageContent];
   975 	}
   976 	
   977 	//Send any file we were told to send to every participating list object (anyone remember the AOL mass mailing zareW scene?)
   978 	if (fileURL && fileURL.path.length) {
   979 		
   980 		for (AIListContact *listContact in self) {
   981 			AIListContact   *targetFileTransferContact;
   982 			
   983 			//Make sure we know where we are sending the file by finding the best contact for
   984 			//sending CONTENT_FILE_TRANSFER_TYPE.
   985 			if ((targetFileTransferContact = [adium.contactController preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
   986 																						forListContact:listContact])) {
   987 				[adium.fileTransferController sendFile:[fileURL path]
   988 										   toListContact:targetFileTransferContact];
   989 			} else {
   990 				AILogWithSignature(@"No contact available to receive files to %@", listContact);
   991 				NSBeep();
   992 			}
   993 		}
   994 	}
   995 	
   996 	return nil;
   997 }
   998 
   999 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
  1000 {
  1001 	return [self.containedObjects countByEnumeratingWithState:state objects:stackbuf count:len];
  1002 }
  1003 
  1004 - (BOOL) canContainObject:(id)obj
  1005 {
  1006 	return [obj isKindOfClass:[AIListContact class]];
  1007 }
  1008 
  1009 @end