Updated Address Book scripts to work with new AppleScript API.
[adiumx.git] / Source / AIChatController.m
blob102021436f7f4f11e92d263063c46c851c1aa4c6
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 <Adium/AIService.h>
22 #import <AIUtilities/AIArrayAdditions.h>
23 #import <AIUtilities/AIMenuAdditions.h>
25 @interface AIChatController (PRIVATE)
26 - (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
27 - (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys;
28 @end
30 /*!
31  * @class AIChatController
32  * @brief Core controller for chats
33  *
34  * This is the only class which should vend AIChat objects (via openChat... or chatWith:...).
35  * AIChat objects should never be created directly.
36  */
37 @implementation AIChatController
39 /*!
40  * @brief Initialize the controller
41  */
42 - (id)init
43 {       
44         if ((self = [super init])) {
45                 mostRecentChat = nil;
46                 chatObserverArray = [[NSMutableArray alloc] init];
47                 adiumChatEvents = [[AdiumChatEvents alloc] init];
49                 //Chat tracking
50                 openChats = [[NSMutableSet alloc] init];
51         }
52         return self;
56 /*!
57  * @brief Controller loaded
58  */
59 - (void)controllerDidLoad
60 {       
61         //Observe content so we can update the most recent chat
62     [[adium notificationCenter] addObserver:self 
63                                                                    selector:@selector(didExchangeContent:) 
64                                                                            name:CONTENT_MESSAGE_RECEIVED
65                                                                          object:nil];
66         
67     [[adium notificationCenter] addObserver:self 
68                                                                    selector:@selector(didExchangeContent:) 
69                                                                            name:CONTENT_MESSAGE_RECEIVED_GROUP
70                                                                          object:nil];
71         
72         [[adium notificationCenter] addObserver:self 
73                                                                    selector:@selector(didExchangeContent:) 
74                                                                            name:CONTENT_MESSAGE_SENT
75                                                                          object:nil];
76         
77         [[adium notificationCenter] addObserver:self
78                                                                    selector:@selector(adiumWillTerminate:)
79                                                                            name:AIAppWillTerminateNotification
80                                                                          object:nil];
82         //Ignore menu item for contacts in group chats
83         menuItem_ignore = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@""
84                                                                                                                                                    target:self
85                                                                                                                                                    action:@selector(toggleIgnoreOfContact:)
86                                                                                                                                         keyEquivalent:@""];
87         [[adium menuController] addContextualMenuItem:menuItem_ignore toLocation:Context_Contact_GroupChatAction];
88         
89         [adiumChatEvents controllerDidLoad];
93 /*!
94  * @brief Controller will close
95  */
96 - (void)controllerWillClose
98         
102  * @brief Adium will terminate
104  * Post the Chat_WillClose for each open chat so any closing behavior can be performed
105  */
106 - (void)adiumWillTerminate:(NSNotification *)inNotification
108         NSEnumerator    *enumerator = [openChats objectEnumerator];
109         AIChat                  *chat;
110         
111         //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.
112         while ((chat = [enumerator nextObject])) {
113                 [self closeChat:chat];
114         }
118  * @brief Deallocate
119  */
120 - (void)dealloc
122         [openChats release]; openChats = nil;
123         [chatObserverArray release]; chatObserverArray = nil;
124         [[adium notificationCenter] removeObserver:self];
126         [super dealloc];
128         
130  * @brief Register a chat observer
132  * Chat observers are notified when status objects are changed on chats
134  * @param inObserver An observer, which must conform to AIChatObserver
135  */
136 - (void)registerChatObserver:(id <AIChatObserver>)inObserver
138         //Add the observer
139     [chatObserverArray addObject:[NSValue valueWithNonretainedObject:inObserver]];
140         
141     //Let the new observer process all existing chats
142         [self updateAllChatsForObserver:inObserver];
146  * @brief Unregister a chat observer
147  */
148 - (void)unregisterChatObserver:(id <AIChatObserver>)inObserver
150     [chatObserverArray removeObject:[NSValue valueWithNonretainedObject:inObserver]];
154  * @brief Chat status changed
156  * Called by AIChat after it changes one or more status keys.
157  */
158 - (void)chatStatusChanged:(AIChat *)inChat modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
160         NSSet                   *modifiedAttributeKeys;
161         
162     //Let all observers know the chat's status has changed before performing any further notifications
163         modifiedAttributeKeys = [self _informObserversOfChatStatusChange:inChat withKeys:inModifiedKeys silent:silent];
164         
165     //Post an attributes changed message (if necessary)
166     if ([modifiedAttributeKeys count]) {
167                 [self chatAttributesChanged:inChat modifiedKeys:modifiedAttributeKeys];
168     }   
172  * @brief Chat attributes changed
174  * Called by -[AIChatController chatStatusChanged:modifiedStatusKeys:silent:] if any observers changed attributes
175  */
176 - (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys
178         //Post an attributes changed message
179         [[adium notificationCenter] postNotificationName:Chat_AttributesChanged
180                                                                                           object:inChat
181                                                                                         userInfo:(inModifiedKeys ? [NSDictionary dictionaryWithObject:inModifiedKeys 
182                                                                                                                                                                                                    forKey:@"Keys"] : nil)];
186  * @brief Send each chat in turn to an observer with a nil modifiedStatusKeys argument
188  * This lets an observer use its normal update mechanism to update every chat in some manner
189  */
190 - (void)updateAllChatsForObserver:(id <AIChatObserver>)observer
192         NSEnumerator    *enumerator = [openChats objectEnumerator];
193         AIChat                  *chat;
194         
195         while ((chat = [enumerator nextObject])) {
196                 [self chatStatusChanged:chat modifiedStatusKeys:nil silent:NO];
197         }
201  * @brief Notify observers of a status change.  Returns the modified attribute keys
202  */
203 - (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
205         NSMutableSet    *attrChange = nil;
206         NSEnumerator    *enumerator;
207         NSValue                 *observerValue;
208         
209         //Let our observers know
210         enumerator = [chatObserverArray objectEnumerator];
211         while ((observerValue = [enumerator nextObject])) {
212                 id <AIChatObserver>     observer;
213                 NSSet                           *newKeys;
214                 
215                 observer = [observerValue nonretainedObjectValue];
216                 if ((newKeys = [observer updateChat:inChat keys:modifiedKeys silent:silent])) {
217                         if (!attrChange) attrChange = [NSMutableSet set];
218                         [attrChange unionSet:newKeys];
219                 }
220         }
221         
222         //Send out the notification for other observers
223         [[adium notificationCenter] postNotificationName:Chat_StatusChanged
224                                                                                           object:inChat
225                                                                                         userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys 
226                                                                                                                                                                                                  forKey:@"Keys"] : nil)];
227         
228         return attrChange;
231 //Chats -------------------------------------------------------------------------------------------------
232 #pragma mark Chats
234  * @brief Opens a chat for communication with the contact, creating if necessary.
236  * The interface controller will then be asked to open the UI for the new chat.
238  * @param inContact The AIListContact on which to open a chat. If an AIMetaContact, an appropriate contained contact will be selected.
239  * @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:].
240  */
241 - (AIChat *)openChatWithContact:(AIListContact *)inContact onPreferredAccount:(BOOL)onPreferredAccount
243         AIChat  *chat;
245         if (onPreferredAccount) {
246                 inContact = [[adium contactController] preferredContactForContentType:CONTENT_MESSAGE_TYPE
247                                                                                                                            forListContact:inContact];
248         }
250         chat = [self chatWithContact:inContact];
251         if (chat) [[adium interfaceController] openChat:chat]; 
253         return chat;
257  * @brief Creates a chat for communication with the contact, but does not make the chat active
259  * No window or tab is opened for the chat.
260  * If a chat with this contact already exists, it is returned.
261  * If a chat with a contact within the same metaContact at this contact exists, it is switched to this contact
262  * and then returned.
264  * The passed contact, if an AIListContact, will be used exactly -- that is, [inContact account] is the account on which the chat will be opened.
265  * If the passed contact is an AIMetaContact, an appropriate contact/account pair will be automatically selected by this method.
267  * @param inContact The contact with which to open a chat. See description above.
268  */
269 - (AIChat *)chatWithContact:(AIListContact *)inContact
271         NSEnumerator    *enumerator;
272         AIChat                  *chat = nil;
273         AIListContact   *targetContact = inContact;
275         /*
276          If we're dealing with a meta contact, open a chat with the preferred contact for this meta contact
277          It's a good idea for the caller to pick the preferred contact for us, since they know the content type
278          being sent and more information - but we'll do it here as well just to be safe.
279          */
280         if ([inContact containsMultipleContacts]) {
281                 targetContact = [[adium contactController] preferredContactForContentType:CONTENT_MESSAGE_TYPE
282                                                                                                                                    forListContact:inContact];
283                 
284                 /*
285                  If we have no accounts online, preferredContactForContentType:forListContact will return nil.
286                  We'd rather open up the chat window on a useless contact than do nothing, so just pick the 
287                  preferredContact from the metaContact.
288                  */
289                 if (!targetContact) {
290                         targetContact = [(AIMetaContact *)inContact preferredContact];
291                 }
292         }
293         
294         //If we can't get a contact, we're not going to be able to get a chat... return nil
295         if (!targetContact) {
296                 AILog(@"Warning: -[AIChatController chatWithContact:%@] got a nil targetContact.",inContact);
297                 NSLog(@"Warning: -[AIChatController chatWithContact:%@] got a nil targetContact.",inContact);
298                 return nil;
299         }
300         
301         //XXX Temporary.. make sure that the fixes early in adium-1.1svn are right.
302         NSAssert1(![targetContact isMemberOfClass:[AIMetaContact class]], @"Should not get this far in chatWithContact: with an AIMetaContact (%@)!",targetContact);
303         
304         //Search for an existing chat we can switch instead of replacing
305         enumerator = [openChats objectEnumerator];
306         while ((chat = [enumerator nextObject])) {
307                 //If a chat for this object already exists
308                 if ([[chat uniqueChatID] isEqualToString:[targetContact internalObjectID]]) {
309                         if (!([chat listObject] == targetContact)) {
310                                 [self switchChat:chat toAccount:[targetContact account]];
311                         }
312                         
313                         break;
314                 }
315                 
316                 //If this object is within a meta contact, and a chat for an object in that meta contact already exists
317                 if ([[targetContact containingObject] isKindOfClass:[AIMetaContact class]] && 
318                    [[chat listObject] containingObject] == [targetContact containingObject]) {
320                         //Switch the chat to be on this contact (and its account) now
321                         [self switchChat:chat toListContact:targetContact usingContactAccount:YES];
322                         
323                         break;
324                 }
325         }
327         if (!chat) {
328                 AIAccount       *account = [targetContact account];
330                 //Create a new chat
331                 chat = [AIChat chatForAccount:account];
332                 [chat addObject:targetContact];
333                 [openChats addObject:chat];
334                 AILog(@"chatWithContact: Added <<%@>> [%@]",chat,openChats);
336                 //Inform the account of its creation
337                 if (![[targetContact account] openChat:chat]) {
338                         [openChats removeObject:chat];
339                         AILog(@"chatWithContact: Immediately removed <<%@>> [%@]",chat,openChats);
340                         chat = nil;
341                 }
342         }
344         return chat;
348  * @brief Return a pre-existing chat with a contact.
350  * @result The chat, or nil if no chat with the contact exists
351  */
352 - (AIChat *)existingChatWithContact:(AIListContact *)inContact
354         NSEnumerator    *enumerator;
355         AIChat                  *chat = nil;
357         if ([inContact isKindOfClass:[AIMetaContact class]]) {
358                 //Search for a chat with any contact within this AIMetaContact
359                 enumerator = [openChats objectEnumerator];
360                 while ((chat = [enumerator nextObject])) {
361                         if ([[(AIMetaContact *)inContact containedObjects] containsObjectIdenticalTo:[chat listObject]]) break;
362                 }
364         } else {
365                 //Search for a chat with this AIListContact
366                 enumerator = [openChats objectEnumerator];
367                 while ((chat = [enumerator nextObject])) {
368                         if ([chat listObject] == inContact) break;
369                 }
370         }
371         
372         return chat;
376  * @brief Open a group chat
378  * @param inName The name of the chat; in general, the chat room name
379  * @param account The account on which to create the group chat
380  * @param chatCreationInfo A dictionary of information which may be used by the account when joining the chat serverside
381  * @brief opens a chat with the above parameters. Assigns chatroom info to the created AIChat object.
382  */
383 - (AIChat *)chatWithName:(NSString *)name identifier:(id)identifier onAccount:(AIAccount *)account chatCreationInfo:(NSDictionary *)chatCreationInfo
385         AIChat                  *chat = nil;
387         name = [[account service] normalizeChatName:name];
389         if (identifier) {
390                 chat = [self existingChatWithIdentifier:identifier onAccount:account];
391                 if (!chat) {
392                         //See if a chat was made with this name but which doesn't yet have an identifier. If so, take ownership!
393                         chat = [self existingChatWithName:name onAccount:account];
394                         if (chat && ![chat identifier]) [chat setIdentifier:identifier];
395                 }
397         } else {
398                 //If the caller doesn't care about the identifier, do a search based on name to avoid creating a new chat incorrectly
399                 chat = [self existingChatWithName:name onAccount:account];
400         }
402         AILog(@"chatWithName %@ identifier %@ existing --> %@", name, identifier, chat);
403         if (!chat) {
404                 //Create a new chat
405                 chat = [AIChat chatForAccount:account];
406                 [chat setName:name];
407                 [chat setIdentifier:identifier];
408                 [chat setIsGroupChat:YES];
409                 [chat setChatCreationDictionary:chatCreationInfo];
410                                         
411                 [openChats addObject:chat];
412                 AILog(@"chatWithName:%@ identifier:%@ onAccount:%@ added <<%@>> [%@]",name,identifier,account,chat,openChats);
415                 //Inform the account of its creation
416                 if (![account openChat:chat]) {
417                         [openChats removeObject:chat];
418                         AILog(@"chatWithName: Immediately removed <<%@>> [%@]",chat,openChats);
419                         chat = nil;
420                 }
421         }
423         AILog(@"chatWithName %@ created --> %@",name,chat);
424         return chat;
428 * @brief Find an existing group chat
430  * @result The group AIChat, or nil if no such chat exists
431  */
432 - (AIChat *)existingChatWithName:(NSString *)name onAccount:(AIAccount *)account
434         NSEnumerator    *enumerator;
435         AIChat                  *chat = nil;
436         
437         name = [[account service] normalizeChatName:name];
439         enumerator = [openChats objectEnumerator];
440         
441         while ((chat = [enumerator nextObject])) {
442                 if (([chat account] == account) &&
443                         ([[chat name] isEqualToString:name])) {
444                         break;
445                 }
446         }       
447         
448         return chat;
452  * @brief Find an existing group chat
454  * @result The group AIChat, or nil if no such chat exists
455  */
456 - (AIChat *)existingChatWithIdentifier:(id)identifier onAccount:(AIAccount *)account
458         NSEnumerator    *enumerator;
459         AIChat                  *chat = nil;
460         
461         enumerator = [openChats objectEnumerator];
463         while ((chat = [enumerator nextObject])) {
464                 if (([chat account] == account) &&
465                    ([[chat identifier] isEqual:identifier])) {
466                         break;
467                 }
468         }       
469         
470         return chat;
474  * @brief Find an existing chat by unique chat ID
476  * @result The AIChat, or nil if no such chat exists
477  */
478 - (AIChat *)existingChatWithUniqueChatID:(NSString *)uniqueChatID
480         NSEnumerator    *enumerator;
481         AIChat                  *chat = nil;
482         
483         enumerator = [openChats objectEnumerator];
484         
485         while ((chat = [enumerator nextObject])) {
486                 if ([[chat uniqueChatID] isEqualToString:uniqueChatID]) {
487                         break;
488                 }
489         }       
490         
491         return chat;
495  * @brief Close a chat
497  * This should be called only by the interface controller. To close a chat programatically, use the interface controller's closeChat:.
499  * @result YES the chat was removed succesfully; NO if it was not
500  */
501 - (BOOL)closeChat:(AIChat *)inChat
502 {       
503         BOOL    shouldRemove;
504         
505         /* If we are currently passing a content object for this chat through our content filters, don't remove it from
506          * our openChats set as it will become needed soon. If we were to remove it, and a second message came in which was
507          * also before the first message is done filtering, we would otherwise mistakenly think we needed to create a new
508          * chat, generating a duplicate.
509          */
510         shouldRemove = ![[adium contentController] chatIsReceivingContent:inChat];
512         [inChat retain];
514         if (mostRecentChat == inChat) {
515                 [mostRecentChat release];
516                 mostRecentChat = nil;
517         }
518         
519         //Send out the Chat_WillClose notification
520         [[adium notificationCenter] postNotificationName:Chat_WillClose object:inChat userInfo:nil];
522         //Remove the chat
523         if (shouldRemove) {
524                 /* If we didn't remove the chat because we're waiting for it to reopen, don't cause the account
525                  * to close down the chat.
526                  */
527                 [[inChat account] closeChat:inChat];
528                 [openChats removeObject:inChat];
529                 AILog(@"closeChat: Removed <<%@>> [%@]",inChat, openChats);
530         } else {
531                 AILog(@"closeChat: Did not remove <<%@>> [%@]",inChat, openChats);              
532         }
533         
534         [inChat setIsOpen:NO];
535         [inChat release];
537         return shouldRemove;
541  * @brief Switch a chat from one account to another
543  * The target list contact for the chat is changed to be an 'identical' one on the target account; that is, a contact
544  * with the same UID but an account and service appropriate for newAccount.
545  */
546 - (void)switchChat:(AIChat *)chat toAccount:(AIAccount *)newAccount
548         AIAccount       *oldAccount = [chat account];
549         if (newAccount != oldAccount) {
550                 //Hang onto stuff until we're done
551                 [chat retain];
553                 //Close down the chat on account A
554                 [oldAccount closeChat:chat];
556                 //Set the account and the listObject
557                 {
558                         [chat setAccount:newAccount];
560                         //We want to keep the same destination for the chat but switch it to a listContact on the desired account.
561                         AIListContact   *newContact = [[adium contactController] contactWithService:[newAccount service]
562                                                                                                                                                                 account:newAccount
563                                                                                                                                                                         UID:[[chat listObject] UID]];
564                         [chat setListObject:newContact];
565                 }
567                 //Open the chat on account B
568                 [newAccount openChat:chat];
569                 
570                 //Clean up
571                 [chat release];
572         }
576  * @brief Switch the list contact of a chat
578  * @param chat The chat
579  * @param inContact The contact with which the chat will now take place
580  * @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.
581  */
582 - (void)switchChat:(AIChat *)chat toListContact:(AIListContact *)inContact usingContactAccount:(BOOL)useContactAccount
584         AIAccount               *newAccount = (useContactAccount ? [inContact account] : [chat account]);
586         //Switch the inContact over to a contact on the new account so we send messages to the right place.
587         AIListContact   *newContact = [[adium contactController] contactWithService:[newAccount service]
588                                                                                                                                                 account:newAccount
589                                                                                                                                                         UID:[inContact UID]];
590         if (newContact != [chat listObject]) {
591                 //Hang onto stuff until we're done
592                 [chat retain];
593                 
594                 //Close down the chat on the account, as the account may need to perform actions such as closing a connection
595                 [[chat account] closeChat:chat];
596                 
597                 //Set to the new listContact and account as needed
598                 [chat setListObject:newContact];
599                 if (useContactAccount) [chat setAccount:newAccount];
601                 //Reopen the chat on the account
602                 [[chat account] openChat:chat];
603                 
604                 //Clean up
605                 [chat release];
606         }
610  * @brief Find all open chats with a contact
612  * @param inContact The contact. If inContact is an AIMetaContact, all chats with all contacts within the metaContact will be returned.
613  * @result An NSSet with all chats with the contact.  In general, will contain 0 or 1 AIChat objects, though it may contain more.
614  */
615 - (NSSet *)allChatsWithContact:(AIListContact *)inContact
617     NSMutableSet        *foundChats = nil;
618         
619         //Scan the objects participating in each chat, looking for the requested object
620         if ([inContact isKindOfClass:[AIMetaContact class]]) {
621                 if ([openChats count]) {
622                         NSEnumerator    *enumerator;
623                         AIListContact   *listContact;
625                         enumerator = [[(AIMetaContact *)inContact listContacts] objectEnumerator];
626                         while ((listContact = [enumerator nextObject])) {
627                                 NSSet           *listContactChats;
628                                 if ((listContactChats = [self allChatsWithContact:listContact])) {
629                                         if (!foundChats) foundChats = [NSMutableSet set];
630                                         [foundChats unionSet:listContactChats];
631                                 }
632                         }
633                 }
634                 
635         } else {
636                 NSEnumerator    *enumerator;
637                 AIChat                  *chat;
638                 
639                 enumerator = [openChats objectEnumerator];
640                 while ((chat = [enumerator nextObject])) {
641                         if (![chat isGroupChat] &&
642                                 [[[chat listObject] internalObjectID] isEqualToString:[inContact internalObjectID]] &&
643                                 [chat isOpen]) {
644                                 if (!foundChats) foundChats = [NSMutableSet set];
645                                 [foundChats addObject:chat];
646                         }
647                 }
648         }
650     return foundChats;
654  * @brief All open chats
656  * Open chats from the chatController may include chats which are not currently displayed by the interface.
657  */
658 - (NSSet *)openChats
660     return openChats;
664  * @brief Find the chat which most recently received content which has not yet been seen
666  * @result An AIChat with unviewed content, or nil if no chats current have unviewed content
667  */
668 - (AIChat *)mostRecentUnviewedChat
670         AIChat  *mostRecentUnviewedChat = nil;
671         
672         if (mostRecentChat && [mostRecentChat unviewedContentCount]) {
673                 //First choice: switch to the chat which received chat most recently if it has unviewed content
674                 mostRecentUnviewedChat = mostRecentChat;
675                 
676         } else {
677                 //Second choice: switch to the first chat we can find which has unviewed content
678                 NSEnumerator    *enumerator = [openChats objectEnumerator];
679                 AIChat                  *chat;
680                 while ((chat = [enumerator nextObject]) && ![chat unviewedContentCount]);
681                 
682                 if (chat) mostRecentUnviewedChat = chat;
683         }
684         
685         return mostRecentUnviewedChat;
689  * @brief Gets the total number of unviewed messages
690  * 
691  * @result The number of unviewed messages
692  */
693 - (int)unviewedContentCount
695         int                             count = 0;
696         AIChat                  *chat;
697         NSEnumerator    *enumerator;
699         enumerator = [[self openChats] objectEnumerator];
700         while ((chat = [enumerator nextObject])) {
701                 count += [chat unviewedContentCount];
702         }
703         return count;
707  * @brief Is the passed contact in a group chat?
709  * @result YES if the contact is in an open group chat; NO if not.
710  */
711 - (BOOL)contactIsInGroupChat:(AIListContact *)listContact
713         NSEnumerator    *chatEnumerator = [openChats objectEnumerator];
714         AIChat                  *chat;
715         BOOL                    contactIsInGroupChat = NO;
716         
717         while ((chat = [chatEnumerator nextObject])) {
718                 if ([chat isGroupChat] &&
719                         [chat containsObject:listContact]) {
720                         
721                         contactIsInGroupChat = YES;
722                         break;
723                 }
724         }
725         
726         return contactIsInGroupChat;
730  * @brief Called when content is sent or received
732  * Update the most recent chat
733  */
734 - (void)didExchangeContent:(NSNotification *)notification
736         AIContentObject *contentObject = [[notification userInfo] objectForKey:@"AIContentObject"];
738         //Update our most recent chat
739         if ([contentObject trackContent]) {
740                 AIChat  *chat = [contentObject chat];
741                 
742                 if (chat != mostRecentChat) {
743                         [mostRecentChat release];
744                         mostRecentChat = [chat retain];
745                 }
746         }
749 #pragma mark Ignore
751  * @brief Toggle ignoring of a contact
753  * Must be called from the contextual menu for the contact within a chat
754  */
755 - (void)toggleIgnoreOfContact:(id)sender
757         AIListObject    *listObject = [[adium menuController] currentContextMenuObject];
758         AIChat                  *chat = [[adium menuController] currentContextMenuChat];
759         
760         if ([listObject isKindOfClass:[AIListContact class]]) {
761                 BOOL                    isIgnored = [chat isListContactIgnored:(AIListContact *)listObject];
762                 [chat setListContact:(AIListContact *)listObject isIgnored:!isIgnored];
763         }
767  * @brief Menu item validation
769  * When asked to validate our ignore menu item, set its title to ignore/un-ignore as appropriate for the contact
770  */
771 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
773         if (menuItem == menuItem_ignore) {
774                 AIListObject    *listObject = [[adium menuController] currentContextMenuObject];
775                 AIChat                  *chat = [[adium menuController] currentContextMenuChat];
776                 
777                 if ([listObject isKindOfClass:[AIListContact class]]) {
778                         if ([chat isListContactIgnored:(AIListContact *)listObject]) {
779                                 [menuItem setTitle:AILocalizedString(@"Un-ignore","Un-ignore means begin receiving messages from this contact again in a chat")];
780                                 
781                         } else {
782                                 [menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
783                         }
784                 } else {
785                         [menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
786                         return NO;
787                 }
788         }
789         
790         return YES;
793 #pragma mark Chat contact addition and removal
796  * @brief A chat added a listContact to its participatants list
798  * @param chat The chat
799  * @param inContact The contact
800  * @param notify If YES, trigger the contact joined event if this is a group chat.  Ignored if this is not a group chat.
801  */
802 - (void)chat:(AIChat *)chat addedListContact:(AIListContact *)inContact notify:(BOOL)notify
804         if (notify && [chat isGroupChat]) {
805                 /* Prevent triggering of the event when we are informed that the chat's own account entered the chat
806                  * If the UID of a contact in a chat differs from a normal UID, such as is the case with Jabber where a chat
807                  * contact has the form "roomname@conferenceserver/handle" this will fail, but it's better than nothing.
808                  */
809                 if (![[[inContact account] UID] isEqualToString:[inContact UID]]) {
810                         [adiumChatEvents chat:chat addedListContact:inContact];
811                 }
812         }
814         //Always notify Adium that the list changed so it can be updated, caches can be modified, etc.
815         [[adium notificationCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
816                                                                                           object:chat];
820  * @brief A chat removed a listContact from its participants list
822  * @param chat The chat
823  * @param inContact The contact
824  */
825 - (void)chat:(AIChat *)chat removedListContact:(AIListContact *)inContact
827         if ([chat isGroupChat]) {
828                 [adiumChatEvents chat:chat removedListContact:inContact];
829         }
831         [[adium notificationCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
832                                                                                           object:chat];
835 - (NSString *)defaultInvitationMessageForRoom:(NSString *)room account:(AIAccount *)inAccount
837         return [NSString stringWithFormat:AILocalizedString(@"%@ invites you to join the chat \"%@\"", nil), [inAccount formattedUID], room];
840 @end