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 again

wbowling / adium (fork of adium / adium)

Fork of Adium for patches/improvements

Clone this repository (size: 338.7 MB): HTTPS / SSH
hg clone https://bitbucket.org/wbowling/adium
hg clone ssh://hg@bitbucket.org/wbowling/adium

adium / Source / AIChatController.m

  1
  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);
 */