William Bowling is sharing code with you
Bitbucket is a code hosting site. Unlimited public and private repositories. Free for small teams.
Don't show this again1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 | //
// AIChatController.m
// Adium
//
// Created by Evan Schoenberg on 6/10/05.
//
#import "AIChatController.h"
#import <Adium/AIContentControllerProtocol.h>
#import <Adium/AIContactControllerProtocol.h>
#import <Adium/AIInterfaceControllerProtocol.h>
#import <Adium/AIMenuControllerProtocol.h>
#import <Adium/AIStatusControllerProtocol.h>
#import "AdiumChatEvents.h"
#import <Adium/AIAccount.h>
#import <Adium/AIChat.h>
#import <Adium/AIContentObject.h>
#import <Adium/AIContentMessage.h>
#import <Adium/AIListContact.h>
#import <Adium/AIListBookmark.h>
#import <Adium/AIMetaContact.h>
#import <Adium/AIService.h>
#import <AIUtilities/AIArrayAdditions.h>
#import <AIUtilities/AIMenuAdditions.h>
#define SHOW_JOIN_LEAVE_TITLE AILocalizedString(@"Show Join/Leave Messages", nil)
@interface AIChatController ()
- (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
- (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys;
@end
/*!
* @class AIChatController
* @brief Core controller for chats
*
* This is the only class which should vend AIChat objects (via openChat... or chatWith:...).
* AIChat objects should never be created directly.
*/
@implementation AIChatController
/*!
* @brief Initialize the controller
*/
- (id)init
{
if ((self = [super init])) {
mostRecentChat = nil;
chatObserverArray = [[NSMutableArray alloc] init];
adiumChatEvents = [[AdiumChatEvents alloc] init];
//Chat tracking
openChats = [[NSMutableSet alloc] init];
}
return self;
}
/*!
* @brief Controller loaded
*/
- (void)controllerDidLoad
{
//Observe content so we can update the most recent chat
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didExchangeContent:)
name:CONTENT_MESSAGE_RECEIVED
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didExchangeContent:)
name:CONTENT_MESSAGE_RECEIVED_GROUP
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didExchangeContent:)
name:CONTENT_MESSAGE_SENT
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didExchangeContent:)
name:CONTENT_MESSAGE_SENT_GROUP
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(adiumWillTerminate:)
name:AIAppWillTerminateNotification
object:nil];
//Ignore menu item for contacts in group chats
menuItem_ignore = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@""
target:self
action:@selector(toggleIgnoreOfContact:)
keyEquivalent:@""];
[adium.menuController addContextualMenuItem:menuItem_ignore toLocation:Context_Contact_GroupChat_ParticipantAction];
menuItem_joinLeave = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:SHOW_JOIN_LEAVE_TITLE
target:self
action:@selector(toggleShowJoinLeave:)
keyEquivalent:@""];
[adium.menuController addMenuItem:menuItem_joinLeave toLocation:LOC_Display_MessageControl];
[adium.menuController addContextualMenuItem:[[menuItem_joinLeave copy] autorelease] toLocation:Context_GroupChat_Action];
[adiumChatEvents controllerDidLoad];
}
/*!
* @brief Controller will close
*/
- (void)controllerWillClose
{
}
/*!
* @brief Adium will terminate
*
* Post the Chat_WillClose for each open chat so any closing behavior can be performed
*/
- (void)adiumWillTerminate:(NSNotification *)inNotification
{
//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.
while ([openChats count] > 0) {
[self closeChat:[openChats anyObject]];
}
}
/*!
* @brief Deallocate
*/
- (void)dealloc
{
[openChats release]; openChats = nil;
[chatObserverArray release]; chatObserverArray = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
[super dealloc];
}
/*!
* @brief Register a chat observer
*
* Chat observers are notified when properties are changed on chats
*
* @param inObserver An observer, which must conform to AIChatObserver
*/
- (void)registerChatObserver:(id <AIChatObserver>)inObserver
{
//Add the observer
[chatObserverArray addObject:[NSValue valueWithNonretainedObject:inObserver]];
//Let the new observer process all existing chats
[self updateAllChatsForObserver:inObserver];
}
/*!
* @brief Unregister a chat observer
*/
- (void)unregisterChatObserver:(id <AIChatObserver>)inObserver
{
[chatObserverArray removeObject:[NSValue valueWithNonretainedObject:inObserver]];
}
/*!
* @brief Chat status changed
*
* Called by AIChat after it changes one or more properties.
*/
- (void)chatStatusChanged:(AIChat *)inChat modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
{
NSSet *modifiedAttributeKeys;
//Let all observers know the chat's status has changed before performing any further notifications
modifiedAttributeKeys = [self _informObserversOfChatStatusChange:inChat withKeys:inModifiedKeys silent:silent];
//Post an attributes changed message (if necessary)
if ([modifiedAttributeKeys count]) {
[self chatAttributesChanged:inChat modifiedKeys:modifiedAttributeKeys];
}
}
/*!
* @brief Chat attributes changed
*
* Called by -[AIChatController chatStatusChanged:modifiedStatusKeys:silent:] if any observers changed attributes
*/
- (void)chatAttributesChanged:(AIChat *)inChat modifiedKeys:(NSSet *)inModifiedKeys
{
//Post an attributes changed message
[[NSNotificationCenter defaultCenter] postNotificationName:Chat_AttributesChanged
object:inChat
userInfo:(inModifiedKeys ? [NSDictionary dictionaryWithObject:inModifiedKeys
forKey:@"Keys"] : nil)];
}
/*!
* @brief Send each chat in turn to an observer with a nil modifiedStatusKeys argument
*
* This lets an observer use its normal update mechanism to update every chat in some manner
*/
- (void)updateAllChatsForObserver:(id <AIChatObserver>)observer
{
for (AIChat *chat in openChats) {
[self chatStatusChanged:chat modifiedStatusKeys:nil silent:NO];
}
}
/*!
* @brief Notify observers of a status change. Returns the modified attribute keys
*/
- (NSSet *)_informObserversOfChatStatusChange:(AIChat *)inChat withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
{
NSMutableSet *attrChange = nil;
NSValue *observerValue;
//Let our observers know
for (observerValue in chatObserverArray) {
id <AIChatObserver> observer;
NSSet *newKeys;
observer = [observerValue nonretainedObjectValue];
if ((newKeys = [observer updateChat:inChat keys:modifiedKeys silent:silent])) {
if (!attrChange) attrChange = [NSMutableSet set];
[attrChange unionSet:newKeys];
}
}
//Send out the notification for other observers
[[NSNotificationCenter defaultCenter] postNotificationName:Chat_StatusChanged
object:inChat
userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys
forKey:@"Keys"] : nil)];
return attrChange;
}
//Chats -------------------------------------------------------------------------------------------------
#pragma mark Chats
/*!
* @brief Opens a chat for communication with the contact, creating if necessary.
*
* The interface controller will then be asked to open the UI for the new chat.
*
* @param inContact The AIListContact on which to open a chat. If an AIMetaContact, an appropriate contained contact will be selected.
* @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:].
*/
- (AIChat *)openChatWithContact:(AIListContact *)inContact onPreferredAccount:(BOOL)onPreferredAccount
{
if ([inContact isKindOfClass:[AIListBookmark class]])
return [(AIListBookmark *)inContact openChat];
if (onPreferredAccount) {
inContact = [adium.contactController preferredContactForContentType:CONTENT_MESSAGE_TYPE
forListContact:inContact];
}
AIChat *chat = [self chatWithContact:inContact];
if (chat) [adium.interfaceController openChat:chat];
return chat;
}
/*!
* @brief Creates a chat for communication with the contact, but does not make the chat active
*
* No window or tab is opened for the chat.
* If a chat with this contact already exists, it is returned.
* If a chat with a contact within the same metaContact at this contact exists, it is switched to this contact
* and then returned.
*
* The passed contact, if an AIListContact, will be used exactly -- that is, inContact.account is the account on which the chat will be opened.
* If the passed contact is an AIMetaContact, an appropriate contact/account pair will be automatically selected by this method.
*
* @param inContact The contact with which to open a chat. See description above.
*/
- (AIChat *)chatWithContact:(AIListContact *)inContact
{
AIListContact *targetContact = inContact;
AIChat *chat = nil;
/*
If we're dealing with a meta contact, open a chat with the preferred contact for this meta contact
It's a good idea for the caller to pick the preferred contact for us, since they know the content type
being sent and more information - but we'll do it here as well just to be safe.
*/
if ([inContact isKindOfClass:[AIMetaContact class]]) {
targetContact = [adium.contactController preferredContactForContentType:CONTENT_MESSAGE_TYPE
forListContact:inContact];
/*
If we have no accounts online, preferredContactForContentType:forListContact will return nil.
We'd rather open up the chat window on a useless contact than do nothing, so just pick the
preferredContact from the metaContact.
*/
if (!targetContact) {
targetContact = [(AIMetaContact *)inContact preferredContact];
}
}
//If we can't get a contact, we're not going to be able to get a chat... return nil
if (!targetContact) {
AILog(@"Warning: -[AIChatController chatWithContact:%@] got a nil targetContact.",inContact);
NSLog(@"Warning: -[AIChatController chatWithContact:%@] got a nil targetContact.",inContact);
return nil;
}
//Search for an existing chat we can switch instead of replacing
for (chat in openChats) {
//If a chat for this object already exists
if ([chat.uniqueChatID isEqualToString:targetContact.internalObjectID]) {
if (!(chat.listObject == targetContact)) {
[self switchChat:chat toAccount:targetContact.account];
}
break;
}
//If this object is within a meta contact, and a chat for an object in that meta contact already exists
if (chat.listObject.parentContact == targetContact.parentContact) {
//Switch the chat to be on this contact (and its account) now
[self switchChat:chat toListContact:targetContact usingContactAccount:YES];
break;
}
}
if (!chat) {
AIAccount *account = targetContact.account;
//Create a new chat
chat = [AIChat chatForAccount:account];
[chat addParticipatingListObject:targetContact notify:YES];
[openChats addObject:chat];
AILog(@"chatWithContact: Added <<%@>> [%@]",chat,openChats);
//Inform the account of its creation
if (![targetContact.account openChat:chat]) {
[openChats removeObject:chat];
AILog(@"chatWithContact: Immediately removed <<%@>> [%@]",chat,openChats);
chat = nil;
}
}
return chat;
}
/*!
* @brief Return a pre-existing chat with a contact.
*
* @result The chat, or nil if no chat with the contact exists
*/
- (AIChat *)existingChatWithContact:(AIListContact *)inContact
{
AIChat *chat = nil;
if ([inContact isKindOfClass:[AIMetaContact class]]) {
//Search for a chat with any contact within this AIMetaContact
for (chat in openChats) {
if (!chat.isGroupChat &&
[[(AIMetaContact *)inContact containedObjects] containsObjectIdenticalTo:chat.listObject]) break;
}
} else {
//Search for a chat with this AIListContact
for (chat in openChats) {
if (!chat.isGroupChat &&
chat.listObject == inContact) break;
}
}
return chat;
}
/*!
* @brief Open a group chat
*
* @param inName The name of the chat; in general, the chat room name
* @param account The account on which to create the group chat
* @param chatCreationInfo A dictionary of information which may be used by the account when joining the chat serverside
* @brief opens a chat with the above parameters. Assigns chatroom info to the created AIChat object.
*/
- (AIChat *)chatWithName:(NSString *)name identifier:(id)identifier onAccount:(AIAccount *)account chatCreationInfo:(NSDictionary *)chatCreationInfo
{
AIChat *chat = nil;
name = [account.service normalizeChatName:name];
if (identifier) {
chat = [self existingChatWithIdentifier:identifier onAccount:account];
if (!chat) {
//See if a chat was made with this name but which doesn't yet have an identifier. If so, take ownership!
chat = [self existingChatWithName:name onAccount:account];
if (chat && ![chat identifier])
[chat setIdentifier:identifier];
// If existingChatWithName:onAccount: finds a chat, make sure it has the right identifier.
else if ([chat identifier] != identifier)
chat = nil;
}
} else {
//If the caller doesn't care about the identifier, do a search based on name to avoid creating a new chat incorrectly
chat = [self existingChatWithName:name onAccount:account];
}
AILog(@"chatWithName %@ identifier %@ existing --> %@", name, identifier, chat);
if (!chat) {
//Create a new chat
chat = [AIChat chatForAccount:account];
chat.name = [account.service normalizeChatName:name];
chat.displayName = name;
chat.identifier = identifier;
chat.isGroupChat = YES;
chat.chatCreationDictionary = chatCreationInfo;
[openChats addObject:chat];
AILog(@"chatWithName:%@ identifier:%@ onAccount:%@ added <<%@>> [%@] [%@]",name,identifier,account,chat,openChats,chatCreationInfo);
//Inform the account of its creation
if (![account openChat:chat]) {
[openChats removeObject:chat];
AILog(@"chatWithName: Immediately removed <<%@>> [%@]",chat,openChats);
chat = nil;
}
}
AILog(@"chatWithName %@ created --> %@",name,chat);
return chat;
}
/*!
* @brief Find an existing group chat
*
* @result The group AIChat, or nil if no such chat exists
*/
- (AIChat *)existingChatWithName:(NSString *)name onAccount:(AIAccount *)account
{
AIChat *chat = nil;
name = [account.service normalizeChatName:name];
for (chat in openChats) {
if ((chat.account == account) &&
([chat.name isEqualToString:name])) {
break;
}
}
return chat;
}
/*!
* @brief Find an existing group chat
*
* @result The group AIChat, or nil if no such chat exists
*/
- (AIChat *)existingChatWithIdentifier:(id)identifier onAccount:(AIAccount *)account
{
AIChat *chat = nil;
for (chat in openChats) {
if ((chat.account == account) &&
([[chat identifier] isEqual:identifier])) {
break;
}
}
return chat;
}
/*!
* @brief Find an existing chat by unique chat ID
*
* @result The AIChat, or nil if no such chat exists
*/
- (AIChat *)existingChatWithUniqueChatID:(NSString *)uniqueChatID
{
AIChat *chat = nil;
for (chat in openChats) {
if ([chat.uniqueChatID isEqualToString:uniqueChatID]) {
break;
}
}
return chat;
}
/*!
* @brief Close a chat
*
* This should be called only by the interface controller. To close a chat programatically, use the interface controller's closeChat:.
*
* @result YES the chat was removed succesfully; NO if it was not
*/
- (BOOL)closeChat:(AIChat *)inChat
{
BOOL shouldRemove;
/* If we are currently passing a content object for this chat through our content filters, don't remove it from
* our openChats set as it will become needed soon. If we were to remove it, and a second message came in which was
* also before the first message is done filtering, we would otherwise mistakenly think we needed to create a new
* chat, generating a duplicate.
*/
shouldRemove = ![adium.contentController chatIsReceivingContent:inChat];
[inChat retain];
if (mostRecentChat == inChat) {
[mostRecentChat release];
mostRecentChat = nil;
}
//Send out the Chat_WillClose notification
[[NSNotificationCenter defaultCenter] postNotificationName:Chat_WillClose object:inChat userInfo:nil];
//Remove the chat
if (shouldRemove) {
/* If we didn't remove the chat because we're waiting for it to reopen, don't cause the account
* to close down the chat.
*/
[inChat.account closeChat:inChat];
[openChats removeObject:inChat];
AILog(@"closeChat: Removed <<%@>> [%@]",inChat, openChats);
} else {
AILog(@"closeChat: Did not remove <<%@>> [%@]",inChat, openChats);
}
[inChat setIsOpen:NO];
[inChat release];
return shouldRemove;
}
/*!
* @brief Called by an account to notifiy the chat controller that it left a chat
*
* Typically this is called in response to -[AIAccout closeChat:] caled in -[self closeChat:] above.
* However, if the chat is never opened, accountDidCloseChat: may be called without closeChat: being called first.
*/
- (void)accountDidCloseChat:(AIChat *)inChat
{
/* If the chat is not open and the account told us that it was closed,
* ensure that it's no longer in the open chats list, as the user will have no further
* interaction with it. This is poarticularly important if the chat closes before it is
* ever opened, such as when an error occurs while joining a group chat.
*/
if (![inChat isOpen])
[openChats removeObject:inChat];
}
/*!
* @brief Switch a chat from one account to another
*
* The target list contact for the chat is changed to be an 'identical' one on the target account; that is, a contact
* with the same UID but an account and service appropriate for newAccount.
*/
- (void)switchChat:(AIChat *)chat toAccount:(AIAccount *)newAccount
{
AIAccount *oldAccount = chat.account;
if (newAccount != oldAccount) {
//Hang onto stuff until we're done
[chat retain];
//Close down the chat on account A
[oldAccount closeChat:chat];
//Set the account and the listObject
{
[chat setAccount:newAccount];
//We want to keep the same destination for the chat but switch it to a listContact on the desired account.
AIListContact *newContact = [adium.contactController contactWithService:newAccount.service
account:newAccount
UID:chat.listObject.UID];
[chat setListObject:newContact];
}
//Open the chat on account B
[newAccount openChat:chat];
//Clean up
[chat release];
}
}
/*!
* @brief Switch the list contact of a chat
*
* @param chat The chat
* @param inContact The contact with which the chat will now take place
* @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.
*/
- (void)switchChat:(AIChat *)chat toListContact:(AIListContact *)inContact usingContactAccount:(BOOL)useContactAccount
{
AIAccount *newAccount = (useContactAccount ? inContact.account : chat.account);
//Switch the inContact over to a contact on the new account so we send messages to the right place.
AIListContact *newContact = [adium.contactController contactWithService:newAccount.service
account:newAccount
UID:inContact.UID];
if (newContact != chat.listObject) {
//Hang onto stuff until we're done
[chat retain];
//Close down the chat on the account, as the account may need to perform actions such as closing a connection
[chat.account closeChat:chat];
//Set to the new listContact and account as needed
[chat setListObject:newContact];
if (useContactAccount || ![inContact.service.serviceClass isEqualToString:chat.account.service.serviceClass])
[chat setAccount:newAccount];
//Reopen the chat on the account
[chat.account openChat:chat];
//Clean up
[chat release];
}
}
/*!
* @brief Find all open chats with a contact
*
* @param inContact The contact. If inContact is an AIMetaContact, all chats with all contacts within the metaContact will be returned.
* @result An NSSet with all chats with the contact. In general, will contain 0 or 1 AIChat objects, though it may contain more.
*/
- (NSSet *)allChatsWithContact:(AIListContact *)inContact
{
NSMutableSet *foundChats = [NSMutableSet set];
//Scan the objects participating in each chat, looking for the requested object
if ([inContact isKindOfClass:[AIMetaContact class]]) {
if ([openChats count]) {
for (AIListContact *listContact in ((AIMetaContact *)inContact).uniqueContainedObjects) {
[foundChats unionSet:[self allChatsWithContact:listContact]];
}
}
} else {
for (AIChat *chat in openChats) {
if (!chat.isGroupChat &&
[chat.listObject.internalObjectID isEqualToString:inContact.internalObjectID] &&
chat.isOpen) {
[foundChats addObject:chat];
}
}
}
return foundChats;
}
/*!
* @brief Find all open chats with a contact
*
* @param inContact The contact. If inContact is an AIMetaContact, all chats with all contacts within the metaContact will be returned.
* @result An NSSet with all chats with the contact.
*/
- (NSSet *)allGroupChatsContainingContact:(AIListContact *)inContact
{
NSMutableSet *groupChats = [NSMutableSet set];
//Search for a chat containing this AIListContact
if ([inContact isKindOfClass:[AIMetaContact class]]) {
//Search for a chat with any contact within this AIMetaContact
for (AIChat *chat in openChats) {
if (!chat.isGroupChat)
continue;
for (AIListContact *contact in (AIMetaContact *)inContact) {
if([chat containsObject:contact]) {
[groupChats addObject:chat];
break;
}
}
}
} else {
//Search for a chat with this AIListContact
for (AIChat *chat in openChats) {
if (chat.isGroupChat && [chat containsObject:inContact]) {
[groupChats addObject:chat];
}
}
}
return groupChats;
}
/*!
* @brief All open chats
*
* Open chats from the chatController may include chats which are not currently displayed by the interface.
*/
- (NSSet *)openChats
{
return [[openChats copy] autorelease];
}
/*!
* @brief Find the chat which most recently received content which has not yet been seen
*
* @result An AIChat with unviewed content, or nil if no chats current have unviewed content
*/
- (AIChat *)mostRecentUnviewedChat
{
BOOL onlyMentions = [[adium.preferenceController preferenceForKey:KEY_STATUS_MENTION_COUNT
group:PREF_GROUP_STATUS_PREFERENCES] boolValue];
if (mostRecentChat && mostRecentChat.unviewedContentCount && (!mostRecentChat.isGroupChat || !onlyMentions || mostRecentChat.unviewedMentionCount)) {
//First choice: switch to the chat which received chat most recently if it has unviewed content
return mostRecentChat;
} else {
//Second choice: switch to the first chat we can find which has unviewed content
for (AIChat *chat in openChats) {
if (chat.unviewedContentCount && (!chat.isGroupChat || !onlyMentions || chat.unviewedMentionCount))
return chat;
}
}
return nil;
}
/*!
* @brief Gets the total number of unviewed messages
*
* @result The number of unviewed messages
*/
- (NSUInteger)unviewedContentCount
{
NSUInteger count = 0;
for (AIChat *chat in openChats) {
if (chat.isGroupChat &&
[[adium.preferenceController preferenceForKey:KEY_STATUS_MENTION_COUNT
group:PREF_GROUP_STATUS_PREFERENCES] boolValue]) {
count += [chat unviewedMentionCount];
} else {
count += [chat unviewedContentCount];
}
}
return count;
}
/*!
* @brief Gets the total number of conversations with unviewed messages
*
* @result The number of conversations with unviewed messages
*/
- (NSUInteger)unviewedConversationCount
{
NSUInteger count = 0;
for (AIChat *chat in openChats) {
if (chat.isGroupChat &&
[[adium.preferenceController preferenceForKey:KEY_STATUS_MENTION_COUNT
group:PREF_GROUP_STATUS_PREFERENCES] boolValue]) {
if (chat.unviewedMentionCount) {
count++;
}
} else if (chat.unviewedContentCount) {
count++;
}
}
return count;
}
/*!
* @brief Is the passed contact in a group chat?
*
* @result YES if the contact is in an open group chat; NO if not.
*/
- (BOOL)contactIsInGroupChat:(AIListContact *)listContact
{
BOOL contactIsInGroupChat = NO;
for (AIChat *chat in openChats) {
if (chat.isGroupChat &&
[chat containsObject:listContact]) {
contactIsInGroupChat = YES;
break;
}
}
return contactIsInGroupChat;
}
/*!
* @brief Called when content is sent or received
*
* Update the most recent chat
*/
- (void)didExchangeContent:(NSNotification *)notification
{
AIContentObject *contentObject = [[notification userInfo] objectForKey:@"AIContentObject"];
//Update our most recent chat
if (contentObject.trackContent) {
AIChat *chat = contentObject.chat;
if (chat != mostRecentChat) {
[mostRecentChat release];
mostRecentChat = [chat retain];
}
}
}
#pragma mark Menu Items
/*!
* @brief Toggle ignoring of a contact
*
* Must be called from the contextual menu for the contact within a chat
*/
- (void)toggleIgnoreOfContact:(id)sender
{
AIListObject *listObject = adium.menuController.currentContextMenuObject;
AIChat *chat = [adium.menuController currentContextMenuChat];
if ([listObject isKindOfClass:[AIListContact class]]) {
BOOL isIgnored = [chat isListContactIgnored:(AIListContact *)listObject];
[chat setListContact:(AIListContact *)listObject isIgnored:!isIgnored];
}
}
/*!
* @brief Toggle displaying of show/part messages for a chat
*
* Effects the currently active chat.
*/
- (void)toggleShowJoinLeave:(id)sender
{
AIChat *chat = nil;
if (sender == menuItem_joinLeave) {
chat = adium.interfaceController.activeChat;
} else {
chat = adium.menuController.currentContextMenuChat;
}
chat.showJoinLeave = !chat.showJoinLeave;
}
/*!
* @brief Menu item validation
*
* When asked to validate our ignore menu item, set its title to ignore/un-ignore as appropriate for the contact
*/
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{
if (menuItem == menuItem_ignore) {
AIListObject *listObject = adium.menuController.currentContextMenuObject;
AIChat *chat = [adium.menuController currentContextMenuChat];
if ([listObject isKindOfClass:[AIListContact class]]) {
if ([chat isListContactIgnored:(AIListContact *)listObject]) {
[menuItem setTitle:AILocalizedString(@"Un-ignore","Un-ignore means begin receiving messages from this contact again in a chat")];
} else {
[menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
}
} else {
[menuItem setTitle:AILocalizedString(@"Ignore","Ignore means no longer receive messages from this contact in a chat")];
return NO;
}
} else if ([menuItem.title isEqualToString:SHOW_JOIN_LEAVE_TITLE]) {
// We're using multiple menu items for the same goal, and WKMV makes a copy of the contextual ones.
// Validate based on the title.
AIChat *chat = nil;
if (menuItem == menuItem_joinLeave) {
chat = adium.interfaceController.activeChat;
} else {
chat = adium.menuController.currentContextMenuChat;
}
if (chat.isGroupChat) {
[menuItem setState:chat.showJoinLeave];
return YES;
}
return NO;
}
return YES;
}
#pragma mark Chat contact addition and removal
/*!
* @brief A chat added a listContact to its participatants list
*
* @param chat The chat
* @param inContact The contact
* @param notify If YES, trigger the contact joined event if this is a group chat. Ignored if this is not a group chat.
*/
- (void)chat:(AIChat *)chat addedListContacts:(NSArray *)inObjects notify:(BOOL)notify
{
if (notify && chat.isGroupChat) {
/* Prevent triggering of the event when we are informed that the chat's own account entered the chat
* If the UID of a contact in a chat differs from a normal UID, such as is the case with Jabber where a chat
* contact has the form "roomname@conferenceserver/handle" this will fail, but it's better than nothing.
*/
for (AIListContact *inContact in inObjects) {
if (![inContact.account.UID isEqualToString:inContact.UID]) {
[adiumChatEvents chat:chat addedListContact:inContact];
}
}
}
//Always notify Adium that the list changed so it can be updated, caches can be modified, etc.
[[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
object:chat];
}
/*!
* @brief A chat removed a listContact from its participants list
*
* @param chat The chat
* @param inContact The contact
*/
- (void)chat:(AIChat *)chat removedListContact:(AIListContact *)inContact
{
if (chat.isGroupChat) {
[adiumChatEvents chat:chat removedListContact:inContact];
}
[[NSNotificationCenter defaultCenter] postNotificationName:Chat_ParticipatingListObjectsChanged
object:chat];
}
- (NSString *)defaultInvitationMessageForRoom:(NSString *)room account:(AIAccount *)inAccount
{
return [NSString stringWithFormat:AILocalizedString(@"%@ invites you to join the chat \"%@\"", nil), inAccount.formattedUID, room];
}
@end
/*
* These strings were used previously; we may want them again. Keeping the translations around for now.
AILocalizedString("%@ joined the chat", nil);
AILocalizedString("%@ left the chat", nil);
*/
|