2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
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.
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.
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.
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;
64 * @class AIContentController
65 * @brief Controller to manage incoming and outgoing content and chats.
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.
73 @implementation AIContentController
76 * @brief Initialize the controller
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;
90 openChats = [[NSMutableSet alloc] init];
91 objectsBeingReceived = [[NSMutableSet alloc] init];
92 adiumContentFiltering = [[AdiumContentFiltering alloc] init];
93 adiumChatEvents = [[AdiumChatEvents alloc] init];
94 [adiumChatEvents controllerDidLoad];
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];
105 menuItem_ignore = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@""
107 action:@selector(toggleIgnoreOfContact:)
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
119 NSEnumerator *enumerator = [openChats objectEnumerator];
122 //Every open chat is about to close.
123 while((chat = [enumerator nextObject])){
124 [[adium notificationCenter] postNotificationName:Chat_WillClose
131 * @brief Close the controller
133 - (void)closeController
143 [emoticonPacks release]; emoticonPacks = nil;
144 [emoticonsArray release]; emoticonsArray = nil;
145 [textEntryFilterArray release];
146 [textEntryContentFilterArray release];
147 [textEntryViews release];
149 [chatObserverArray release]; chatObserverArray = nil;
150 [objectsBeingReceived release];
156 //Default Formatting -------------------------------------------------------------------------------------------------
157 #pragma mark Default Formatting
158 - (void)setDefaultFormattingAttributes:(NSDictionary *)inDict
160 if(defaultFormattingAttributes != inDict){
161 [defaultFormattingAttributes release];
162 defaultFormattingAttributes = [inDict retain];
166 * @brief Default formatting attributes
168 * This dictionary of attributes will be used for new text entry views, messages, etc.
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:)]);
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];
191 [textEntryFilterArray addObject:inFilter];
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;
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];
214 //A text entry view's content changed
215 - (void)contentsChangedInTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
217 NSEnumerator *enumerator;
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];
227 //A text entry view was opened
228 - (void)didOpenTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
230 NSEnumerator *enumerator;
234 [textEntryViews addObject:inTextEntryView];
236 //Notify all text entry filters
237 enumerator = [textEntryContentFilterArray objectEnumerator];
238 while((filter = [enumerator nextObject])){
239 [filter didOpenTextEntryView:inTextEntryView];
241 enumerator = [textEntryFilterArray objectEnumerator];
242 while((filter = [enumerator nextObject])){
243 [filter didOpenTextEntryView:inTextEntryView];
247 //A text entry view was closed
248 - (void)willCloseTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
250 NSEnumerator *enumerator;
253 //Stop tracking the view
254 [textEntryViews removeObject:inTextEntryView];
256 //Notify all text entry filters
257 enumerator = [textEntryContentFilterArray objectEnumerator];
258 while((filter = [enumerator nextObject])){
259 [filter willCloseTextEntryView:inTextEntryView];
261 enumerator = [textEntryFilterArray objectEnumerator];
262 while((filter = [enumerator nextObject])){
263 [filter willCloseTextEntryView:inTextEntryView];
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
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
314 filterContext:filterContext
315 notifyingTarget:target
319 - (void)delayedFilterDidFinish:(NSAttributedString *)attributedString uniqueID:(unsigned long long)uniqueID
321 [adiumContentFiltering delayedFilterDidFinish:attributedString
325 //Messaging ------------------------------------------------------------------------------------------------------------
326 #pragma mark Messaging
327 //Receiving step 1: Add an incoming content object - entry point
328 - (void)receiveContentObject:(AIContentObject *)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
339 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:inObject,@"Object",nil]];
342 //Track that we are in the process of receiving this object
343 [objectsBeingReceived addObject:inObject];
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
352 selector:@selector(didFilterAttributedString:receivingContext:)
356 [self finishReceiveContentObject:inObject];
362 //Receiving step 2: filtering callback
363 - (void)didFilterAttributedString:(NSAttributedString *)filteredMessage receivingContext:(AIContentObject *)inObject
365 [inObject setMessage:filteredMessage];
367 [self finishReceiveContentObject:inObject];
370 //Receiving step 3: Display the content
371 - (void)finishReceiveContentObject:(AIContentObject *)inObject
373 //Display the content
374 [self displayContentObject:inObject];
376 //Update our most recent chat
377 if([inObject trackContent]){
378 AIChat *chat = [inObject chat];
380 if(chat != mostRecentChat){
381 [mostRecentChat release];
382 mostRecentChat = [chat retain];
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
397 selector:@selector(didFilterAttributedString:contentSendingContext:)
401 [self finishSendContentObject:inObject];
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
420 selector:@selector(didFilterAttributedString:autoreplySendingContext:)
423 [self finishSendContentObject:inObject];
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];
441 //Notify: Will Send Content
442 if([inObject trackContent]){
443 [[adium notificationCenter] postNotificationName:Content_WillSendContent
445 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:inObject,@"Object",nil]];
449 if ([inObject sendContent]){
450 if([(AIAccount *)[inObject source] sendContentObject:inObject]){
451 if([inObject displayContent]){
453 [self displayContentObject:inObject];
456 if([inObject trackContent]){
458 [[adium contactAlertsController] generateEvent:CONTENT_MESSAGE_SENT
459 forListObject:[chat listObject]
460 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:chat,@"AIChat",inObject,@"AIContentObject",nil]
461 previouslyPerformedActionIDs:nil];
464 if(mostRecentChat != chat){
465 [mostRecentChat release];
466 mostRecentChat = [chat retain];
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
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){
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)
503 [self displayContentObject:inObject immediately:YES];
507 //Filter in the filter thread
508 [self filterAttributedString:[inObject message]
509 usingFilterType:AIFilterContent
510 direction:([inObject isOutgoing] ? AIFilterOutgoing : AIFilterIncoming)
511 filterContext:inObject
513 selector:@selector(didFilterAttributedString:contentFilterDisplayContext:)
518 [self displayContentObject:inObject immediately:immediately];
522 - (void)didFilterAttributedString:(NSAttributedString *)filteredString contentFilterDisplayContext:(AIContentObject *)inObject
524 [inObject setMessage:filteredString];
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);
547 //Set it after filtering in the main thread, then display it
548 [inObject setMessage:[self filterAttributedString:[inObject message]
549 usingFilterType:filterType
552 [self finishDisplayContentObject:inObject];
555 //Filter in the filtering thread
556 [self filterAttributedString:[inObject message]
557 usingFilterType:filterType
559 filterContext:inObject
561 selector:@selector(didFilterAttributedString:displayContext:)
566 [self finishDisplayContentObject:inObject];
571 - (void)didFilterAttributedString:(NSAttributedString *)filteredString displayContext:(AIContentObject *)inObject
573 [inObject setMessage:filteredString];
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
589 chatIsOpen = [chat isOpen];
590 contentReceived = (([inObject isMemberOfClass:[AIContentMessage class]]) &&
591 (![inObject isOutgoing]));
592 shouldPostContentReceivedEvents = contentReceived && [inObject trackContent];
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.
599 [[adium interfaceController] openChat:chat];
602 userInfo = [NSDictionary dictionaryWithObjectsAndKeys:chat, @"AIChat", inObject, @"AIContentObject", nil];
604 //Add this content to the chat
605 [chat addContentObject:inObject];
607 //Notify: Content Object Added
608 [[adium notificationCenter] postNotificationName:Content_ContentObjectAdded
612 if(shouldPostContentReceivedEvents){
613 NSSet *previouslyPerformedActionIDs = nil;
614 AIListObject *listObject = [chat listObject];
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
621 previouslyPerformedActionIDs:nil];
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
629 previouslyPerformedActionIDs:previouslyPerformedActionIDs];
632 [[adium contactAlertsController] generateEvent:CONTENT_MESSAGE_RECEIVED
633 forListObject:listObject
635 previouslyPerformedActionIDs:previouslyPerformedActionIDs];
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;
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]
656 message:attributedMessage
658 [attributedMessage release];
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
679 [chatObserverArray addObject:[NSValue valueWithNonretainedObject:inObserver]];
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;
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];
703 - (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys
705 //Post an attributes changed message
706 [[adium notificationCenter] postNotificationName:Chat_AttributesChanged
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];
718 while (chat = [enumerator nextObject]){
719 [self chatStatusChanged:chat modifiedStatusKeys:nil silent:NO];
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;
736 observer = [observerValue nonretainedObjectValue];
737 if(newKeys = [observer updateChat:inChat keys:modifiedKeys silent:silent]){
738 if (!attrChange) attrChange = [NSMutableSet set];
739 [attrChange unionSet:newKeys];
743 //Send out the notification for other observers
744 [[adium notificationCenter] postNotificationName:Chat_StatusChanged
746 userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys
747 forKey:@"Keys"] : nil)];
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
761 //Clear unviewed content
762 - (void)clearUnviewedContentOfChat:(AIChat *)inChat
764 [inChat setStatusObject:nil forKey:KEY_UNVIEWED_CONTENT notify:YES];
767 //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];
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;
785 AIListContact *targetContact = inContact;
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.
792 if ([inContact isKindOfClass:[AIMetaContact class]]){
793 targetContact = [[adium contactController] preferredContactForContentType:CONTENT_MESSAGE_TYPE
794 forListContact:inContact];
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.
802 targetContact = [(AIMetaContact *)inContact preferredContact];
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;
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]];
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];
834 account = [targetContact account];
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];
846 [openChats removeObject:chat];
847 AILog(@"chatWithContact: Immediately removed <<%@>> [%@]",chat,openChats);
856 * @brief Return a pre-existing chat with a contact.
858 * @result The chat, or nil if no chat with the contact exists
860 - (AIChat *)existingChatWithContact:(AIListContact *)inContact
862 NSEnumerator *enumerator;
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;
873 //Search for a chat with this AIListContact
874 enumerator = [openChats objectEnumerator];
875 while ((chat = [enumerator nextObject])) {
876 if ([chat listObject] == inContact) break;
883 - (AIChat *)chatWithName:(NSString *)inName onAccount:(AIAccount *)account chatCreationInfo:(NSDictionary *)chatCreationInfo
887 //Search for an existing chat we can use instead of creating a new one
888 chat = [self existingChatWithName:inName onAccount:account];
892 chat = [AIChat chatForAccount:account];
893 [chat setName:inName];
894 [openChats addObject:chat];
895 AILog(@"chatWithName:%@ onAccount:%@ added <<%@>> [%@]",inName,account,chat,openChats);
897 if (chatCreationInfo) [chat setStatusObject:chatCreationInfo
898 forKey:@"ChatCreationInfo"
901 [chat setStatusObject:[NSNumber numberWithBool:YES]
902 forKey:@"AlwaysShowUserList"
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];
909 [openChats removeObject:chat];
910 AILog(@"chatWithName: Immediately removed <<%@>> [%@]",chat,openChats);
917 - (void)openChat:(AIChat *)chat
920 [openChats addObject:chat];
921 AILog(@"openChat: Added <<%@>> [%@]",chat,openChats);
922 [[adium interfaceController] openChat:chat];
926 - (AIChat *)existingChatWithName:(NSString *)inName onAccount:(AIAccount *)account
928 NSEnumerator *enumerator;
931 enumerator = [openChats objectEnumerator];
933 while(chat = [enumerator nextObject]){
934 if(([chat account] == account) &&
935 ([[chat name] isEqualToString:inName])){
943 - (AIChat *)existingChatWithUniqueChatID:(NSString *)uniqueChatID
945 NSEnumerator *enumerator;
948 enumerator = [openChats objectEnumerator];
950 while(chat = [enumerator nextObject]){
951 if([[chat uniqueChatID] isEqualToString:uniqueChatID]){
960 - (BOOL)closeChat:(AIChat *)inChat
962 BOOL shouldRemove = YES;
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.
969 NSEnumerator *objectsBeingReceivedEnumerator = [objectsBeingReceived objectEnumerator];
970 AIContentObject *contentObject;
971 while(contentObject = [objectsBeingReceivedEnumerator nextObject]){
972 if([contentObject chat] == inChat){
978 if(mostRecentChat == inChat){
979 [mostRecentChat release];
980 mostRecentChat = nil;
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];
987 //Remove the chat's content (it retains the chat, so this must be done separately)
988 [inChat removeAllContent];
992 [openChats removeObject:inChat];
993 AILog(@"closeChat: Removed <<%@>> [%@]",inChat, openChats);
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
1007 //Close down the chat on account A
1008 [oldAccount closeChat:chat];
1010 //Set the account and the listObject
1012 [chat setAccount:newAccount];
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];
1021 //Open the chat on account B
1022 [newAccount openChat:chat];
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]);
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]
1037 UID:[inContact UID]];
1038 if (newContact != [chat listObject]){
1039 //Hang onto stuff until we're done
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];
1045 //Set to the new listContact and account as needed
1046 [chat setListObject:newContact];
1047 if (useContactAccount) [chat setAccount:newAccount];
1050 //Reopen the chat on the account
1051 [[chat account] openChat:chat];
1058 //Returns all chats with the object
1059 - (NSSet *)allChatsWithContact:(AIListContact *)inContact
1061 NSMutableSet *foundChats = nil;
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];
1071 enumerator = [[(AIMetaContact *)inContact listContacts] objectEnumerator];
1072 while(listContact = [enumerator nextObject]){
1073 NSSet *listContactChats;
1074 if (listContactChats = [self allChatsWithContact:listContact]){
1075 [foundChats unionSet:listContactChats];
1080 NSEnumerator *chatEnumerator = [openChats objectEnumerator];
1082 while((chat = [chatEnumerator nextObject])){
1084 [[[chat listObject] internalObjectID] isEqualToString:[inContact internalObjectID]]){
1085 if (!foundChats) foundChats = [NSMutableSet set];
1086 [foundChats addObject:chat];
1094 - (NSSet *)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;
1109 //Second choice: switch to the first chat we can find which has unviewed content
1110 NSEnumerator *enumerator = [openChats objectEnumerator];
1112 while ((chat = [enumerator nextObject]) && ![chat integerStatusObjectForKey:KEY_UNVIEWED_CONTENT]);
1114 if (chat) newActiveChat = chat;
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.
1125 - (BOOL)contactIsInGroupChat:(AIListContact *)listContact
1127 NSEnumerator *chatEnumerator = [openChats objectEnumerator];
1129 BOOL contactIsInGroupChat = NO;
1131 while((chat = [chatEnumerator nextObject])){
1133 [[chat participatingListObjects] containsObjectIdenticalTo:listContact]){
1135 contactIsInGroupChat = YES;
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];
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]
1160 UID:[inObject UID]];
1161 //Does the account know this object?
1162 if(contactForAccount){
1163 knowsObject = [account availableForSendingContentType:inType toContact:contactForAccount];
1166 //Could the account send this
1167 couldSendContent = [account availableForSendingContentType:inType toContact:nil];
1169 if((inPreferred && knowsObject) || (!inPreferred && !knowsObject && couldSendContent)){
1170 [sourceAccounts addObject:account];
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;
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);
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);
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;
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)";
1245 return(description);
1248 - (NSString *)longDescriptionForEventID:(NSString *)eventID forListObject:(AIListObject *)listObject
1250 NSString *description = nil;
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);
1269 name = ([listObject isKindOfClass:[AIListGroup class]] ?
1270 [NSString stringWithFormat:AILocalizedString(@"a member of %@",nil),[listObject displayName]] :
1271 [listObject displayName]);
1273 description = [NSString stringWithFormat:format, name];
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);
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;
1301 NSParameterAssert([userInfo isKindOfClass:[NSDictionary class]]);
1303 contentObject = [(NSDictionary *)userInfo objectForKey:@"AIContentObject"];
1304 messageText = [[[contentObject message] attributedStringByConvertingAttachmentsToStrings] string];
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"),
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"),
1328 description = messageText;
1331 return(description);
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)
1346 action:@selector(selectedEncryptionPreference:)
1349 [menuItem setTag:EncryptedChat_Never];
1350 [encryptionMenu addItem:menuItem];
1353 menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Encrypt chats as requested",nil)
1355 action:@selector(selectedEncryptionPreference:)
1358 [menuItem setTag:EncryptedChat_Manually];
1359 [encryptionMenu addItem:menuItem];
1362 menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Encrypt chats automatically",nil)
1364 action:@selector(selectedEncryptionPreference:)
1367 [menuItem setTag:EncryptedChat_Automatically];
1368 [encryptionMenu addItem:menuItem];
1371 menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Force encryption and refuse plaintext",nil)
1373 action:@selector(selectedEncryptionPreference:)
1376 [menuItem setTag:EncryptedChat_RejectUnencryptedMessages];
1377 [encryptionMenu addItem:menuItem];
1381 [encryptionMenu addItem:[NSMenuItem separatorItem]];
1383 NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Default",nil)
1385 action:@selector(selectedEncryptionPreference:)
1388 [menuItem setTag:EncryptedChat_Default];
1389 [encryptionMenu addItem:menuItem];
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];
1404 - (void)toggleIgnoreOfContact:(id)sender
1406 AIListObject *listObject = [[adium menuController] currentContextMenuObject];
1407 AIChat *chat = [[adium menuController] currentContextMenuChat];
1409 if ([listObject isKindOfClass:[AIListContact class]]) {
1410 BOOL isIgnored = [chat isListContactIgnored:(AIListContact *)listObject];
1411 [chat setListContact:(AIListContact *)listObject isIgnored:!isIgnored];
1415 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1417 if (menuItem == menuItem_ignore) {
1418 AIListObject *listObject = [[adium menuController] currentContextMenuObject];
1419 AIChat *chat = [[adium menuController] currentContextMenuChat];
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")];
1426 [menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
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.
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.
1450 if (![[[inContact account] UID] isEqualToString:[inContact UID]]) {
1451 [adiumChatEvents chat:chat addedListContact:inContact];
1453 [[adium contentController] displayStatusMessage:[NSString stringWithFormat:AILocalizedString(@"%@ joined the chat",nil),[inContact displayName]]
1454 ofType:@"contact_joined"
1459 //Always notify Adium that the list changed so it can be updated, caches can be modified, etc.
1460 [[adium notificationCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
1465 * @brief A chat removed a listContact from its participants list
1467 * @param chat The chat
1468 * @param inContact The contact
1470 - (void)chat:(AIChat *)chat removedListContact:(AIListContact *)inContact
1473 [adiumChatEvents chat:chat removedListContact:inContact];
1475 [[adium contentController] displayStatusMessage:[NSString stringWithFormat:AILocalizedString(@"%@ left the chat.",nil),[inContact displayName]]
1476 ofType:@"contact_left"
1480 [[adium notificationCenter] postNotificationName:Chat_ParticipatingListObjectsChanged