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