Differentiated connection errors from other errors in Bonjour. Only use connection...
[adiumx.git] / Source / AIChatController.m
blob0afd05271dcc5d80708880ff5fbcc9e7a59a7571
1 //
2 //  AIChatController.m
3 //  Adium
4 //
5 //  Created by Evan Schoenberg on 6/10/05.
6 //
8 #import "AIChatController.h"
10 #import <Adium/AIContentControllerProtocol.h>
11 #import <Adium/AIContactControllerProtocol.h>
12 #import <Adium/AIInterfaceControllerProtocol.h>
13 #import <Adium/AIMenuControllerProtocol.h>
14 #import "AdiumChatEvents.h"
15 #import <Adium/AIAccount.h>
16 #import <Adium/AIChat.h>
17 #import <Adium/AIContentObject.h>
18 #import <Adium/AIContentMessage.h>
19 #import <Adium/AIListContact.h>
20 #import <Adium/AIMetaContact.h>
21 #import <AIUtilities/AIArrayAdditions.h>
22 #import <AIUtilities/AIMenuAdditions.h>
24 @interface AIChatController (PRIVATE)
25 - (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
26 - (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys;
27 @end
29 /*!
30  * @class AIChatController
31  * @brief Core controller for chats
32  *
33  * This is the only class which should vend AIChat objects (via openChat... or chatWith:...).
34  * AIChat objects should never be created directly.
35  */
36 @implementation AIChatController
38 /*!
39  * @brief Initialize the controller
40  */
41 - (id)init
42 {       
43         if ((self = [super init])) {
44                 mostRecentChat = nil;
45                 chatObserverArray = [[NSMutableArray alloc] init];
46                 adiumChatEvents = [[AdiumChatEvents alloc] init];
48                 //Chat tracking
49                 openChats = [[NSMutableSet alloc] init];
50         }
51         return self;
55 /*!
56  * @brief Controller loaded
57  */
58 - (void)controllerDidLoad
59 {       
60         //Observe content so we can update the most recent chat
61     [[adium notificationCenter] addObserver:self 
62                                                                    selector:@selector(didExchangeContent:) 
63                                                                            name:CONTENT_MESSAGE_RECEIVED
64                                                                          object:nil];
65         
66     [[adium notificationCenter] addObserver:self 
67                                                                    selector:@selector(didExchangeContent:) 
68                                                                            name:CONTENT_MESSAGE_RECEIVED_GROUP
69                                                                          object:nil];
70         
71         [[adium notificationCenter] addObserver:self 
72                                                                    selector:@selector(didExchangeContent:) 
73                                                                            name:CONTENT_MESSAGE_SENT
74                                                                          object:nil];
75         
76         [[adium notificationCenter] addObserver:self
77                                                                    selector:@selector(adiumWillTerminate:)
78                                                                            name:AIAppWillTerminateNotification
79                                                                          object:nil];
81         //Ignore menu item for contacts in group chats
82         menuItem_ignore = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@""
83                                                                                                                                                    target:self
84                                                                                                                                                    action:@selector(toggleIgnoreOfContact:)
85                                                                                                                                         keyEquivalent:@""];
86         [[adium menuController] addContextualMenuItem:menuItem_ignore toLocation:Context_Contact_GroupChatAction];
87         
88         [adiumChatEvents controllerDidLoad];
92 /*!
93  * @brief Controller will close
94  */
95 - (void)controllerWillClose
97         
101  * @brief Adium will terminate
103  * Post the Chat_WillClose for each open chat so any closing behavior can be performed
104  */
105 - (void)adiumWillTerminate:(NSNotification *)inNotification
107         NSEnumerator    *enumerator = [openChats objectEnumerator];
108         AIChat                  *chat;
109         
110         //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.
111         while ((chat = [enumerator nextObject])) {
112                 [self closeChat:chat];
113         }
117  * @brief Deallocate
118  */
119 - (void)dealloc
121         [openChats release]; openChats = nil;
122         [chatObserverArray release]; chatObserverArray = nil;
123         [[adium notificationCenter] removeObserver:self];
125         [super dealloc];
127         
129  * @brief Register a chat observer
131  * Chat observers are notified when status objects are changed on chats
133  * @param inObserver An observer, which must conform to AIChatObserver
134  */
135 - (void)registerChatObserver:(id <AIChatObserver>)inObserver
137         //Add the observer
138     [chatObserverArray addObject:[NSValue valueWithNonretainedObject:inObserver]];
139         
140     //Let the new observer process all existing chats
141         [self updateAllChatsForObserver:inObserver];
145  * @brief Unregister a chat observer
146  */
147 - (void)unregisterChatObserver:(id <AIChatObserver>)inObserver
149     [chatObserverArray removeObject:[NSValue valueWithNonretainedObject:inObserver]];
153  * @brief Chat status changed
155  * Called by AIChat after it changes one or more status keys.
156  */
157 - (void)chatStatusChanged:(AIChat *)inChat modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
159         NSSet                   *modifiedAttributeKeys;
160         
161     //Let all observers know the chat's status has changed before performing any further notifications
162         modifiedAttributeKeys = [self _informObserversOfChatStatusChange:inChat withKeys:inModifiedKeys silent:silent];
163         
164     //Post an attributes changed message (if necessary)
165     if ([modifiedAttributeKeys count]) {
166                 [self chatAttributesChanged:inChat modifiedKeys:modifiedAttributeKeys];
167     }   
171  * @brief Chat attributes changed
173  * Called by -[AIChatController chatStatusChanged:modifiedStatusKeys:silent:] if any observers changed attributes
174  */
175 - (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys
177         //Post an attributes changed message
178         [[adium notificationCenter] postNotificationName:Chat_AttributesChanged
179                                                                                           object:inChat
180                                                                                         userInfo:(inModifiedKeys ? [NSDictionary dictionaryWithObject:inModifiedKeys 
181                                                                                                                                                                                                    forKey:@"Keys"] : nil)];
185  * @brief Send each chat in turn to an observer with a nil modifiedStatusKeys argument
187  * This lets an observer use its normal update mechanism to update every chat in some manner
188  */
189 - (void)updateAllChatsForObserver:(id <AIChatObserver>)observer
191         NSEnumerator    *enumerator = [openChats objectEnumerator];
192         AIChat                  *chat;
193         
194         while ((chat = [enumerator nextObject])) {
195                 [self chatStatusChanged:chat modifiedStatusKeys:nil silent:NO];
196         }
200  * @brief Notify observers of a status change.  Returns the modified attribute keys
201  */
202 - (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
204         NSMutableSet    *attrChange = nil;
205         NSEnumerator    *enumerator;
206         NSValue                 *observerValue;
207         
208         //Let our observers know
209         enumerator = [chatObserverArray objectEnumerator];
210         while ((observerValue = [enumerator nextObject])) {
211                 id <AIChatObserver>     observer;
212                 NSSet                           *newKeys;
213                 
214                 observer = [observerValue nonretainedObjectValue];
215                 if ((newKeys = [observer updateChat:inChat keys:modifiedKeys silent:silent])) {
216                         if (!attrChange) attrChange = [NSMutableSet set];
217                         [attrChange unionSet:newKeys];
218                 }
219         }
220         
221         //Send out the notification for other observers
222         [[adium notificationCenter] postNotificationName:Chat_StatusChanged
223                                                                                           object:inChat
224                                                                                         userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys 
225                                                                                                                                                                                                  forKey:@"Keys"] : nil)];
226         
227         return attrChange;
230 //Chats -------------------------------------------------------------------------------------------------
231 #pragma mark Chats
233  * @brief Opens a chat for communication with the contact, creating if necessary.
235  * The interface controller will then be asked to open the UI for the new chat.
237  * @param inContact The AIListContact on which to open a chat. If an AIMetaContact, an appropriate contained contact will be selected.
238  * @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:].
239  */
240 - (AIChat *)openChatWithContact:(AIListContact *)inContact onPreferredAccount:(BOOL)onPreferredAccount
242         AIChat  *chat;
244         if (onPreferredAccount) {
245                 inContact = [[adium contactController] preferredContactForContentType:CONTENT_MESSAGE_TYPE
246                                                                                                                            forListContact:inContact];
247         }
249         chat = [self chatWithContact:inContact];
250         if (chat) [[adium interfaceController] openChat:chat]; 
252         return chat;
256  * @brief Creates a chat for communication with the contact, but does not make the chat active
258  * No window or tab is opened for the chat.
259  * If a chat with this contact already exists, it is returned.
260  * If a chat with a contact within the same metaContact at this contact exists, it is switched to this contact
261  * and then returned.
263  * The passed contact, if an AIListContact, will be used exactly -- that is, [inContact account] is the account on which the chat will be opened.
264  * If the passed contact is an AIMetaContact, an appropriate contact/account pair will be automatically selected by this method.
266  * @param inContact The contact with which to open a chat. See description above.
267  */
268 - (AIChat *)chatWithContact:(AIListContact *)inContact
270         NSEnumerator    *enumerator;
271         AIChat                  *chat = nil;
272         AIListContact   *targetContact = inContact;
274         /*
275          If we're dealing with a meta contact, open a chat with the preferred contact for this meta contact
276          It's a good idea for the caller to pick the preferred contact for us, since they know the content type
277          being sent and more information - but we'll do it here as well just to be safe.
278          */
279         if ([inContact containsMultipleContacts]) {
280                 targetContact = [[adium contactController] preferredContactForContentType:CONTENT_MESSAGE_TYPE
281                                                                                                                                    forListContact:inContact];
282                 
283                 /*
284                  If we have no accounts online, preferredContactForContentType:forListContact will return nil.
285                  We'd rather open up the chat window on a useless contact than do nothing, so just pick the 
286                  preferredContact from the metaContact.
287                  */
288                 if (!targetContact) {
289                         targetContact = [(AIMetaContact *)inContact preferredContact];
290                 }
291         }
292         
293         //If we can't get a contact, we're not going to be able to get a chat... return nil
294         if (!targetContact) {
295                 AILog(@"Warning: -[AIChatController chatWithContact:%@] got a nil targetContact.",inContact);
296                 NSLog(@"Warning: -[AIChatController chatWithContact:%@] got a nil targetContact.",inContact);
297                 return nil;
298         }
299         
300         //XXX Temporary.. make sure that the fixes early in adium-1.1svn are right.
301         NSAssert1(![targetContact isMemberOfClass:[AIMetaContact class]], @"Should not get this far in chatWithContact: with an AIMetaContact (%@)!",targetContact);
302         
303         //Search for an existing chat we can switch instead of replacing
304         enumerator = [openChats objectEnumerator];
305         while ((chat = [enumerator nextObject])) {
306                 //If a chat for this object already exists
307                 if ([[chat uniqueChatID] isEqualToString:[targetContact internalObjectID]]) {
308                         if (!([chat listObject] == targetContact)) {
309                                 [self switchChat:chat toAccount:[targetContact account]];
310                         }
311                         
312                         break;
313                 }
314                 
315                 //If this object is within a meta contact, and a chat for an object in that meta contact already exists
316                 if ([[targetContact containingObject] isKindOfClass:[AIMetaContact class]] && 
317                    [[chat listObject] containingObject] == [targetContact containingObject]) {
319                         //Switch the chat to be on this contact (and its account) now
320                         [self switchChat:chat toListContact:targetContact usingContactAccount:YES];
321                         
322                         break;
323                 }
324         }
326         if (!chat) {
327                 AIAccount       *account = [targetContact account];
329                 //Create a new chat
330                 chat = [AIChat chatForAccount:account];
331                 [chat addObject:targetContact];
332                 [openChats addObject:chat];
333                 AILog(@"chatWithContact: Added <<%@>> [%@]",chat,openChats);
335                 //Inform the account of its creation
336                 if (![[targetContact account] openChat:chat]) {
337                         [openChats removeObject:chat];
338                         AILog(@"chatWithContact: Immediately removed <<%@>> [%@]",chat,openChats);
339                         chat = nil;
340                 }
341         }
343         return chat;
347  * @brief Return a pre-existing chat with a contact.
349  * @result The chat, or nil if no chat with the contact exists
350  */
351 - (AIChat *)existingChatWithContact:(AIListContact *)inContact
353         NSEnumerator    *enumerator;
354         AIChat                  *chat = nil;
356         if ([inContact isKindOfClass:[AIMetaContact class]]) {
357                 //Search for a chat with any contact within this AIMetaContact
358                 enumerator = [openChats objectEnumerator];
359                 while ((chat = [enumerator nextObject])) {
360                         if ([[(AIMetaContact *)inContact containedObjects] containsObjectIdenticalTo:[chat listObject]]) break;
361                 }
363         } else {
364                 //Search for a chat with this AIListContact
365                 enumerator = [openChats objectEnumerator];
366                 while ((chat = [enumerator nextObject])) {
367                         if ([chat listObject] == inContact) break;
368                 }
369         }
370         
371         return chat;
375  * @brief Open a group chat
377  * @param inName The name of the chat; in general, the chat room name
378  * @param account The account on which to create the group chat
379  * @param chatCreationInfo A dictionary of information which may be used by the account when joining the chat serverside
380  * @brief opens a chat with the above parameters. Assigns chatroom info to the created AIChat object.
381  */
382 - (AIChat *)chatWithName:(NSString *)name identifier:(id)identifier onAccount:(AIAccount *)account chatCreationInfo:(NSDictionary *)chatCreationInfo
384         AIChat                  *chat = nil;
386         if (identifier) {
387                 chat = [self existingChatWithIdentifier:identifier onAccount:account];
388                 if (!chat) {
389                         //See if a chat was made with this name but which doesn't yet have an identifier. If so, take ownership!
390                         chat = [self existingChatWithName:name onAccount:account];
391                         if (chat && ![chat identifier]) [chat setIdentifier:identifier];
392                 }
394         } else {
395                 //If the caller doesn't care about the identifier, do a search based on name to avoid creating a new chat incorrectly
396                 chat = [self existingChatWithName:name onAccount:account];
397         }
399         AILog(@"chatWithName %@ identifier %@ existing --> %@", name, identifier, chat);
400         if (!chat) {
401                 //Create a new chat
402                 chat = [AIChat chatForAccount:account];
403                 [chat setName:name];
404                 [chat setIdentifier:identifier];
405                 [chat setIsGroupChat:YES];
406                 [chat setChatCreationDictionary:chatCreationInfo];
407                                         
408                 [openChats addObject:chat];
409                 AILog(@"chatWithName:%@ identifier:%@ onAccount:%@ added <<%@>> [%@]",name,identifier,account,chat,openChats);
412                 //Inform the account of its creation
413                 if (![account openChat:chat]) {
414                         [openChats removeObject:chat];
415                         AILog(@"chatWithName: Immediately removed <<%@>> [%@]",chat,openChats);
416                         chat = nil;
417                 }
418         }
420         AILog(@"chatWithName %@ created --> %@",name,chat);
421         return chat;
425 * @brief Find an existing group chat
427  * @result The group AIChat, or nil if no such chat exists
428  */
429 - (AIChat *)existingChatWithName:(NSString *)name onAccount:(AIAccount *)account
431         NSEnumerator    *enumerator;
432         AIChat                  *chat = nil;
433         
434         enumerator = [openChats objectEnumerator];
435         
436         while ((chat = [enumerator nextObject])) {
437                 if (([chat account] == account) &&
438                         ([[chat name] isEqualToString:name])) {
439                         break;
440                 }
441         }       
442         
443         return chat;
447  * @brief Find an existing group chat
449  * @result The group AIChat, or nil if no such chat exists
450  */
451 - (AIChat *)existingChatWithIdentifier:(id)identifier onAccount:(AIAccount *)account
453         NSEnumerator    *enumerator;
454         AIChat                  *chat = nil;
455         
456         enumerator = [openChats objectEnumerator];
458         while ((chat = [enumerator nextObject])) {
459                 if (([chat account] == account) &&
460                    ([[chat identifier] isEqual:identifier])) {
461                         break;
462                 }
463         }       
464         
465         return chat;
469  * @brief Find an existing chat by unique chat ID
471  * @result The AIChat, or nil if no such chat exists
472  */
473 - (AIChat *)existingChatWithUniqueChatID:(NSString *)uniqueChatID
475         NSEnumerator    *enumerator;
476         AIChat                  *chat = nil;
477         
478         enumerator = [openChats objectEnumerator];
479         
480         while ((chat = [enumerator nextObject])) {
481                 if ([[chat uniqueChatID] isEqualToString:uniqueChatID]) {
482                         break;
483                 }
484         }       
485         
486         return chat;
490  * @brief Close a chat
492  * This should be called only by the interface controller. To close a chat programatically, use the interface controller's closeChat:.
494  * @result YES the chat was removed succesfully; NO if it was not
495  */
496 - (BOOL)closeChat:(AIChat *)inChat
497 {       
498         BOOL    shouldRemove;
499         
500         /* If we are currently passing a content object for this chat through our content filters, don't remove it from
501          * our openChats set as it will become needed soon. If we were to remove it, and a second message came in which was
502          * also before the first message is done filtering, we would otherwise mistakenly think we needed to create a new
503          * chat, generating a duplicate.
504          */
505         shouldRemove = ![[adium contentController] chatIsReceivingContent:inChat];
507         [inChat retain];
509         if (mostRecentChat == inChat) {
510                 [mostRecentChat release];
511                 mostRecentChat = nil;
512         }
513         
514         //Send out the Chat_WillClose notification
515         [[adium notificationCenter] postNotificationName:Chat_WillClose object:inChat userInfo:nil];
517         //Remove the chat
518         if (shouldRemove) {
519                 /* If we didn't remove the chat because we're waiting for it to reopen, don't cause the account
520                  * to close down the chat.
521                  */
522                 [[inChat account] closeChat:inChat];
523                 [openChats removeObject:inChat];
524                 AILog(@"closeChat: Removed <<%@>> [%@]",inChat, openChats);
525         } else {
526                 AILog(@"closeChat: Did not remove <<%@>> [%@]",inChat, openChats);              
527         }
528         
529         [inChat setIsOpen:NO];
530         [inChat release];
532         return shouldRemove;
536  * @brief Switch a chat from one account to another
538  * The target list contact for the chat is changed to be an 'identical' one on the target account; that is, a contact
539  * with the same UID but an account and service appropriate for newAccount.
540  */
541 - (void)switchChat:(AIChat *)chat toAccount:(AIAccount *)newAccount
543         AIAccount       *oldAccount = [chat account];
544         if (newAccount != oldAccount) {
545                 //Hang onto stuff until we're done
546                 [chat retain];
548                 //Close down the chat on account A
549                 [oldAccount closeChat:chat];
551                 //Set the account and the listObject
552                 {
553                         [chat setAccount:newAccount];
555                         //We want to keep the same destination for the chat but switch it to a listContact on the desired account.
556                         AIListContact   *newContact = [[adium contactController] contactWithService:[newAccount service]
557                                                                                                                                                                 account:newAccount
558                                                                                                                                                                         UID:[[chat listObject] UID]];
559                         [chat setListObject:newContact];
560                 }
562                 //Open the chat on account B
563                 [newAccount openChat:chat];
564                 
565                 //Clean up
566                 [chat release];
567         }
571  * @brief Switch the list contact of a chat
573  * @param chat The chat
574  * @param inContact The contact with which the chat will now take place
575  * @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.
576  */
577 - (void)switchChat:(AIChat *)chat toListContact:(AIListContact *)inContact usingContactAccount:(BOOL)useContactAccount
579         AIAccount               *newAccount = (useContactAccount ? [inContact account] : [chat account]);
581         //Switch the inContact over to a contact on the new account so we send messages to the right place.
582         AIListContact   *newContact = [[adium contactController] contactWithService:[newAccount service]
583                                                                                                                                                 account:newAccount
584                                                                                                                                                         UID:[inContact UID]];
585         if (newContact != [chat listObject]) {
586                 //Hang onto stuff until we're done
587                 [chat retain];
588                 
589                 //Close down the chat on the account, as the account may need to perform actions such as closing a connection
590                 [[chat account] closeChat:chat];
591                 
592                 //Set to the new listContact and account as needed
593                 [chat setListObject:newContact];
594                 if (useContactAccount) [chat setAccount:newAccount];
596                 //Reopen the chat on the account
597                 [[chat account] openChat:chat];
598                 
599                 //Clean up
600                 [chat release];
601         }
605  * @brief Find all open chats with a contact
607  * @param inContact The contact. If inContact is an AIMetaContact, all chats with all contacts within the metaContact will be returned.
608  * @result An NSSet with all chats with the contact.  In general, will contain 0 or 1 AIChat objects, though it may contain more.
609  */
610 - (NSSet *)allChatsWithContact:(AIListContact *)inContact
612     NSMutableSet        *foundChats = nil;
613         
614         //Scan the objects participating in each chat, looking for the requested object
615         if ([inContact isKindOfClass:[AIMetaContact class]]) {
616                 if ([openChats count]) {
617                         NSEnumerator    *enumerator;
618                         AIListContact   *listContact;
620                         enumerator = [[(AIMetaContact *)inContact listContacts] objectEnumerator];
621                         while ((listContact = [enumerator nextObject])) {
622                                 NSSet           *listContactChats;
623                                 if ((listContactChats = [self allChatsWithContact:listContact])) {
624                                         if (!foundChats) foundChats = [NSMutableSet set];
625                                         [foundChats unionSet:listContactChats];
626                                 }
627                         }
628                 }
629                 
630         } else {
631                 NSEnumerator    *enumerator;
632                 AIChat                  *chat;
633                 
634                 enumerator = [openChats objectEnumerator];
635                 while ((chat = [enumerator nextObject])) {
636                         if (![chat isGroupChat] &&
637                                 [[[chat listObject] internalObjectID] isEqualToString:[inContact internalObjectID]] &&
638                                 [chat isOpen]) {
639                                 if (!foundChats) foundChats = [NSMutableSet set];
640                                 [foundChats addObject:chat];
641                         }
642                 }
643         }
645     return foundChats;
649  * @brief All open chats
651  * Open chats from the chatController may include chats which are not currently displayed by the interface.
652  */
653 - (NSSet *)openChats
655     return openChats;
659  * @brief Find the chat which most recently received content which has not yet been seen
661  * @result An AIChat with unviewed content, or nil if no chats current have unviewed content
662  */
663 - (AIChat *)mostRecentUnviewedChat
665         AIChat  *mostRecentUnviewedChat = nil;
666         
667         if (mostRecentChat && [mostRecentChat unviewedContentCount]) {
668                 //First choice: switch to the chat which received chat most recently if it has unviewed content
669                 mostRecentUnviewedChat = mostRecentChat;
670                 
671         } else {
672                 //Second choice: switch to the first chat we can find which has unviewed content
673                 NSEnumerator    *enumerator = [openChats objectEnumerator];
674                 AIChat                  *chat;
675                 while ((chat = [enumerator nextObject]) && ![chat unviewedContentCount]);
676                 
677                 if (chat) mostRecentUnviewedChat = chat;
678         }
679         
680         return mostRecentUnviewedChat;
684  * @brief Gets the total number of unviewed messages
685  * 
686  * @result The number of unviewed messages
687  */
688 - (int)unviewedContentCount
690         int                             count = 0;
691         AIChat                  *chat;
692         NSEnumerator    *enumerator;
694         enumerator = [[self openChats] objectEnumerator];
695         while ((chat = [enumerator nextObject])) {
696                 count += [chat unviewedContentCount];
697         }
698         return count;
702  * @brief Is the passed contact in a group chat?
704  * @result YES if the contact is in an open group chat; NO if not.
705  */
706 - (BOOL)contactIsInGroupChat:(AIListContact *)listContact
708         NSEnumerator    *chatEnumerator = [openChats objectEnumerator];
709         AIChat                  *chat;
710         BOOL                    contactIsInGroupChat = NO;
711         
712         while ((chat = [chatEnumerator nextObject])) {
713                 if ([chat isGroupChat] &&
714                         [chat containsObject:listContact]) {
715                         
716                         contactIsInGroupChat = YES;
717                         break;
718                 }
719         }
720         
721         return contactIsInGroupChat;
725  * @brief Called when content is sent or received
727  * Update the most recent chat
728  */
729 - (void)didExchangeContent:(NSNotification *)notification
731         AIContentObject *contentObject = [[notification userInfo] objectForKey:@"AIContentObject"];
733         //Update our most recent chat
734         if ([contentObject trackContent]) {
735                 AIChat  *chat = [contentObject chat];
736                 
737                 if (chat != mostRecentChat) {
738                         [mostRecentChat release];
739                         mostRecentChat = [chat retain];
740                 }
741         }
744 #pragma mark Ignore
746  * @brief Toggle ignoring of a contact
748  * Must be called from the contextual menu for the contact within a chat
749  */
750 - (void)toggleIgnoreOfContact:(id)sender
752         AIListObject    *listObject = [[adium menuController] currentContextMenuObject];
753         AIChat                  *chat = [[adium menuController] currentContextMenuChat];
754         
755         if ([listObject isKindOfClass:[AIListContact class]]) {
756                 BOOL                    isIgnored = [chat isListContactIgnored:(AIListContact *)listObject];
757                 [chat setListContact:(AIListContact *)listObject isIgnored:!isIgnored];
758         }
762  * @brief Menu item validation
764  * When asked to validate our ignore menu item, set its title to ignore/un-ignore as appropriate for the contact
765  */
766 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
768         if (menuItem == menuItem_ignore) {
769                 AIListObject    *listObject = [[adium menuController] currentContextMenuObject];
770                 AIChat                  *chat = [[adium menuController] currentContextMenuChat];
771                 
772                 if ([listObject isKindOfClass:[AIListContact class]]) {
773                         if ([chat isListContactIgnored:(AIListContact *)listObject]) {
774                                 [menuItem setTitle:AILocalizedString(@"Un-ignore","Un-ignore means begin receiving messages from this contact again in a chat")];
775                                 
776                         } else {
777                                 [menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
778                         }
779                 } else {
780                         [menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
781                         return NO;
782                 }
783         }
784         
785         return YES;
788 #pragma mark Chat contact addition and removal
791  * @brief A chat added a listContact to its participatants list
793  * @param chat The chat
794  * @param inContact The contact
795  * @param notify If YES, trigger the contact joined event if this is a group chat.  Ignored if this is not a group chat.
796  */
797 - (void)chat:(AIChat *)chat addedListContact:(AIListContact *)inContact notify:(BOOL)notify
799         if (notify && [chat isGroupChat]) {
800                 /* Prevent triggering of the event when we are informed that the chat's own account entered the chat
801                  * If the UID of a contact in a chat differs from a normal UID, such as is the case with Jabber where a chat
802                  * contact has the form "roomname@conferenceserver/handle" this will fail, but it's better than nothing.
803                  */
804                 if (![[[inContact account] UID] isEqualToString:[inContact UID]]) {
805                         [adiumChatEvents chat:chat addedListContact:inContact];
806                 }
807         }
809         //Always notify Adium that the list changed so it can be updated, caches can be modified, etc.
810         [[adium notificationCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
811                                                                                           object:chat];
815  * @brief A chat removed a listContact from its participants list
817  * @param chat The chat
818  * @param inContact The contact
819  */
820 - (void)chat:(AIChat *)chat removedListContact:(AIListContact *)inContact
822         if ([chat isGroupChat]) {
823                 [adiumChatEvents chat:chat removedListContact:inContact];
824         }
826         [[adium notificationCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
827                                                                                           object:chat];
830 - (NSString *)defaultInvitationMessageForRoom:(NSString *)room account:(AIAccount *)inAccount
832         return [NSString stringWithFormat:AILocalizedString(@"%@ invites you to join the chat \"%@\"", nil), [inAccount formattedUID], room];
835 @end