Source/AIChatController.m
author Thijs Alkemade <thijsalkemade@gmail.com>
Sat Aug 28 22:01:12 2010 +0200 (21 months ago)
changeset 3277 21ab21e877e0
parent 3206 0b104481dea6
child 3344 dc4039a09c76
permissions -rw-r--r--
Add a Reopen Closed Tab menu item to the File menu that will restore the most recently closed tab, similar to Chrome. Fixes #12537

Does not work with MSN group chats (and probably other protocols that have unnamed MUCs).

r=wix
     1 //
     2 //  AIChatController.m
     3 //  Adium
     4 //
     5 //  Created by Evan Schoenberg on 6/10/05.
     6 //
     7 
     8 #import "AIChatController.h"
     9 
    10 #import <Adium/AIContentControllerProtocol.h>
    11 #import <Adium/AIContactControllerProtocol.h>
    12 #import <Adium/AIInterfaceControllerProtocol.h>
    13 #import <Adium/AIMenuControllerProtocol.h>
    14 #import <Adium/AIStatusControllerProtocol.h>
    15 #import "AdiumChatEvents.h"
    16 #import <Adium/AIAccount.h>
    17 #import <Adium/AIChat.h>
    18 #import <Adium/AIContentObject.h>
    19 #import <Adium/AIContentMessage.h>
    20 #import <Adium/AIListContact.h>
    21 #import <Adium/AIListBookmark.h>
    22 #import <Adium/AIMetaContact.h>
    23 #import <Adium/AIService.h>
    24 #import <AIUtilities/AIArrayAdditions.h>
    25 #import <AIUtilities/AIMenuAdditions.h>
    26 
    27 #define SHOW_JOIN_LEAVE_TITLE		AILocalizedString(@"Show Join/Leave Messages", nil)
    28 
    29 @interface AIChatController ()
    30 - (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
    31 - (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys;
    32 
    33 - (void)toggleIgnoreOfContact:(id)sender;
    34 - (void)toggleShowJoinLeave:(id)sender;
    35 - (void)didExchangeContent:(NSNotification *)notification;
    36 
    37 - (void)adiumWillTerminate:(NSNotification *)inNotification;
    38 @end
    39 
    40 /*!
    41  * @class AIChatController
    42  * @brief Core controller for chats
    43  *
    44  * This is the only class which should vend AIChat objects (via openChat... or chatWith:...).
    45  * AIChat objects should never be created directly.
    46  */
    47 @implementation AIChatController
    48 
    49 /*!
    50  * @brief Initialize the controller
    51  */
    52 - (id)init
    53 {	
    54 	if ((self = [super init])) {
    55 		mostRecentChat = nil;
    56 		chatObserverArray = [[NSMutableArray alloc] init];
    57 		adiumChatEvents = [[AdiumChatEvents alloc] init];
    58 
    59 		//Chat tracking
    60 		openChats = [[NSMutableSet alloc] init];
    61 	}
    62 	return self;
    63 }
    64 
    65 
    66 /*!
    67  * @brief Controller loaded
    68  */
    69 - (void)controllerDidLoad
    70 {	
    71 	//Observe content so we can update the most recent chat
    72     [[NSNotificationCenter defaultCenter] addObserver:self 
    73 								   selector:@selector(didExchangeContent:) 
    74 									   name:CONTENT_MESSAGE_RECEIVED
    75 									 object:nil];
    76 	
    77     [[NSNotificationCenter defaultCenter] addObserver:self 
    78 								   selector:@selector(didExchangeContent:) 
    79 									   name:CONTENT_MESSAGE_RECEIVED_GROUP
    80 									 object:nil];
    81 	
    82 	[[NSNotificationCenter defaultCenter] addObserver:self 
    83 								   selector:@selector(didExchangeContent:) 
    84 									   name:CONTENT_MESSAGE_SENT
    85 									 object:nil];
    86 	
    87 	[[NSNotificationCenter defaultCenter] addObserver:self
    88 								   selector:@selector(adiumWillTerminate:)
    89 									   name:AIAppWillTerminateNotification
    90 									 object:nil];
    91 
    92 	//Ignore menu item for contacts in group chats
    93 	menuItem_ignore = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@""
    94 																		   target:self
    95 																		   action:@selector(toggleIgnoreOfContact:)
    96 																	keyEquivalent:@""];
    97 	[adium.menuController addContextualMenuItem:menuItem_ignore toLocation:Context_Contact_GroupChat_ParticipantAction];
    98 	
    99 	menuItem_joinLeave = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:SHOW_JOIN_LEAVE_TITLE
   100 																				target:self
   101 																			  action:@selector(toggleShowJoinLeave:)
   102 																		 keyEquivalent:@""];
   103 	
   104 	[adium.menuController addMenuItem:menuItem_joinLeave toLocation:LOC_Display_MessageControl];
   105 	[adium.menuController addContextualMenuItem:[[menuItem_joinLeave copy] autorelease] toLocation:Context_GroupChat_Action];
   106 
   107 	[adiumChatEvents controllerDidLoad];
   108 }
   109 
   110 
   111 /*!
   112  * @brief Controller will close
   113  */
   114 - (void)controllerWillClose
   115 {
   116 	
   117 }
   118 
   119 /*!
   120  * @brief Adium will terminate
   121  *
   122  * Post the Chat_WillClose for each open chat so any closing behavior can be performed
   123  */
   124 - (void)adiumWillTerminate:(NSNotification *)inNotification
   125 {
   126 	//Every open chat is about to close. We perform the internal closing here rather than calling on the interface controller since the UI need not change.
   127 	while ([openChats count] > 0) {
   128 		[self closeChat:[openChats anyObject]];
   129 	}
   130 }
   131 
   132 /*!
   133  * @brief Deallocate
   134  */
   135 - (void)dealloc
   136 {
   137 	[openChats release]; openChats = nil;
   138 	[chatObserverArray release]; chatObserverArray = nil;
   139 	[[NSNotificationCenter defaultCenter] removeObserver:self];
   140 
   141 	[super dealloc];
   142 }
   143 	
   144 /*!
   145  * @brief Register a chat observer
   146  *
   147  * Chat observers are notified when properties are changed on chats
   148  *
   149  * @param inObserver An observer, which must conform to AIChatObserver
   150  */
   151 - (void)registerChatObserver:(id <AIChatObserver>)inObserver
   152 {
   153 	//Add the observer
   154     [chatObserverArray addObject:[NSValue valueWithNonretainedObject:inObserver]];
   155 	
   156     //Let the new observer process all existing chats
   157 	[self updateAllChatsForObserver:inObserver];
   158 }
   159 
   160 /*!
   161  * @brief Unregister a chat observer
   162  */
   163 - (void)unregisterChatObserver:(id <AIChatObserver>)inObserver
   164 {
   165     [chatObserverArray removeObject:[NSValue valueWithNonretainedObject:inObserver]];
   166 }
   167 
   168 /*!
   169  * @brief Chat status changed
   170  *
   171  * Called by AIChat after it changes one or more properties.
   172  */
   173 - (void)chatStatusChanged:(AIChat *)inChat modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
   174 {
   175 	NSSet			*modifiedAttributeKeys;
   176 	
   177     //Let all observers know the chat's status has changed before performing any further notifications
   178 	modifiedAttributeKeys = [self _informObserversOfChatStatusChange:inChat withKeys:inModifiedKeys silent:silent];
   179 	
   180     //Post an attributes changed message (if necessary)
   181     if ([modifiedAttributeKeys count]) {
   182 		[self chatAttributesChanged:inChat modifiedKeys:modifiedAttributeKeys];
   183     }	
   184 }
   185 
   186 /*!
   187  * @brief Chat attributes changed
   188  *
   189  * Called by -[AIChatController chatStatusChanged:modifiedStatusKeys:silent:] if any observers changed attributes
   190  */
   191 - (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys
   192 {
   193 	//Post an attributes changed message
   194 	[[NSNotificationCenter defaultCenter] postNotificationName:Chat_AttributesChanged
   195 											  object:inChat
   196 											userInfo:(inModifiedKeys ? [NSDictionary dictionaryWithObject:inModifiedKeys 
   197 																								   forKey:@"Keys"] : nil)];
   198 }
   199 
   200 /*!
   201  * @brief Send each chat in turn to an observer with a nil modifiedStatusKeys argument
   202  *
   203  * This lets an observer use its normal update mechanism to update every chat in some manner
   204  */
   205 - (void)updateAllChatsForObserver:(id <AIChatObserver>)observer
   206 {	
   207 	for (AIChat *chat in openChats) {
   208 		[self chatStatusChanged:chat modifiedStatusKeys:nil silent:NO];
   209 	}
   210 }
   211 
   212 /*!
   213  * @brief Notify observers of a status change.  Returns the modified attribute keys
   214  */
   215 - (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
   216 {
   217 	NSMutableSet	*attrChange = nil;
   218 	NSValue			*observerValue;
   219 	
   220 	//Let our observers know
   221 	for (observerValue in chatObserverArray) {
   222 		id <AIChatObserver>	observer;
   223 		NSSet				*newKeys;
   224 		
   225 		observer = [observerValue nonretainedObjectValue];
   226 		if ((newKeys = [observer updateChat:inChat keys:modifiedKeys silent:silent])) {
   227 			if (!attrChange) attrChange = [NSMutableSet set];
   228 			[attrChange unionSet:newKeys];
   229 		}
   230 	}
   231 	
   232 	//Send out the notification for other observers
   233 	[[NSNotificationCenter defaultCenter] postNotificationName:Chat_StatusChanged
   234 											  object:inChat
   235 											userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys 
   236 																								 forKey:@"Keys"] : nil)];
   237 	
   238 	return attrChange;
   239 }
   240 
   241 //Chats -------------------------------------------------------------------------------------------------
   242 #pragma mark Chats
   243 /*!
   244  * @brief Opens a chat for communication with the contact, creating if necessary.
   245  *
   246  * The interface controller will then be asked to open the UI for the new chat.
   247  *
   248  * @param inContact The AIListContact on which to open a chat. If an AIMetaContact, an appropriate contained contact will be selected.
   249  * @param onPreferredAccount If YES, Adium will determine the account on which the chat should be opened. If NO, inContact.account will be used. Value is treated as YES for AIMetaContacts by the action of -[AIChatController chatWithContact:].
   250  */
   251 - (AIChat *)openChatWithContact:(AIListContact *)inContact onPreferredAccount:(BOOL)onPreferredAccount
   252 {
   253 	if ([inContact isKindOfClass:[AIListBookmark class]])
   254 		return [(AIListBookmark *)inContact openChat];
   255 
   256 	if (onPreferredAccount) {
   257 		inContact = [adium.contactController preferredContactForContentType:CONTENT_MESSAGE_TYPE
   258 															   forListContact:inContact];
   259 	}
   260 
   261 	AIChat *chat = [self chatWithContact:inContact];
   262 	if (chat) [adium.interfaceController openChat:chat]; 
   263 
   264 	return chat;
   265 }
   266 
   267 /*!
   268  * @brief Creates a chat for communication with the contact, but does not make the chat active
   269  *
   270  * No window or tab is opened for the chat.
   271  * If a chat with this contact already exists, it is returned.
   272  * If a chat with a contact within the same metaContact at this contact exists, it is switched to this contact
   273  * and then returned.
   274  *
   275  * The passed contact, if an AIListContact, will be used exactly -- that is, inContact.account is the account on which the chat will be opened.
   276  * If the passed contact is an AIMetaContact, an appropriate contact/account pair will be automatically selected by this method.
   277  *
   278  * @param inContact The contact with which to open a chat. See description above.
   279  */
   280 - (AIChat *)chatWithContact:(AIListContact *)inContact
   281 {
   282 	AIListContact	*targetContact = inContact;
   283 	AIChat			*chat = nil;
   284 
   285 	/*
   286 	 If we're dealing with a meta contact, open a chat with the preferred contact for this meta contact
   287 	 It's a good idea for the caller to pick the preferred contact for us, since they know the content type
   288 	 being sent and more information - but we'll do it here as well just to be safe.
   289 	 */
   290 	if ([inContact isKindOfClass:[AIMetaContact class]]) {
   291 		targetContact = [adium.contactController preferredContactForContentType:CONTENT_MESSAGE_TYPE
   292 																   forListContact:inContact];
   293 		
   294 		/*
   295 		 If we have no accounts online, preferredContactForContentType:forListContact will return nil.
   296 		 We'd rather open up the chat window on a useless contact than do nothing, so just pick the 
   297 		 preferredContact from the metaContact.
   298 		 */
   299 		if (!targetContact) {
   300 			targetContact = [(AIMetaContact *)inContact preferredContact];
   301 		}
   302 	}
   303 	
   304 	//If we can't get a contact, we're not going to be able to get a chat... return nil
   305 	if (!targetContact) {
   306 		AILog(@"Warning: -[AIChatController chatWithContact:%@] got a nil targetContact.",inContact);
   307 		NSLog(@"Warning: -[AIChatController chatWithContact:%@] got a nil targetContact.",inContact);
   308 		return nil;
   309 	}
   310 
   311 	//Search for an existing chat we can switch instead of replacing
   312 	for (chat in openChats) {
   313 		//If a chat for this object already exists
   314 		if ([chat.uniqueChatID isEqualToString:targetContact.internalObjectID]) {
   315 			if (!(chat.listObject == targetContact)) {
   316 				[self switchChat:chat toAccount:targetContact.account];
   317 			}
   318 			
   319 			break;
   320 		}
   321 		
   322 		//If this object is within a meta contact, and a chat for an object in that meta contact already exists
   323 		if (chat.listObject.parentContact == targetContact.parentContact) {
   324 
   325 			//Switch the chat to be on this contact (and its account) now
   326 			[self switchChat:chat toListContact:targetContact usingContactAccount:YES];
   327 			
   328 			break;
   329 		}
   330 	}
   331 
   332 	if (!chat) {
   333 		AIAccount	*account = targetContact.account;
   334 
   335 		//Create a new chat
   336 		chat = [AIChat chatForAccount:account];
   337 		[chat addParticipatingListObject:targetContact notify:YES];
   338 		[openChats addObject:chat];
   339 		AILog(@"chatWithContact: Added <<%@>> [%@]",chat,openChats);
   340 
   341 		//Inform the account of its creation
   342 		if (![targetContact.account openChat:chat]) {
   343 			[openChats removeObject:chat];
   344 			AILog(@"chatWithContact: Immediately removed <<%@>> [%@]",chat,openChats);
   345 			chat = nil;
   346 		}
   347 	}
   348 
   349 	return chat;
   350 }
   351 
   352 /*!
   353  * @brief Return a pre-existing chat with a contact.
   354  *
   355  * @result The chat, or nil if no chat with the contact exists
   356  */
   357 - (AIChat *)existingChatWithContact:(AIListContact *)inContact
   358 {
   359 	AIChat			*chat = nil;
   360 
   361 	if ([inContact isKindOfClass:[AIMetaContact class]]) {
   362 		//Search for a chat with any contact within this AIMetaContact
   363 		for (chat in openChats) {
   364 			if (!chat.isGroupChat &&
   365 				[[(AIMetaContact *)inContact containedObjects] containsObjectIdenticalTo:chat.listObject]) break;
   366 		}
   367 
   368 	} else {
   369 		//Search for a chat with this AIListContact
   370 		for (chat in openChats) {
   371 			if (!chat.isGroupChat &&
   372 				chat.listObject == inContact) break;
   373 		}
   374 	}
   375 	
   376 	return chat;
   377 }
   378 
   379 /*!
   380  * @brief Open a group chat
   381  *
   382  * @param inName The name of the chat; in general, the chat room name
   383  * @param account The account on which to create the group chat
   384  * @param chatCreationInfo A dictionary of information which may be used by the account when joining the chat serverside
   385  * @brief opens a chat with the above parameters. Assigns chatroom info to the created AIChat object.
   386  */
   387 - (AIChat *)chatWithName:(NSString *)name identifier:(id)identifier onAccount:(AIAccount *)account chatCreationInfo:(NSDictionary *)chatCreationInfo
   388 {
   389 	AIChat			*chat = nil;
   390 
   391 	name = [account.service normalizeChatName:name];
   392 
   393  	if (identifier) {
   394  		chat = [self existingChatWithIdentifier:identifier onAccount:account];
   395 
   396 		if (!chat) {
   397 			//See if a chat was made with this name but which doesn't yet have an identifier. If so, take ownership!
   398 			chat = [self existingChatWithName:name onAccount:account];
   399 
   400 			if (chat && ![chat identifier])
   401                 [chat setIdentifier:identifier];
   402             // If existingChatWithName:onAccount: finds a chat, make sure it has the right identifier. 
   403             else if ([chat identifier] != identifier)
   404                 chat = nil;
   405 		}
   406 
   407 	} else {
   408 		//If the caller doesn't care about the identifier, do a search based on name to avoid creating a new chat incorrectly
   409 		chat = [self existingChatWithName:name onAccount:account];
   410 	}
   411 
   412 	AILog(@"chatWithName %@ identifier %@ existing --> %@", name, identifier, chat);
   413 	if (!chat) {
   414 		//Create a new chat
   415 		chat = [AIChat chatForAccount:account];
   416 		
   417 		chat.name = [account.service normalizeChatName:name];
   418 		chat.displayName = name;
   419 		chat.identifier = identifier;
   420 		chat.isGroupChat = YES;
   421 		chat.chatCreationDictionary = chatCreationInfo;
   422 		/* Negative preference so (default == NO) -> showing join/leave messages */
   423 		chat.showJoinLeave = ![[[adium preferenceController] preferenceForKey:[NSString stringWithFormat:@"HideJoinLeave-%@", name]
   424 																	    group:PREF_GROUP_STATUS_PREFERENCES] boolValue];		
   425 		[openChats addObject:chat];
   426 		
   427 		AILog(@"chatWithName:%@ identifier:%@ onAccount:%@ added <<%@>> [%@] [%@]",name,identifier,account,chat,openChats,chatCreationInfo);
   428 
   429 		//Inform the account of its creation
   430 		if (![account openChat:chat]) {
   431 			[openChats removeObject:chat];
   432 			AILog(@"chatWithName: Immediately removed <<%@>> [%@]",chat,openChats);
   433 			chat = nil;
   434 		}
   435 	}
   436 
   437 	AILog(@"chatWithName %@ created --> %@",name,chat);
   438 	return chat;
   439 }
   440 
   441 /*!
   442 * @brief Find an existing group chat
   443  *
   444  * @result The group AIChat, or nil if no such chat exists
   445  */
   446 - (AIChat *)existingChatWithName:(NSString *)name onAccount:(AIAccount *)account
   447 {
   448 	AIChat			*chat = nil;
   449 	
   450 	name = [account.service normalizeChatName:name];
   451 	
   452 	for (chat in openChats) {
   453 		if ((chat.account == account) &&
   454 			([chat.name isEqualToString:name])) {
   455 			break;
   456 		}
   457 	}	
   458 	
   459 	return chat;
   460 }
   461 
   462 /*!
   463  * @brief Find an existing group chat
   464  *
   465  * @result The group AIChat, or nil if no such chat exists
   466  */
   467 - (AIChat *)existingChatWithIdentifier:(id)identifier onAccount:(AIAccount *)account
   468 {
   469 	AIChat			*chat = nil;
   470 	
   471 
   472 	for (chat in openChats) {
   473 		if ((chat.account == account) &&
   474 		   ([[chat identifier] isEqual:identifier])) {
   475 			break;
   476 		}
   477 	}	
   478 	
   479 	return chat;
   480 }
   481 
   482 /*!
   483  * @brief Find an existing chat by unique chat ID
   484  *
   485  * @result The AIChat, or nil if no such chat exists
   486  */
   487 - (AIChat *)existingChatWithUniqueChatID:(NSString *)uniqueChatID
   488 {
   489 	AIChat			*chat = nil;
   490 	
   491 	
   492 	for (chat in openChats) {
   493 		if ([chat.uniqueChatID isEqualToString:uniqueChatID]) {
   494 			break;
   495 		}
   496 	}	
   497 	
   498 	return chat;
   499 }
   500 
   501 /*!
   502  * @brief Close a chat
   503  *
   504  * This should be called only by the interface controller. To close a chat programatically, use the interface controller's closeChat:.
   505  *
   506  * @result YES the chat was removed succesfully; NO if it was not
   507  */
   508 - (BOOL)closeChat:(AIChat *)inChat
   509 {	
   510 	BOOL	shouldRemove;
   511 	
   512 	/* If we are currently passing a content object for this chat through our content filters, don't remove it from
   513 	 * our openChats set as it will become needed soon. If we were to remove it, and a second message came in which was
   514 	 * also before the first message is done filtering, we would otherwise mistakenly think we needed to create a new
   515 	 * chat, generating a duplicate.
   516 	 */
   517 	shouldRemove = ![adium.contentController chatIsReceivingContent:inChat];
   518 
   519 	[inChat retain];
   520 
   521 	if (mostRecentChat == inChat) {
   522 		[mostRecentChat release];
   523 		mostRecentChat = nil;
   524 	}
   525 	
   526 	//Send out the Chat_WillClose notification
   527 	[[NSNotificationCenter defaultCenter] postNotificationName:Chat_WillClose object:inChat userInfo:nil];
   528 
   529 	//Remove the chat
   530 	if (shouldRemove) {
   531 		/* If we didn't remove the chat because we're waiting for it to reopen, don't cause the account
   532 		 * to close down the chat.
   533 		 */
   534 		[inChat.account closeChat:inChat];
   535 		[openChats removeObject:inChat];
   536 		AILog(@"closeChat: Removed <<%@>> [%@]",inChat, openChats);
   537 	} else {
   538 		AILog(@"closeChat: Did not remove <<%@>> [%@]",inChat, openChats);		
   539 	}
   540 	
   541 	[inChat setIsOpen:NO];
   542 	[inChat release];
   543 
   544 	return shouldRemove;
   545 }
   546 
   547 - (void)restoreChat:(AIChat *)inChat
   548 {
   549 	[openChats addObject:inChat];
   550 }
   551 
   552 /*!
   553  * @brief Called by an account to notifiy the chat controller that it left a chat
   554  *
   555  * Typically this is called in response to -[AIAccout closeChat:] caled in -[self closeChat:] above.
   556  * However, if the chat is never opened, accountDidCloseChat: may be called without closeChat: being called first.
   557  */
   558 - (void)accountDidCloseChat:(AIChat *)inChat
   559 {
   560 	/* If the chat is not open and the account told us that it was closed,
   561 	 * ensure that it's no longer in the open chats list, as the user will have no further
   562 	 * interaction with it. This is poarticularly important if the chat closes before it is
   563 	 * ever opened, such as when an error occurs while joining a group chat.
   564 	 */
   565 	if (![inChat isOpen])
   566 		[openChats removeObject:inChat];
   567 }
   568 
   569 /*!
   570  * @brief Switch a chat from one account to another
   571  *
   572  * The target list contact for the chat is changed to be an 'identical' one on the target account; that is, a contact
   573  * with the same UID but an account and service appropriate for newAccount.
   574  */
   575 - (void)switchChat:(AIChat *)chat toAccount:(AIAccount *)newAccount
   576 {
   577 	AIAccount	*oldAccount = chat.account;
   578 	if (newAccount != oldAccount) {
   579 		//Hang onto stuff until we're done
   580 		[chat retain];
   581 
   582 		//Close down the chat on account A
   583 		[oldAccount closeChat:chat];
   584 
   585 		//Set the account and the listObject
   586 		{
   587 			[chat setAccount:newAccount];
   588 
   589 			//We want to keep the same destination for the chat but switch it to a listContact on the desired account.
   590 			AIListContact	*newContact = [adium.contactController contactWithService:newAccount.service
   591 																				account:newAccount
   592 																					UID:chat.listObject.UID];
   593 			[chat setListObject:newContact];
   594 		}
   595 
   596 		//Open the chat on account B
   597 		[newAccount openChat:chat];
   598 		
   599 		//Clean up
   600 		[chat release];
   601 	}
   602 }
   603 
   604 /*!
   605  * @brief Switch the list contact of a chat
   606  *
   607  * @param chat The chat
   608  * @param inContact The contact with which the chat will now take place
   609  * @param useContactAccount If YES, the chat is also set to inContact.account as its account. If NO, the account and service of chat are unchanged.
   610  */
   611 - (void)switchChat:(AIChat *)chat toListContact:(AIListContact *)inContact usingContactAccount:(BOOL)useContactAccount
   612 {
   613 	AIAccount		*newAccount = (useContactAccount ? inContact.account : chat.account);
   614 
   615 	//Switch the inContact over to a contact on the new account so we send messages to the right place.
   616 	AIListContact	*newContact = [adium.contactController contactWithService:newAccount.service
   617 																		account:newAccount
   618 																			UID:inContact.UID];
   619 	if (newContact != chat.listObject) {
   620 		//Hang onto stuff until we're done
   621 		[chat retain];
   622 		
   623 		//Close down the chat on the account, as the account may need to perform actions such as closing a connection
   624 		[chat.account closeChat:chat];
   625 		
   626 		//Set to the new listContact and account as needed
   627 		[chat setListObject:newContact];
   628 		if (useContactAccount || ![inContact.service.serviceClass isEqualToString:chat.account.service.serviceClass])
   629 			[chat setAccount:newAccount];
   630 
   631 		//Reopen the chat on the account
   632 		[chat.account openChat:chat];
   633 		
   634 		//Clean up
   635 		[chat release];
   636 	}
   637 }
   638 
   639 /*!
   640  * @brief Find all open chats with a contact
   641  *
   642  * @param inContact The contact. If inContact is an AIMetaContact, all chats with all contacts within the metaContact will be returned.
   643  * @result An NSSet with all chats with the contact.  In general, will contain 0 or 1 AIChat objects, though it may contain more.
   644  */
   645 - (NSSet *)allChatsWithContact:(AIListContact *)inContact
   646 {
   647     NSMutableSet	*foundChats = [NSMutableSet set];
   648 	
   649 	//Scan the objects participating in each chat, looking for the requested object
   650 	if ([inContact isKindOfClass:[AIMetaContact class]]) {
   651 		if ([openChats count]) {
   652 			for (AIListContact *listContact in ((AIMetaContact *)inContact).uniqueContainedObjects) {
   653 				[foundChats unionSet:[self allChatsWithContact:listContact]];
   654 			}
   655 		}
   656 		
   657 	} else {
   658 		for (AIChat *chat in openChats) {
   659 			if (!chat.isGroupChat &&
   660 				[chat.listObject.internalObjectID isEqualToString:inContact.internalObjectID] &&
   661 				chat.isOpen) {
   662 				[foundChats addObject:chat];
   663 			}
   664 		}
   665 	}
   666 
   667     return foundChats;
   668 }
   669 
   670 /*!
   671  * @brief Find all open chats with a contact
   672  *
   673  * @param inContact The contact. If inContact is an AIMetaContact, all chats with all contacts within the metaContact will be returned.
   674  * @result An NSSet with all chats with the contact.
   675  */
   676 - (NSSet *)allGroupChatsContainingContact:(AIListContact *)inContact
   677 {
   678 	NSMutableSet *groupChats = [NSMutableSet set];
   679 	
   680 	//Search for a chat containing this AIListContact
   681 	if ([inContact isKindOfClass:[AIMetaContact class]]) {
   682 		//Search for a chat with any contact within this AIMetaContact
   683 		for (AIChat *chat in openChats) {
   684 			if (!chat.isGroupChat)
   685 				continue;
   686 			
   687 			for (AIListContact *contact in (AIMetaContact *)inContact) {
   688 				if([chat containsObject:contact]) {
   689 					[groupChats addObject:chat];
   690 					break;
   691 				}
   692 			}
   693 		}
   694 		
   695 	} else {
   696 		//Search for a chat with this AIListContact
   697 		for (AIChat *chat in openChats) {
   698 			if (chat.isGroupChat && [chat containsObject:inContact]) {
   699 				[groupChats addObject:chat];
   700 			}
   701 		}
   702 	}
   703 	
   704 	return groupChats;
   705 }
   706 
   707 /*!
   708  * @brief All open chats
   709  *
   710  * Open chats from the chatController may include chats which are not currently displayed by the interface.
   711  */
   712 - (NSSet *)openChats
   713 {
   714     return [[openChats copy] autorelease];
   715 }
   716 
   717 /*!
   718  * @brief Find the chat which most recently received content which has not yet been seen
   719  *
   720  * @result An AIChat with unviewed content, or nil if no chats current have unviewed content
   721  */
   722 - (AIChat *)mostRecentUnviewedChat
   723 {
   724 	BOOL onlyMentions = [[adium.preferenceController preferenceForKey:KEY_STATUS_MENTION_COUNT
   725 																group:PREF_GROUP_STATUS_PREFERENCES] boolValue];
   726 	
   727 	if (mostRecentChat && mostRecentChat.unviewedContentCount && (!mostRecentChat.isGroupChat || !onlyMentions || mostRecentChat.unviewedMentionCount)) {
   728 		//First choice: switch to the chat which received chat most recently if it has unviewed content
   729 		return mostRecentChat;
   730 		
   731 	} else {
   732 		//Second choice: switch to the first chat we can find which has unviewed content
   733 		for (AIChat *chat in openChats) {
   734 			if (chat.unviewedContentCount && (!chat.isGroupChat || !onlyMentions || chat.unviewedMentionCount))
   735 				return chat;
   736 		}
   737 	}
   738 	
   739 	return nil;
   740 }
   741 
   742 /*!
   743  * @brief Gets the total number of unviewed messages
   744  * 
   745  * @result The number of unviewed messages
   746  */
   747 - (NSUInteger)unviewedContentCount
   748 {
   749 	NSUInteger	count = 0;
   750 
   751 	for (AIChat *chat in openChats) {
   752 		if (chat.isGroupChat &&
   753 			[[adium.preferenceController preferenceForKey:KEY_STATUS_MENTION_COUNT
   754 													group:PREF_GROUP_STATUS_PREFERENCES] boolValue]) {
   755 			count += [chat unviewedMentionCount];
   756 		} else {
   757 			count += [chat unviewedContentCount];
   758 		}
   759 	}
   760 	return count;
   761 }
   762 
   763 /*!
   764  * @brief Gets the total number of conversations with unviewed messages
   765  * 
   766  * @result The number of conversations with unviewed messages
   767  */
   768 - (NSUInteger)unviewedConversationCount
   769 {
   770 	NSUInteger count = 0;
   771 
   772 	for (AIChat *chat in openChats) {
   773 		if (chat.isGroupChat &&
   774 			[[adium.preferenceController preferenceForKey:KEY_STATUS_MENTION_COUNT
   775 													group:PREF_GROUP_STATUS_PREFERENCES] boolValue]) {
   776 			if (chat.unviewedMentionCount) {
   777 				count++;
   778 			}
   779 		} else if (chat.unviewedContentCount) {
   780 			count++;
   781 		}
   782 	}
   783 	return count;
   784 }
   785 
   786 /*!
   787  * @brief Is the passed contact in a group chat?
   788  *
   789  * @result YES if the contact is in an open group chat; NO if not.
   790  */
   791 - (BOOL)contactIsInGroupChat:(AIListContact *)listContact
   792 {
   793 	BOOL			contactIsInGroupChat = NO;
   794 	
   795 	for (AIChat *chat in openChats) {
   796 		if (chat.isGroupChat &&
   797 			[chat containsObject:listContact]) {
   798 			
   799 			contactIsInGroupChat = YES;
   800 			break;
   801 		}
   802 	}
   803 	
   804 	return contactIsInGroupChat;
   805 }
   806 
   807 /*!
   808  * @brief Called when content is sent or received
   809  *
   810  * Update the most recent chat
   811  */
   812 - (void)didExchangeContent:(NSNotification *)notification
   813 {
   814 	AIContentObject	*contentObject = [[notification userInfo] objectForKey:@"AIContentObject"];
   815 
   816 	//Update our most recent chat
   817 	if (contentObject.trackContent) {
   818 		AIChat	*chat = contentObject.chat;
   819 		
   820 		if (chat != mostRecentChat) {
   821 			[mostRecentChat release];
   822 			mostRecentChat = [chat retain];
   823 		}
   824 	}
   825 }
   826 
   827 #pragma mark Menu Items
   828 /*!
   829  * @brief Toggle ignoring of a contact
   830  *
   831  * Must be called from the contextual menu for the contact within a chat
   832  */
   833 - (void)toggleIgnoreOfContact:(id)sender
   834 {
   835 	AIListObject	*listObject = adium.menuController.currentContextMenuObject;
   836 	AIChat			*chat = [adium.menuController currentContextMenuChat];
   837 	
   838 	if ([listObject isKindOfClass:[AIListContact class]]) {
   839 		BOOL			isIgnored = [chat isListContactIgnored:(AIListContact *)listObject];
   840 		[chat setListContact:(AIListContact *)listObject isIgnored:!isIgnored];
   841 	}
   842 }
   843 
   844 /*!
   845  * @brief Toggle displaying of show/part messages for a chat
   846  *
   847  * Effects the currently active chat.
   848  */
   849 - (void)toggleShowJoinLeave:(id)sender
   850 {
   851 	AIChat *chat = nil;
   852 	
   853 	if (sender == menuItem_joinLeave) {
   854 		chat = adium.interfaceController.activeChat;
   855 	} else {
   856 		chat = adium.menuController.currentContextMenuChat;
   857 	}
   858 
   859 	chat.showJoinLeave = !chat.showJoinLeave;
   860 
   861 	[[adium preferenceController] setPreference:[NSNumber numberWithBool:!chat.showJoinLeave]
   862 										 forKey:[NSString stringWithFormat:@"HideJoinLeave-%@", chat.name]
   863 										  group:PREF_GROUP_STATUS_PREFERENCES];
   864 }
   865 
   866 /*!
   867  * @brief Menu item validation
   868  *
   869  * When asked to validate our ignore menu item, set its title to ignore/un-ignore as appropriate for the contact
   870  */
   871 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
   872 {
   873 	if (menuItem == menuItem_ignore) {
   874 		AIListObject	*listObject = adium.menuController.currentContextMenuObject;
   875 		AIChat			*chat = [adium.menuController currentContextMenuChat];
   876 		
   877 		if ([listObject isKindOfClass:[AIListContact class]]) {
   878 			if ([chat isListContactIgnored:(AIListContact *)listObject]) {
   879 				[menuItem setTitle:AILocalizedString(@"Un-ignore","Un-ignore means begin receiving messages from this contact again in a chat")];
   880 				
   881 			} else {
   882 				[menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
   883 			}
   884 		} else {
   885 			[menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
   886 			return NO;
   887 		}
   888 	} else if ([menuItem.title isEqualToString:SHOW_JOIN_LEAVE_TITLE]) {
   889 		// We're using multiple menu items for the same goal, and WKMV makes a copy of the contextual ones.
   890 		// Validate based on the title.
   891 		AIChat *chat = nil;
   892 		if (menuItem == menuItem_joinLeave) {
   893 			chat = adium.interfaceController.activeChat;
   894 		} else {
   895 			chat = adium.menuController.currentContextMenuChat;
   896 		}
   897 			
   898 		if (chat.isGroupChat) {
   899 			[menuItem setState:chat.showJoinLeave];
   900 			return YES;
   901 		}
   902 		
   903 		return NO;		
   904 	}
   905 	
   906 	return YES;
   907 }
   908 
   909 #pragma mark Chat contact addition and removal
   910 
   911 /*!
   912  * @brief A chat added a listContact to its participatants list
   913  *
   914  * @param chat The chat
   915  * @param inContact The contact
   916  * @param notify If YES, trigger the contact joined event if this is a group chat.  Ignored if this is not a group chat.
   917  */
   918 - (void)chat:(AIChat *)chat addedListContacts:(NSArray *)inObjects notify:(BOOL)notify
   919 {
   920 	if (notify && chat.isGroupChat) {
   921 		/* Prevent triggering of the event when we are informed that the chat's own account entered the chat
   922 		 * If the UID of a contact in a chat differs from a normal UID, such as is the case with Jabber where a chat
   923 		 * contact has the form "roomname@conferenceserver/handle" this will fail, but it's better than nothing.
   924 		 */
   925 		for (AIListContact *inContact in inObjects) {
   926 			if (![inContact.account.UID isEqualToString:inContact.UID]) {
   927 				[adiumChatEvents chat:chat addedListContact:inContact];
   928 			}
   929 		}
   930 	}
   931 
   932 	//Always notify Adium that the list changed so it can be updated, caches can be modified, etc.
   933 	[[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
   934 											  object:chat];
   935 }
   936 
   937 /*!
   938  * @brief A chat removed a listContact from its participants list
   939  *
   940  * @param chat The chat
   941  * @param inContact The contact
   942  */
   943 - (void)chat:(AIChat *)chat removedListContact:(AIListContact *)inContact
   944 {
   945 	if (chat.isGroupChat) {
   946 		[adiumChatEvents chat:chat removedListContact:inContact];
   947 	}
   948 
   949 	[[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
   950 											  object:chat];
   951 }
   952 
   953 - (NSString *)defaultInvitationMessageForRoom:(NSString *)room account:(AIAccount *)inAccount
   954 {
   955 	return [NSString stringWithFormat:AILocalizedString(@"%@ invites you to join the chat \"%@\"", nil), inAccount.formattedUID, room];
   956 }
   957 
   958 @end
   959 
   960 /*
   961  * These strings were used previously; we may want them again. Keeping the translations around for now.
   962   AILocalizedString("%@ joined the chat", nil);
   963   AILocalizedString("%@ left the chat", nil);
   964  */