root/tags/2D14/JVChatController.m

Revision 2615, 41.0 kB (checked in by timothy, 4 years ago)

Smart Transcripts now show the number of new messages in parentheses next to the title in the menu.

Line 
1 #import <ChatCore/MVChatConnection.h>
2 #import <ChatCore/MVChatRoom.h>
3 #import <ChatCore/MVChatUser.h>
4 #import <ChatCore/NSAttributedStringAdditions.h>
5
6 #import "JVChatController.h"
7 #import "MVConnectionsController.h"
8 #import "JVChatWindowController.h"
9 #import "JVTabbedChatWindowController.h"
10 #import "JVNotificationController.h"
11 #import "JVChatTranscriptPanel.h"
12 #import "JVSmartTranscriptPanel.h"
13 #import "JVDirectChatPanel.h"
14 #import "JVChatRoomPanel.h"
15 #import "JVChatConsolePanel.h"
16 #import "JVChatMessage.h"
17 #import "JVChatRoomMember.h"
18
19 #import <libxml/parser.h>
20
21 static JVChatController *sharedInstance = nil;
22 static NSMenu *smartTranscriptMenu = nil;
23
24 @interface JVChatController (JVChatControllerPrivate)
25 - (void) _addWindowController:(JVChatWindowController *) windowController;
26 - (void) _addViewControllerToPreferedWindowController:(id <JVChatViewController>) controller andFocus:(BOOL) focus;
27 @end
28
29 #pragma mark -
30
31 @implementation JVChatController
32 + (JVChatController *) defaultController {
33         extern JVChatController *sharedInstance;
34         return ( sharedInstance ? sharedInstance : ( sharedInstance = [[self alloc] init] ) );
35 }
36
37 + (NSMenu *) smartTranscriptMenu {
38         extern NSMenu *smartTranscriptMenu;
39         [self refreshSmartTranscriptMenu];
40         return smartTranscriptMenu;
41 }
42
43 + (void) refreshSmartTranscriptMenu {
44         extern NSMenu *smartTranscriptMenu;
45         if( ! smartTranscriptMenu ) smartTranscriptMenu = [[NSMenu alloc] initWithTitle:@""];
46
47         NSMenuItem *menuItem = nil;
48         NSEnumerator *enumerator = [[[[smartTranscriptMenu itemArray] copy] autorelease] objectEnumerator];
49         while( ( menuItem = [enumerator nextObject] ) )
50                 [smartTranscriptMenu removeItem:menuItem];
51
52         NSMutableArray *items = [NSMutableArray arrayWithArray:[[[self defaultController] smartTranscripts] allObjects]];
53         [items sortUsingSelector:@selector( compare: )];
54
55         enumerator = [items objectEnumerator];
56
57         JVSmartTranscriptPanel *panel = nil;
58
59         while( ( panel = [enumerator nextObject] ) ) {
60                 NSString *title = [panel title];
61                 if( [panel newMessagesWaiting] > 0 ) title = [NSString stringWithFormat:@"%@ (%d)", [panel title], [panel newMessagesWaiting]];
62                 menuItem = [[[NSMenuItem alloc] initWithTitle:title action:@selector( showView: ) keyEquivalent:@""] autorelease];
63                 if( [panel newMessagesWaiting] ) [menuItem setImage:[NSImage imageNamed:@"smartTranscriptTabActivity"]];
64                 else [menuItem setImage:[NSImage imageNamed:@"smartTranscriptTab"]];
65                 [menuItem setTarget:[self defaultController]];
66                 [menuItem setRepresentedObject:panel];
67                 [smartTranscriptMenu addItem:menuItem];
68         }
69
70         if( ! [items count] ) {
71                 menuItem = [[[NSMenuItem alloc] initWithTitle:NSLocalizedString( @"No Smart Transcripts", "no smart transcripts menu title" ) action:NULL keyEquivalent:@""] autorelease];
72                 [smartTranscriptMenu addItem:menuItem];
73         }
74
75         [smartTranscriptMenu addItem:[NSMenuItem separatorItem]];
76
77         menuItem = [[[NSMenuItem alloc] initWithTitle:NSLocalizedString( @"New Smart Transcript...", "new smart transcript menu title" ) action:@selector( _newSmartTranscript: ) keyEquivalent:@"n"] autorelease];
78         [menuItem setKeyEquivalentModifierMask:(NSCommandKeyMask | NSAlternateKeyMask)];
79         [menuItem setTarget:[JVChatController defaultController]];
80         [smartTranscriptMenu addItem:menuItem];
81 }
82
83 #pragma mark -
84
85 - (id) init {
86         if( ( self = [super init] ) ) {
87                 _chatWindows = [[NSMutableArray array] retain];
88                 _chatControllers = [[NSMutableArray array] retain];
89
90                 NSEnumerator *smartTranscriptsEnumerator = [[[NSUserDefaults standardUserDefaults] objectForKey:@"JVSmartTranscripts"] objectEnumerator];
91                 NSData *archivedSmartTranscript = nil;
92                 while( ( archivedSmartTranscript = [smartTranscriptsEnumerator nextObject] ) )
93                         [_chatControllers addObject:[NSKeyedUnarchiver unarchiveObjectWithData:archivedSmartTranscript]];
94
95                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _joinedRoom: ) name:MVChatRoomJoinedNotification object:nil];
96                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _invitedToRoom: ) name:MVChatRoomInvitedNotification object:nil];
97                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _gotPrivateMessage: ) name:MVChatConnectionGotPrivateMessageNotification object:nil];
98                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _gotRoomMessage: ) name:MVChatRoomGotMessageNotification object:nil];
99         }
100         return self;
101 }
102
103 - (void) dealloc {
104         extern JVChatController *sharedInstance;
105
106         [[NSNotificationCenter defaultCenter] removeObserver:self];
107         if( self == sharedInstance ) sharedInstance = nil;
108
109         [_chatWindows release];
110         [_chatControllers release];
111
112         _chatWindows = nil;
113         _chatControllers = nil;
114
115         [super dealloc];
116 }
117
118 #pragma mark -
119
120 - (NSSet *) allChatWindowControllers {
121         return [NSSet setWithArray:_chatWindows];
122 }
123
124 - (JVChatWindowController *) newChatWindowController {
125         JVChatWindowController *windowController = nil;
126         if( [[NSUserDefaults standardUserDefaults] boolForKey:@"JVUseTabbedWindows"] )
127                 windowController = [[[JVTabbedChatWindowController alloc] init] autorelease];
128         else windowController = [[[JVChatWindowController alloc] init] autorelease];
129         [self _addWindowController:windowController];
130         return windowController;
131 }
132
133 - (void) disposeChatWindowController:(JVChatWindowController *) controller {
134         NSParameterAssert( controller != nil );
135
136         id view = nil;
137         NSEnumerator *enumerator = [[controller allChatViewControllers] objectEnumerator];
138         while( ( view = [enumerator nextObject] ) )
139                 [self disposeViewController:view];
140
141         [_chatWindows removeObject:controller];
142 }
143
144 #pragma mark -
145
146 - (NSSet *) allChatViewControllers {
147         return [NSSet setWithArray:_chatControllers];
148 }
149
150 - (NSSet *) chatViewControllersWithConnection:(MVChatConnection *) connection {
151         NSMutableSet *ret = [NSMutableSet set];
152         id <JVChatViewController> item = nil;
153         NSEnumerator *enumerator = nil;
154
155         NSParameterAssert( connection != nil );
156
157         enumerator = [_chatControllers objectEnumerator];
158         while( ( item = [enumerator nextObject] ) )
159                 if( [item connection] == connection )
160                         [ret addObject:item];
161
162         return ret;
163 }
164
165 - (NSSet *) chatViewControllersOfClass:(Class) class {
166         NSMutableSet *ret = [NSMutableSet set];
167         id <JVChatViewController> item = nil;
168         NSEnumerator *enumerator = nil;
169
170         NSParameterAssert( class != NULL );
171
172         enumerator = [_chatControllers objectEnumerator];
173         while( ( item = [enumerator nextObject] ) )
174                 if( [item isMemberOfClass:class] )
175                         [ret addObject:item];
176
177         return ret;
178 }
179
180 - (NSSet *) chatViewControllersKindOfClass:(Class) class {
181         NSMutableSet *ret = [NSMutableSet set];
182         id <JVChatViewController> item = nil;
183         NSEnumerator *enumerator = nil;
184
185         NSParameterAssert( class != NULL );
186
187         enumerator = [_chatControllers objectEnumerator];
188         while( ( item = [enumerator nextObject] ) )
189                 if( [item isKindOfClass:class] )
190                         [ret addObject:item];
191
192         return ret;
193 }
194
195 #pragma mark -
196
197 - (JVChatRoomPanel *) chatViewControllerForRoom:(MVChatRoom *) room ifExists:(BOOL) exists {
198         NSParameterAssert( room != nil );
199
200         NSEnumerator *enumerator = [_chatControllers objectEnumerator];
201         id ret = nil;
202
203         while( ( ret = [enumerator nextObject] ) )
204                 if( [ret isMemberOfClass:[JVChatRoomPanel class]] && [[ret target] isEqual:room] )
205                         break;
206
207         if( ! ret && ! exists ) {
208                 if( ( ret = [[[JVChatRoomPanel alloc] initWithTarget:room] autorelease] ) ) {
209                         [_chatControllers addObject:ret];
210                         [self _addViewControllerToPreferedWindowController:ret andFocus:YES];
211                 }
212         }
213
214         return ret;
215 }
216
217 - (JVDirectChatPanel *) chatViewControllerForUser:(MVChatUser *) user ifExists:(BOOL) exists {
218         return [self chatViewControllerForUser:user ifExists:exists userInitiated:YES];
219 }
220
221 - (JVDirectChatPanel *) chatViewControllerForUser:(MVChatUser *) user ifExists:(BOOL) exists userInitiated:(BOOL) initiated {
222         NSParameterAssert( user != nil );
223
224         NSEnumerator *enumerator = [_chatControllers objectEnumerator];
225         id ret = nil;
226
227         while( ( ret = [enumerator nextObject] ) )
228                 if( [ret isMemberOfClass:[JVDirectChatPanel class]] && [[ret target] isEqual:user] )
229                         break;
230
231         if( ! ret && ! exists ) {
232                 if( ( ret = [[[JVDirectChatPanel alloc] initWithTarget:user] autorelease] ) ) {
233                         [_chatControllers addObject:ret];
234                         [self _addViewControllerToPreferedWindowController:ret andFocus:initiated];
235                 }
236         }
237
238         return ret;
239 }
240
241 - (JVChatTranscriptPanel *) chatViewControllerForTranscript:(NSString *) filename {
242         id ret = nil;
243         if( ( ret = [[[JVChatTranscriptPanel alloc] initWithTranscript:filename] autorelease] ) ) {
244                 [_chatControllers addObject:ret];
245                 [self _addViewControllerToPreferedWindowController:ret andFocus:YES];
246         }
247         return ret;
248 }
249
250 #pragma mark -
251
252 - (JVSmartTranscriptPanel *) newSmartTranscript {
253         JVSmartTranscriptPanel *ret = nil;
254         if( ( ret = [[[JVSmartTranscriptPanel alloc] initWithSettings:nil] autorelease] ) ) {
255                 [_chatControllers addObject:ret];
256                 [self _addViewControllerToPreferedWindowController:ret andFocus:YES];
257                 [ret editSettings:nil];
258         }
259         return ret;
260 }
261
262 - (NSSet *) smartTranscripts {
263         return [self chatViewControllersOfClass:[JVSmartTranscriptPanel class]];
264 }
265
266 - (void) saveSmartTranscripts {
267         NSMutableArray *smartTranscripts = [NSMutableArray array];
268         NSEnumerator *enumerator = [[self smartTranscripts] objectEnumerator];
269         JVSmartTranscriptPanel *smartTranscript = nil;
270
271         while( ( smartTranscript = [enumerator nextObject] ) )
272                 [smartTranscripts addObject:[NSKeyedArchiver archivedDataWithRootObject:smartTranscript]];
273
274         [[self class] refreshSmartTranscriptMenu];
275         [[NSUserDefaults standardUserDefaults] setObject:smartTranscripts forKey:@"JVSmartTranscripts"];
276 }
277
278 - (void) disposeSmartTranscript:(JVSmartTranscriptPanel *) panel {
279         NSParameterAssert( panel != nil );
280
281         if( [panel respondsToSelector:@selector( willDispose )] )
282                 [(NSObject *)panel willDispose];
283
284         [[panel windowController] removeChatViewController:panel];
285         [_chatControllers removeObject:panel];
286
287         [self saveSmartTranscripts];
288 }
289
290 #pragma mark -
291
292 - (JVChatConsolePanel *) chatConsoleForConnection:(MVChatConnection *) connection ifExists:(BOOL) exists {
293         NSParameterAssert( connection != nil );
294
295         NSEnumerator *enumerator = [_chatControllers objectEnumerator];
296         id <JVChatViewController> ret = nil;
297
298         while( ( ret = [enumerator nextObject] ) )
299                 if( [ret isMemberOfClass:[JVChatConsolePanel class]] && [ret connection] == connection )
300                         break;
301
302         if( ! ret && ! exists ) {
303                 if( ( ret = [[[JVChatConsolePanel alloc] initWithConnection:connection] autorelease] ) ) {
304                         [_chatControllers addObject:ret];
305                         [self _addViewControllerToPreferedWindowController:ret andFocus:YES];
306                 }
307         }
308
309         return (JVChatConsolePanel *)ret;
310 }
311
312 #pragma mark -
313
314 - (void) disposeViewController:(id <JVChatViewController>) controller {
315         NSParameterAssert( controller != nil );
316
317         if( [controller respondsToSelector:@selector( willDispose )] )
318                 [(NSObject *)controller willDispose];
319
320         [[controller windowController] removeChatViewController:controller];
321
322         if( [controller isKindOfClass:[JVSmartTranscriptPanel class]] ) return;
323
324         [_chatControllers removeObject:controller];
325 }
326
327 - (void) detachViewController:(id <JVChatViewController>) controller {
328         NSParameterAssert( controller != nil );
329
330         [controller retain];
331
332         JVChatWindowController *windowController = [self newChatWindowController];
333         [[controller windowController] removeChatViewController:controller];
334
335         [[windowController window] setFrameUsingName:[NSString stringWithFormat:@"Chat Window %@", [controller identifier]]];
336
337         NSRect frame = [[windowController window] frame];
338         NSPoint point = [[windowController window] cascadeTopLeftFromPoint:NSMakePoint( NSMinX( frame ), NSMaxY( frame ) )];
339         [[windowController window] setFrameTopLeftPoint:point];
340
341         [[windowController window] saveFrameUsingName:[NSString stringWithFormat:@"Chat Window %@", [controller identifier]]];
342
343         [windowController addChatViewController:controller];
344        
345         [controller release];
346 }
347
348 #pragma mark -
349
350 - (IBAction) showView:(id) sender {
351         id <JVChatViewController> view = [sender representedObject];
352         if( ! view ) return;
353         if( [view windowController] ) [[view windowController] showChatViewController:view];
354         else [self _addViewControllerToPreferedWindowController:view andFocus:YES];
355 }
356
357 - (IBAction) detachView:(id) sender {
358         id <JVChatViewController> view = [sender representedObject];
359         if( ! view ) return;
360         [self detachViewController:view];
361 }
362
363 #pragma mark -
364 #pragma mark Ignores
365
366 - (JVIgnoreMatchResult) shouldIgnoreUser:(MVChatUser *) user withMessage:(NSAttributedString *) message inView:(id <JVChatViewController>) view {
367         JVIgnoreMatchResult ignoreResult = JVNotIgnored;
368         NSEnumerator *renum = [[[MVConnectionsController defaultController] ignoreRulesForConnection:[view connection]] objectEnumerator];
369         KAIgnoreRule *rule = nil;
370
371         while( ( ignoreResult == JVNotIgnored ) && ( ( rule = [renum nextObject] ) ) )
372                 ignoreResult = [rule matchUser:user message:[message string] inView:view];
373
374         return ignoreResult;
375 }
376 @end
377
378 #pragma mark -
379
380 @implementation JVChatController (JVChatControllerPrivate)
381 - (void) _joinedRoom:(NSNotification *) notification {
382         MVChatRoom *rm = [notification object];
383         if( ! [[MVConnectionsController defaultController] managesConnection:[rm connection]] ) return;
384         JVChatRoomPanel *room = [self chatViewControllerForRoom:rm ifExists:NO];
385         [room joined];
386 }
387
388 - (void) _invitedToRoom:(NSNotification *) notification {
389         NSString *room = [[notification userInfo] objectForKey:@"room"];
390         MVChatUser *user = [[notification userInfo] objectForKey:@"user"];
391         MVChatConnection *connection = [notification object];
392
393         if( ! [[MVConnectionsController defaultController] managesConnection:connection] ) return;
394
395         NSString *title = NSLocalizedString( @"Chat Room Invite", "member invited to room title" );
396         NSString *message = [NSString stringWithFormat:NSLocalizedString( @"You were invited to join %@ by %@. Would you like to accept this invitation and join this room?", "you were invited to join a chat room status message" ), room, [user nickname]];
397
398         if( NSRunInformationalAlertPanel( title, message, NSLocalizedString( @"Join", "join button" ), NSLocalizedString( @"Decline", "decline button" ), nil ) == NSOKButton )
399                 [connection joinChatRoomNamed:room];
400
401         NSMutableDictionary *context = [NSMutableDictionary dictionary];
402         [context setObject:NSLocalizedString( @"Invited to Chat", "bubble title invited to room" ) forKey:@"title"];
403         [context setObject:[NSString stringWithFormat:NSLocalizedString( @"You were invited to %@ by %@.", "bubble message invited to room" ), room, [user nickname]] forKey:@"description"];
404         [[JVNotificationController defaultController] performNotification:@"JVChatRoomInvite" withContextInfo:context];
405 }
406
407 - (void) _gotPrivateMessage:(NSNotification *) notification {
408         BOOL hideFromUser = NO;
409         MVChatUser *user = [notification object];
410         NSData *message = [[notification userInfo] objectForKey:@"message"];
411
412         if( ! [[MVConnectionsController defaultController] managesConnection:[user connection]] ) return;
413
414         if( [[[notification userInfo] objectForKey:@"notice"] boolValue] ) {
415                 MVChatConnection *connection = [user connection];
416
417                 if( ! [self chatViewControllerForUser:user ifExists:YES] )
418                         hideFromUser = YES;
419
420                 if( [[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatAlwaysShowNotices"] )
421                         hideFromUser = NO;
422
423                 NSMutableDictionary *options = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithUnsignedInt:[connection encoding]], @"StringEncoding", [NSNumber numberWithBool:[[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatStripMessageColors"]], @"IgnoreFontColors", [NSNumber numberWithBool:[[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatStripMessageFormatting"]], @"IgnoreFontTraits", [NSFont systemFontOfSize:11.], @"BaseFont", nil];
424                 NSAttributedString *messageString = [NSAttributedString attributedStringWithChatFormat:message options:options];
425                 if( ! messageString ) {
426                         [options setObject:[NSNumber numberWithUnsignedInt:[NSString defaultCStringEncoding]] forKey:@"StringEncoding"];
427                         messageString = [NSAttributedString attributedStringWithChatFormat:message options:options];
428                 }
429
430                 if( [[user nickname] isEqualToString:@"NickServ"] || [[user nickname] isEqualToString:@"MemoServ"] ) {
431                         if( [[user nickname] isEqualToString:@"NickServ"] ) {
432                                 if( [[messageString string] rangeOfString:@"password accepted" options:NSCaseInsensitiveSearch].location != NSNotFound ) {
433                                         NSMutableDictionary *context = [NSMutableDictionary dictionary];
434                                         [context setObject:NSLocalizedString( @"You Have Been Identified", "identified bubble title" ) forKey:@"title"];
435                                         [context setObject:[NSString stringWithFormat:@"%@ on %@", [messageString string], [connection server]] forKey:@"description"];
436                                         [context setObject:[NSImage imageNamed:@"Keychain"] forKey:@"image"];
437                                         [[JVNotificationController defaultController] performNotification:@"JVNickNameIdentifiedWithServer" withContextInfo:context];
438                                 }
439                         } else if( [[user nickname] isEqualToString:@"MemoServ"] ) {
440                                 if( [[messageString string] rangeOfString:@"new memo" options:NSCaseInsensitiveSearch].location != NSNotFound && [[messageString string] rangeOfString:@" no " options:NSCaseInsensitiveSearch].location == NSNotFound ) {
441                                         NSMutableDictionary *context = [NSMutableDictionary dictionary];
442                                         [context setObject:NSLocalizedString( @"You Have New Memos", "new memos bubble title" ) forKey:@"title"];
443                                         [context setObject:messageString forKey:@"description"];
444                                         [context setObject:[NSImage imageNamed:@"Stickies"] forKey:@"image"];
445                                         [context setObject:self forKey:@"target"];
446                                         [context setObject:NSStringFromSelector( @selector( _checkMemos: ) ) forKey:@"action"];
447                                         [context setObject:connection forKey:@"representedObject"];
448                                         [[JVNotificationController defaultController] performNotification:@"JVNewMemosFromServer" withContextInfo:context];
449                                 }
450                         }
451                 } else {
452                         NSMutableDictionary *context = [NSMutableDictionary dictionary];
453                         [context setObject:[NSString stringWithFormat:NSLocalizedString( @"Notice from %@", "notice message from user title" ), [user displayName]] forKey:@"title"];
454                         [context setObject:messageString forKey:@"description"];
455                         [context setObject:[NSImage imageNamed:@"activityNewImportant"] forKey:@"image"];
456                         NSString *type = ( hideFromUser ? @"JVChatUnhandledNoticeMessage" : @"JVChatNoticeMessage" );
457                         [[JVNotificationController defaultController] performNotification:type withContextInfo:context];
458                 }
459         }
460
461         if( ! hideFromUser && ( [self shouldIgnoreUser:user withMessage:nil inView:nil] == JVNotIgnored ) ) {
462                 JVDirectChatPanel *controller = [self chatViewControllerForUser:user ifExists:NO userInitiated:NO];
463                 JVChatMessageType type = ( [[[notification userInfo] objectForKey:@"notice"] boolValue] ? JVChatMessageNoticeType : JVChatMessageNormalType );
464                 [controller addMessageToDisplay:message fromUser:user asAction:[[[notification userInfo] objectForKey:@"action"] boolValue] withIdentifier:[[notification userInfo] objectForKey:@"identifier"] andType:type];
465         }
466 }
467
468 - (void) _gotRoomMessage:(NSNotification *) notification {
469         // we do this here to make sure we catch early messages right when we join (this includes dircproxy's dump)
470         MVChatRoom *room = [notification object];
471         JVChatRoomPanel *controller = [self chatViewControllerForRoom:room ifExists:NO];
472         [controller handleRoomMessageNotification:notification];
473 }
474
475 - (void) _addWindowController:(JVChatWindowController *) windowController {
476         [_chatWindows addObject:windowController];
477 }
478
479 - (void) _addViewControllerToPreferedWindowController:(id <JVChatViewController>) controller andFocus:(BOOL) focus {
480         JVChatWindowController *windowController = nil;
481         id <JVChatViewController> viewController = nil;
482         Class modeClass = NULL;
483         NSEnumerator *enumerator = nil;
484         BOOL kindOfClass = NO;
485
486         NSParameterAssert( controller != nil );
487
488         int mode = [[NSUserDefaults standardUserDefaults] integerForKey:[NSStringFromClass( [controller class] ) stringByAppendingString:@"PreferredOpenMode"]];
489         BOOL groupByServer = (BOOL) mode & 32;
490         mode &= ~32;
491
492         switch( mode ) {
493         case 0: break; // open in new window
494         case 1: // open in existing window
495         default:
496                 enumerator = [_chatWindows objectEnumerator];
497                 while( ( windowController = [enumerator nextObject] ) )
498                         if( [[windowController window] isMainWindow] || ! [[NSApplication sharedApplication] isActive] )
499                                 break;
500                 if( ! windowController ) windowController = [_chatWindows lastObject];
501                 break;
502         case 2: // group with other rooms
503                 modeClass = [JVChatRoomPanel class];
504                 goto groupByClass;
505         case 3: // group with other direct chats
506                 modeClass = [JVDirectChatPanel class];
507                 goto groupByClass;
508         case 4: // group with other transcripts
509                 modeClass = [JVChatTranscriptPanel class];
510                 goto groupByClass;
511         case 5: // group with other consoles
512                 modeClass = [JVChatConsolePanel class];
513                 goto groupByClass;
514         case 6: // group with other chats
515                 modeClass = [JVDirectChatPanel class];
516                 kindOfClass = YES;
517                 goto groupByClass;
518         groupByClass:
519                 if( groupByServer ) {
520                         if( kindOfClass ) enumerator = [[self chatViewControllersKindOfClass:modeClass] objectEnumerator];
521                         else enumerator = [[self chatViewControllersOfClass:modeClass] objectEnumerator];
522                         while( ( viewController = [enumerator nextObject] ) ) {
523                                 if( controller != viewController && [viewController connection] == [controller connection] ) {
524                                         windowController = [viewController windowController];
525                                         if( windowController ) break;
526                                 }
527                         }
528                 } else {
529                         if( kindOfClass ) enumerator = [[self chatViewControllersKindOfClass:modeClass] objectEnumerator];
530                         else enumerator = [[self chatViewControllersOfClass:modeClass] objectEnumerator];
531                         while( ( viewController = [enumerator nextObject] ) ) {
532                                 if( controller != viewController ) {
533                                         windowController = [viewController windowController];
534                                         if( windowController ) break;
535                                 }
536                         }
537                 }
538                 break;
539         }
540
541         if( ! windowController ) windowController = [self newChatWindowController];
542
543         if( [[[NSApplication sharedApplication] currentEvent] modifierFlags] & NSCommandKeyMask ) focus = NO;
544         if( [[[NSApplication sharedApplication] currentEvent] modifierFlags] & NSShiftKeyMask ) focus = NO;
545
546         [windowController addChatViewController:controller];
547         if( focus || [[windowController allChatViewControllers] count] == 1 ) {
548                 [windowController showChatViewController:controller];
549                 if( focus ) [[windowController window] makeKeyAndOrderFront:nil];
550         }
551
552         if( ! [[windowController window] isVisible] )
553                 [[windowController window] orderFront:nil];
554 }
555
556 - (IBAction) _checkMemos:(id) sender {
557         MVChatConnection *connection = [sender representedObject];
558         NSAttributedString *message = [[[NSAttributedString alloc] initWithString:@"read all"] autorelease];
559         MVChatUser *user = [connection chatUserWithUniqueIdentifier:@"MemoServ"];
560         [user sendMessage:message withEncoding:[connection encoding] asAction:NO];
561         [self chatViewControllerForUser:user ifExists:NO];
562 }
563
564 - (IBAction) _newSmartTranscript:(id) sender {
565         [[JVChatController defaultController] newSmartTranscript];
566 }
567 @end
568
569 #pragma mark -
570
571 @implementation JVChatTranscriptPanel (JVChatTranscriptObjectSpecifier)
572 - (NSScriptObjectSpecifier *) objectSpecifier {
573         id classDescription = [NSClassDescription classDescriptionForClass:[NSApplication class]];
574         NSScriptObjectSpecifier *container = [[NSApplication sharedApplication] objectSpecifier];
575         return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:classDescription containerSpecifier:container key:@"chatTranscripts" uniqueID:[self uniqueIdentifier]] autorelease];
576 }
577 @end
578
579 #pragma mark -
580
581 @implementation JVDirectChatPanel (JVDirectChatPanelObjectSpecifier)
582 - (NSScriptObjectSpecifier *) objectSpecifier {
583         id classDescription = [NSClassDescription classDescriptionForClass:[NSApplication class]];
584         NSScriptObjectSpecifier *container = [[NSApplication sharedApplication] objectSpecifier];
585         return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:classDescription containerSpecifier:container key:@"directChats" uniqueID:[self uniqueIdentifier]] autorelease];
586 }
587 @end
588
589 #pragma mark -
590
591 @implementation JVChatRoomPanel (JVChatRoomPanelObjectSpecifier)
592 - (NSScriptObjectSpecifier *) objectSpecifier {
593         id classDescription = [NSClassDescription classDescriptionForClass:[NSApplication class]];
594         NSScriptObjectSpecifier *container = [[NSApplication sharedApplication] objectSpecifier];
595         return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:classDescription containerSpecifier:container key:@"chatRooms" uniqueID:[self uniqueIdentifier]] autorelease];
596 }
597 @end
598
599 #pragma mark -
600
601 @implementation JVChatConsolePanel (JVChatConsolePanelObjectSpecifier)
602 - (NSScriptObjectSpecifier *) objectSpecifier {
603         id classDescription = [NSClassDescription classDescriptionForClass:[NSApplication class]];
604         NSScriptObjectSpecifier *container = [[NSApplication sharedApplication] objectSpecifier];
605         return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:classDescription containerSpecifier:container key:@"chatConsoles" uniqueID:[self uniqueIdentifier]] autorelease];
606 }
607 @end
608
609 #pragma mark -
610
611 @interface JVStartChatScriptCommand : NSScriptCommand {}
612 @end
613
614 #pragma mark -
615
616 @implementation JVStartChatScriptCommand
617 - (id) performDefaultImplementation {
618         NSDictionary *args = [self evaluatedArguments];
619         id target = [args objectForKey:@"target"];
620
621         if( target && [target isKindOfClass:[NSString class]] ) {
622                 MVChatConnection *connection = [args objectForKey:@"connection"];
623                 if( ! connection ) {
624                         [self setScriptErrorNumber:1000];
625                         [self setScriptErrorString:@"The connection parameter was missing and is required when the user is a nickname string."];
626                         return nil;
627                 }
628
629                 if( ! [connection isConnected] ) {
630                         [self setScriptErrorNumber:1000];
631                         [self setScriptErrorString:@"The connection needs to be connected before you can find a chat user by their nickname."];
632                         return nil;
633                 }
634
635                 NSString *nickname = target;
636                 target = [[connection chatUsersWithNickname:nickname] anyObject];
637
638                 if( ! target ) {
639                         [self setScriptErrorNumber:1000];
640                         [self setScriptErrorString:[NSString stringWithFormat:@"The connection did not find a chat user with the nickname \"%@\".", nickname]];
641                         return nil;
642                 }
643         }
644
645         if( ! target || ( ! [target isKindOfClass:[MVChatUser class]] && ! [target isKindOfClass:[JVChatRoomMember class]] ) ) {
646                 [self setScriptErrorNumber:1000];
647                 [self setScriptErrorString:@"The \"for\" parameter was missing or not a chat user or member object."];
648                 return nil;
649         }
650
651         if( [target isKindOfClass:[MVChatUser class]] && [(MVChatUser *)target type] == MVChatWildcardUserType ) {
652                 [self setScriptErrorNumber:1000];
653                 [self setScriptErrorString:@"The \"for\" parameter cannot be a wildcard user."];
654                 return nil;
655         }
656
657         if( [target isKindOfClass:[JVChatRoomMember class]] )
658                 target = [(JVChatRoomMember *)target user];
659
660         JVDirectChatPanel *panel = [[JVChatController defaultController] chatViewControllerForUser:target ifExists:NO];
661         [[panel windowController] showChatViewController:panel];
662
663         return panel;
664 }
665 @end
666
667 #pragma mark -
668
669 @implementation NSApplication (JVChatControllerScripting)
670 - (void) scriptErrorChantAddToChatViews {
671         [[NSScriptCommand currentCommand] setScriptErrorString:@"Can't add, insert or replace a panel at the application level."];
672         [[NSScriptCommand currentCommand] setScriptErrorNumber:1000];
673 }
674
675 #pragma mark -
676
677 - (NSArray *) chatViews {
678         return [[[JVChatController defaultController] allChatViewControllers] allObjects];
679 }
680
681 - (id <JVChatViewController>) valueInChatViewsAtIndex:(unsigned) index {
682         return [[self chatViews] objectAtIndex:index];
683 }
684
685 - (id <JVChatViewController>) valueInChatViewsWithUniqueID:(id) identifier {
686         NSEnumerator *enumerator = [[[JVChatController defaultController] allChatViewControllers] objectEnumerator];
687         id <JVChatViewController, JVChatListItemScripting> view = nil;
688
689         while( ( view = [enumerator nextObject] ) )
690                 if( [view conformsToProtocol:@protocol( JVChatListItemScripting )] &&
691                         [[view uniqueIdentifier] isEqual:identifier] ) return view;
692
693         return nil;
694 }
695
696 - (id <JVChatViewController>) valueInChatViewsWithName:(NSString *) name {
697         NSEnumerator *enumerator = [[[JVChatController defaultController] allChatViewControllers] objectEnumerator];
698         id <JVChatViewController> view = nil;
699
700         while( ( view = [enumerator nextObject] ) )
701                 if( [[view title] isEqualToString:name] )
702                         return view;
703
704         return nil;
705 }
706
707 - (void) addInChatViews:(id <JVChatViewController>) view {
708         [self scriptErrorChantAddToChatViews];
709 }
710
711 - (void) insertInChatViews:(id <JVChatViewController>) view {
712         [self scriptErrorChantAddToChatViews];
713 }
714
715 - (void) insertInChatViews:(id <JVChatViewController>) view atIndex:(unsigned) index {
716         [self scriptErrorChantAddToChatViews];
717 }
718
719 - (void) removeFromChatViewsAtIndex:(unsigned) index {
720         id <JVChatViewController> view = [[self chatViews] objectAtIndex:index];
721         if( view ) [[JVChatController defaultController] disposeViewController:view];
722 }
723
724 - (void) replaceInChatViews:(id <JVChatViewController>) view atIndex:(unsigned) index {
725         [self scriptErrorChantAddToChatViews];
726 }
727
728 #pragma mark -
729
730 - (NSArray *) chatViewsWithClass:(Class) class {
731         return [[[JVChatController defaultController] chatViewControllersOfClass:class] allObjects];
732 }
733
734 - (id <JVChatViewController>) valueInChatViewsAtIndex:(unsigned) index withClass:(Class) class {
735         return [[self chatViewsWithClass:class] objectAtIndex:index];
736 }
737
738 - (id <JVChatViewController>) valueInChatViewsWithUniqueID:(id) identifier andClass:(Class) class {
739         return [self valueInChatViewsWithUniqueID:identifier];
740 }
741
742 - (id <JVChatViewController>) valueInChatViewsWithName:(NSString *) name andClass:(Class) class {
743         NSEnumerator *enumerator = [[self chatViewsWithClass:class] objectEnumerator];
744         id <JVChatViewController> view = nil;
745
746         while( (