Add a Reopen Closed Tab menu item to the File menu that will restore the most recently closed tab, similar to Chrome. Fixes #12537
Does not work with MSN group chats (and probably other protocols that have unnamed MUCs).
r=wix
5 // Created by Evan Schoenberg on 6/10/05.
8 #import "AIChatController.h"
10 #import <Adium/AIContentControllerProtocol.h>
11 #import <Adium/AIContactControllerProtocol.h>
12 #import <Adium/AIInterfaceControllerProtocol.h>
13 #import <Adium/AIMenuControllerProtocol.h>
14 #import <Adium/AIStatusControllerProtocol.h>
15 #import "AdiumChatEvents.h"
16 #import <Adium/AIAccount.h>
17 #import <Adium/AIChat.h>
18 #import <Adium/AIContentObject.h>
19 #import <Adium/AIContentMessage.h>
20 #import <Adium/AIListContact.h>
21 #import <Adium/AIListBookmark.h>
22 #import <Adium/AIMetaContact.h>
23 #import <Adium/AIService.h>
24 #import <AIUtilities/AIArrayAdditions.h>
25 #import <AIUtilities/AIMenuAdditions.h>
27 #define SHOW_JOIN_LEAVE_TITLE AILocalizedString(@"Show Join/Leave Messages", nil)
29 @interface AIChatController ()
30 - (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
31 - (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys;
33 - (void)toggleIgnoreOfContact:(id)sender;
34 - (void)toggleShowJoinLeave:(id)sender;
35 - (void)didExchangeContent:(NSNotification *)notification;
37 - (void)adiumWillTerminate:(NSNotification *)inNotification;
41 * @class AIChatController
42 * @brief Core controller for chats
44 * This is the only class which should vend AIChat objects (via openChat... or chatWith:...).
45 * AIChat objects should never be created directly.
47 @implementation AIChatController
50 * @brief Initialize the controller
54 if ((self = [super init])) {
56 chatObserverArray = [[NSMutableArray alloc] init];
57 adiumChatEvents = [[AdiumChatEvents alloc] init];
60 openChats = [[NSMutableSet alloc] init];
67 * @brief Controller loaded
69 - (void)controllerDidLoad
71 //Observe content so we can update the most recent chat
72 [[NSNotificationCenter defaultCenter] addObserver:self
73 selector:@selector(didExchangeContent:)
74 name:CONTENT_MESSAGE_RECEIVED
77 [[NSNotificationCenter defaultCenter] addObserver:self
78 selector:@selector(didExchangeContent:)
79 name:CONTENT_MESSAGE_RECEIVED_GROUP
82 [[NSNotificationCenter defaultCenter] addObserver:self
83 selector:@selector(didExchangeContent:)
84 name:CONTENT_MESSAGE_SENT
87 [[NSNotificationCenter defaultCenter] addObserver:self
88 selector:@selector(adiumWillTerminate:)
89 name:AIAppWillTerminateNotification
92 //Ignore menu item for contacts in group chats
93 menuItem_ignore = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@""
95 action:@selector(toggleIgnoreOfContact:)
97 [adium.menuController addContextualMenuItem:menuItem_ignore toLocation:Context_Contact_GroupChat_ParticipantAction];
99 menuItem_joinLeave = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:SHOW_JOIN_LEAVE_TITLE
101 action:@selector(toggleShowJoinLeave:)
104 [adium.menuController addMenuItem:menuItem_joinLeave toLocation:LOC_Display_MessageControl];
105 [adium.menuController addContextualMenuItem:[[menuItem_joinLeave copy] autorelease] toLocation:Context_GroupChat_Action];
107 [adiumChatEvents controllerDidLoad];
112 * @brief Controller will close
114 - (void)controllerWillClose
120 * @brief Adium will terminate
122 * Post the Chat_WillClose for each open chat so any closing behavior can be performed
124 - (void)adiumWillTerminate:(NSNotification *)inNotification
126 //Every open chat is about to close. We perform the internal closing here rather than calling on the interface controller since the UI need not change.
127 while ([openChats count] > 0) {
128 [self closeChat:[openChats anyObject]];
137 [openChats release]; openChats = nil;
138 [chatObserverArray release]; chatObserverArray = nil;
139 [[NSNotificationCenter defaultCenter] removeObserver:self];
145 * @brief Register a chat observer
147 * Chat observers are notified when properties are changed on chats
149 * @param inObserver An observer, which must conform to AIChatObserver
151 - (void)registerChatObserver:(id <AIChatObserver>)inObserver
154 [chatObserverArray addObject:[NSValue valueWithNonretainedObject:inObserver]];
156 //Let the new observer process all existing chats
157 [self updateAllChatsForObserver:inObserver];
161 * @brief Unregister a chat observer
163 - (void)unregisterChatObserver:(id <AIChatObserver>)inObserver
165 [chatObserverArray removeObject:[NSValue valueWithNonretainedObject:inObserver]];
169 * @brief Chat status changed
171 * Called by AIChat after it changes one or more properties.
173 - (void)chatStatusChanged:(AIChat *)inChat modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
175 NSSet *modifiedAttributeKeys;
177 //Let all observers know the chat's status has changed before performing any further notifications
178 modifiedAttributeKeys = [self _informObserversOfChatStatusChange:inChat withKeys:inModifiedKeys silent:silent];
180 //Post an attributes changed message (if necessary)
181 if ([modifiedAttributeKeys count]) {
182 [self chatAttributesChanged:inChat modifiedKeys:modifiedAttributeKeys];
187 * @brief Chat attributes changed
189 * Called by -[AIChatController chatStatusChanged:modifiedStatusKeys:silent:] if any observers changed attributes
191 - (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys
193 //Post an attributes changed message
194 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_AttributesChanged
196 userInfo:(inModifiedKeys ? [NSDictionary dictionaryWithObject:inModifiedKeys
197 forKey:@"Keys"] : nil)];
201 * @brief Send each chat in turn to an observer with a nil modifiedStatusKeys argument
203 * This lets an observer use its normal update mechanism to update every chat in some manner
205 - (void)updateAllChatsForObserver:(id <AIChatObserver>)observer
207 for (AIChat *chat in openChats) {
208 [self chatStatusChanged:chat modifiedStatusKeys:nil silent:NO];
213 * @brief Notify observers of a status change. Returns the modified attribute keys
215 - (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
217 NSMutableSet *attrChange = nil;
218 NSValue *observerValue;
220 //Let our observers know
221 for (observerValue in chatObserverArray) {
222 id <AIChatObserver> observer;
225 observer = [observerValue nonretainedObjectValue];
226 if ((newKeys = [observer updateChat:inChat keys:modifiedKeys silent:silent])) {
227 if (!attrChange) attrChange = [NSMutableSet set];
228 [attrChange unionSet:newKeys];
232 //Send out the notification for other observers
233 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_StatusChanged
235 userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys
236 forKey:@"Keys"] : nil)];
241 //Chats -------------------------------------------------------------------------------------------------
244 * @brief Opens a chat for communication with the contact, creating if necessary.
246 * The interface controller will then be asked to open the UI for the new chat.
248 * @param inContact The AIListContact on which to open a chat. If an AIMetaContact, an appropriate contained contact will be selected.
249 * @param onPreferredAccount If YES, Adium will determine the account on which the chat should be opened. If NO, inContact.account will be used. Value is treated as YES for AIMetaContacts by the action of -[AIChatController chatWithContact:].
251 - (AIChat *)openChatWithContact:(AIListContact *)inContact onPreferredAccount:(BOOL)onPreferredAccount
253 if ([inContact isKindOfClass:[AIListBookmark class]])
254 return [(AIListBookmark *)inContact openChat];
256 if (onPreferredAccount) {
257 inContact = [adium.contactController preferredContactForContentType:CONTENT_MESSAGE_TYPE
258 forListContact:inContact];
261 AIChat *chat = [self chatWithContact:inContact];
262 if (chat) [adium.interfaceController openChat:chat];
268 * @brief Creates a chat for communication with the contact, but does not make the chat active
270 * No window or tab is opened for the chat.
271 * If a chat with this contact already exists, it is returned.
272 * If a chat with a contact within the same metaContact at this contact exists, it is switched to this contact
275 * The passed contact, if an AIListContact, will be used exactly -- that is, inContact.account is the account on which the chat will be opened.
276 * If the passed contact is an AIMetaContact, an appropriate contact/account pair will be automatically selected by this method.
278 * @param inContact The contact with which to open a chat. See description above.
280 - (AIChat *)chatWithContact:(AIListContact *)inContact
282 AIListContact *targetContact = inContact;
286 If we're dealing with a meta contact, open a chat with the preferred contact for this meta contact
287 It's a good idea for the caller to pick the preferred contact for us, since they know the content type
288 being sent and more information - but we'll do it here as well just to be safe.
290 if ([inContact isKindOfClass:[AIMetaContact class]]) {
291 targetContact = [adium.contactController preferredContactForContentType:CONTENT_MESSAGE_TYPE
292 forListContact:inContact];
295 If we have no accounts online, preferredContactForContentType:forListContact will return nil.
296 We'd rather open up the chat window on a useless contact than do nothing, so just pick the
297 preferredContact from the metaContact.
299 if (!targetContact) {
300 targetContact = [(AIMetaContact *)inContact preferredContact];
304 //If we can't get a contact, we're not going to be able to get a chat... return nil
305 if (!targetContact) {
306 AILog(@"Warning: -[AIChatController chatWithContact:%@] got a nil targetContact.",inContact);
307 NSLog(@"Warning: -[AIChatController chatWithContact:%@] got a nil targetContact.",inContact);
311 //Search for an existing chat we can switch instead of replacing
312 for (chat in openChats) {
313 //If a chat for this object already exists
314 if ([chat.uniqueChatID isEqualToString:targetContact.internalObjectID]) {
315 if (!(chat.listObject == targetContact)) {
316 [self switchChat:chat toAccount:targetContact.account];
322 //If this object is within a meta contact, and a chat for an object in that meta contact already exists
323 if (chat.listObject.parentContact == targetContact.parentContact) {
325 //Switch the chat to be on this contact (and its account) now
326 [self switchChat:chat toListContact:targetContact usingContactAccount:YES];
333 AIAccount *account = targetContact.account;
336 chat = [AIChat chatForAccount:account];
337 [chat addParticipatingListObject:targetContact notify:YES];
338 [openChats addObject:chat];
339 AILog(@"chatWithContact: Added <<%@>> [%@]",chat,openChats);
341 //Inform the account of its creation
342 if (![targetContact.account openChat:chat]) {
343 [openChats removeObject:chat];
344 AILog(@"chatWithContact: Immediately removed <<%@>> [%@]",chat,openChats);
353 * @brief Return a pre-existing chat with a contact.
355 * @result The chat, or nil if no chat with the contact exists
357 - (AIChat *)existingChatWithContact:(AIListContact *)inContact
361 if ([inContact isKindOfClass:[AIMetaContact class]]) {
362 //Search for a chat with any contact within this AIMetaContact
363 for (chat in openChats) {
364 if (!chat.isGroupChat &&
365 [[(AIMetaContact *)inContact containedObjects] containsObjectIdenticalTo:chat.listObject]) break;
369 //Search for a chat with this AIListContact
370 for (chat in openChats) {
371 if (!chat.isGroupChat &&
372 chat.listObject == inContact) break;
380 * @brief Open a group chat
382 * @param inName The name of the chat; in general, the chat room name
383 * @param account The account on which to create the group chat
384 * @param chatCreationInfo A dictionary of information which may be used by the account when joining the chat serverside
385 * @brief opens a chat with the above parameters. Assigns chatroom info to the created AIChat object.
387 - (AIChat *)chatWithName:(NSString *)name identifier:(id)identifier onAccount:(AIAccount *)account chatCreationInfo:(NSDictionary *)chatCreationInfo
391 name = [account.service normalizeChatName:name];
394 chat = [self existingChatWithIdentifier:identifier onAccount:account];
397 //See if a chat was made with this name but which doesn't yet have an identifier. If so, take ownership!
398 chat = [self existingChatWithName:name onAccount:account];
400 if (chat && ![chat identifier])
401 [chat setIdentifier:identifier];
402 // If existingChatWithName:onAccount: finds a chat, make sure it has the right identifier.
403 else if ([chat identifier] != identifier)
408 //If the caller doesn't care about the identifier, do a search based on name to avoid creating a new chat incorrectly
409 chat = [self existingChatWithName:name onAccount:account];
412 AILog(@"chatWithName %@ identifier %@ existing --> %@", name, identifier, chat);
415 chat = [AIChat chatForAccount:account];
417 chat.name = [account.service normalizeChatName:name];
418 chat.displayName = name;
419 chat.identifier = identifier;
420 chat.isGroupChat = YES;
421 chat.chatCreationDictionary = chatCreationInfo;
422 /* Negative preference so (default == NO) -> showing join/leave messages */
423 chat.showJoinLeave = ![[[adium preferenceController] preferenceForKey:[NSString stringWithFormat:@"HideJoinLeave-%@", name]
424 group:PREF_GROUP_STATUS_PREFERENCES] boolValue];
425 [openChats addObject:chat];
427 AILog(@"chatWithName:%@ identifier:%@ onAccount:%@ added <<%@>> [%@] [%@]",name,identifier,account,chat,openChats,chatCreationInfo);
429 //Inform the account of its creation
430 if (![account openChat:chat]) {
431 [openChats removeObject:chat];
432 AILog(@"chatWithName: Immediately removed <<%@>> [%@]",chat,openChats);
437 AILog(@"chatWithName %@ created --> %@",name,chat);
442 * @brief Find an existing group chat
444 * @result The group AIChat, or nil if no such chat exists
446 - (AIChat *)existingChatWithName:(NSString *)name onAccount:(AIAccount *)account
450 name = [account.service normalizeChatName:name];
452 for (chat in openChats) {
453 if ((chat.account == account) &&
454 ([chat.name isEqualToString:name])) {
463 * @brief Find an existing group chat
465 * @result The group AIChat, or nil if no such chat exists
467 - (AIChat *)existingChatWithIdentifier:(id)identifier onAccount:(AIAccount *)account
472 for (chat in openChats) {
473 if ((chat.account == account) &&
474 ([[chat identifier] isEqual:identifier])) {
483 * @brief Find an existing chat by unique chat ID
485 * @result The AIChat, or nil if no such chat exists
487 - (AIChat *)existingChatWithUniqueChatID:(NSString *)uniqueChatID
492 for (chat in openChats) {
493 if ([chat.uniqueChatID isEqualToString:uniqueChatID]) {
502 * @brief Close a chat
504 * This should be called only by the interface controller. To close a chat programatically, use the interface controller's closeChat:.
506 * @result YES the chat was removed succesfully; NO if it was not
508 - (BOOL)closeChat:(AIChat *)inChat
512 /* If we are currently passing a content object for this chat through our content filters, don't remove it from
513 * our openChats set as it will become needed soon. If we were to remove it, and a second message came in which was
514 * also before the first message is done filtering, we would otherwise mistakenly think we needed to create a new
515 * chat, generating a duplicate.
517 shouldRemove = ![adium.contentController chatIsReceivingContent:inChat];
521 if (mostRecentChat == inChat) {
522 [mostRecentChat release];
523 mostRecentChat = nil;
526 //Send out the Chat_WillClose notification
527 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_WillClose object:inChat userInfo:nil];
531 /* If we didn't remove the chat because we're waiting for it to reopen, don't cause the account
532 * to close down the chat.
534 [inChat.account closeChat:inChat];
535 [openChats removeObject:inChat];
536 AILog(@"closeChat: Removed <<%@>> [%@]",inChat, openChats);
538 AILog(@"closeChat: Did not remove <<%@>> [%@]",inChat, openChats);
541 [inChat setIsOpen:NO];
547 - (void)restoreChat:(AIChat *)inChat
549 [openChats addObject:inChat];
553 * @brief Called by an account to notifiy the chat controller that it left a chat
555 * Typically this is called in response to -[AIAccout closeChat:] caled in -[self closeChat:] above.
556 * However, if the chat is never opened, accountDidCloseChat: may be called without closeChat: being called first.
558 - (void)accountDidCloseChat:(AIChat *)inChat
560 /* If the chat is not open and the account told us that it was closed,
561 * ensure that it's no longer in the open chats list, as the user will have no further
562 * interaction with it. This is poarticularly important if the chat closes before it is
563 * ever opened, such as when an error occurs while joining a group chat.
565 if (![inChat isOpen])
566 [openChats removeObject:inChat];
570 * @brief Switch a chat from one account to another
572 * The target list contact for the chat is changed to be an 'identical' one on the target account; that is, a contact
573 * with the same UID but an account and service appropriate for newAccount.
575 - (void)switchChat:(AIChat *)chat toAccount:(AIAccount *)newAccount
577 AIAccount *oldAccount = chat.account;
578 if (newAccount != oldAccount) {
579 //Hang onto stuff until we're done
582 //Close down the chat on account A
583 [oldAccount closeChat:chat];
585 //Set the account and the listObject
587 [chat setAccount:newAccount];
589 //We want to keep the same destination for the chat but switch it to a listContact on the desired account.
590 AIListContact *newContact = [adium.contactController contactWithService:newAccount.service
592 UID:chat.listObject.UID];
593 [chat setListObject:newContact];
596 //Open the chat on account B
597 [newAccount openChat:chat];
605 * @brief Switch the list contact of a chat
607 * @param chat The chat
608 * @param inContact The contact with which the chat will now take place
609 * @param useContactAccount If YES, the chat is also set to inContact.account as its account. If NO, the account and service of chat are unchanged.
611 - (void)switchChat:(AIChat *)chat toListContact:(AIListContact *)inContact usingContactAccount:(BOOL)useContactAccount
613 AIAccount *newAccount = (useContactAccount ? inContact.account : chat.account);
615 //Switch the inContact over to a contact on the new account so we send messages to the right place.
616 AIListContact *newContact = [adium.contactController contactWithService:newAccount.service
619 if (newContact != chat.listObject) {
620 //Hang onto stuff until we're done
623 //Close down the chat on the account, as the account may need to perform actions such as closing a connection
624 [chat.account closeChat:chat];
626 //Set to the new listContact and account as needed
627 [chat setListObject:newContact];
628 if (useContactAccount || ![inContact.service.serviceClass isEqualToString:chat.account.service.serviceClass])
629 [chat setAccount:newAccount];
631 //Reopen the chat on the account
632 [chat.account openChat:chat];
640 * @brief Find all open chats with a contact
642 * @param inContact The contact. If inContact is an AIMetaContact, all chats with all contacts within the metaContact will be returned.
643 * @result An NSSet with all chats with the contact. In general, will contain 0 or 1 AIChat objects, though it may contain more.
645 - (NSSet *)allChatsWithContact:(AIListContact *)inContact
647 NSMutableSet *foundChats = [NSMutableSet set];
649 //Scan the objects participating in each chat, looking for the requested object
650 if ([inContact isKindOfClass:[AIMetaContact class]]) {
651 if ([openChats count]) {
652 for (AIListContact *listContact in ((AIMetaContact *)inContact).uniqueContainedObjects) {
653 [foundChats unionSet:[self allChatsWithContact:listContact]];
658 for (AIChat *chat in openChats) {
659 if (!chat.isGroupChat &&
660 [chat.listObject.internalObjectID isEqualToString:inContact.internalObjectID] &&
662 [foundChats addObject:chat];
671 * @brief Find all open chats with a contact
673 * @param inContact The contact. If inContact is an AIMetaContact, all chats with all contacts within the metaContact will be returned.
674 * @result An NSSet with all chats with the contact.
676 - (NSSet *)allGroupChatsContainingContact:(AIListContact *)inContact
678 NSMutableSet *groupChats = [NSMutableSet set];
680 //Search for a chat containing this AIListContact
681 if ([inContact isKindOfClass:[AIMetaContact class]]) {
682 //Search for a chat with any contact within this AIMetaContact
683 for (AIChat *chat in openChats) {
684 if (!chat.isGroupChat)
687 for (AIListContact *contact in (AIMetaContact *)inContact) {
688 if([chat containsObject:contact]) {
689 [groupChats addObject:chat];
696 //Search for a chat with this AIListContact
697 for (AIChat *chat in openChats) {
698 if (chat.isGroupChat && [chat containsObject:inContact]) {
699 [groupChats addObject:chat];
708 * @brief All open chats
710 * Open chats from the chatController may include chats which are not currently displayed by the interface.
714 return [[openChats copy] autorelease];
718 * @brief Find the chat which most recently received content which has not yet been seen
720 * @result An AIChat with unviewed content, or nil if no chats current have unviewed content
722 - (AIChat *)mostRecentUnviewedChat
724 BOOL onlyMentions = [[adium.preferenceController preferenceForKey:KEY_STATUS_MENTION_COUNT
725 group:PREF_GROUP_STATUS_PREFERENCES] boolValue];
727 if (mostRecentChat && mostRecentChat.unviewedContentCount && (!mostRecentChat.isGroupChat || !onlyMentions || mostRecentChat.unviewedMentionCount)) {
728 //First choice: switch to the chat which received chat most recently if it has unviewed content
729 return mostRecentChat;
732 //Second choice: switch to the first chat we can find which has unviewed content
733 for (AIChat *chat in openChats) {
734 if (chat.unviewedContentCount && (!chat.isGroupChat || !onlyMentions || chat.unviewedMentionCount))
743 * @brief Gets the total number of unviewed messages
745 * @result The number of unviewed messages
747 - (NSUInteger)unviewedContentCount
749 NSUInteger count = 0;
751 for (AIChat *chat in openChats) {
752 if (chat.isGroupChat &&
753 [[adium.preferenceController preferenceForKey:KEY_STATUS_MENTION_COUNT
754 group:PREF_GROUP_STATUS_PREFERENCES] boolValue]) {
755 count += [chat unviewedMentionCount];
757 count += [chat unviewedContentCount];
764 * @brief Gets the total number of conversations with unviewed messages
766 * @result The number of conversations with unviewed messages
768 - (NSUInteger)unviewedConversationCount
770 NSUInteger count = 0;
772 for (AIChat *chat in openChats) {
773 if (chat.isGroupChat &&
774 [[adium.preferenceController preferenceForKey:KEY_STATUS_MENTION_COUNT
775 group:PREF_GROUP_STATUS_PREFERENCES] boolValue]) {
776 if (chat.unviewedMentionCount) {
779 } else if (chat.unviewedContentCount) {
787 * @brief Is the passed contact in a group chat?
789 * @result YES if the contact is in an open group chat; NO if not.
791 - (BOOL)contactIsInGroupChat:(AIListContact *)listContact
793 BOOL contactIsInGroupChat = NO;
795 for (AIChat *chat in openChats) {
796 if (chat.isGroupChat &&
797 [chat containsObject:listContact]) {
799 contactIsInGroupChat = YES;
804 return contactIsInGroupChat;
808 * @brief Called when content is sent or received
810 * Update the most recent chat
812 - (void)didExchangeContent:(NSNotification *)notification
814 AIContentObject *contentObject = [[notification userInfo] objectForKey:@"AIContentObject"];
816 //Update our most recent chat
817 if (contentObject.trackContent) {
818 AIChat *chat = contentObject.chat;
820 if (chat != mostRecentChat) {
821 [mostRecentChat release];
822 mostRecentChat = [chat retain];
827 #pragma mark Menu Items
829 * @brief Toggle ignoring of a contact
831 * Must be called from the contextual menu for the contact within a chat
833 - (void)toggleIgnoreOfContact:(id)sender
835 AIListObject *listObject = adium.menuController.currentContextMenuObject;
836 AIChat *chat = [adium.menuController currentContextMenuChat];
838 if ([listObject isKindOfClass:[AIListContact class]]) {
839 BOOL isIgnored = [chat isListContactIgnored:(AIListContact *)listObject];
840 [chat setListContact:(AIListContact *)listObject isIgnored:!isIgnored];
845 * @brief Toggle displaying of show/part messages for a chat
847 * Effects the currently active chat.
849 - (void)toggleShowJoinLeave:(id)sender
853 if (sender == menuItem_joinLeave) {
854 chat = adium.interfaceController.activeChat;
856 chat = adium.menuController.currentContextMenuChat;
859 chat.showJoinLeave = !chat.showJoinLeave;
861 [[adium preferenceController] setPreference:[NSNumber numberWithBool:!chat.showJoinLeave]
862 forKey:[NSString stringWithFormat:@"HideJoinLeave-%@", chat.name]
863 group:PREF_GROUP_STATUS_PREFERENCES];
867 * @brief Menu item validation
869 * When asked to validate our ignore menu item, set its title to ignore/un-ignore as appropriate for the contact
871 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
873 if (menuItem == menuItem_ignore) {
874 AIListObject *listObject = adium.menuController.currentContextMenuObject;
875 AIChat *chat = [adium.menuController currentContextMenuChat];
877 if ([listObject isKindOfClass:[AIListContact class]]) {
878 if ([chat isListContactIgnored:(AIListContact *)listObject]) {
879 [menuItem setTitle:AILocalizedString(@"Un-ignore","Un-ignore means begin receiving messages from this contact again in a chat")];
882 [menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
885 [menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
888 } else if ([menuItem.title isEqualToString:SHOW_JOIN_LEAVE_TITLE]) {
889 // We're using multiple menu items for the same goal, and WKMV makes a copy of the contextual ones.
890 // Validate based on the title.
892 if (menuItem == menuItem_joinLeave) {
893 chat = adium.interfaceController.activeChat;
895 chat = adium.menuController.currentContextMenuChat;
898 if (chat.isGroupChat) {
899 [menuItem setState:chat.showJoinLeave];
909 #pragma mark Chat contact addition and removal
912 * @brief A chat added a listContact to its participatants list
914 * @param chat The chat
915 * @param inContact The contact
916 * @param notify If YES, trigger the contact joined event if this is a group chat. Ignored if this is not a group chat.
918 - (void)chat:(AIChat *)chat addedListContacts:(NSArray *)inObjects notify:(BOOL)notify
920 if (notify && chat.isGroupChat) {
921 /* Prevent triggering of the event when we are informed that the chat's own account entered the chat
922 * If the UID of a contact in a chat differs from a normal UID, such as is the case with Jabber where a chat
923 * contact has the form "roomname@conferenceserver/handle" this will fail, but it's better than nothing.
925 for (AIListContact *inContact in inObjects) {
926 if (![inContact.account.UID isEqualToString:inContact.UID]) {
927 [adiumChatEvents chat:chat addedListContact:inContact];
932 //Always notify Adium that the list changed so it can be updated, caches can be modified, etc.
933 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
938 * @brief A chat removed a listContact from its participants list
940 * @param chat The chat
941 * @param inContact The contact
943 - (void)chat:(AIChat *)chat removedListContact:(AIListContact *)inContact
945 if (chat.isGroupChat) {
946 [adiumChatEvents chat:chat removedListContact:inContact];
949 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
953 - (NSString *)defaultInvitationMessageForRoom:(NSString *)room account:(AIAccount *)inAccount
955 return [NSString stringWithFormat:AILocalizedString(@"%@ invites you to join the chat \"%@\"", nil), inAccount.formattedUID, room];
961 * These strings were used previously; we may want them again. Keeping the translations around for now.
962 AILocalizedString("%@ joined the chat", nil);
963 AILocalizedString("%@ left the chat", nil);