AIHyperlinks universal building
[adiumx.git] / Source / AIContentController.m
blobf136c23fcc689bf4756d0ab914ffd1e51d4f9e6d
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 // $Id$
19 #import "AIAccountController.h"
20 #import "AIContactController.h"
21 #import "ESContactAlertsController.h"
22 #import "AIContentController.h"
23 #import "AdiumContentFiltering.h"
24 #import "AIInterfaceController.h"
25 #import "AIMenuController.h"
27 #import "AdiumChatEvents.h"
28 //XXX - why are we importing specific headers here, rather than just importing the umbrella headers? --boredzo
29 #import <AIUtilities/AIAttributedStringAdditions.h>
30 #import <AIUtilities/AIArrayAdditions.h>
31 #import <AIUtilities/ESImageAdditions.h>
32 #import <AIUtilities/AIMenuAdditions.h>
33 #import <AIUtilities/ESExpandedRecursiveLock.h>
34 #import <AIUtilities/ESImageAdditions.h>
35 #import <Adium/AIAccount.h>
36 #import <Adium/AIChat.h>
37 #import <Adium/AIContentMessage.h>
38 #import <Adium/AIContentObject.h>
39 #import <Adium/AIContentStatus.h>
40 #import <Adium/AIListContact.h>
41 #import <Adium/AIListGroup.h>
42 #import <Adium/AIListObject.h>
43 #import <Adium/AIMetaContact.h>
44 #import <Adium/NDRunLoopMessenger.h>
46 @interface AIContentController (PRIVATE)
48 - (void)finishReceiveContentObject:(AIContentObject *)inObject;
49 - (void)finishSendContentObject:(AIContentObject *)inObject;
50 - (void)finishDisplayContentObject:(AIContentObject *)inObject;
52 - (NSAttributedString *)_filterAttributedString:(NSAttributedString *)inString forContentObject:(AIContentObject *)inObject listObjectContext:(AIListObject *)inListObject usingFilterArray:(NSArray *)inArray;
53 - (NSString *)_filterString:(NSString *)inString forContentObject:(AIContentObject *)inObject listObjectContext:(AIListObject *)inListObject/* usingFilterArray:(NSArray *)inArray*/;
54 - (void)_filterContentObject:(AIContentObject *)inObject usingFilterArray:(NSArray *)inArray;
55 - (NSAttributedString *)thread_filterAttributedString:(NSAttributedString *)attributedString contentFilter:(NSArray *)inContentFilterArray filterContext:(id)filterContext invocation:(NSInvocation *)invocation;
56 - (NSAttributedString *)_filterAttributedString:(NSAttributedString *)attributedString contentFilter:(NSArray *)inContentFilterArray filterContext:(id)filterContext usingLock:(NSRecursiveLock *)inLock;
58 - (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
59 - (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys;
61 @end
64  * @class AIContentController
65  * @brief Controller to manage incoming and outgoing content and chats.
66  *
67  * This controller handles default formatting and text entry filters, which can respond as text is entered in a message
68  * window.  It the center for content filtering, including registering/unregistering of content filters.
69  * It handles sending and receiving of content objects.  It manages chat observers, which are objects notified as
70  * status objects are set and removed on AIChat objects.  It manages chats themselves, tracking open ones, closing
71  * them when needed, etc.  Finally, it provides Events related to sending and receiving content, such as Message Received.
72  */
73 @implementation AIContentController
76  * @brief Initialize the controller
77  */
78 - (void)initController
80     //Text entry filtering and tracking
81     textEntryFilterArray = [[NSMutableArray alloc] init];
82     textEntryContentFilterArray = [[NSMutableArray alloc] init];
83     textEntryViews = [[NSMutableArray alloc] init];
84         chatObserverArray = [[NSMutableArray alloc] init];
85     defaultFormattingAttributes = nil;
86         emoticonPacks = nil;
87         emoticonsArray = nil;
88         
89     //Chat tracking
90     openChats = [[NSMutableSet alloc] init];
91         objectsBeingReceived = [[NSMutableSet alloc] init];
92         adiumContentFiltering = [[AdiumContentFiltering alloc] init];
93         adiumChatEvents = [[AdiumChatEvents alloc] init];       
94         [adiumChatEvents controllerDidLoad];    
96     //Emoticons array
97     emoticonsArray = nil;
98         
99         //Register the events we generate
100         [[adium contactAlertsController] registerEventID:CONTENT_MESSAGE_SENT withHandler:self inGroup:AIMessageEventHandlerGroup globalOnly:NO];
101         [[adium contactAlertsController] registerEventID:CONTENT_MESSAGE_RECEIVED withHandler:self inGroup:AIMessageEventHandlerGroup globalOnly:NO];
102         [[adium contactAlertsController] registerEventID:CONTENT_MESSAGE_RECEIVED_FIRST withHandler:self inGroup:AIMessageEventHandlerGroup globalOnly:NO];
103         [[adium contactAlertsController] registerEventID:CONTENT_MESSAGE_RECEIVED_BACKGROUND withHandler:self inGroup:AIMessageEventHandlerGroup globalOnly:NO];
104         
105         menuItem_ignore = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@""
106                                                                                                                                                    target:self
107                                                                                                                                                    action:@selector(toggleIgnoreOfContact:)
108                                                                                                                                         keyEquivalent:@""];
109         [[adium menuController] addContextualMenuItem:menuItem_ignore toLocation:Context_Contact_ChatAction];
113  * @brief Begin closing the controller
115  * Post Chat_WillClose for all chats which are still open before the controller closes
116  */
117 - (void)beginClosing
119         NSEnumerator    *enumerator = [openChats objectEnumerator];
120         AIChat                  *chat;
122         //Every open chat is about to close.
123         while((chat = [enumerator nextObject])){
124                 [[adium notificationCenter] postNotificationName:Chat_WillClose 
125                                                                                                   object:chat
126                                                                                                 userInfo:nil];
127         }
131  * @brief Close the controller
132  */
133 - (void)closeController
135         
139  * @brief Deallocate
140  */
141 - (void)dealloc
143         [emoticonPacks release]; emoticonPacks = nil;
144         [emoticonsArray release]; emoticonsArray = nil;
145     [textEntryFilterArray release];
146     [textEntryContentFilterArray release];
147     [textEntryViews release];
148     [openChats release];
149         [chatObserverArray release]; chatObserverArray = nil;
150         [objectsBeingReceived release];
152     [super dealloc];
156 //Default Formatting -------------------------------------------------------------------------------------------------
157 #pragma mark Default Formatting
158 - (void)setDefaultFormattingAttributes:(NSDictionary *)inDict
160     if(defaultFormattingAttributes != inDict){
161            [defaultFormattingAttributes release];
162            defaultFormattingAttributes  = [inDict retain];
163     }
166  * @brief Default formatting attributes
168  * This dictionary of attributes will be used for new text entry views, messages, etc.
169  */
170 - (NSDictionary *)defaultFormattingAttributes
172         return defaultFormattingAttributes;
176 //Text Entry Filtering -------------------------------------------------------------------------------------------------
177 #pragma mark Text Entry Filtering
179 //Text entry filters process content as it is entered by the user.
181 - (void)registerTextEntryFilter:(id)inFilter
183         NSParameterAssert([inFilter respondsToSelector:@selector(didOpenTextEntryView:)] &&
184                                           [inFilter respondsToSelector:@selector(willCloseTextEntryView:)]);
185         
186         //For performance reasons, we place filters that actually monitor content in a separate array
187         if([inFilter respondsToSelector:@selector(stringAdded:toTextEntryView:)] &&
188            [inFilter respondsToSelector:@selector(contentsChangedInTextEntryView:)]){
189                 [textEntryContentFilterArray addObject:inFilter];
190         }else{
191                 [textEntryFilterArray addObject:inFilter];
192         }
195 - (void)unregisterTextEntryFilter:(id)inFilter
197         [textEntryContentFilterArray removeObject:inFilter];
198         [textEntryFilterArray removeObject:inFilter];
201 //A string was added to a text entry view
202 - (void)stringAdded:(NSString *)inString toTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
204     NSEnumerator        *enumerator;
205     id                          filter;
206         
207     //Notify all text entry filters (that are interested in filtering content)
208     enumerator = [textEntryContentFilterArray objectEnumerator];
209     while((filter = [enumerator nextObject])){
210         [filter stringAdded:inString toTextEntryView:inTextEntryView];
211     }
214 //A text entry view's content changed
215 - (void)contentsChangedInTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
217     NSEnumerator        *enumerator;
218     id                          filter;
219         
220     //Notify all text entry filters (that are interested in filtering content)
221     enumerator = [textEntryContentFilterArray objectEnumerator];
222     while((filter = [enumerator nextObject])){
223         [filter contentsChangedInTextEntryView:inTextEntryView];
224     }
227 //A text entry view was opened
228 - (void)didOpenTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
230     NSEnumerator        *enumerator;
231     id                          filter;
232         
233     //Track the view
234     [textEntryViews addObject:inTextEntryView];
235     
236     //Notify all text entry filters
237     enumerator = [textEntryContentFilterArray objectEnumerator];
238     while((filter = [enumerator nextObject])){
239         [filter didOpenTextEntryView:inTextEntryView];
240     }
241     enumerator = [textEntryFilterArray objectEnumerator];
242     while((filter = [enumerator nextObject])){
243         [filter didOpenTextEntryView:inTextEntryView];
244     }
247 //A text entry view was closed
248 - (void)willCloseTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
250     NSEnumerator        *enumerator;
251     id                          filter;
252         
253     //Stop tracking the view
254     [textEntryViews removeObject:inTextEntryView];
255         
256     //Notify all text entry filters
257     enumerator = [textEntryContentFilterArray objectEnumerator];
258     while((filter = [enumerator nextObject])){
259         [filter willCloseTextEntryView:inTextEntryView];
260     }
261     enumerator = [textEntryFilterArray objectEnumerator];
262     while((filter = [enumerator nextObject])){
263         [filter willCloseTextEntryView:inTextEntryView];
264     }
267 //Returns all currently open text entry views
268 - (NSArray *)openTextEntryViews
270     return(textEntryViews);
274 //Content Filtering ----------------------------------------------------------------------------------------------------
275 #pragma mark Content Filtering
276 - (void)registerContentFilter:(id <AIContentFilter>)inFilter
277                                            ofType:(AIFilterType)type
278                                         direction:(AIFilterDirection)direction {
279         [adiumContentFiltering registerContentFilter:inFilter ofType:type direction:direction];
281 - (void)registerDelayedContentFilter:(id <AIDelayedContentFilter>)inFilter
282                                                           ofType:(AIFilterType)type
283                                                    direction:(AIFilterDirection)direction {
284         [adiumContentFiltering registerDelayedContentFilter:inFilter ofType:type direction:direction];
286 - (void)unregisterContentFilter:(id <AIContentFilter>)inFilter {
287         [adiumContentFiltering unregisterContentFilter:inFilter];
289 - (void)registerFilterStringWhichRequiresPolling:(NSString *)inPollString {
290         [adiumContentFiltering registerFilterStringWhichRequiresPolling:inPollString];
292 - (BOOL)shouldPollToUpdateString:(NSString *)inString {
293         return [adiumContentFiltering shouldPollToUpdateString:inString];
295 - (NSAttributedString *)filterAttributedString:(NSAttributedString *)attributedString
296                                                            usingFilterType:(AIFilterType)type
297                                                                          direction:(AIFilterDirection)direction
298                                                                            context:(id)context {
299         return [adiumContentFiltering filterAttributedString:attributedString
300                                                                                  usingFilterType:type
301                                                                                            direction:direction
302                                                                                                  context:context];
304 - (void)filterAttributedString:(NSAttributedString *)attributedString
305                            usingFilterType:(AIFilterType)type
306                                          direction:(AIFilterDirection)direction
307                                  filterContext:(id)filterContext
308                            notifyingTarget:(id)target
309                                           selector:(SEL)selector
310                                            context:(id)context {
311         [adiumContentFiltering filterAttributedString:attributedString
312                                                                   usingFilterType:type
313                                                                                 direction:direction
314                                                                         filterContext:filterContext
315                                                                   notifyingTarget:target
316                                                                                  selector:selector
317                                                                                   context:context];
319 - (void)delayedFilterDidFinish:(NSAttributedString *)attributedString uniqueID:(unsigned long long)uniqueID
321         [adiumContentFiltering delayedFilterDidFinish:attributedString
322                                                                                  uniqueID:uniqueID];
325 //Messaging ------------------------------------------------------------------------------------------------------------
326 #pragma mark Messaging
327 //Receiving step 1: Add an incoming content object - entry point
328 - (void)receiveContentObject:(AIContentObject *)inObject
330         if (inObject) {
331                 AIChat                  *chat = [inObject chat];
333                 //Only proceed if the contact is not ignored
334                 if (![chat isListContactIgnored:[inObject source]]) {
335                         //Notify: Will Receive Content
336                         if ([inObject trackContent]) {
337                                 [[adium notificationCenter] postNotificationName:Content_WillReceiveContent
338                                                                                                                   object:chat
339                                                                                                                 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:inObject,@"Object",nil]];
340                         }
342                         //Track that we are in the process of receiving this object
343                         [objectsBeingReceived addObject:inObject];
344                         
345                         //Run the object through our incoming content filters
346                         if ([inObject filterContent]) {
347                                 [self filterAttributedString:[inObject message]
348                                                          usingFilterType:AIFilterContent
349                                                                    direction:AIFilterIncoming
350                                                            filterContext:inObject
351                                                          notifyingTarget:self
352                                                                         selector:@selector(didFilterAttributedString:receivingContext:)
353                                                                          context:inObject];
354                                 
355                         } else {
356                                 [self finishReceiveContentObject:inObject];
357                         }
358                 }
359     }
362 //Receiving step 2: filtering callback
363 - (void)didFilterAttributedString:(NSAttributedString *)filteredMessage receivingContext:(AIContentObject *)inObject
365         [inObject setMessage:filteredMessage];
366         
367         [self finishReceiveContentObject:inObject];
370 //Receiving step 3: Display the content
371 - (void)finishReceiveContentObject:(AIContentObject *)inObject
373         //Display the content
374         [self displayContentObject:inObject];
375         
376         //Update our most recent chat
377         if([inObject trackContent]){
378                 AIChat  *chat = [inObject chat];
379                 
380                 if(chat != mostRecentChat){
381                         [mostRecentChat release];
382                         mostRecentChat = [chat retain];
383                 }
384         }
387 //Sending step 1: Entry point for any method in Adium which sends content
388 - (BOOL)sendContentObject:(AIContentObject *)inObject
390     //Run the object through our outgoing content filters
391     if([inObject filterContent]){
392                 [self filterAttributedString:[inObject message]
393                                          usingFilterType:AIFilterContent
394                                                    direction:AIFilterOutgoing
395                                            filterContext:inObject
396                                          notifyingTarget:self
397                                                         selector:@selector(didFilterAttributedString:contentSendingContext:)
398                                                          context:inObject];
399                 
400     }else{
401                 [self finishSendContentObject:inObject];
402         }
403         
404         // XXX
405         return YES;
408 //Sending step 2: Sending filter callback
409 -(void)didFilterAttributedString:(NSAttributedString *)filteredString contentSendingContext:(AIContentObject *)inObject
411         [inObject setMessage:filteredString];
413         //Special outgoing content filter for AIM away message bouncing.  Used to filter %n,%t,...
414         if([inObject isKindOfClass:[AIContentMessage class]] && [(AIContentMessage *)inObject isAutoreply]){
415                 [self filterAttributedString:[inObject message]
416                                          usingFilterType:AIFilterAutoReplyContent
417                                                    direction:AIFilterOutgoing
418                                            filterContext:inObject
419                                          notifyingTarget:self
420                                                         selector:@selector(didFilterAttributedString:autoreplySendingContext:)
421                                                          context:inObject];
422         }else{          
423                 [self finishSendContentObject:inObject];
424         }
427 //Sending step 3, applicable only when sending an autreply: Filter callback
428 -(void)didFilterAttributedString:(NSAttributedString *)filteredString autoreplySendingContext:(AIContentObject *)inObject
430         [inObject setMessage:filteredString];
432         [self finishSendContentObject:inObject];
435 //Sending step 4: Post notifications and ask the account to actually send the content.
436 //The account is responsible for invoking step 5 which will lead to the actual display.
437 - (void)finishSendContentObject:(AIContentObject *)inObject
439     AIChat              *chat = [inObject chat];
440         
441         //Notify: Will Send Content
442     if([inObject trackContent]){
443         [[adium notificationCenter] postNotificationName:Content_WillSendContent
444                                                                                                   object:chat 
445                                                                                                 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:inObject,@"Object",nil]];
446     }
447         
448     //Send the object
449         if ([inObject sendContent]){
450                 if([(AIAccount *)[inObject source] sendContentObject:inObject]){
451                         if([inObject displayContent]){
452                                 //Add the object
453                                 [self displayContentObject:inObject];
454                         }
455                         
456                         if([inObject trackContent]){
457                                 //Did send content
458                                 [[adium contactAlertsController] generateEvent:CONTENT_MESSAGE_SENT
459                                                                                                  forListObject:[chat listObject]
460                                                                                                           userInfo:[NSDictionary dictionaryWithObjectsAndKeys:chat,@"AIChat",inObject,@"AIContentObject",nil]
461                                                                   previouslyPerformedActionIDs:nil];                            
462                         }
463                         
464                         if(mostRecentChat != chat){
465                                 [mostRecentChat release];
466                                 mostRecentChat = [chat retain];
467                         }
468 //                      sent = YES;
469                 }
470         }else{
471                 //We shouldn't send the content, so something was done with it.. clear the text entry view
472                 //XXX - Nobody is observing this notification... -ai
473                 [[adium notificationCenter] postNotificationName:Interface_ShouldClearTextEntryView
474                                                                                                   object:chat 
475                                                                                                 userInfo:nil];
476         }
477         
478 //    return(sent);
481 //Display a content object
482 //Add content to the message view.  Doesn't do any sending or receiving, just adds the content.
483 - (void)displayContentObject:(AIContentObject *)inObject usingContentFilters:(BOOL)useContentFilters
485         [self displayContentObject:inObject usingContentFilters:useContentFilters immediately:NO];
488 //Immediately YES means the main thread will halt until the content object is displayed;
489 //Immediately NO shuffles it off into the filtering thread, which will handle content sequentially but allows the main
490 //thread to continue operation.  
491 //This facility primarily exists for message history, which needs to put its display in before the first message;
492 //without this, the use of threaded filtering means that message history shows up after the first message.
493 - (void)displayContentObject:(AIContentObject *)inObject usingContentFilters:(BOOL)useContentFilters immediately:(BOOL)immediately
495         if (useContentFilters){
496                 
497                 if (immediately){
498                         //Filter in the main thread, set the message, and continue
499                         [inObject setMessage:[self filterAttributedString:[inObject message]
500                                                                                           usingFilterType:AIFilterContent
501                                                                                                         direction:([inObject isOutgoing] ? AIFilterOutgoing : AIFilterIncoming)
502                                                                                                           context:inObject]];
503                         [self displayContentObject:inObject immediately:YES];
504                         
505                         
506                 }else{
507                         //Filter in the filter thread
508                         [self filterAttributedString:[inObject message]
509                                                  usingFilterType:AIFilterContent
510                                                            direction:([inObject isOutgoing] ? AIFilterOutgoing : AIFilterIncoming)
511                                                    filterContext:inObject
512                                                  notifyingTarget:self
513                                                                 selector:@selector(didFilterAttributedString:contentFilterDisplayContext:)
514                                                                  context:inObject];
515                 }
516         }else{
517                 //Just continue
518                 [self displayContentObject:inObject immediately:immediately];
519         }
522 - (void)didFilterAttributedString:(NSAttributedString *)filteredString contentFilterDisplayContext:(AIContentObject *)inObject
524         [inObject setMessage:filteredString];
525         
526         //Continue
527         [self displayContentObject:inObject immediately:NO];
530 //Display a content object
531 //Add content to the message view.  Doesn't do any sending or receiving, just adds the content.
532 - (void)displayContentObject:(AIContentObject *)inObject
534         [self displayContentObject:inObject immediately:NO];
537 - (void)displayContentObject:(AIContentObject *)inObject immediately:(BOOL)immediately
539     //Filter the content object
540     if([inObject filterContent]){
541                 BOOL                            message = ([inObject isKindOfClass:[AIContentMessage class]] && ![(AIContentMessage *)inObject isAutoreply]);
542                 AIFilterType            filterType = (message ? AIFilterMessageDisplay : AIFilterDisplay);
543                 AIFilterDirection       direction = ([inObject isOutgoing] ? AIFilterOutgoing : AIFilterIncoming);
544                 
545                 if (immediately){
546                         
547                         //Set it after filtering in the main thread, then display it
548                         [inObject setMessage:[self filterAttributedString:[inObject message]
549                                                                                           usingFilterType:filterType
550                                                                                                         direction:direction
551                                                                                                           context:inObject]];
552                         [self finishDisplayContentObject:inObject];             
553                         
554                 }else{
555                         //Filter in the filtering thread
556                         [self filterAttributedString:[inObject message]
557                                                  usingFilterType:filterType
558                                                            direction:direction
559                                                    filterContext:inObject
560                                                  notifyingTarget:self
561                                                                 selector:@selector(didFilterAttributedString:displayContext:)
562                                                                  context:inObject];
563                 }
564                 
565     }else{
566                 [self finishDisplayContentObject:inObject];
567         }
571 - (void)didFilterAttributedString:(NSAttributedString *)filteredString displayContext:(AIContentObject *)inObject
573         [inObject setMessage:filteredString];
574         
575         [self finishDisplayContentObject:inObject];
578 - (void)finishDisplayContentObject:(AIContentObject *)inObject
580     //Check if the object should display
581     if([inObject displayContent] && ([[inObject message] length] > 0)){
582                 AIChat                  *chat = [inObject chat];
583                 NSDictionary    *userInfo;
584                 BOOL                    contentReceived, shouldPostContentReceivedEvents, chatIsOpen;
586                 //If the chat of the content object has been cleared, we can't do anything with it, so simply return
587                 if(!chat) return;
588                 
589                 chatIsOpen = [chat isOpen];
590                 contentReceived = (([inObject isMemberOfClass:[AIContentMessage class]]) &&
591                                                    (![inObject isOutgoing]));
592                 shouldPostContentReceivedEvents = contentReceived && [inObject trackContent];
593                 
594                 if(!chatIsOpen){
595                         /*
596                          Tell the interface to open the chat
597                          For incoming messages, we don't open the chat until we're sure that new content is being received.
598                          */
599                         [[adium interfaceController] openChat:chat];
600                 }
602                 userInfo = [NSDictionary dictionaryWithObjectsAndKeys:chat, @"AIChat", inObject, @"AIContentObject", nil];
604                 //Add this content to the chat
605                 [chat addContentObject:inObject];
606                 
607                 //Notify: Content Object Added
608                 [[adium notificationCenter] postNotificationName:Content_ContentObjectAdded
609                                                                                                   object:chat
610                                                                                                 userInfo:userInfo];
612                 if(shouldPostContentReceivedEvents){
613                         NSSet                   *previouslyPerformedActionIDs = nil;
614                         AIListObject    *listObject = [chat listObject];
615                         
616                         if(!chatIsOpen){
617                                 //If the chat wasn't open before, generate CONTENT_MESSAGE_RECEIVED_FIRST
618                                 previouslyPerformedActionIDs = [[adium contactAlertsController] generateEvent:CONTENT_MESSAGE_RECEIVED_FIRST
619                                                                                                                                                                 forListObject:listObject
620                                                                                                                                                                          userInfo:userInfo
621                                                                                                                                  previouslyPerformedActionIDs:nil];     
622                         }
623                         
624                         if(chat != [[adium interfaceController] activeChat]){
625                                 //If the chat is not currently active, generate CONTENT_MESSAGE_RECEIVED_BACKGROUND
626                                 previouslyPerformedActionIDs = [[adium contactAlertsController] generateEvent:CONTENT_MESSAGE_RECEIVED_BACKGROUND
627                                                                                                                                                                 forListObject:listObject
628                                                                                                                                                                          userInfo:userInfo
629                                                                                                                                  previouslyPerformedActionIDs:previouslyPerformedActionIDs];
630                         }
631                         
632                         [[adium contactAlertsController] generateEvent:CONTENT_MESSAGE_RECEIVED
633                                                                                          forListObject:listObject
634                                                                                                   userInfo:userInfo
635                                                           previouslyPerformedActionIDs:previouslyPerformedActionIDs];
636                 }
637     }
639         //We are no longer in the process of receiving this object
640         [objectsBeingReceived removeObject:inObject];
641         AILog(@"objectsBeingReceived: %@",([objectsBeingReceived count] ? [objectsBeingReceived description] : @"(empty)"));
644 - (void)displayStatusMessage:(NSString *)message ofType:(NSString *)type inChat:(AIChat *)inChat
646         AIContentStatus         *content;
647         NSAttributedString      *attributedMessage;
648         
649         //Create our content object
650         attributedMessage = [[NSAttributedString alloc] initWithString:message
651                                                                                                                 attributes:[self defaultFormattingAttributes]];
652         content = [AIContentStatus statusInChat:inChat
653                                                                  withSource:[inChat listObject]
654                                                                 destination:[inChat account]
655                                                                            date:[NSDate date]
656                                                                         message:attributedMessage
657                                                                    withType:type];
658         [attributedMessage release];
660         //Add the object
661         [self receiveContentObject:content];
664 //Returns YES if the account/chat is available for sending content
665 - (BOOL)availableForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact onAccount:(AIAccount *)inAccount 
667         return([inAccount availableForSendingContentType:inType toContact:inContact]);
672 //Chat Status -------------------------------------------------------------------------------------------------
673 #pragma mark Chat Status
675 //Registers code to observe handle status changes
676 - (void)registerChatObserver:(id <AIChatObserver>)inObserver
678         //Add the observer
679     [chatObserverArray addObject:[NSValue valueWithNonretainedObject:inObserver]];
680         
681     //Let the new observer process all existing chats
682         [self updateAllChatsForObserver:inObserver];
685 - (void)unregisterChatObserver:(id <AIChatObserver>)inObserver
687     [chatObserverArray removeObject:[NSValue valueWithNonretainedObject:inObserver]];
690 - (void)chatStatusChanged:(AIChat *)inChat modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
692         NSSet                   *modifiedAttributeKeys;
693         
694     //Let all observers know the contact's status has changed before performing any further notifications
695         modifiedAttributeKeys = [self _informObserversOfChatStatusChange:inChat withKeys:inModifiedKeys silent:silent];
697     //Post an attributes changed message (if necessary)
698     if([modifiedAttributeKeys count]){
699                 [self chatAttributesChanged:inChat modifiedKeys:modifiedAttributeKeys];
700     }   
703 - (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys
705         //Post an attributes changed message
706         [[adium notificationCenter] postNotificationName:Chat_AttributesChanged
707                                                                                           object:inChat
708                                                                                         userInfo:(inModifiedKeys ? [NSDictionary dictionaryWithObject:inModifiedKeys 
709                                                                                                                                                                                                    forKey:@"Keys"] : nil)];
712 //Send a chatStatusChanged message for each open chat with a nil modifiedStatusKeys array
713 - (void)updateAllChatsForObserver:(id <AIChatObserver>)observer
715         NSEnumerator    *enumerator = [openChats objectEnumerator];
716         AIChat                  *chat;
717         
718         while (chat = [enumerator nextObject]){
719                 [self chatStatusChanged:chat modifiedStatusKeys:nil silent:NO];
720         }
723 //Notify observers of a status change.  Returns the modified attribute keys
724 - (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
726         NSMutableSet    *attrChange = nil;
727         NSEnumerator    *enumerator;
728         NSValue                 *observerValue;
730         //Let our observers know
731         enumerator = [chatObserverArray objectEnumerator];
732         while((observerValue = [enumerator nextObject])){
733                 id <AIChatObserver>     observer;
734                 NSSet                           *newKeys;
735                 
736                 observer = [observerValue nonretainedObjectValue];
737                 if(newKeys = [observer updateChat:inChat keys:modifiedKeys silent:silent]){
738                         if (!attrChange) attrChange = [NSMutableSet set];
739                         [attrChange unionSet:newKeys];
740                 }
741         }
742         
743         //Send out the notification for other observers
744         [[adium notificationCenter] postNotificationName:Chat_StatusChanged
745                                                                                           object:inChat
746                                                                                         userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys 
747                                                                                                                                                                                                  forKey:@"Keys"] : nil)];
748         
749         return(attrChange);
752 //Increase unviewed content
753 - (void)increaseUnviewedContentOfChat:(AIChat *)inChat
755         int currentUnviewed = [inChat integerStatusObjectForKey:KEY_UNVIEWED_CONTENT];
756         [inChat setStatusObject:[NSNumber numberWithInt:(currentUnviewed+1)]
757                                          forKey:KEY_UNVIEWED_CONTENT
758                                          notify:YES];
761 //Clear unviewed content
762 - (void)clearUnviewedContentOfChat:(AIChat *)inChat
764         [inChat setStatusObject:nil forKey:KEY_UNVIEWED_CONTENT notify:YES];
767 //Chats -------------------------------------------------------------------------------------------------
768 #pragma mark Chats
769 //Opens a chat for communication with the contact, creating if necessary.  The new chat will be made active.
770 - (AIChat *)openChatWithContact:(AIListContact *)inContact
772         AIChat  *chat = [self chatWithContact:inContact];
774         if(chat) [[adium interfaceController] openChat:chat]; 
776         return(chat);   
779 //Creates a chat for communication with the contact, but does not make the chat active (Doesn't open a chat window)
780 //If a chat already exists it will be returned
781 - (AIChat *)chatWithContact:(AIListContact *)inContact
783         NSEnumerator    *enumerator;
784         AIChat                  *chat = nil;
785         AIListContact   *targetContact = inContact;
786                 
787         /*
788          If we're dealing with a meta contact, open a chat with the preferred contact for this meta contact
789          It's a good idea for the caller to pick the preferred contact for us, since they know the content type
790          being sent and more information - but we'll do it here as well just to be safe.
791          */
792         if ([inContact isKindOfClass:[AIMetaContact class]]){
793                 targetContact = [[adium contactController] preferredContactForContentType:CONTENT_MESSAGE_TYPE
794                                                                                                                                    forListContact:inContact];
795                 
796                 /*
797                  If we have no accounts online, preferredContactForContentType:forListContact will return nil.
798                  We'd rather open up the chat window on a useless contact than do nothing, so just pick the 
799                  preferredContact from the metaContact.
800                  */
801                 if (!targetContact){
802                         targetContact = [(AIMetaContact *)inContact preferredContact];
803                 }
804         }
805         
806         //If we can't get a contact, we're not going to be able to get a chat... return nil
807         if(!targetContact) return nil;
808         
809         //Search for an existing chat we can switch instead of replacing
810         enumerator = [openChats objectEnumerator];
811         while(chat = [enumerator nextObject]){
812                 //If a chat for this object already exists
813                 if([[chat uniqueChatID] isEqualToString:[targetContact internalObjectID]]) {
814                         if(!([chat listObject] == targetContact)){
815                                 [self switchChat:chat toAccount:[targetContact account]];
816                         }
817                         
818                         break;
819                 }
820                 
821                 //If this object is within a meta contact, and a chat for an object in that meta contact already exists
822                 if([[targetContact containingObject] isKindOfClass:[AIMetaContact class]] && 
823                    [[chat listObject] containingObject] == [targetContact containingObject]){
825                         //Switch the chat to be on this contact (and its account) now
826                         [self switchChat:chat toListContact:targetContact usingContactAccount:YES];
827                         
828                         break;
829                 }
830         }
832         if(!chat){
833                 AIAccount       *account;
834                 account = [targetContact account];
835                 
836                 //Create a new chat
837                 chat = [AIChat chatForAccount:account];
838                 [chat addParticipatingListObject:targetContact];
839                 [openChats addObject:chat];
840                 AILog(@"chatWithContact: Added <<%@>> [%@]",chat,openChats);
842                 //Inform the account of its creation and post a notification if successful
843                 if([[targetContact account] openChat:chat]){
844                         [[adium notificationCenter] postNotificationName:Chat_Created object:chat userInfo:nil];
845                 }else{
846                         [openChats removeObject:chat];
847                         AILog(@"chatWithContact: Immediately removed <<%@>> [%@]",chat,openChats);
848                         chat = nil;
849                 }
850         }
852         return(chat);
856 * @brief Return a pre-existing chat with a contact.
858  * @result The chat, or nil if no chat with the contact exists
859  */
860 - (AIChat *)existingChatWithContact:(AIListContact *)inContact
862         NSEnumerator    *enumerator;
863         AIChat                  *chat = nil;
864         
865         if ([inContact isKindOfClass:[AIMetaContact class]]) {
866                 //Search for a chat with any contact within this AIMetaContact
867                 enumerator = [openChats objectEnumerator];
868                 while ((chat = [enumerator nextObject])) {
869                         if ([[(AIMetaContact *)inContact containedObjects] containsObjectIdenticalTo:[chat listObject]]) break;
870                 }
871                 
872         } else {
873                 //Search for a chat with this AIListContact
874                 enumerator = [openChats objectEnumerator];
875                 while ((chat = [enumerator nextObject])) {
876                         if ([chat listObject] == inContact) break;
877                 }
878         }
879         
880         return chat;
883 - (AIChat *)chatWithName:(NSString *)inName onAccount:(AIAccount *)account chatCreationInfo:(NSDictionary *)chatCreationInfo
885         AIChat                  *chat = nil;
886         
887         //Search for an existing chat we can use instead of creating a new one
888         chat = [self existingChatWithName:inName onAccount:account];
889         
890         if(!chat){
891                 //Create a new chat
892                 chat = [AIChat chatForAccount:account];
893                 [chat setName:inName];
894                 [openChats addObject:chat];
895                 AILog(@"chatWithName:%@ onAccount:%@ added <<%@>> [%@]",inName,account,chat,openChats);
896                 
897                 if (chatCreationInfo) [chat setStatusObject:chatCreationInfo
898                                                                                          forKey:@"ChatCreationInfo"
899                                                                                          notify:NotifyNever];
900                 
901                 [chat setStatusObject:[NSNumber numberWithBool:YES]
902                                            forKey:@"AlwaysShowUserList"
903                                            notify:NotifyNever];
904                 
905                 //Inform the account of its creation and post a notification if successful
906                 if([account openChat:chat]){
907                         [[adium notificationCenter] postNotificationName:Chat_Created object:chat userInfo:nil];
908                 }else{
909                         [openChats removeObject:chat];
910                         AILog(@"chatWithName: Immediately removed <<%@>> [%@]",chat,openChats);
911                         chat = nil;
912                 }
913         }
914         return(chat);
917 - (void)openChat:(AIChat *)chat
919         if(chat){               
920                 [openChats addObject:chat];
921                 AILog(@"openChat: Added <<%@>> [%@]",chat,openChats);
922                 [[adium interfaceController] openChat:chat]; 
923         }
926 - (AIChat *)existingChatWithName:(NSString *)inName onAccount:(AIAccount *)account
928         NSEnumerator    *enumerator;
929         AIChat                  *chat = nil;
930         
931         enumerator = [openChats objectEnumerator];
933         while(chat = [enumerator nextObject]){
934                 if(([chat account] == account) &&
935                    ([[chat name] isEqualToString:inName])){
936                         break;
937                 }
938         }       
939         
940         return chat;
943 - (AIChat *)existingChatWithUniqueChatID:(NSString *)uniqueChatID
945         NSEnumerator    *enumerator;
946         AIChat                  *chat = nil;
947         
948         enumerator = [openChats objectEnumerator];
949         
950         while(chat = [enumerator nextObject]){
951                 if([[chat uniqueChatID] isEqualToString:uniqueChatID]){
952                         break;
953                 }
954         }       
955         
956         return chat;
959 //Close a chat
960 - (BOOL)closeChat:(AIChat *)inChat
961 {       
962         BOOL    shouldRemove = YES;
963         
964         /* If we are currently passing a content object for this chat through our content filters, don't remove it from
965          * our openChats set as it will become needed soon. If we were to remove it, and a second message came in which was
966          * also before the first message is done filtering, we would otherwise mistakenly think we needed to create a new
967          * chat, generating a duplicate.
968          */
969         NSEnumerator    *objectsBeingReceivedEnumerator = [objectsBeingReceived objectEnumerator];
970         AIContentObject *contentObject;
971         while(contentObject = [objectsBeingReceivedEnumerator nextObject]){
972                 if([contentObject chat] == inChat){
973                         shouldRemove = NO;
974                         break;
975                 }
976         }
978         if(mostRecentChat == inChat){
979                 [mostRecentChat release];
980                 mostRecentChat = nil;
981         }
982         
983         //Notify the account and send out the Chat_WillClose notification
984         [[inChat account] closeChat:inChat];
985         [[adium notificationCenter] postNotificationName:Chat_WillClose object:inChat userInfo:nil];
986         
987         //Remove the chat's content (it retains the chat, so this must be done separately)
988         [inChat removeAllContent];
990         //Remove the chat
991         if(shouldRemove){
992                 [openChats removeObject:inChat];
993                 AILog(@"closeChat: Removed <<%@>> [%@]",inChat, openChats);
994         }
996         return YES;
999 //Switch a chat from one account to another, updating the target list contact to be an 'identical' one on the target account.
1000 - (void)switchChat:(AIChat *)chat toAccount:(AIAccount *)newAccount
1002         AIAccount       *oldAccount = [chat account];
1003         if (newAccount != oldAccount){
1004                 //Hang onto stuff until we're done
1005                 [chat retain];
1007                 //Close down the chat on account A
1008                 [oldAccount closeChat:chat];
1010                 //Set the account and the listObject
1011                 {
1012                         [chat setAccount:newAccount];
1013                         
1014                         //We want to keep the same destination for the chat but switch it to a listContact on the desired account.
1015                         AIListContact   *newContact = [[adium contactController] contactWithService:[[chat listObject] service]
1016                                                                                                                                                                 account:[chat account]
1017                                                                                                                                                                         UID:[[chat listObject] UID]];
1018                         [chat setListObject:newContact];
1019                 }
1020                 
1021                 //Open the chat on account B
1022                 [newAccount openChat:chat];
1023                 
1024                 //Clean up
1025                 [chat release];
1026         }
1029 //Switch the list contact of the account; this does not change the source account - use switchChat:toAccount: for that.
1030 - (void)switchChat:(AIChat *)chat toListContact:(AIListContact *)inContact usingContactAccount:(BOOL)useContactAccount
1032         AIAccount               *newAccount = (useContactAccount ? [inContact account] : [chat account]);
1033         
1034         //Switch the inContact over to a contact on the new account so we send messages to the right place.
1035         AIListContact   *newContact = [[adium contactController] contactWithService:[inContact service]
1036                                                                                                                                                 account:newAccount
1037                                                                                                                                                         UID:[inContact UID]];
1038         if (newContact != [chat listObject]){
1039                 //Hang onto stuff until we're done
1040                 [chat retain];
1041                 
1042                 //Close down the chat on the account, as the account may need to perform actions such as closing a connection
1043                 [[chat account] closeChat:chat];
1044                 
1045                 //Set to the new listContact and account as needed
1046                 [chat setListObject:newContact];
1047                 if (useContactAccount) [chat setAccount:newAccount];
1048                 
1049                 
1050                 //Reopen the chat on the account
1051                 [[chat account] openChat:chat];
1052                 
1053                 //Clean up
1054                 [chat release];
1055         }
1058 //Returns all chats with the object
1059 - (NSSet *)allChatsWithContact:(AIListContact *)inContact
1061     NSMutableSet        *foundChats = nil;
1062         
1063         //Scan the objects participating in each chat, looking for the requested object
1064         if([inContact isKindOfClass:[AIMetaContact class]]){
1066                 NSEnumerator    *enumerator;
1067                 AIListContact   *listContact;
1069                 foundChats = [NSMutableSet set];
1070                 
1071                 enumerator = [[(AIMetaContact *)inContact listContacts] objectEnumerator];
1072                 while(listContact = [enumerator nextObject]){
1073                         NSSet           *listContactChats;
1074                         if (listContactChats = [self allChatsWithContact:listContact]){
1075                                 [foundChats unionSet:listContactChats];
1076                         }
1077                 }
1078                 
1079         }else{
1080                 NSEnumerator    *chatEnumerator = [openChats objectEnumerator];
1081                 AIChat                  *chat;
1082                 while((chat = [chatEnumerator nextObject])){
1083                         if(![chat name] &&
1084                                 [[[chat listObject] internalObjectID] isEqualToString:[inContact internalObjectID]]){
1085                                 if (!foundChats) foundChats = [NSMutableSet set];
1086                                 [foundChats addObject:chat];
1087                         }
1088                 }
1089         }
1090         
1091     return(foundChats);
1094 - (NSSet *)openChats
1096     return openChats;
1099 //Switch to a chat with the most recent unviewed content.  Returns YES if one existed
1100 - (AIChat *)mostRecentUnviewedChat
1102         AIChat  *newActiveChat = nil;
1104     if(mostRecentChat && [mostRecentChat integerStatusObjectForKey:KEY_UNVIEWED_CONTENT]){
1105                 //First choice: switch to the chat which received chat most recently if it has unviewed content
1106                 newActiveChat = mostRecentChat;
1107                 
1108         }else{
1109                 //Second choice: switch to the first chat we can find which has unviewed content
1110                 NSEnumerator    *enumerator = [openChats objectEnumerator];
1111                 AIChat                  *chat;
1112                 while ((chat = [enumerator nextObject]) && ![chat integerStatusObjectForKey:KEY_UNVIEWED_CONTENT]);
1113                 
1114                 if (chat) newActiveChat = chat;
1115         }
1117         return newActiveChat;
1121  * @brief Is the passed contact in a group chat?
1123  * @result YES if the contact is in an open group chat; NO if not.
1124  */
1125 - (BOOL)contactIsInGroupChat:(AIListContact *)listContact
1127         NSEnumerator    *chatEnumerator = [openChats objectEnumerator];
1128         AIChat                  *chat;
1129         BOOL                    contactIsInGroupChat = NO;
1131         while((chat = [chatEnumerator nextObject])){
1132                 if([chat name] &&
1133                    [[chat participatingListObjects] containsObjectIdenticalTo:listContact]){
1135                         contactIsInGroupChat = YES;
1136                         break;
1137                 }
1138         }
1139         
1140         return contactIsInGroupChat;
1143 //Content Source & Destination -----------------------------------------------------------------------------------------
1144 #pragma mark Content Source & Destination
1145 //Returns the available account for sending content to a specified contact
1146 - (NSArray *)sourceAccountsForSendingContentType:(NSString *)inType
1147                                                                         toListObject:(AIListObject *)inObject
1148                                                                            preferred:(BOOL)inPreferred
1150         NSMutableArray  *sourceAccounts = [NSMutableArray array];
1151         NSEnumerator    *enumerator = [[[adium accountController] accountArray] objectEnumerator];
1152         AIAccount               *account;
1153         
1154         while(account = [enumerator nextObject]){
1155                 if([inObject service] == [account service]){
1156                         BOOL                    knowsObject = NO;
1157                         BOOL                    couldSendContent = NO;
1158                         AIListContact   *contactForAccount = [[adium contactController] existingContactWithService:[inObject service]
1159                                                                                                                                                                                            account:account
1160                                                                                                                                                                                                    UID:[inObject UID]];
1161                         //Does the account know this object?
1162                         if(contactForAccount){
1163                                 knowsObject = [account availableForSendingContentType:inType toContact:contactForAccount];
1164                         }
1165                         
1166                         //Could the account send this
1167                         couldSendContent = [account availableForSendingContentType:inType toContact:nil];
1168                         
1169                         if((inPreferred && knowsObject) || (!inPreferred && !knowsObject && couldSendContent)){
1170                                 [sourceAccounts addObject:account];
1171                         }
1172                 }
1173         }
1174         
1175         return(sourceAccounts);
1178 //Returns the available contacts for receiving content to a specific contact
1179 - (NSArray *)destinationObjectsForContentType:(NSString *)inType
1180                                                                  toListObject:(AIListObject *)inObject
1181                                                                         preferred:(BOOL)inPreferred
1183         //meta contact special case here, return any contacts in the user defined meta contact
1184         return([NSArray arrayWithObject:inObject]);
1187 #pragma mark Contact Alerts
1189 - (NSString *)shortDescriptionForEventID:(NSString *)eventID
1191         NSString        *description;
1192         
1193         if([eventID isEqualToString:CONTENT_MESSAGE_SENT]){
1194                 description = AILocalizedString(@"Is sent a message",nil);
1195         }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED]){
1196                 description = AILocalizedString(@"Sends a message",nil);
1197         }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_FIRST]){
1198                 description = AILocalizedString(@"Sends an initial message",nil);
1199         }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_BACKGROUND]){
1200                 description = AILocalizedString(@"Sends a message in a background chat",nil);
1201         }else{
1202                 description = @"";
1203         }
1204         
1205         return(description);
1208 - (NSString *)globalShortDescriptionForEventID:(NSString *)eventID
1210         NSString        *description;
1212         if([eventID isEqualToString:CONTENT_MESSAGE_SENT]){
1213                 description = AILocalizedString(@"Message sent",nil);
1214         }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED]){
1215                 description = AILocalizedString(@"Message received",nil);
1216         }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_FIRST]){
1217                 description = AILocalizedString(@"Message received (Initial)",nil);
1218         }else if ([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_BACKGROUND]){
1219                 description = AILocalizedString(@"Message received (Background chat)",nil);
1220         }else{
1221                 description = @"";
1222         }
1224         return(description);
1227 //Evan: This exists because old X(tras) relied upon matching the description of event IDs, and I don't feel like making
1228 //a converter for old packs.  If anyone wants to fix this situation, please feel free :)
1229 - (NSString *)englishGlobalShortDescriptionForEventID:(NSString *)eventID
1231         NSString        *description;
1232         
1233         if([eventID isEqualToString:CONTENT_MESSAGE_SENT]){
1234                 description = @"Message Sent";
1235         }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED]){
1236                 description = @"Message Received";
1237         }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_FIRST]){
1238                 description = @"Message Received (New)";
1239         }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_BACKGROUND]){
1240                 description = @"Message Received (Background Chat)";
1241         }else{
1242                 description = @"";
1243         }
1244         
1245         return(description);
1248 - (NSString *)longDescriptionForEventID:(NSString *)eventID forListObject:(AIListObject *)listObject
1250         NSString        *description = nil;
1251         
1252         if(listObject){
1253                 NSString        *name;
1254                 NSString        *format;
1256                 if([eventID isEqualToString:CONTENT_MESSAGE_SENT]){
1257                         format = AILocalizedString(@"When you send %@ a message",nil);
1258                 }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED]){
1259                         format = AILocalizedString(@"When %@ sends a message to you",nil);
1260                 }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_FIRST]){
1261                         format = AILocalizedString(@"When %@ sends an initial message to you",nil);
1262                 }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_BACKGROUND]){
1263                         format = AILocalizedString(@"When %@ sends a message to you in a background chat",nil);                 
1264                 }else{
1265                         format = nil;
1266                 }
1268                 if(format){
1269                         name = ([listObject isKindOfClass:[AIListGroup class]] ?
1270                                         [NSString stringWithFormat:AILocalizedString(@"a member of %@",nil),[listObject displayName]] :
1271                                         [listObject displayName]);
1272                         
1273                         description = [NSString stringWithFormat:format, name];
1274                 }
1275                 
1276         }else{
1277                 if([eventID isEqualToString:CONTENT_MESSAGE_SENT]){
1278                         description = AILocalizedString(@"When you send a message",nil);
1279                 }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED]){
1280                         description = AILocalizedString(@"When you receive any message",nil);
1281                 }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_FIRST]){
1282                         description = AILocalizedString(@"When you receive an initial message",nil);
1283                 }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_BACKGROUND]){
1284                         description = AILocalizedString(@"When you receive a message in a background chat",nil);                        
1285                 }
1286         }
1287         
1288         return(description);
1291 - (NSString *)naturalLanguageDescriptionForEventID:(NSString *)eventID
1292                                                                                 listObject:(AIListObject *)listObject
1293                                                                                   userInfo:(id)userInfo
1294                                                                         includeSubject:(BOOL)includeSubject
1296         NSString                *description = nil;
1297         AIContentObject *contentObject;
1298         NSString                *messageText;
1299         NSString                *displayName;
1300         
1301         NSParameterAssert([userInfo isKindOfClass:[NSDictionary class]]);
1302         
1303         contentObject = [(NSDictionary *)userInfo objectForKey:@"AIContentObject"];
1304         messageText = [[[contentObject message] attributedStringByConvertingAttachmentsToStrings] string];
1305         
1306         if(includeSubject){
1307                 
1308                 if([eventID isEqualToString:CONTENT_MESSAGE_SENT]){
1309                         displayName = (listObject ? [listObject displayName] : [[contentObject chat] name]);
1311                         description = [NSString stringWithFormat:
1312                                 AILocalizedString(@"You said %@ to %@","You said Message to Contact"),
1313                                 messageText,
1314                                 displayName];
1315                         
1316                 }else if([eventID isEqualToString:CONTENT_MESSAGE_RECEIVED] ||
1317                                  [eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_FIRST] ||
1318                                  [eventID isEqualToString:CONTENT_MESSAGE_RECEIVED_BACKGROUND]){
1319                         displayName = (listObject ? [listObject displayName] : [[contentObject source] displayName]);
1321                         description = [NSString stringWithFormat:
1322                                 AILocalizedString(@"%@ said %@","Contact said Message"),
1323                                 displayName,
1324                                 messageText];
1325                 }       
1326                 
1327         }else{
1328                 description = messageText;
1329         }
1330         
1331         return(description);
1334 /*! 
1335 * @brief Generate a menu of encryption preference choices
1337 - (NSMenu *)encryptionMenuNotifyingTarget:(id)target withDefault:(BOOL)withDefault
1339         NSMenu          *encryptionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
1340         NSMenuItem      *menuItem;
1342         [encryptionMenu setTitle:ENCRYPTION_MENU_TITLE];
1344         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Disable chat encryption",nil)
1345                                                                                   target:target
1346                                                                                   action:@selector(selectedEncryptionPreference:)
1347                                                                    keyEquivalent:@""];
1348         
1349         [menuItem setTag:EncryptedChat_Never];
1350         [encryptionMenu addItem:menuItem];
1351         [menuItem release];
1352         
1353         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Encrypt chats as requested",nil)
1354                                                                                   target:target
1355                                                                                   action:@selector(selectedEncryptionPreference:)
1356                                                                    keyEquivalent:@""];
1357         
1358         [menuItem setTag:EncryptedChat_Manually];
1359         [encryptionMenu addItem:menuItem];
1360         [menuItem release];
1361         
1362         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Encrypt chats automatically",nil)
1363                                                                                   target:target
1364                                                                                   action:@selector(selectedEncryptionPreference:)
1365                                                                    keyEquivalent:@""];
1366         
1367         [menuItem setTag:EncryptedChat_Automatically];
1368         [encryptionMenu addItem:menuItem];
1369         [menuItem release];
1370         
1371         menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Force encryption and refuse plaintext",nil)
1372                                                                                   target:target
1373                                                                                   action:@selector(selectedEncryptionPreference:)
1374                                                                    keyEquivalent:@""];
1375         
1376         [menuItem setTag:EncryptedChat_RejectUnencryptedMessages];
1377         [encryptionMenu addItem:menuItem];
1378         [menuItem release];
1379         
1380         if(withDefault){
1381                 [encryptionMenu addItem:[NSMenuItem separatorItem]];
1382                 
1383                 NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Default",nil)
1384                                                                                                                   target:target
1385                                                                                                                   action:@selector(selectedEncryptionPreference:)
1386                                                                                                    keyEquivalent:@""];
1387                 
1388                 [menuItem setTag:EncryptedChat_Default];
1389                 [encryptionMenu addItem:menuItem];
1390                 [menuItem release];
1391         }
1392         
1393         return [encryptionMenu autorelease];
1396 - (NSImage *)imageForEventID:(NSString *)eventID
1398         static NSImage  *eventImage = nil;
1399         if(!eventImage) eventImage = [[NSImage imageNamed:@"message" forClass:[self class]] retain];
1400         return eventImage;
1403 #pragma mark Ignore
1404 - (void)toggleIgnoreOfContact:(id)sender
1406         AIListObject    *listObject = [[adium menuController] currentContextMenuObject];
1407         AIChat                  *chat = [[adium menuController] currentContextMenuChat];
1408         
1409         if ([listObject isKindOfClass:[AIListContact class]]) {
1410                 BOOL                    isIgnored = [chat isListContactIgnored:(AIListContact *)listObject];
1411                 [chat setListContact:(AIListContact *)listObject isIgnored:!isIgnored];
1412         }
1415 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1417         if (menuItem == menuItem_ignore) {
1418                 AIListObject    *listObject = [[adium menuController] currentContextMenuObject];
1419                 AIChat                  *chat = [[adium menuController] currentContextMenuChat];
1420                 
1421                 if ([listObject isKindOfClass:[AIListContact class]]) {
1422                         if ([chat isListContactIgnored:(AIListContact *)listObject]) {
1423                                 [menuItem setTitle:AILocalizedString(@"Un-ignore","Un-ignore means begin receiving messages from this contact again in a chat")];
1424                                 
1425                         } else {
1426                                 [menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
1427                         }
1428                 }
1429         }
1430         
1431         return YES;
1434 #pragma mark Chat contact addition and removal
1437  * @brief A chat added a listContact to its participatants list
1439  * @param chat The chat
1440  * @param inContact The contact
1441  * @param notify If YES, trigger the contact joined event if this is a group chat.  Ignored if this is not a group chat.
1442  */
1443 - (void)chat:(AIChat *)chat addedListContact:(AIListContact *)inContact notify:(BOOL)notify
1445         if (notify && [chat name]) {
1446                 /* Prevent triggering of the event when we are informed that the chat's own account entered the chat
1447                 * If the UID of a contact in a chat differs from a normal UID, such as is the case with Jabber where a chat
1448                 * contact has the form "roomname@conferenceserver/handle" this will fail, but it's better than nothing.
1449                 */
1450                 if (![[[inContact account] UID] isEqualToString:[inContact UID]]) {
1451                         [adiumChatEvents chat:chat addedListContact:inContact];
1452                         
1453                         [[adium contentController] displayStatusMessage:[NSString stringWithFormat:AILocalizedString(@"%@ joined the chat",nil),[inContact displayName]]
1454                                                                                                          ofType:@"contact_joined"
1455                                                                                                          inChat:chat];
1456                 }
1457         }
1458         
1459         //Always notify Adium that the list changed so it can be updated, caches can be modified, etc.
1460         [[adium notificationCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
1461                                                                                           object:chat];
1465  * @brief A chat removed a listContact from its participants list
1467  * @param chat The chat
1468  * @param inContact The contact
1469  */
1470 - (void)chat:(AIChat *)chat removedListContact:(AIListContact *)inContact
1472         if ([chat name]) {
1473                 [adiumChatEvents chat:chat removedListContact:inContact];
1474                 
1475                 [[adium contentController] displayStatusMessage:[NSString stringWithFormat:AILocalizedString(@"%@ left the chat.",nil),[inContact displayName]]
1476                                                                                                  ofType:@"contact_left"
1477                                                                                                  inChat:chat];          
1478         }
1479         
1480         [[adium notificationCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
1481                                                                                           object:chat];
1486 @end