#import "CQActivityWindowController.h" #import #import "MVFileTransfer.h" #import "MVFileTransferController.h" #import "JVChatController.h" #import "CQDownloadCell.h" #import "CQSubtitleCell.h" #import "CQGroupCell.h" #define CQFileTransferInactiveWaitLimit 300 // in seconds #define CQExpandCollapseRowInterval .5 NSString *CQActivityTypeChatInvite = @"CQActivityTypeChatInvite"; NSString *CQActivityTypeDirectChatInvite = @"CQActivityTypeDirectChatInvite"; NSString *CQActivityTypeDirectDownload = @"CQActivityTypeDirectDownload"; NSString *CQActivityTypeFileTransfer = @"CQActivityTypeFileTransfer"; NSString *CQActivityStatusAccepted = @"CQActivityStatusAccepted"; NSString *CQActivityStatusError = @"CQActivityStatusError"; NSString *CQActivityStatusFinished = @"CQActivityStatusFinished"; NSString *CQActivityStatusPending = @"CQActivityStatusPending"; NSString *CQActivityStatusRejected = @"CQActivityStatusRejected"; NSString *CQDirectChatConnectionKey = @"CQDirectChatConnectionKey"; NSString *CQDirectDownloadKey = @"CQDirectDownloadKey"; MVInline double CQUnitKilobyte (void); MVInline double CQUnitMegabyte (void); MVInline double CQUnitGigabyte (void); MVInline double CQUnitTerabyte (void); MVInline double CQUnitKilobyte (void) { static NSUInteger result = 0; if (!result) result = (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_5) ? 1024 : 1000; return result; } MVInline double CQUnitMegabyte (void) { static NSUInteger result = 0; if (!result) result = CQUnitKilobyte() * CQUnitKilobyte(); return result; } MVInline double CQUnitGigabyte (void) { static NSUInteger result = 0; if (!result) result = CQUnitMegabyte() * CQUnitKilobyte(); return result; } MVInline double CQUnitTerabyte (void) { static NSUInteger result = 0; if (!result) result = CQUnitGigabyte() * CQUnitKilobyte(); return result; } NSString *MVPrettyFileSize (CQFileSize size) { if (size == 0) return NSLocalizedString(@"Zero bytes", "no file size"); if (size < CQUnitKilobyte()) return [NSString stringWithFormat:NSLocalizedString(@"%lu bytes", "file size measured in bytes"), size]; if (size < CQUnitMegabyte()) return [NSString stringWithFormat:NSLocalizedString(@"%.0f KB", "file size measured in kilobytes"), (size / CQUnitKilobyte())]; if (size < CQUnitGigabyte()) return [NSString stringWithFormat:NSLocalizedString(@"%.1f MB", "file size measured in megabytes"), (size / CQUnitMegabyte())]; if (size < CQUnitTerabyte()) return [NSString stringWithFormat:NSLocalizedString(@"%.2f GB", "file size measured in gigabytes"), (size / CQUnitGigabyte())]; return [NSString stringWithFormat:NSLocalizedString(@"%.3f TB", "file size measured in terabytes"), (size / CQUnitTerabyte())]; } NSString *MVReadableTime (NSTimeInterval date, BOOL longFormat) { NSTimeInterval secs = [[NSDate date] timeIntervalSince1970] - date; static NSArray *desc = nil; if (!desc) desc = [[NSArray alloc] initWithObjects:NSLocalizedString(@"second", "singular second"), NSLocalizedString(@"minute", "singular minute"), NSLocalizedString(@"hour", "singular hour"), NSLocalizedString(@"day", "singular day"), NSLocalizedString(@"week", "singular week"), NSLocalizedString(@"month", "singular month"), NSLocalizedString(@"year", "singular year"), nil]; static NSArray *plural = nil; if (!plural) plural = [[NSArray alloc] initWithObjects:NSLocalizedString(@"seconds", "plural seconds"), NSLocalizedString(@"minutes", "plural minutes"), NSLocalizedString(@"hours", "plural hours"), NSLocalizedString(@"days", "plural days"), NSLocalizedString(@"weeks", "plural weeks"), NSLocalizedString(@"months", "plural months"), NSLocalizedString(@"years", "plural years"), nil]; static NSArray *values = nil; if (!values) values = [[NSArray alloc] initWithObjects:[NSNumber numberWithUnsignedInteger:1], [NSNumber numberWithUnsignedInteger:60], [NSNumber numberWithUnsignedInteger:3600], [NSNumber numberWithUnsignedInteger:86400], [NSNumber numberWithUnsignedInteger:604800], [NSNumber numberWithUnsignedInteger:2628000], [NSNumber numberWithUnsignedInteger:31536000], nil]; if (secs < 0) secs = fabs(secs); NSUInteger i = 0; while (i < values.count && secs >= [[values objectAtIndex:i] doubleValue]) i++; if (i) i--; NSUInteger stop = [[values objectAtIndex:i] unsignedIntegerValue]; NSUInteger val = (NSUInteger)(secs / (float)stop); NSArray *use = (val > 1 ? plural : desc); NSString *retval = [NSString stringWithFormat:@"%u %@", val, [use objectAtIndex:i]]; if (!longFormat || i <= 0) return retval; NSUInteger rest = (NSUInteger)((NSUInteger)secs % stop); stop = [[values objectAtIndex:--i] unsignedIntegerValue]; rest = (NSUInteger)(rest / (float)stop); if (rest > 0) { use = (rest > 1 ? plural : desc); retval = [retval stringByAppendingFormat:@" %u %@", rest, [use objectAtIndex:i--]]; } return retval; } @interface CQActivityWindowController (Private) - (void) _loadImagesForButtonsWithFormat:(NSString *) format; - (void) _setDestinationForTransfer:(MVFileTransfer *) transfer shouldAsk:(BOOL) shouldAsk; - (NSUInteger) _directChatConnectionCount; - (NSUInteger) _directDownloadCount; - (NSUInteger) _fileTransferCountForConnection:(MVChatConnection *) connection; - (NSUInteger) _invitationCountForConnection:(MVChatConnection *) connection; - (BOOL) _isGroupItem:(id) item; - (BOOL) _shouldExpandOrCollapse; - (void) _appendActivity:(id) activity forConnection:(id) connection; @end #pragma mark - @implementation CQActivityWindowController + (CQActivityWindowController *) sharedController { static CQActivityWindowController *sharedActivityWindowController = nil; static BOOL creatingSharedInstance = NO; if (sharedActivityWindowController) return sharedActivityWindowController; creatingSharedInstance = YES; sharedActivityWindowController = [[CQActivityWindowController alloc] init]; return sharedActivityWindowController; } - (id) init { if (!(self = [super initWithWindowNibName:@"CQActivityWindow"])) return nil; _activity = [[NSMapTable alloc] initWithKeyOptions:NSMapTableZeroingWeakMemory valueOptions:NSMapTableStrongMemory capacity:[[MVConnectionsController defaultController] connections].count]; [_activity setObject:[NSMutableArray array] forKey:CQDirectChatConnectionKey]; [_activity setObject:[NSMutableArray array] forKey:CQDirectDownloadKey]; _timeFormatter = [[NSDateFormatter alloc] init]; _timeFormatter.dateStyle = NSDateFormatterNoStyle; _timeFormatter.timeStyle = NSDateFormatterShortStyle; _cellImages = [[NSMutableDictionary alloc] init]; [self _loadImagesForButtonsWithFormat:@"check"]; [self _loadImagesForButtonsWithFormat:@"magnifying"]; [self _loadImagesForButtonsWithFormat:@"retry"]; [self _loadImagesForButtonsWithFormat:@"x"]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(chatRoomInvitationAccepted:) name:MVChatRoomJoinedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(chatRoomInvitationReceived:) name:MVChatRoomInvitedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(directChatDidConnect:) name:MVDirectChatConnectionErrorDomain object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(directChatErrorOccurred:) name:MVDirectChatConnectionDidConnectNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(directChatOfferReceived:) name:MVDirectChatConnectionOfferNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileTransferDidReceiveData:) name:MVFileTransferDataTransferredNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileTransferWasOffered:) name:MVDownloadFileTransferOfferNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileTransferDidStart:) name:MVFileTransferStartedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileTransferDidFinish:) name:MVFileTransferFinishedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileTransferErrorReceived:) name:MVFileTransferErrorOccurredNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(connectionDidConnect:) name:MVChatConnectionDidConnectNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(connectionDidDisconnect:) name:MVChatConnectionDidDisconnectNotification object:nil]; return self; } - (void) dealloc { [_titleCell release]; [_groupCell release]; [_activity release]; [_timeFormatter release]; [super dealloc]; } #pragma mark - - (void) _loadImagesForButtonsWithFormat:(NSString *) format { NSMutableDictionary *images = [[NSMutableDictionary alloc] init]; [images setObject:[NSImage imageNamed:[NSString stringWithFormat:@"%@-default.png", format]] forKey:CQMouseStateDefaultKey]; [images setObject:[NSImage imageNamed:[NSString stringWithFormat:@"%@-hover.png", format]] forKey:CQMouseStateHoverKey]; [images setObject:[NSImage imageNamed:[NSString stringWithFormat:@"%@-click.png", format]] forKey:CQMouseStateClickKey]; [_cellImages setObject:images forKey:format]; } #pragma mark - - (IBAction) showActivityWindow:(id) sender { [self.window makeKeyAndOrderFront:nil]; } - (IBAction) hideActivityWindow:(id) sender { [self.window orderOut:nil]; } - (void) orderFrontIfNecessary { if (![self.window isVisible]) [self.window makeKeyAndOrderFront:nil]; } #pragma mark - - (void) connectionDidConnect:(NSNotification *) notification { MVChatConnection *connection = notification.object; [_activity setObject:[NSMutableArray array] forKey:connection]; } - (void) connectionDidDisconnect:(NSNotification *) notification { [_outlineView reloadData]; } #pragma mark - - (void) chatRoomInvitationAccepted:(NSNotification *) notification { MVChatRoom *room = notification.object; for (NSMutableDictionary *dictionary in [_activity objectForKey:room.connection]) { if ([dictionary objectForKey:@"type"] != CQActivityTypeChatInvite) continue; id activityRoom = [dictionary objectForKey:@"room"]; if ([activityRoom isKindOfClass:[MVChatRoom class]]) activityRoom = ((MVChatRoom *)activityRoom).name; if (![room.name isEqualToString:activityRoom]) continue; [dictionary setObject:CQActivityStatusAccepted forKey:@"status"]; [_outlineView reloadItem:dictionary]; break; } } - (void) chatRoomInvitationReceived:(NSNotification *) notification { if ([[NSUserDefaults standardUserDefaults] boolForKey:@"JVAutoJoinChatRoomOnInvite"]) return; NSString *name = [notification.userInfo objectForKey:@"room"]; // The notification object is a string if we receive an invite in the first few seconds of Colloquy being open, work around that and make sure we have a MVChatConnection to work with instead. id connection = notification.object; if (![connection isKindOfClass:[MVChatConnection class]]) connection = [[MVConnectionsController defaultController] connectionForServerAddress:connection]; if (!connection) { NSLog(@"Failed to find a connection for: %@. Unable to join room after invite.", notification.object); return; } for (NSDictionary *dictionary in [_activity objectForKey:connection]) { // if we already have an invite and its pending, ignore it id room = [dictionary objectForKey:@"room"]; if ([room isKindOfClass:[MVChatRoom class]]) room = ((MVChatRoom *)room).name; if ([room isCaseInsensitiveEqualToString:name]) if ([dictionary objectForKey:@"status"] == CQActivityStatusPending) return; } NSMutableDictionary *chatRoomInfo = [notification.userInfo mutableCopy]; [chatRoomInfo setObject:CQActivityTypeChatInvite forKey:@"type"]; [chatRoomInfo setObject:CQActivityStatusPending forKey:@"status"]; [chatRoomInfo setObject:[NSDate date] forKey:@"date"]; [self _appendActivity:chatRoomInfo forConnection:connection]; [chatRoomInfo release]; [_outlineView reloadData]; [self orderFrontIfNecessary]; } #pragma mark - - (void) directChatDidConnect:(NSNotification *) notification { MVDirectChatConnection *connection = notification.object; for (NSDictionary *dictionary in [_activity objectForKey:CQDirectChatConnectionKey]) { if ([dictionary objectForKey:@"connection"] != connection) continue; [_outlineView reloadItem:dictionary]; break; } } - (void) directChatErrorOccurred:(NSNotification *) notification { MVDirectChatConnection *connection = notification.object; for (NSDictionary *dictionary in [_activity objectForKey:CQDirectChatConnectionKey]) { if ([dictionary objectForKey:@"connection"] != connection) continue; [_outlineView reloadItem:dictionary]; break; } [self orderFrontIfNecessary]; } - (void) directChatOfferReceived:(NSNotification *) notification { MVDirectChatConnection *connection = notification.object; NSMutableDictionary *chatRoomInfo = [notification.userInfo mutableCopy]; [chatRoomInfo setObject:CQActivityTypeDirectChatInvite forKey:@"type"]; [chatRoomInfo setObject:connection forKey:@"connection"]; [self _appendActivity:chatRoomInfo forConnection:CQDirectChatConnectionKey]; [chatRoomInfo release]; [_outlineView reloadData]; [self orderFrontIfNecessary]; } #pragma mark - - (void) downloadDidFinish:(NSURLDownload *) download { for (NSMutableDictionary *dictionary in [_activity objectForKey:CQDirectDownloadKey]) { if ([dictionary objectForKey:@"download"] != download) continue; [dictionary setObject:CQActivityStatusFinished forKey:@"status"]; [[MVFileTransferController defaultController] fileAtPathDidFinish:[dictionary objectForKey:@"path"]]; CQDownloadCell *cell = [dictionary objectForKey:@"cell"]; [cell hideProgressIndicator]; [dictionary removeObjectForKey:@"cell"]; if ([[NSUserDefaults standardUserDefaults] integerForKey:@"JVRemoveTransferredItems"] == 2) { [[_activity objectForKey:CQDirectDownloadKey] removeObject:dictionary]; [_outlineView reloadData]; return; } [_outlineView reloadData]; break; } [self orderFrontIfNecessary]; } - (void) downloadFileAtURL:(NSURL *) url toLocalFile:(NSString *) path { WebDownload *download = [[WebDownload alloc] initWithRequest:[NSURLRequest requestWithURL:url] delegate:self]; if (!download) { NSBeginAlertSheet(NSLocalizedString(@"Invalid URL", "Invalid URL title"), nil, nil, nil, self.window, nil, nil, nil, nil, NSLocalizedString(@"The download URL is either invalid or unsupported.", "Invalid URL message")); return; } if (!path.length) path = [[MVFileTransferController userPreferredDownloadFolder] stringByAppendingPathComponent:url.path.lastPathComponent]; [download setDestination:path allowOverwrite:NO]; NSMutableDictionary *item = [[NSMutableDictionary alloc] init]; [item setObject:[NSNumber numberWithUnsignedLongLong:0] forKey:@"transferred"]; [item setObject:[NSNumber numberWithDouble:0.] forKey:@"rate"]; [item setObject:[NSNumber numberWithUnsignedLongLong:0] forKey:@"size"]; [item setObject:download forKey:@"download"]; [item setObject:url forKey:@"url"]; [item setObject:CQActivityTypeDirectDownload forKey:@"type"]; [item setObject:path.length ? path : url.path.lastPathComponent forKey:@"path"]; [[_activity objectForKey:CQDirectDownloadKey] addObject:item]; [item release]; [download release]; [_outlineView reloadData]; [self orderFrontIfNecessary]; } - (void) downloadFileSavePanelDidEnd:(NSSavePanel *) sheet returnCode:(int) returnCode contextInfo:(void *) contextInfo { WebDownload *download = [(WebDownload *)contextInfo retain]; if (returnCode == NSOKButton) { for (NSMutableDictionary *dictionary in [_activity objectForKey:CQDirectDownloadKey]) { if ([dictionary objectForKey:@"download"] != download) continue; if (sheet) [dictionary setObject:[sheet filename] forKey:@"path"]; [download setDestination:[dictionary objectForKey:@"path"] allowOverwrite:YES]; break; } } else { [download cancel]; for (NSMutableDictionary *dictionary in [_activity objectForKey:CQDirectDownloadKey]) { if ([dictionary objectForKey:@"download"] != download) continue; [dictionary setObject:CQActivityStatusRejected forKey:@"status"]; break; } } [download release]; } - (NSWindow *) downloadWindowForAuthenticationSheet:(WebDownload *) download { return self.window; } - (void) download:(NSURLDownload *) download decideDestinationWithSuggestedFilename:(NSString *) filename { if (![[NSUserDefaults standardUserDefaults] boolForKey:@"JVAskForTransferSaveLocation"]) { NSString *path = [[MVFileTransferController userPreferredDownloadFolder] stringByAppendingPathComponent:filename]; for (NSMutableDictionary *info in [_activity objectForKey:CQDirectDownloadKey]) { if ([info objectForKey:@"download"] != download) continue; [info setObject:path forKey:@"path"]; break; } [self downloadFileSavePanelDidEnd:nil returnCode:NSOKButton contextInfo:download]; } else { NSSavePanel *savePanel = [NSSavePanel savePanel]; savePanel.nameFieldLabel = filename; [savePanel beginSheetForDirectory:[MVFileTransferController userPreferredDownloadFolder] file:filename modalForWindow:nil modalDelegate:self didEndSelector:@selector(downloadFileSavePanelDidEnd:returnCode:contextInfo:) contextInfo:download]; } } - (void) download:(NSURLDownload *) download didCreateDestination:(NSString *) path { for (NSMutableDictionary *dictionary in [_activity objectForKey:CQDirectDownloadKey]) { if ([dictionary objectForKey:@"download"] != download) continue; if (![[dictionary objectForKey:@"path"] isEqualToString:path]) [dictionary setObject:path forKey:@"path"]; break; } } - (void) download:(NSURLDownload *) download didFailWithError:(NSError *) error { for (NSMutableDictionary *dictionary in [_activity objectForKey:CQDirectDownloadKey]) { if ([dictionary objectForKey:@"download"] != download) continue; [dictionary setObject:CQActivityStatusError forKey:@"status"]; [dictionary setObject:error forKey:@"error"]; [_outlineView reloadData]; break; } [self orderFrontIfNecessary]; } - (void) download:(NSURLDownload *) download didReceiveDataOfLength:(NSUInteger) length { for (NSMutableDictionary *dictionary in [_activity objectForKey:CQDirectDownloadKey]) { if ([dictionary objectForKey:@"download"] != download) continue; NSTimeInterval timeslice = [[dictionary objectForKey:@"started"] timeIntervalSinceNow] * -1; CQFileSize transferred = [[dictionary objectForKey:@"transferred"] unsignedLongLongValue] + length; [dictionary setObject:CQActivityStatusAccepted forKey:@"status"]; [dictionary setObject:[NSNumber numberWithUnsignedLongLong:transferred] forKey:@"transferred"]; CQFileSize size = [[dictionary objectForKey:@"size"] unsignedLongLongValue]; if (transferred > size) [dictionary setObject:[NSNumber numberWithUnsignedLongLong:transferred] forKey:@"size"]; if (transferred != size) [dictionary setObject:[NSNumber numberWithDouble:(transferred / timeslice)] forKey:@"rate"]; [_outlineView reloadItem:dictionary]; break; } } - (void) download:(NSURLDownload *) download didReceiveResponse:(NSURLResponse *) response { for (NSMutableDictionary *dictionary in [_activity objectForKey:CQDirectDownloadKey]) { if ([dictionary objectForKey:@"download"] != download) continue; if ([response.URL.scheme hasCaseInsensitivePrefix:@"HTTP"] && ((NSHTTPURLResponse *)response).statusCode != 200) { [dictionary setObject:CQActivityStatusError forKey:@"status"]; [_outlineView reloadData]; return; } [dictionary setObject:CQActivityStatusAccepted forKey:@"status"]; [dictionary setObject:[NSNumber numberWithUnsignedLongLong:0] forKey:@"transferred"]; unsigned long size = [response expectedContentLength]; if ((long)size == -1) size = 0; [dictionary setObject:[NSNumber numberWithUnsignedLongLong:size] forKey:@"size"]; if (![dictionary objectForKey:@"started"]) [dictionary setObject:[NSDate date] forKey:@"started"]; [_outlineView reloadData]; break; } } - (BOOL) download:(NSURLDownload *) download shouldDecodeSourceDataOfMIMEType:(NSString *) encodingType { return NO; } #pragma mark - - (void) fileTransferDidStart:(NSNotification *) notification { MVFileTransfer *transfer = notification.object; for (NSDictionary *dictionary in [_activity objectForKey:transfer.user.connection]) { if ([dictionary objectForKey:@"transfer"] != transfer) continue; [_outlineView reloadItem:dictionary]; break; } [self orderFrontIfNecessary]; } - (void) fileTransferDidReceiveData:(NSNotification *) notification { MVFileTransfer *transfer = notification.object; for (NSMutableDictionary *dictionary in [_activity objectForKey:transfer.user.connection]) { if ([dictionary objectForKey:@"transfer"] != transfer) continue; NSTimeInterval timeslice = [transfer.startDate timeIntervalSinceNow] * -1; if (transfer.transferred != transfer.finalSize) [dictionary setObject:[NSNumber numberWithDouble:(transfer.transferred / timeslice)] forKey:@"rate"]; [_outlineView reloadItem:dictionary]; break; } } - (void) fileTransferDidFinish:(NSNotification *) notification { MVFileTransfer *transfer = notification.object; for (NSMutableDictionary *dictionary in [_activity objectForKey:transfer.user.connection]) { if ([dictionary objectForKey:@"transfer"] != transfer) continue; CQDownloadCell *cell = [dictionary objectForKey:@"cell"]; [cell hideProgressIndicator]; [dictionary removeObjectForKey:@"cell"]; if ([transfer isDownload]) [[MVFileTransferController defaultController] fileAtPathDidFinish:((MVDownloadFileTransfer *)transfer).destination]; if ([[NSUserDefaults standardUserDefaults] integerForKey:@"JVRemoveTransferredItems"] == 2) { [[_activity objectForKey:transfer.user.connection] removeObjectIdenticalTo:dictionary]; [_outlineView reloadData]; return; } [_outlineView reloadItem:dictionary]; break; } [self orderFrontIfNecessary]; } - (void) fileTransferErrorReceived:(NSNotification *) notification { MVFileTransfer *transfer = notification.object; for (NSDictionary *dictionary in [_activity objectForKey:transfer.user.connection]) { if ([dictionary objectForKey:@"transfer"] != transfer) continue; [_outlineView reloadItem:dictionary]; break; } [self orderFrontIfNecessary]; } - (void) fileTransferWasOffered:(NSNotification *) notification { MVFileTransfer *transfer = notification.object; NSMutableDictionary *fileTransferInfo = [[NSMutableDictionary dictionaryWithObjectsAndKeys:CQActivityTypeFileTransfer, @"type", transfer, @"transfer", nil] mutableCopy]; [fileTransferInfo setObject:CQActivityTypeFileTransfer forKey:@"type"]; [self _appendActivity:fileTransferInfo forConnection:transfer.user.connection]; [fileTransferInfo release]; if ([[NSUserDefaults standardUserDefaults] integerForKey:@"JVAutoAcceptFilesFrom"] == 3) [self _setDestinationForTransfer:transfer shouldAsk:NO]; else if ([[NSUserDefaults standardUserDefaults] integerForKey:@"JVAutoAcceptFilesFrom"] == 2) { // JVBuddy *buddy = [[MVBuddyListController sharedBuddyList] buddyForNickname:[transfer user] onServer:[(MVChatConnection *)[transfer connection] server]]; // if (buddy) [self _setDestinationForTransfer:transfer shouldAsk:NO] // else [self _setDestinationForTransfer:transfer shouldAsk:YES] [self _setDestinationForTransfer:transfer shouldAsk:YES]; } else if ([[NSUserDefaults standardUserDefaults] integerForKey:@"JVAutoAcceptFilesFrom"] == 1) [self _setDestinationForTransfer:transfer shouldAsk:YES]; [_outlineView reloadData]; [self orderFrontIfNecessary]; } #pragma mark - - (id) outlineView:(NSOutlineView *) outlineView child:(NSInteger) childAtIndex ofItem:(id) item { if (!item) { NSInteger count = 0; for (id key in _activity) { NSArray *activity = [_activity objectForKey:key]; if (!activity.count) continue; if (childAtIndex == count) return key; count++; } } return [[_activity objectForKey:item] objectAtIndex:childAtIndex]; } - (BOOL) outlineView:(NSOutlineView *) outlineView isItemExpandable:(id) item { return [self _isGroupItem:item]; // top level, shows the connection name } - (NSInteger) outlineView:(NSOutlineView *) outlineView numberOfChildrenOfItem:(id) item { if (!item) { NSUInteger count = 0; for (id key in _activity) if (((NSArray *)[_activity objectForKey:key]).count) count++; return count; } return ((NSArray *)[_activity objectForKey:item]).count; } - (id) outlineView:(NSOutlineView *) outlineView objectValueForTableColumn:(NSTableColumn *) tableColumn byItem:(id) item { if ([item isKindOfClass:[MVChatConnection class]]) return ((MVChatConnection *)item).server; if (item == CQDirectChatConnectionKey) return NSLocalizedString(@"Direct Chat Invites", @"Direct Chat Invites group title"); if (item == CQDirectDownloadKey) return NSLocalizedString(@"Downloads", @"Downloads group title"); return [item description]; } - (BOOL) outlineView:(NSOutlineView *) outlineView isGroupItem:(id) item { return [self _isGroupItem:item]; // top level, shows the connection name } #pragma mark - - (NSCell *) outlineView:(NSOutlineView *) outlineView dataCellForTableColumn:(NSTableColumn *) tableColumn item:(id) item { if ([self _isGroupItem:item]) { if (!_groupCell) _groupCell = [[CQGroupCell alloc] initTextCell:@""]; return _groupCell; } NSString *type = [item objectForKey:@"type"]; if (type == CQActivityTypeChatInvite || type == CQActivityTypeDirectChatInvite) { if (!_titleCell) _titleCell = [[CQSubtitleCell alloc] init]; return _titleCell; } if (type == CQActivityTypeFileTransfer || type == CQActivityTypeDirectDownload) { if (type == CQActivityTypeFileTransfer) { MVFileTransfer *transfer = [item objectForKey:@"transfer"]; if (transfer.status == MVFileTransferNormalStatus) { CQDownloadCell *fileTransferCell = [item objectForKey:@"cell"]; if (!fileTransferCell) { // Make a new cell for each file transfer; otherwise we'll be reusing the same progress indicator view for multiple cells. fileTransferCell = [[CQDownloadCell alloc] init]; [item setObject:fileTransferCell forKey:@"cell"]; } return fileTransferCell; } } else { if ([item objectForKey:@"status"] == CQActivityStatusAccepted) { CQDownloadCell *fileTransferCell = [item objectForKey:@"cell"]; if (!fileTransferCell) { // Make a new cell for each file transfer; otherwise we'll be reusing the same progress indicator view for multiple cells. fileTransferCell = [[CQDownloadCell alloc] init]; [item setObject:fileTransferCell forKey:@"cell"]; } return fileTransferCell; } } if (!_titleCell) _titleCell = [[CQSubtitleCell alloc] init]; return _titleCell; } return nil; } - (CGFloat) outlineView:(NSOutlineView *) outlineView heightOfRowByItem:(id) item { if (item && [self _isGroupItem:item]) return 19.; CQDownloadCell *cell = [item objectForKey:@"cell"]; if (cell) { if ([item objectForKey:@"type"] == CQActivityTypeFileTransfer) { if (((MVFileTransfer *)[item objectForKey:@"transfer"]).status == MVFileTransferNormalStatus) return 50.; } else { if ([item objectForKey:@"status"] == CQActivityStatusAccepted) return 50.; } } return 40.; } - (BOOL) outlineView:(NSOutlineView *) outlineView shouldCollapseItem:(id) item { if ([self _shouldExpandOrCollapse]) { for (NSDictionary *dictionary in [_activity objectForKey:item]) { CQDownloadCell *cell = [dictionary objectForKey:@"cell"]; if (cell) [cell hideProgressIndicator]; } return YES; } return NO; } - (BOOL) outlineView:(NSOutlineView *) outlineView shouldEditTableColumn:(NSTableColumn *) tableColumn item:(id) item { return NO; } - (BOOL) outlineView:(NSOutlineView *) outlineView shouldExpandItem:(id) item { return [self _shouldExpandOrCollapse]; } - (BOOL) outlineView:(NSOutlineView *) outlineView shouldSelectItem:(id) item { return ![self _isGroupItem:item]; } - (NSString *) outlineView:(NSOutlineView *) outlineView toolTipForCell:(NSCell *) cell rect:(NSRectPointer) rect tableColumn:(NSTableColumn *) tableColumn item:(id) item mouseLocation:(NSPoint) mouseLocation { if ([item isKindOfClass:[MVChatConnection class]]) { NSUInteger invites = [self _invitationCountForConnection:item]; NSUInteger fileTransfers = [self _fileTransferCountForConnection:item]; if (invites) { if (invites > 1) { if (fileTransfers) { if (fileTransfers > 1) return [NSString stringWithFormat:NSLocalizedString(@"%ld file transfers and %ld chat room invites on %@", @"tooltip"), fileTransfers, invites, ((MVChatConnection *)item).server]; return [NSString stringWithFormat:NSLocalizedString(@"1 file transfer and %ld chat room invites on %@", @"tooltip"), invites, ((MVChatConnection *)item).server]; } return [NSString stringWithFormat:NSLocalizedString(@"%ld chat room invites on %@", @"tooltip"), fileTransfers, ((MVChatConnection *)item).server]; } if (fileTransfers) { if (fileTransfers > 1) return [NSString stringWithFormat:NSLocalizedString(@"%ld file transfers and 1 chat room invite on %@", @"tooltip"), fileTransfers, ((MVChatConnection *)item).server]; return [NSString stringWithFormat:NSLocalizedString(@"1 file transfer and 1 chat room invite on %@", @"tooltip"), fileTransfers, ((MVChatConnection *)item).server]; } return [NSString stringWithFormat:NSLocalizedString(@"1 chat room invite on %@", @"tooltip"), fileTransfers, ((MVChatConnection *)item).server]; } if (fileTransfers) { if (fileTransfers > 1) return [NSString stringWithFormat:NSLocalizedString(@"%ld file transfers on %@", @"tooltip"), fileTransfers, ((MVChatConnection *)item).server]; return [NSString stringWithFormat:NSLocalizedString(@"1 file transfer on %@", @"tooltip"), fileTransfers, ((MVChatConnection *)item).server]; } } if (item == CQDirectChatConnectionKey) { NSUInteger count = [self _directChatConnectionCount]; if (count > 1) return [NSString stringWithFormat:NSLocalizedString(@"%ld direct chat invitations", @"tooltip"), count]; return [NSString stringWithFormat:NSLocalizedString(@"1 direct chat invitation", @"tooltip"), count]; } if (item == CQDirectDownloadKey) { NSUInteger count = [self _directDownloadCount]; if (count > 1) return [NSString stringWithFormat:NSLocalizedString(@"%ld downloads", @"tooltip"), count]; return [NSString stringWithFormat:NSLocalizedString(@"1 download", @"tooltip"), count]; } return nil; } - (void) outlineView:(NSOutlineView *) outlineView willDisplayCell:(id) cell forTableColumn:(NSTableColumn *) tableColumn item:(id) item { if ([cell isKindOfClass:[CQGroupCell class]]) { CQGroupCell *groupCell = (CQGroupCell *)cell; if (item == CQDirectChatConnectionKey) groupCell.title = NSLocalizedString(@"Direct Chat Invites", @"Direct Chat Invites group title"); else if (item == CQDirectDownloadKey) groupCell.title = NSLocalizedString(@"Downloads", @"Downloads group title"); else groupCell.title = ((MVChatConnection *)item).server; groupCell.unansweredActivityCount = [outlineView isItemExpanded:item] ? 0 :((NSArray *)[_activity objectForKey:item]).count; return; } CQTitleCell *titleCell = (CQTitleCell *)cell; titleCell.leftButtonCell.target = self; titleCell.rightButtonCell.target = self; NSString *title = nil; NSString *subtitle = nil; BOOL hidesLeftButton = NO; NSString *titleFormat = nil; NSString *type = [item objectForKey:@"type"]; MVChatUser *user = [item objectForKey:@"user"]; NSDate *date = [item objectForKey:@"date"]; NSMutableDictionary *mouseStates = [[NSMutableDictionary alloc] init]; if (type == CQActivityTypeChatInvite) { [mouseStates setObject:[_cellImages objectForKey:@"x"] forKey:CQDualButtonRightDictionaryKey]; NSString *status = [item objectForKey:@"status"]; if (status == CQActivityStatusAccepted) { titleFormat = NSLocalizedString(@"Joined %@ on %@", @"cell label text format"); // subtitle:@"lastMessageHere"; titleCell.leftButtonCell.action = @selector(showChatPanel:); titleCell.rightButtonCell.action = @selector(removeRowFromWindow:); [mouseStates setObject:[_cellImages objectForKey:@"magnifying"] forKey:CQDualButtonLeftDictionaryKey]; } else if (status == CQActivityStatusPending) { titleFormat = NSLocalizedString(@"Invited to %@ on %@", @"cell label text format"); subtitle = [[NSString alloc] initWithFormat:NSLocalizedString(@"By %@ at %@", @"by (user) at (time) cell label subtitle text"), user.nickname, [_timeFormatter stringFromDate:date]]; titleCell.leftButtonCell.action = @selector(acceptChatInvite:); titleCell.rightButtonCell.action = @selector(rejectChatInvite:); [mouseStates setObject:[_cellImages objectForKey:@"check"] forKey:CQDualButtonLeftDictionaryKey]; } else if (status == CQActivityStatusRejected) { titleFormat = NSLocalizedString(@"Ignored invite to %@ on %@", @"Ignored invite to (room) on (server) cell label text format"); subtitle = [[NSString alloc] initWithFormat:NSLocalizedString(@"Invited by %@ at %@", @"by (user) at (time) cell label subtitle text"), user.nickname, [_timeFormatter stringFromDate:date]]; titleCell.leftButtonCell.action = @selector(requestChatInvite:); titleCell.rightButtonCell.action = @selector(removeRowFromWindow:); [mouseStates setObject:[_cellImages objectForKey:@"retry"] forKey:CQDualButtonLeftDictionaryKey]; } title = [[NSString alloc] initWithFormat:titleFormat, [item objectForKey:@"room"], ((MVChatConnection *)[_outlineView parentForItem:item]).server]; } if (type == CQActivityTypeDirectChatInvite) { [mouseStates setObject:[_cellImages objectForKey:@"x"] forKey:CQDualButtonRightDictionaryKey]; MVDirectChatConnection *connection = [item objectForKey:@"connection"]; switch (connection.status) { case MVDirectChatConnectionConnectedStatus: titleFormat = NSLocalizedString(@"Accepted direct chat with %@", @"cell label text format"); // subtitle:show last chat line titleCell.leftButtonCell.action = @selector(showChatPanel:); titleCell.rightButtonCell.action = @selector(removeRowFromWindow:); [mouseStates setObject:[_cellImages objectForKey:@"magnifying"] forKey:CQDualButtonLeftDictionaryKey]; break; case MVDirectChatConnectionWaitingStatus: titleFormat = NSLocalizedString(@"Direct chat request from %@", @"cell label text format"); // show shared chat rooms titleCell.leftButtonCell.action = @selector(acceptChatInvite:); titleCell.rightButtonCell.action = @selector(rejectChatInvite:); [mouseStates setObject:[_cellImages objectForKey:@"check"] forKey:CQDualButtonLeftDictionaryKey]; break; case MVDirectChatConnectionDisconnectedStatus: hidesLeftButton = YES; titleFormat = NSLocalizedString(@"Ended direct chat with %@", @"cell label text format"); // show last chat line titleCell.leftButtonCell.action = @selector(showChatPanel:); titleCell.rightButtonCell.action = @selector(removeRowFromWindow:); [mouseStates setObject:[_cellImages objectForKey:@"magnifying"] forKey:CQDualButtonLeftDictionaryKey]; break; case MVDirectChatConnectionErrorStatus: titleFormat = NSLocalizedString(@"Error during direct chat with %@", @"cell label text format"); // show error reason titleCell.leftButtonCell.action = @selector(requestChatInvite:); titleCell.rightButtonCell.action = @selector(removeRowFromWindow:); [mouseStates setObject:[_cellImages objectForKey:@"retry"] forKey:CQDualButtonLeftDictionaryKey]; break; } title = [[NSString alloc] initWithFormat:titleFormat, connection.user.displayName]; } if (type == CQActivityTypeDirectDownload) { NSString *size = MVPrettyFileSize([[item objectForKey:@"size"] unsignedLongLongValue]); NSString *transferred = MVPrettyFileSize([[item objectForKey:@"transferred"] unsignedLongLongValue]); NSString *rate = MVPrettyFileSize([[item objectForKey:@"rate"] unsignedLongLongValue]); CQFileSize remainingBytes = [[item objectForKey:@"size"] unsignedLongLongValue] - [[item objectForKey:@"transferred"] unsignedLongLongValue]; CQFileSize rateValue = [[item objectForKey:@"rate"] unsignedLongLongValue]; NSString *eta = nil; if (rateValue) { NSTimeInterval etaValue = [[NSDate date] timeIntervalSince1970] + (remainingBytes / rateValue); eta = MVReadableTime(etaValue, YES); } NSError *error = [item objectForKey:@"error"]; title = [[[item objectForKey:@"path"] lastPathComponent] retain]; NSString *status = [item objectForKey:@"status"]; BOOL rightButtonActionSet = NO; BOOL removeProgressIndicatorFromScreen = YES; if (status == CQActivityStatusError) { subtitle = [[NSString alloc] initWithFormat:NSLocalizedString(@"%@ of %@ (%@ - %ld)", @"x bytes of y bytes (error domain, error code) subtitle"), transferred, size, error.domain, error.code]; titleCell.leftButtonCell.action = @selector(retryDownload:); [mouseStates setObject:[_cellImages objectForKey:@"retry"] forKey:CQDualButtonLeftDictionaryKey]; } else if (status == CQActivityStatusFinished) { subtitle = [size retain]; hidesLeftButton = YES; [item removeObjectForKey:@"cell"]; } else if (status == CQActivityStatusAccepted) { subtitle = [[NSString alloc] initWithFormat:NSLocalizedString(@"%@ of %@ (%@/sec) — %@", @"x bytes of y bytes, (rate) - eta subtitle"), transferred, size, rate, eta]; CQDownloadCell *downloadCell = [item objectForKey:@"cell"]; CQFileSize transferred = [[item objectForKey:@"transferred"] unsignedLongLongValue]; CQFileSize size = [[item objectForKey:@"size"] unsignedLongLongValue]; downloadCell.progressIndicator.doubleValue = ((double)transferred / (double)size); titleCell.leftButtonCell.action = @selector(cancelDownload:); [mouseStates setObject:[_cellImages objectForKey:@"x"] forKey:CQDualButtonLeftDictionaryKey]; removeProgressIndicatorFromScreen = NO; } else if (status == CQActivityStatusPending) { subtitle = NSLocalizedString(@"Preparing to download.", @"Preparing to download subtitle"); hidesLeftButton = YES; titleCell.rightButtonCell.action = @selector(cancelDownload:); rightButtonActionSet = YES; [mouseStates setObject:[_cellImages objectForKey:@"x"] forKey:CQDualButtonRightDictionaryKey]; } else if (status == CQActivityStatusRejected) { subtitle = [[NSString alloc] initWithFormat:NSLocalizedString(@"%@ of %@ — stopped", @"x bytes of y bytes - stopped subtitle"), transferred, size]; titleCell.leftButtonCell.action = @selector(retryDownload:); [mouseStates setObject:[_cellImages objectForKey:@"retry"] forKey:CQDualButtonLeftDictionaryKey]; } if (!rightButtonActionSet) { titleCell.rightButtonCell.action = @selector(showFileInFinder:); [mouseStates setObject:[_cellImages objectForKey:@"magnifying"] forKey:CQDualButtonRightDictionaryKey]; } if (removeProgressIndicatorFromScreen) { CQDownloadCell *downloadCell = [item objectForKey:@"cell"]; [downloadCell hideProgressIndicator]; } } if (type == CQActivityTypeFileTransfer) { MVFileTransfer *transfer = [item objectForKey:@"transfer"]; NSString *size = MVPrettyFileSize(transfer.finalSize); NSString *transferred = MVPrettyFileSize(transfer.transferred); NSString *rate = MVPrettyFileSize([[item objectForKey:@"rate"] unsignedLongLongValue]); CQFileSize remainingBytes = transfer.finalSize - transfer.transferred; CQFileSize rateValue = [[item objectForKey:@"rate"] unsignedLongLongValue]; NSString *eta = nil; if (rateValue) { NSTimeInterval etaValue = [[NSDate date] timeIntervalSince1970] + (remainingBytes / rateValue); eta = MVReadableTime(etaValue, YES); } NSError *error = transfer.lastError; if ([transfer isDownload]) title = [((MVDownloadFileTransfer *)transfer).originalFileName retain]; else title = [[((MVUploadFileTransfer *)transfer).source lastPathComponent] retain]; BOOL rightButtonActionSet = NO; CQDownloadCell *downloadCell = nil; switch (transfer.status) { case MVFileTransferErrorStatus: subtitle = [[NSString alloc] initWithFormat:NSLocalizedString(@"%@ of %@ (%@ - %ld)", @"x bytes of y bytes (error domain, error code) subtitle"), transferred, size, error.domain, error.code]; hidesLeftButton = YES; break; case MVFileTransferDoneStatus: subtitle = [size retain]; hidesLeftButton = YES; break; case MVFileTransferNormalStatus: subtitle = [[NSString alloc] initWithFormat:NSLocalizedString(@"%@ of %@ (%@/sec) — %@", @"x bytes of y bytes, (rate) - eta subtitle"), transferred, size, rate, eta]; downloadCell = [item objectForKey:@"cell"]; downloadCell.progressIndicator.doubleValue = (transfer.transferred / transfer.finalSize); titleCell.leftButtonCell.action = @selector(cancelFileTransfer:); [mouseStates setObject:[_cellImages objectForKey:@"x"] forKey:CQDualButtonLeftDictionaryKey]; break; case MVFileTransferHoldingStatus: subtitle = [[NSString alloc] initWithFormat:NSLocalizedString(@"From %@ on %@", @"From %@ on %@ subtitle"), transfer.user.nickname, transfer.user.connection.server]; titleCell.leftButtonCell.action = @selector(acceptFileTransfer:); titleCell.rightButtonCell.action = @selector(rejectFileTransfer:); rightButtonActionSet = YES; break; case MVFileTransferStoppedStatus: subtitle = [[NSString alloc] initWithFormat:NSLocalizedString(@"%@ of %@ — stopped", @"x bytes of y bytes - stopped subtitle"), transferred, size]; hidesLeftButton = YES; break; } if (!rightButtonActionSet) { titleCell.rightButtonCell.action = @selector(showFileInFinder:); [mouseStates setObject:[_cellImages objectForKey:@"magnifying"] forKey:CQDualButtonRightDictionaryKey]; } } if (hidesLeftButton) { titleCell.hidesLeftButton = YES; titleCell.leftButtonCell.action = NULL; } else titleCell.hidesLeftButton = NO; titleCell.titleText = title; [title release]; if ([cell respondsToSelector:@selector(setSubtitleText:)]) { [cell setSubtitleText:subtitle]; [subtitle release]; } titleCell.mouseStates = mouseStates; [mouseStates release]; } #pragma mark - - (NSString *) panel:(id) sender userEnteredFilename:(NSString *) filename confirmed:(BOOL) confirmed { return (confirmed ? [filename stringByAppendingString:@".colloquyFake"] :filename); } #pragma mark - - (id) _itemForTransfer:(MVFileTransfer *) transfer { for (NSDictionary *activity in [_activity objectForKey:transfer.user.connection]) { if ([activity objectForKey:@"type"] != CQActivityTypeFileTransfer) continue; if ([activity objectForKey:@"transfer"] == transfer) return activity; } return nil; } - (void) _setDestination:(NSString *) destination forTransfer:(MVDownloadFileTransfer *) transfer checkIfFileExists:(BOOL) checkIfFileExists { if (!destination.length) destination = transfer.destination; BOOL resumeIfPossible = YES; if (checkIfFileExists) { if ([[NSFileManager defaultManager] fileExistsAtPath:destination]) { NSNumber *size = [[[NSFileManager defaultManager] attributesOfItemAtPath:destination error:nil] objectForKey:NSFileSize]; if (size.unsignedLongLongValue < transfer.finalSize) { // if ([[NSUserDefaults standardUserDefaults] integerForKey:@"JVFileExists"] == 1) // auto resume, do nothing if ([[NSUserDefaults standardUserDefaults] integerForKey:@"JVFileExists"] == 2) // auto cancel return; else if ([[NSUserDefaults standardUserDefaults] integerForKey:@"JVFileExists"] == 3) // auto overwrite resumeIfPossible = NO; // and fall through else { NSInteger result = NSRunAlertPanel(NSLocalizedString(@"Save", "save dialog title"), NSLocalizedString(@"The file %@ in %@ already exists. Would you like to resume from where a previous transfer stopped or replace it?", "replace or resume transfer save dialog message"), NSLocalizedString(@"Resume", "resume button name"), NSLocalizedString(@"Replace", "replace button name"), NSLocalizedString(@"Save As…", "save as button name"), NSLocalizedString(@"Cancel", "cancel button"), [[NSFileManager defaultManager] displayNameAtPath:destination], [destination stringByDeletingLastPathComponent]); if (result == 4) // cancel return; if (result == 3) { // save as [self _setDestinationForTransfer:transfer shouldAsk:YES]; return; } if (result == 2) // replace resumeIfPossible = NO; // else if (result == 1) // resume, do nothing } } else { NSInteger result = NSRunAlertPanel(NSLocalizedString(@"Save", "save dialog title"), NSLocalizedString(@"The file %@ in %@ already exists and can't be resumed. Replace it?", "replace transfer save dialog message"), NSLocalizedString(@"Replace", "replace button name"), NSLocalizedString(@"Save As…", "save as button name"), NSLocalizedString(@"Cancel", "cancel button"), nil, [[NSFileManager defaultManager] displayNameAtPath:destination], [destination stringByDeletingLastPathComponent]); if (result == 2) { // save as [self _setDestinationForTransfer:transfer shouldAsk:YES]; return; } if (result == 3) // cancel return; if (result == 1) // replace resumeIfPossible = NO; } } } else resumeIfPossible = NO; [transfer setDestination:destination renameIfFileExists:NO]; [transfer acceptByResumingIfPossible:resumeIfPossible]; [_outlineView reloadItem:[self _itemForTransfer:transfer]]; } - (void) _fileSavePanelDidEnd:(NSSavePanel *) savePanel returnCode:(int) returnCode contextInfo:(void *) context { if (returnCode == NSCancelButton) return; MVFileTransfer *transfer = (MVFileTransfer *)context; [self _setDestination:savePanel.URL.absoluteString forTransfer:(MVDownloadFileTransfer *)transfer checkIfFileExists:NO]; } - (void) _setDestinationForTransfer:(MVFileTransfer *) transfer shouldAsk:(BOOL) shouldAsk { MVDownloadFileTransfer *downloadFileTransfer = (MVDownloadFileTransfer *)transfer; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"JVAskForTransferSaveLocation"] || shouldAsk) { NSSavePanel *savePanel = [NSSavePanel savePanel]; savePanel.nameFieldLabel = downloadFileTransfer.originalFileName; [savePanel beginSheetForDirectory:[MVFileTransferController userPreferredDownloadFolder] file:downloadFileTransfer.originalFileName modalForWindow:nil modalDelegate:self didEndSelector:@selector(_fileSavePanelDidEnd:returnCode:contextInfo:) contextInfo:transfer]; return; } [self _setDestination:[[MVFileTransferController userPreferredDownloadFolder] stringByAppendingPathComponent:downloadFileTransfer.originalFileName] forTransfer:downloadFileTransfer checkIfFileExists:YES]; } #pragma mark - - (void) cancelFileTransfer:(id) sender { id item = [_outlineView itemAtRow:[sender clickedRow]]; if ([item objectForKey:@"type"] != CQActivityTypeFileTransfer) return; MVFileTransfer *transfer = [item objectForKey:@"transfer"]; [transfer cancel]; [_outlineView reloadItem:item]; } - (void) acceptFileTransfer:(id) sender { id item = [_outlineView itemAtRow:[sender clickedRow]]; MVDownloadFileTransfer *transfer = [item objectForKey:@"transfer"]; [self _setDestinationForTransfer:transfer shouldAsk:NO]; } - (void) rejectFileTransfer:(id) sender { id item = [_outlineView itemAtRow:[sender clickedRow]]; MVDownloadFileTransfer *transfer = [item objectForKey:@"transfer"]; [transfer reject]; [_outlineView reloadItem:item]; } #pragma mark - - (void) showFileInFinder:(id) sender { id item = [_outlineView itemAtRow:[sender clickedRow]]; NSString *file = nil; if ([item objectForKey:@"type"] == CQActivityTypeFileTransfer) { MVFileTransfer *transfer = [item objectForKey:@"transfer"]; if ([transfer isUpload]) { NSString *source = ((MVUploadFileTransfer *)transfer).source; file = [source stringByReplacingOccurrencesOfString:[source lastPathComponent] withString:@""]; } else file = ((MVDownloadFileTransfer *)transfer).destination; } else file = [item objectForKey:@"path"]; [[NSWorkspace sharedWorkspace] selectFile:file inFileViewerRootedAtPath:@""]; } #pragma mark - - (void) cancelDownload:(id) sender { id item = [_outlineView itemAtRow:[sender clickedRow]]; [[item objectForKey:@"download"] cancel]; [item setObject:CQActivityStatusRejected forKey:@"status"]; [_outlineView reloadData]; } - (void) retryDownload:(id) sender { id item = [_outlineView itemAtRow:[sender clickedRow]]; if ([item objectForKey:@"status"] == CQActivityStatusPending) return; WebDownload *oldDownload = [item objectForKey:@"download"]; WebDownload *newDownload = nil; if (oldDownload.resumeData.length) newDownload = [[WebDownload alloc] initWithResumeData:oldDownload.resumeData delegate:self path:[item objectForKey:@"path"]]; else newDownload = [[WebDownload alloc] initWithRequest:[NSURLRequest requestWithURL:oldDownload.request.URL] delegate:self]; [item setObject:newDownload forKey:@"download"]; [item setObject:CQActivityStatusPending forKey:@"status"]; [newDownload release]; [_outlineView reloadData]; } #pragma mark - - (void) acceptChatInvite:(id) sender { id item = [_outlineView itemAtRow:[sender clickedRow]]; if ([item objectForKey:@"type"] == CQDirectChatConnectionKey) { MVDirectChatConnection *connection = [_outlineView parentForItem:item]; [[JVChatController defaultController] chatViewControllerForDirectChatConnection:connection ifExists:NO userInitiated:NO]; [connection initiate]; return; } id room = [item objectForKey:@"room"]; if ([room isKindOfClass:[MVChatRoom class]]) room = ((MVChatRoom *)room).name; [[_outlineView parentForItem:item] joinChatRoomNamed:room]; [item setObject:CQActivityStatusAccepted forKey:@"status"]; [_outlineView reloadItem:item]; } - (void) rejectChatInvite:(id) sender { id item = [_outlineView itemAtRow:[sender clickedRow]]; [item setObject:[NSDate date] forKey:@"date"]; [item setObject:CQActivityStatusRejected forKey:@"status"]; [_outlineView reloadItem:item]; } - (void) requestChatInvite:(id) sender { id item = [_outlineView itemAtRow:[sender clickedRow]]; if ([item objectForKey:@"type"] == CQDirectChatConnectionKey) { // start a new dcc chat return; } // /knock } #pragma mark - - (void) showChatPanel:(id) sender { } - (void) removeRowFromWindow:(id) sender { id item = [_outlineView itemAtRow:[sender clickedRow]]; if ([item objectForKey:@"type"] == CQActivityTypeFileTransfer) [item removeObjectForKey:@"cell"]; [[_activity objectForKey:[_outlineView parentForItem:item]] removeObjectIdenticalTo:item]; [_outlineView reloadData]; } #pragma mark - - (NSUInteger) _countForType:(NSString *) type inConnection:(id) connection { NSUInteger count = 0; for (NSDictionary *dictionary in [_activity objectForKey:connection]) if ([dictionary objectForKey:@"type"] == type) count++; return count; } - (NSUInteger) _directChatConnectionCount { return [self _countForType:CQActivityTypeDirectChatInvite inConnection:CQDirectChatConnectionKey]; } - (NSUInteger) _directDownloadCount { return [self _countForType:CQActivityTypeDirectDownload inConnection:CQDirectDownloadKey]; } - (NSUInteger) _fileTransferCountForConnection:(MVChatConnection *) connection { return [self _countForType:CQActivityTypeFileTransfer inConnection:connection]; } - (NSUInteger) _invitationCountForConnection:(MVChatConnection *) connection { return [self _countForType:CQActivityTypeChatInvite inConnection:connection]; } #pragma mark - - (BOOL) _isGroupItem:(id) item { return ([item isKindOfClass:[MVChatConnection class]] || item == CQDirectChatConnectionKey || item == CQDirectDownloadKey); } - (BOOL) _shouldExpandOrCollapse { if (!_rowLastClickedTime) { _rowLastClickedTime = [NSDate timeIntervalSinceReferenceDate]; return YES; } NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate]; BOOL shouldExpandOrCollapse = ((currentTime - _rowLastClickedTime) > CQExpandCollapseRowInterval); _rowLastClickedTime = currentTime; return shouldExpandOrCollapse; } #pragma mark - - (void) _appendActivity:(NSDictionary *) activity forConnection:(id) connection { NSMutableArray *activities = [_activity objectForKey:connection]; NSString *type = [activity objectForKey:@"type"]; if (type == CQActivityTypeFileTransfer || type == CQActivityTypeDirectDownload) // file transfers are sorted by time added, so just add to the end [activities addObject:activity]; if (type == CQActivityTypeChatInvite) { NSUInteger insertionPoint = 0; for (NSDictionary *existingActivity in activities) { type = [existingActivity objectForKey:@"type"]; if (type == CQActivityTypeFileTransfer || type == CQActivityTypeDirectDownload) // File transfers are at the end and we want to insert above it break; if (type == CQActivityTypeChatInvite) continue; if ([[activity objectForKey:@"room"] compare:[existingActivity objectForKey:@"room"]] == NSOrderedDescending) insertionPoint++; else break; } [activities insertObject:activity atIndex:insertionPoint]; } if (type == CQActivityTypeDirectChatInvite) { NSUInteger insertionPoint = 0; id newUser = [activity objectForKey:@"user"]; for (NSDictionary *existingActivity in activities) { if ([newUser compare:[existingActivity objectForKey:@"user"]] != NSOrderedDescending) // multiple dcc chat sessions for the same username are valid, added to the end, after the current ones. insertionPoint++; else break; } [activities insertObject:activity atIndex:insertionPoint]; } } @end