PageRenderTime 69ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/lilypad/Sources/LPChatController.m

https://github.com/sapo/sapo-messenger-for-mac
Objective C | 2505 lines | 1802 code | 555 blank | 148 comment | 324 complexity | f3da6a704229ecf524559baf8c9aa706 MD5 | raw file
Possible License(s): GPL-2.0, AGPL-1.0, LGPL-2.1, BSD-3-Clause
  1. //
  2. // LPChatController.m
  3. // Lilypad
  4. //
  5. // Copyright (C) 2006-2008 PT.COM, All rights reserved.
  6. // Authors: Joao Pavao <jpavao@co.sapo.pt>
  7. // Jason Kim <jason@512k.org>
  8. //
  9. // For more information on licensing, read the README file.
  10. // Para mais informa›es sobre o licenciamento, leia o ficheiro README.
  11. //
  12. #import "LPChatController.h"
  13. #import "LPCommon.h"
  14. #import "LPAccount.h"
  15. #import "LPRoster.h"
  16. #import "LPContact.h"
  17. #import "LPContactEntry.h"
  18. #import "LPChat.h"
  19. #import "LPChatsManager.h"
  20. #import "LPFileTransfersManager.h"
  21. #import "NSString+HTMLAdditions.h"
  22. #import "LPChatWebView.h"
  23. #import "LPChatTextField.h"
  24. #import "LPChatViewsController.h"
  25. #import "LPColorBackgroundView.h"
  26. #import "NSxString+EmoticonAdditions.h"
  27. #import "LPAccountsController.h"
  28. #import "LPAudiblesDrawerController.h"
  29. #import "LPAudibleSet.h"
  30. #import "LPChatJavaScriptInterface.h"
  31. #import "LPPubManager.h"
  32. #import "LPEventNotificationsHandler.h"
  33. #import "CTBadge.h"
  34. #import "LPRecentMessagesStore.h"
  35. #import "LPFileTransfer.h"
  36. #import "LPJIDEntryView.h"
  37. #import "LPSapoAgents+MenuAdditions.h"
  38. #import "IconFamily.h"
  39. #import <AddressBook/AddressBook.h>
  40. #define INPUT_LINE_HISTORY_ITEMS_MAX 10
  41. // Toolbar item identifiers
  42. static NSString *ToolbarInfoIdentifier = @"ToolbarInfoIdentifier";
  43. static NSString *ToolbarFileSendIdentifier = @"ToolbarFileSendIdentifier";
  44. static NSString *ToolbarSendSMSIdentifier = @"ToolbarSendSMSIdentifier";
  45. static NSString *ToolbarHistoryIdentifier = @"ToolbarHistoryIdentifier";
  46. @interface LPChatController () // Private Methods
  47. - (void)p_syncChatOwnerName;
  48. - (void)p_syncViewsWithContact;
  49. - (void)p_syncStatusMessageTextFieldWithContact;
  50. - (void)p_setChat:(LPChat *)chat;
  51. - (NSAttributedString *)p_attributedTitleOfJIDMenuItemForContactEntry:(LPContactEntry *)entry withFont:(NSFont *)font;
  52. - (NSMenuItem *)p_popupMenuHeaderItemForAccount:(LPAccount *)account;
  53. - (NSMenuItem *)p_popupMenuItemForEntry:(LPContactEntry *)entry;
  54. - (void)p_moveJIDMenuItem:(NSMenuItem *)menuItem toIndex:(int)targetIndex inMenu:(NSMenu *)menu;
  55. - (void)p_syncJIDsPopupMenu;
  56. - (void)p_setSendFieldHidden:(BOOL)hiddenFlag animate:(BOOL)animateFlag;
  57. - (void)p_fixResizeIndicator;
  58. - (NSMutableSet *)p_pendingAudiblesSet;
  59. - (void)p_appendStandardMessageBlockWithInnerHTML:(NSString *)innerHTML timestamp:(NSDate *)timestamp inbound:(BOOL)isInbound saveInHistory:(BOOL)shouldSave scrollMode:(LPScrollToVisibleMode)scrollMode;
  60. - (void)p_appendMessageToWebView:(NSString *)message subject:(NSString *)subject timestamp:(NSDate *)timestamp inbound:(BOOL)isInbound;
  61. - (void)p_appendAudibleWithResourceName:(NSString *)resourceName inbound:(BOOL)inbound;
  62. - (void)p_appendStoredRecentMessagesToWebView;
  63. - (void)p_resizeInputFieldToContentsSize:(NSSize)newSize;
  64. - (void)p_updateChatBackgroundColorFromDefaults;
  65. - (void)p_setupToolbar;
  66. - (void)p_setupChatDocumentTitle;
  67. - (void)p_setSaveChatTranscriptEnabled:(BOOL)flag;
  68. - (void)p_displayAndReloadPubBannerIfNeeded;
  69. - (void)p_incrementUnreadMessagesCount;
  70. - (void)p_resetUnreadMessagesCount;
  71. - (void)p_updateMiniwindowImage;
  72. - (void)p_notifyUserAboutReceivedMessage:(NSString *)msgText notificationsHandlerSelector:(SEL)selector;
  73. - (void)p_reevaluateJIDPanelOKButtonEnabled;
  74. @end
  75. #pragma mark -
  76. @implementation LPChatController
  77. + (void)initialize
  78. {
  79. if (self == [LPChatController class]) {
  80. [self setKeys:[NSArray arrayWithObject:@"numberOfUnreadMessages"]
  81. triggerChangeNotificationsForDependentKey:@"windowTitleSuffix"];
  82. }
  83. }
  84. - initWithDelegate:(id)delegate
  85. {
  86. return [self initWithChat:nil delegate:delegate isIncoming:NO];
  87. }
  88. // Designated Initializer
  89. - initWithChat:(LPChat *)chat delegate:(id)delegate isIncoming:(BOOL)incomingFlag
  90. {
  91. if (self = [self initWithWindowNibName:@"Chat"]) {
  92. [self p_setChat:chat];
  93. [self setContact:[chat contact]];
  94. m_dateStarted = [[NSDate alloc] init];
  95. [self setDelegate:delegate];
  96. m_collapsedHeightWhenLastWentOffline = 0.0;
  97. // Setup KVO
  98. NSUserDefaultsController *prefsCtrl = [NSUserDefaultsController sharedUserDefaultsController];
  99. [prefsCtrl addObserver:self forKeyPath:@"values.ChatBackgroundColor" options:0 context:NULL];
  100. [prefsCtrl addObserver:self forKeyPath:@"values.DisplayEmoticonImages" options:0 context:NULL];
  101. NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  102. LPAudibleSet *as = [LPAudibleSet defaultAudibleSet];
  103. [nc addObserver:self
  104. selector:@selector(audibleSetDidFinishLoadingAudible:)
  105. name:LPAudibleSetAudibleDidFinishLoadingNotification
  106. object:as];
  107. [nc addObserver:self
  108. selector:@selector(audibleSetDidFinishLoadingAudible:)
  109. name:LPAudibleSetAudibleDidFailLoadingNotification
  110. object:as];
  111. // File Transfers status messages
  112. [nc addObserver:self
  113. selector:@selector(fileTransferStateDidChange:)
  114. name:LPFileTransferDidChangeStateNotification
  115. object:nil];
  116. // Chat History
  117. [prefsCtrl addObserver:self forKeyPath:@"values.SaveChatTranscripts" options:0 context:NULL];
  118. [self p_setSaveChatTranscriptEnabled:[[prefsCtrl valueForKeyPath:@"values.SaveChatTranscripts"] boolValue]];
  119. // Input line history
  120. m_inputLineHistory = [[NSMutableArray alloc] init];
  121. LPAccountsController *accountsController = [LPAccountsController sharedAccountsController];
  122. [accountsController addObserver:self forKeyPath:@"name" options:0 context:NULL];
  123. [accountsController addObserver:self forKeyPath:@"online" options:0 context:NULL];
  124. m_dontMakeKeyOnFirstShowWindow = incomingFlag;
  125. }
  126. return self;
  127. }
  128. - initWithIncomingChat:(LPChat *)newChat delegate:(id)delegate
  129. {
  130. return [self initWithChat:newChat delegate:delegate isIncoming:YES];
  131. }
  132. - initOutgoingWithContact:(LPContact *)contact delegate:(id)delegate
  133. {
  134. LPChat *newChat = [[LPChatsManager chatsManager] startChatWithContact:contact];
  135. if (newChat) {
  136. self = [self initWithChat:newChat delegate:delegate isIncoming:NO];
  137. }
  138. else {
  139. [self release];
  140. self = nil;
  141. }
  142. return self;
  143. }
  144. - initOutgoingWithContactEntry:(LPContactEntry *)contactEntry delegate:(id)delegate
  145. {
  146. LPChat *newChat = [[LPChatsManager chatsManager] startChatWithContactEntry:contactEntry];
  147. if (newChat) {
  148. self = [self initWithChat:newChat delegate:delegate isIncoming:NO];
  149. }
  150. else {
  151. [self release];
  152. self = nil;
  153. }
  154. return self;
  155. }
  156. - (void)dealloc
  157. {
  158. NSUserDefaultsController *prefsCtrl = [NSUserDefaultsController sharedUserDefaultsController];
  159. [prefsCtrl removeObserver:self forKeyPath:@"values.ChatBackgroundColor"];
  160. [prefsCtrl removeObserver:self forKeyPath:@"values.DisplayEmoticonImages"];
  161. [prefsCtrl removeObserver:self forKeyPath:@"values.SaveChatTranscripts"];
  162. [[NSNotificationCenter defaultCenter] removeObserver:self];
  163. LPAccountsController *accountsController = [LPAccountsController sharedAccountsController];
  164. [accountsController removeObserver:self forKeyPath:@"name"];
  165. [accountsController removeObserver:self forKeyPath:@"online"];
  166. [self p_setChat:nil];
  167. [self setContact:nil];
  168. [self setDelegate:nil];
  169. [m_dateStarted release];
  170. [m_inputLineHistory release];
  171. [m_autoSaveChatTranscriptTimer invalidate];
  172. [m_autoSaveChatTranscriptTimer release];
  173. [m_unreadMessagesBadge release];
  174. [m_audibleResourceNamesWaitingForLoadCompletion release];
  175. [m_chatJSInterface release];
  176. [super dealloc];
  177. }
  178. - (void)p_syncChatOwnerName
  179. {
  180. NSString *currentOwnerName = [m_chatViewsController ownerName];
  181. NSString *globalName = [[LPAccountsController sharedAccountsController] name];
  182. NSString *newOwnerName = ( [globalName length] > 0 ?
  183. globalName :
  184. [[[m_chat activeContactEntry] account] JID] );
  185. if (![currentOwnerName isEqualToString:newOwnerName])
  186. [m_chatViewsController setOwnerName:newOwnerName];
  187. }
  188. - (void)p_syncViewsWithContact
  189. {
  190. if ([self isWindowLoaded]) {
  191. [m_chatController setContent:[self chat]];
  192. [m_contactController setContent:[self contact]];
  193. [m_chatWebView setChat:m_chat];
  194. [self p_syncChatOwnerName];
  195. [m_topControlsBar setBackgroundColor:
  196. [NSColor colorWithPatternImage:( [[m_chat activeContactEntry] isOnline] ?
  197. [NSImage imageNamed:@"chatIDBackground"] :
  198. [NSImage imageNamed:@"chatIDBackground_Offline"] )]];
  199. [self p_syncStatusMessageTextFieldWithContact];
  200. // Update the addresses popup
  201. [self p_syncJIDsPopupMenu];
  202. [m_addressesPopUp setEnabled:([[m_contact chatContactEntries] count] > 0)];
  203. [self p_setSendFieldHidden:(![[[[self chat] activeContactEntry] account] isOnline] || [[self chat] activeContactEntry] == nil) animate:YES];
  204. [self p_updateMiniwindowImage];
  205. // Make sure the toolbar items are correctly enabled/disabled
  206. [[self window] update];
  207. }
  208. }
  209. - (void)p_syncStatusMessageTextFieldWithContact
  210. {
  211. NSAttributedString *attributedStatusMessage = [m_contact attributedStatusMessage];
  212. if ([attributedStatusMessage length] == 0) {
  213. [m_statusMessageTextField setStringValue:@""];
  214. }
  215. else {
  216. NSMutableAttributedString *newStatusMessage = [attributedStatusMessage mutableCopy];
  217. unsigned int location = 0;
  218. NSRange effectiveRange = { 0, 0 };
  219. NSRange wholeStrRange = NSMakeRange(0, [newStatusMessage length]);
  220. while (NSLocationInRange(location, wholeStrRange)) {
  221. NSColor *existingFGColor = [newStatusMessage attribute:NSForegroundColorAttributeName
  222. atIndex:location
  223. effectiveRange:&effectiveRange];
  224. // If there isn't a foreground color already defined by the string at this range, then add our own.
  225. if (existingFGColor == nil) {
  226. [newStatusMessage addAttribute:NSForegroundColorAttributeName
  227. value:[m_statusMessageTextField textColor]
  228. range:effectiveRange];
  229. }
  230. location = NSMaxRange(effectiveRange);
  231. }
  232. [newStatusMessage addAttribute:NSFontAttributeName
  233. value:[m_statusMessageTextField font]
  234. range:wholeStrRange];
  235. [m_statusMessageTextField setAttributedStringValue:newStatusMessage];
  236. [newStatusMessage release];
  237. }
  238. }
  239. - (void)windowDidLoad
  240. {
  241. [self p_setupToolbar];
  242. [m_audiblesController setChatController:self];
  243. // Workaround for centering the icons.
  244. [m_segmentedButton setLabel:nil forSegment:0];
  245. [m_segmentedButton setLabel:nil forSegment:1];
  246. [[m_segmentedButton cell] setToolTip:NSLocalizedString(@"Choose Emoticon", @"") forSegment:0];
  247. [[m_segmentedButton cell] setToolTip:NSLocalizedString(@"Toggle Audibles Drawer", @"") forSegment:1];
  248. // IB displays a round segmented button that apparently needs less space than the on that ends up
  249. // showing in the app (the flat segmented button used in metal windows).
  250. [m_segmentedButton sizeToFit];
  251. [m_topControlsBar setBorderColor:[NSColor colorWithCalibratedWhite:0.60 alpha:1.0]];
  252. [m_pubElementsView setShadedBackgroundWithOrientation:LPVerticalBackgroundShading
  253. minEdgeColor:[NSColor colorWithCalibratedWhite:0.79 alpha:1.0]
  254. maxEdgeColor:[NSColor colorWithCalibratedWhite:0.49 alpha:1.0]];
  255. [[NSNotificationCenter defaultCenter] addObserver:self
  256. selector:@selector(p_JIDsMenuWillPop:)
  257. name:NSPopUpButtonWillPopUpNotification
  258. object:m_addressesPopUp];
  259. [m_addressesPopUp setAutoenablesItems:NO];
  260. [self p_syncViewsWithContact];
  261. // Post the saved recent messages
  262. [self p_appendStoredRecentMessagesToWebView];
  263. if ([m_chat activeContactEntry]) {
  264. // Post a "system message" to start
  265. NSString *initialSystemMessage = nil;
  266. if ([[[LPAccountsController sharedAccountsController] accounts] count] > 1) {
  267. initialSystemMessage = [NSString stringWithFormat:NSLocalizedString(@"Chat started with contact \"%@\" thru account \"%@\"",
  268. @"status message written to the text transcript of a chat window"),
  269. [[m_chat activeContactEntry] humanReadableAddress],
  270. [[[m_chat activeContactEntry] account] description]];
  271. }
  272. else {
  273. initialSystemMessage = [NSString stringWithFormat:NSLocalizedString(@"Chat started with contact \"%@\"",
  274. @"status message written to the text transcript of a chat window"),
  275. [[m_chat activeContactEntry] humanReadableAddress]];
  276. }
  277. [m_chatViewsController appendDIVBlockToWebViewWithInnerHTML:[initialSystemMessage stringByEscapingHTMLEntities]
  278. divClass:@"systemMessage"
  279. scrollToVisibleMode:LPScrollWithJump];
  280. }
  281. }
  282. - (void)showWindow:(id)sender
  283. {
  284. if (m_contact == nil) {
  285. NSWindow *win = [self window];
  286. BOOL wasVisible = [win isVisible];
  287. [super showWindow:sender];
  288. if (!wasVisible) {
  289. [self p_reevaluateJIDPanelOKButtonEnabled];
  290. [m_chooseJIDPanelJIDEntryView addObserver:self forKeyPath:@"account.online" options:0 context:NULL];
  291. [NSApp beginSheet:m_chooseJIDPanel modalForWindow:win modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
  292. }
  293. }
  294. else {
  295. BOOL windowWasNotLoadedYet = (![self isWindowLoaded]);
  296. NSWindow *win = [self window];
  297. NSRect savedWindowFrame = [[self class] savedWindowFrameForChatWithContactNamed:[[self contact] name]];
  298. if (!NSIsEmptyRect(savedWindowFrame)) {
  299. [win setFrame:savedWindowFrame display:YES];
  300. }
  301. if (windowWasNotLoadedYet && m_dontMakeKeyOnFirstShowWindow && [NSApp mainWindow] != nil) {
  302. [win orderWindow:NSWindowBelow relativeTo:[[NSApp mainWindow] windowNumber]];
  303. }
  304. else {
  305. [super showWindow:sender];
  306. }
  307. }
  308. }
  309. - (id)delegate
  310. {
  311. return m_delegate;
  312. }
  313. - (void)setDelegate:(id)delegate
  314. {
  315. m_delegate = delegate;
  316. }
  317. - (LPChat *)chat
  318. {
  319. return [[m_chat retain] autorelease];
  320. }
  321. - (void)p_setChat:(LPChat *)chat
  322. {
  323. if (m_chat != chat) {
  324. [m_chat endChat];
  325. [self willChangeValueForKey:@"chat"];
  326. [m_chat removeObserver:self forKeyPath:@"activeContactEntry.account.pubManager.chatBotAdsBaseURL"];
  327. [m_chat removeObserver:self forKeyPath:@"activeContactEntry.account.online"];
  328. [m_chat removeObserver:self forKeyPath:@"activeContactEntry.online"];
  329. [m_chat removeObserver:self forKeyPath:@"activeContactEntry"];
  330. [m_chat release];
  331. m_chat = [chat retain];
  332. [chat setDelegate:self];
  333. [m_chat addObserver:self forKeyPath:@"activeContactEntry" options:0 context:NULL];
  334. [m_chat addObserver:self forKeyPath:@"activeContactEntry.online" options:0 context:NULL];
  335. [m_chat addObserver:self forKeyPath:@"activeContactEntry.account.online" options:0 context:NULL];
  336. [m_chat addObserver:self forKeyPath:@"activeContactEntry.account.pubManager.chatBotAdsBaseURL" options:0 context:NULL];
  337. // Post a "system message" to start
  338. NSString *systemMessage;
  339. if ([m_chat activeContactEntry]) {
  340. if ([[[LPAccountsController sharedAccountsController] accounts] count] > 1) {
  341. systemMessage = [NSString stringWithFormat:NSLocalizedString(@"Chat changed to contact \"%@\" thru account \"%@\"",
  342. @"status message written to the text transcript of a chat window"),
  343. [[m_chat activeContactEntry] humanReadableAddress],
  344. [[[m_chat activeContactEntry] account] description]];
  345. }
  346. else {
  347. systemMessage = [NSString stringWithFormat:NSLocalizedString(@"Chat changed to contact \"%@\"",
  348. @"status message written to the text transcript of a chat window"),
  349. [[m_chat activeContactEntry] humanReadableAddress]];
  350. }
  351. }
  352. else {
  353. systemMessage = [NSString stringWithFormat:NSLocalizedString(@"Chat ended.", @"status message written to the text transcript of a chat window")];
  354. }
  355. [m_chatViewsController appendDIVBlockToWebViewWithInnerHTML:[systemMessage stringByEscapingHTMLEntities]
  356. divClass:@"systemMessage"
  357. scrollToVisibleMode:LPScrollWithAnimationIfAtBottom];
  358. [m_chatJSInterface setAccount:[[m_chat activeContactEntry] account]];
  359. [self didChangeValueForKey:@"chat"];
  360. }
  361. }
  362. - (LPContact *)contact
  363. {
  364. return [[m_contact retain] autorelease];
  365. }
  366. - (void)setContact:(LPContact *)contact
  367. {
  368. if (m_contact != contact) {
  369. BOOL hadContact = (m_contact != nil);
  370. [m_contact removeObserver:self forKeyPath:@"avatar"];
  371. [m_contact removeObserver:self forKeyPath:@"chatContactEntries"];
  372. [m_contact removeObserver:self forKeyPath:@"contactEntries"];
  373. [m_contact removeObserver:self forKeyPath:@"attributedStatusMessage"];
  374. [m_contact release];
  375. m_contact = [contact retain];
  376. [m_contact addObserver:self forKeyPath:@"attributedStatusMessage" options:0 context:NULL];
  377. [m_contact addObserver:self forKeyPath:@"contactEntries" options:0 context:NULL];
  378. [m_contact addObserver:self forKeyPath:@"chatContactEntries" options:0 context:NULL];
  379. [m_contact addObserver:self forKeyPath:@"avatar" options:0 context:NULL];
  380. [self p_syncViewsWithContact];
  381. if (!hadContact)
  382. [self p_setupChatDocumentTitle];
  383. if (contact != nil) {
  384. // Show the PUB banner only for contacts with the corresponding capability.
  385. // Check only some seconds from now so that the core has time to fetch the capabilities of the contact.
  386. [self performSelector:@selector(p_displayAndReloadPubBannerIfNeeded) withObject:nil afterDelay:3.0];
  387. }
  388. else {
  389. // Make sure that the delayed perform of p_displayAndReloadPubBannerIfNeeded doesn't fire
  390. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(p_displayAndReloadPubBannerIfNeeded) object:nil];
  391. }
  392. }
  393. }
  394. - (NSDate *)dateStarted
  395. {
  396. return [[m_dateStarted retain] autorelease];
  397. }
  398. - (unsigned int)numberOfUnreadMessages
  399. {
  400. return m_nrUnreadMessages;
  401. }
  402. - (NSString *)windowTitleSuffix
  403. {
  404. return (m_nrUnreadMessages > 0 ?
  405. [NSString stringWithFormat:NSLocalizedString(@" (%d unread)", @"chat window title suffix"), m_nrUnreadMessages] :
  406. @"");
  407. }
  408. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  409. {
  410. if ([keyPath isEqualToString:@"values.ChatBackgroundColor"]) {
  411. [self p_updateChatBackgroundColorFromDefaults];
  412. }
  413. else if ([keyPath isEqualToString:@"values.DisplayEmoticonImages"]) {
  414. BOOL displayImages = [[object valueForKeyPath:keyPath] boolValue];
  415. [m_chatViewsController showEmoticonsAsImages:displayImages];
  416. }
  417. else if ([keyPath isEqualToString:@"values.SaveChatTranscripts"]) {
  418. NSUserDefaultsController *prefsCtrl = [NSUserDefaultsController sharedUserDefaultsController];
  419. [self p_setSaveChatTranscriptEnabled:[[prefsCtrl valueForKeyPath:@"values.SaveChatTranscripts"] boolValue]];
  420. }
  421. else if ([keyPath isEqualToString:@"online"]) {
  422. // [LPAccountsController sharedAccountsController] online status
  423. [[self window] update];
  424. }
  425. else if ([keyPath isEqualToString:@"name"]) {
  426. // [LPAccountsController sharedAccountsController] name
  427. [self p_syncChatOwnerName];
  428. }
  429. else if ([keyPath isEqualToString:@"attributedStatusMessage"]) {
  430. [self p_syncStatusMessageTextFieldWithContact];
  431. }
  432. else if ([keyPath isEqualToString:@"contactEntries"]) {
  433. // Check whether all JIDs have been removed.
  434. if ([[m_contact contactEntries] count] == 0) {
  435. [self performSelector:@selector(close) withObject:nil afterDelay:0.0];
  436. }
  437. }
  438. else if ([keyPath isEqualToString:@"chatContactEntries"]) {
  439. [self p_syncJIDsPopupMenu];
  440. [m_addressesPopUp setEnabled:([[m_contact chatContactEntries] count] > 0)];
  441. }
  442. else if ([keyPath isEqualToString:@"avatar"]) {
  443. [self p_updateMiniwindowImage];
  444. }
  445. else if ([keyPath isEqualToString:@"activeContactEntry.online"]) {
  446. // Changes to the activeContactEntry will also trigger a change notification for the activeContactEntry.online
  447. // keypath. So, everything that must be done when any of these two keypaths change is being taken care of in here.
  448. [self p_syncViewsWithContact];
  449. }
  450. else if ([keyPath isEqualToString:@"activeContactEntry"]) {
  451. LPContactEntry *entry = [m_chat activeContactEntry];
  452. int idx = [m_addressesPopUp indexOfItemWithRepresentedObject:entry];
  453. if (idx >= 0)
  454. [m_addressesPopUp selectItemAtIndex:[m_addressesPopUp indexOfItemWithRepresentedObject:entry]];
  455. // Post a "system message" to signal the change
  456. NSString *systemMessage;
  457. if (entry) {
  458. if ([[[LPAccountsController sharedAccountsController] accounts] count] > 1) {
  459. systemMessage = [NSString stringWithFormat:NSLocalizedString(@"Chat changed to contact \"%@\" thru account \"%@\"",
  460. @"status message written to the text transcript of a chat window"),
  461. [[m_chat activeContactEntry] humanReadableAddress],
  462. [[[m_chat activeContactEntry] account] description]];
  463. }
  464. else {
  465. systemMessage = [NSString stringWithFormat:NSLocalizedString(@"Chat changed to contact \"%@\"",
  466. @"status message written to the text transcript of a chat window"),
  467. [[m_chat activeContactEntry] humanReadableAddress]];
  468. }
  469. }
  470. else {
  471. systemMessage = [NSString stringWithFormat:NSLocalizedString(@"Chat ended.", @"status message written to the text transcript of a chat window")];
  472. }
  473. [m_chatViewsController appendDIVBlockToWebViewWithInnerHTML:[systemMessage stringByEscapingHTMLEntities]
  474. divClass:@"systemMessage"
  475. scrollToVisibleMode:LPScrollWithAnimationIfAtBottom];
  476. [m_chatJSInterface setAccount:[entry account]];
  477. }
  478. else if ([keyPath isEqualToString:@"activeContactEntry.account.online"]) {
  479. // Account online status (Chat window)
  480. [self p_setSendFieldHidden:(![[object valueForKeyPath:keyPath] boolValue] || [m_chat activeContactEntry] == nil)
  481. animate:YES];
  482. }
  483. else if ([keyPath isEqualToString:@"account.online"]) {
  484. // Account online status (JID Entry Panel)
  485. [self p_reevaluateJIDPanelOKButtonEnabled];
  486. }
  487. else if ([keyPath isEqualToString:@"activeContactEntry.account.pubManager.chatBotAdsBaseURL"]) {
  488. [self p_displayAndReloadPubBannerIfNeeded];
  489. }
  490. else {
  491. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  492. }
  493. }
  494. - (void)setMessageTextEntryString:(NSString *)messageText
  495. {
  496. [m_inputTextField setStringValue:messageText];
  497. [m_inputTextField performSelector:@selector(calcContentSize) withObject:nil afterDelay:0.0];
  498. m_lastInputTextFieldStringLength = [messageText length];
  499. }
  500. - (void)sendAudibleWithResourceName:(NSString *)audibleName
  501. {
  502. [self p_appendAudibleWithResourceName:audibleName inbound:NO];
  503. [m_chat sendAudibleWithResourceName:audibleName];
  504. }
  505. - (NSString *)p_fileNameHTMLForFileTransfer:(LPFileTransfer *)ft
  506. {
  507. int transferID = [ft ID];
  508. NSString *fileNameWithLink = [NSString stringWithFormat:
  509. @"<a href=\"javascript:window.chatJSInterface.openFileOfTransfer(%d);\" title=\"%@\">%@</a>",
  510. transferID,
  511. NSLocalizedString(@"Open file", @""),
  512. [[ft filename] stringByEscapingHTMLEntities]];
  513. NSString *revealLink = [NSString stringWithFormat:
  514. @"<a href=\"javascript:window.chatJSInterface.revealFileOfTransfer(%d);\" title=\"%@\"><img src=\"file://%@\"/></a>",
  515. transferID,
  516. NSLocalizedString(@"Reveal in Finder", @""),
  517. [[NSBundle mainBundle] pathForImageResource:@"TransferReveal"]];
  518. return [NSString stringWithFormat:@"\"%@\" %@", fileNameWithLink, revealLink];
  519. }
  520. - (void)updateInfoForFileTransfer:(LPFileTransfer *)ft
  521. {
  522. LPContactEntry *peerContactEntry = [ft peerContactEntry];
  523. LPFileTransferState newState = [ft state];
  524. LPFileTransferType type = [ft type];
  525. int transferID = [ft ID];
  526. if ([peerContactEntry contact] == [self contact]) {
  527. NSString *htmlText = nil;
  528. NSString *divClass = nil;
  529. switch (newState) {
  530. case LPFileTransferPackaging:
  531. case LPFileTransferWaitingToBeAccepted:
  532. {
  533. NSString *elementID = [NSString stringWithFormat:@"fileTransfer_%d", transferID];
  534. if (![m_chatViewsController existsElementWithID:elementID]) {
  535. if (type == LPIncomingTransfer)
  536. {
  537. // Links for JavaScript -> Objective-C actions
  538. NSString *acceptLink = [NSString stringWithFormat:
  539. @"<a href=\"javascript:window.chatJSInterface.acceptTransfer(%d);\">%@</a>",
  540. transferID,
  541. [NSLocalizedString(@"accept", @"for file transfers listed in chat windows") stringByEscapingHTMLEntities]];
  542. NSString *rejectLink = [NSString stringWithFormat:
  543. @"<a href=\"javascript:window.chatJSInterface.rejectTransfer(%d);\">%@</a>",
  544. transferID,
  545. [NSLocalizedString(@"reject", @"for file transfers listed in chat windows") stringByEscapingHTMLEntities]];
  546. // Message text
  547. NSString *str1 = [NSString stringWithFormat:NSLocalizedString(@"Receiving file %@.",
  548. @"for file transfers listed in chat windows"),
  549. [self p_fileNameHTMLForFileTransfer:ft]];
  550. NSString *str2 = [NSString stringWithFormat:NSLocalizedString(@"You may %@ or %@ the transfer.",
  551. @"for file transfers listed in chat windows"),
  552. acceptLink, rejectLink];
  553. htmlText = [NSString stringWithFormat:@"%@<br/><span id=\"%@\">%@</span>",
  554. str1, elementID, str2];
  555. }
  556. else
  557. {
  558. NSString *str1 = [NSString stringWithFormat: NSLocalizedString(@"Sending file %@.",
  559. @"for file transfers listed in chat windows"),
  560. [self p_fileNameHTMLForFileTransfer:ft]];
  561. NSString *str2 = ( newState == LPFileTransferPackaging ?
  562. NSLocalizedString(@"<b>(packaging...)</b>", @"") :
  563. @"" );
  564. htmlText = [NSString stringWithFormat:@"%@<br/><span id=\"%@\">%@</span>",
  565. str1, elementID, str2];
  566. }
  567. divClass = @"smsReceivedReplyBlock";
  568. }
  569. else if (newState == LPFileTransferWaitingToBeAccepted) {
  570. [m_chatViewsController setInnerHTML:NSLocalizedString(@"", @"") forElementWithID:elementID];
  571. }
  572. break;
  573. }
  574. case LPFileTransferWasNotAccepted:
  575. {
  576. NSString *elementID = [NSString stringWithFormat:@"fileTransfer_%d", transferID];
  577. [m_chatViewsController setInnerHTML:NSLocalizedString(@"<b>(rejected)</b>", @"") forElementWithID:elementID];
  578. break;
  579. }
  580. case LPFileTransferRunning:
  581. {
  582. NSString *elementID = [NSString stringWithFormat:@"fileTransfer_%d", transferID];
  583. [m_chatViewsController setInnerHTML:NSLocalizedString(@"<b>(transferring...)</b>", @"") forElementWithID:elementID];
  584. break;
  585. }
  586. case LPFileTransferAbortedWithError:
  587. {
  588. NSString *elementID = [NSString stringWithFormat:@"fileTransfer_%d", transferID];
  589. NSString *formatStr = NSLocalizedString(@"<b>(error: %@)</b>", @"");
  590. NSString *html = [NSString stringWithFormat:formatStr, [[ft lastErrorMessage] stringByEscapingHTMLEntities]];
  591. [m_chatViewsController setInnerHTML:html forElementWithID:elementID];
  592. divClass = @"systemMessage";
  593. htmlText = [NSString stringWithFormat:
  594. NSLocalizedString(@"Transfer of file %@ was <b>aborted</b> with an error: %@.", @""),
  595. [self p_fileNameHTMLForFileTransfer:ft], [[ft lastErrorMessage] stringByEscapingHTMLEntities]];
  596. break;
  597. }
  598. case LPFileTransferCancelled:
  599. {
  600. NSString *elementID = [NSString stringWithFormat:@"fileTransfer_%d", transferID];
  601. [m_chatViewsController setInnerHTML:NSLocalizedString(@"<b>(cancelled)</b>", @"") forElementWithID:elementID];
  602. divClass = @"systemMessage";
  603. htmlText = [NSString stringWithFormat:
  604. NSLocalizedString(@"Transfer of file %@ was <b>cancelled</b>.", @""),
  605. [self p_fileNameHTMLForFileTransfer:ft]];
  606. break;
  607. }
  608. case LPFileTransferCompleted:
  609. {
  610. NSString *elementID = [NSString stringWithFormat:@"fileTransfer_%d", transferID];
  611. [m_chatViewsController setInnerHTML:NSLocalizedString(@"<b>(completed)</b>", @"") forElementWithID:elementID];
  612. divClass = @"systemMessage";
  613. htmlText = [NSString stringWithFormat:
  614. NSLocalizedString(@"Transfer of file %@ has <b>completed successfully</b>.", @""),
  615. [self p_fileNameHTMLForFileTransfer:ft]];
  616. break;
  617. }
  618. default:
  619. break;
  620. }
  621. if (htmlText) {
  622. [m_chatViewsController appendDIVBlockToWebViewWithInnerHTML:htmlText
  623. divClass:divClass
  624. scrollToVisibleMode:LPScrollWithAnimationIfAtBottom];
  625. }
  626. }
  627. }
  628. #pragma mark -
  629. #pragma mark Window Frame Save & Restore
  630. static NSMutableDictionary *s_windowFramesDictionary = nil;
  631. + (NSMutableDictionary *)p_windowFramesDictionary
  632. {
  633. if (s_windowFramesDictionary == nil) {
  634. NSString *framesDictionaryFilepath = [LPOurApplicationSupportFolderPath() stringByAppendingPathComponent:@"WindowFrames.plist"];
  635. s_windowFramesDictionary = [[NSMutableDictionary alloc] initWithContentsOfFile:framesDictionaryFilepath];
  636. if (s_windowFramesDictionary == nil) {
  637. s_windowFramesDictionary = [[NSMutableDictionary alloc] init];
  638. }
  639. }
  640. return s_windowFramesDictionary;
  641. }
  642. + (void)p_saveWindowFramesDictionary
  643. {
  644. if (s_windowFramesDictionary != nil) {
  645. NSString *framesDictionaryFilepath = [LPOurApplicationSupportFolderPath() stringByAppendingPathComponent:@"WindowFrames.plist"];
  646. [s_windowFramesDictionary writeToFile:framesDictionaryFilepath atomically:YES];
  647. }
  648. }
  649. + (NSRect)savedWindowFrameForChatWithContactNamed:(NSString *)contactName
  650. {
  651. NSParameterAssert(contactName);
  652. NSString *rectStr = [[self p_windowFramesDictionary] objectForKey:contactName];
  653. if (rectStr != nil) {
  654. return NSRectFromString(rectStr);
  655. } else {
  656. return NSZeroRect;
  657. }
  658. }
  659. + (void)saveWindowFrame:(NSRect)frame forChatWithContactNamed:(NSString *)contactName
  660. {
  661. NSParameterAssert(contactName);
  662. [[self p_windowFramesDictionary] setObject:NSStringFromRect(frame) forKey:contactName];
  663. [self p_saveWindowFramesDictionary];
  664. }
  665. #pragma mark -
  666. #pragma mark Actions
  667. - (IBAction)segmentClicked:(id)sender
  668. {
  669. int clickedSegment = [sender selectedSegment];
  670. int clickedSegmentTag = [[sender cell] tagForSegment:clickedSegment];
  671. if (clickedSegmentTag == 0) { // emoticons
  672. NSWindow *win = [self window];
  673. NSRect buttonFrame = [sender frame];
  674. NSPoint topRight = [win convertBaseToScreen:[[sender superview] convertPoint:buttonFrame.origin
  675. toView:nil]];
  676. [sender setImage:[NSImage imageNamed:@"emoticonIconPressed"] forSegment:clickedSegment];
  677. [(NSView *)sender display];
  678. [m_chatViewsController pickEmoticonWithMenuTopRightAt:NSMakePoint(topRight.x + [sender widthForSegment:clickedSegment], topRight.y)
  679. parentWindow:[self window]];
  680. [sender setImage:[NSImage imageNamed:@"emoticonIconUnpressed"] forSegment:clickedSegment];
  681. [(NSView *)sender display];
  682. }
  683. else if (clickedSegmentTag == 1) { // audibles
  684. NSDrawerState state = [m_audiblesController drawerState];
  685. if (state == NSDrawerClosedState || state == NSDrawerClosingState) {
  686. // Will be open afterwards
  687. [sender setImage:[NSImage imageNamed:@"bocasIconPressed"] forSegment:clickedSegment];
  688. }
  689. else {
  690. // Will be closed afterwards
  691. [sender setImage:[NSImage imageNamed:@"bocasIconUnpressed"] forSegment:clickedSegment];
  692. }
  693. [m_audiblesController toggleDrawer:sender];
  694. }
  695. }
  696. - (IBAction)sendMessage:(id)sender
  697. {
  698. NSAttributedString *attributedMessage = [m_inputTextField attributedStringValue];
  699. NSString *message = [attributedMessage stringByFlatteningAttachedEmoticons];
  700. // Check if the text is all made of whitespace.
  701. static NSCharacterSet *requiredCharacters = nil;
  702. if (requiredCharacters == nil) {
  703. requiredCharacters = [[[NSCharacterSet whitespaceAndNewlineCharacterSet] invertedSet] retain];
  704. }
  705. if ([message rangeOfCharacterFromSet:requiredCharacters].location != NSNotFound) {
  706. [self p_appendMessageToWebView:message subject:nil timestamp:[NSDate date] inbound:NO];
  707. [m_chat sendMessageWithPlainTextVariant:message XHTMLVariant:nil URLs:nil];
  708. m_hasAlreadyProcessedSomeMessages = YES;
  709. }
  710. // Store it in the input line history
  711. if ([m_inputLineHistory count] > 0)
  712. [m_inputLineHistory replaceObjectAtIndex:0 withObject:attributedMessage];
  713. else
  714. [m_inputLineHistory addObject:attributedMessage];
  715. if ([m_inputLineHistory count] > INPUT_LINE_HISTORY_ITEMS_MAX)
  716. [m_inputLineHistory removeObjectsInRange:NSMakeRange(INPUT_LINE_HISTORY_ITEMS_MAX, [m_inputLineHistory count] - INPUT_LINE_HISTORY_ITEMS_MAX)];
  717. [m_inputLineHistory insertObject:@"" atIndex:0];
  718. m_currentInputLineHistoryEntryIndex = 0;
  719. // Prepare the window to take another message from the user
  720. [[self window] makeFirstResponder:m_inputTextField];
  721. [self setMessageTextEntryString:@""];
  722. }
  723. - (IBAction)sendSMS:(id)sender
  724. {
  725. if ([m_delegate respondsToSelector:@selector(chatController:sendSMSToContact:)]) {
  726. [m_delegate chatController:self sendSMSToContact:[self contact]];
  727. }
  728. }
  729. - (IBAction)sendFile:(id)sender
  730. {
  731. NSOpenPanel *op = [NSOpenPanel openPanel];
  732. [op setPrompt:NSLocalizedString(@"Send", @"button for the file selection sheet")];
  733. [op setCanChooseFiles:YES];
  734. [op setCanChooseDirectories:NO];
  735. [op setResolvesAliases:YES];
  736. [op setAllowsMultipleSelection:NO];
  737. [op beginSheetForDirectory:nil
  738. file:nil
  739. types:nil
  740. modalForWindow:[self window]
  741. modalDelegate:self
  742. didEndSelector:@selector(p_openPanelDidEnd:returnCode:contextInfo:)
  743. contextInfo:NULL];
  744. }
  745. - (void)p_openPanelDidEnd:(NSOpenPanel *)panel returnCode:(int)returnCode contextInfo:(void *)contextInfo
  746. {
  747. if (returnCode == NSOKButton) {
  748. LPContactEntry *contactEntry = [m_chat activeContactEntry];
  749. if (contactEntry && [contactEntry canDoFileTransfer])
  750. [[LPFileTransfersManager fileTransfersManager] startSendingFile:[panel filename]
  751. toContactEntry:[m_chat activeContactEntry]];
  752. }
  753. }
  754. - (IBAction)editContact:(id)sender
  755. {
  756. if ([m_delegate respondsToSelector:@selector(chatController:editContact:)]) {
  757. [m_delegate chatController:self editContact:[self contact]];
  758. }
  759. }
  760. - (IBAction)selectChatAddress:(id)sender
  761. {
  762. LPContactEntry *selectedEntry = [sender representedObject];
  763. [m_chat setActiveContactEntry:selectedEntry];
  764. [m_contact setPreferredContactEntry:selectedEntry];
  765. }
  766. - (IBAction)saveDocumentTo:(id)sender
  767. {
  768. NSSavePanel *sp = [NSSavePanel savePanel];
  769. [sp setCanSelectHiddenExtension:YES];
  770. [sp setRequiredFileType:@"webarchive"];
  771. [sp beginSheetForDirectory:nil
  772. file:[m_chatViewsController chatDocumentTitle]
  773. modalForWindow:[self window]
  774. modalDelegate:self
  775. didEndSelector:@selector(p_savePanelDidEnd:returnCode:contextInfo:)
  776. contextInfo:NULL];
  777. }
  778. - (void)p_savePanelDidEnd:(NSSavePanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
  779. {
  780. if (returnCode == NSOKButton) {
  781. NSError *error;
  782. if (![m_chatViewsController saveDocumentToFile:[sheet filename] hideExtension:[sheet isExtensionHidden] error:&error]) {
  783. [self presentError:error];
  784. }
  785. }
  786. }
  787. - (IBAction)printDocument:(id)sender
  788. {
  789. NSPrintOperation *op = [NSPrintOperation printOperationWithView:[[[m_chatWebView mainFrame] frameView] documentView]];
  790. [op runOperationModalForWindow:[self window]
  791. delegate:nil
  792. didRunSelector:NULL
  793. contextInfo:NULL];
  794. }
  795. #pragma mark Searching
  796. - (IBAction)showFindPanel:(id)sender
  797. {
  798. [[LPChatFindPanelController sharedFindPanel] showWindow:sender];
  799. }
  800. - (IBAction)findNext:(id)sender
  801. {
  802. LPChatFindPanelController *findPanel = [LPChatFindPanelController sharedFindPanel];
  803. NSString *searchStr = [findPanel searchString];
  804. BOOL found = NO;
  805. if ([searchStr length] > 0)
  806. found = [m_chatWebView searchFor:searchStr direction:YES caseSensitive:NO wrap:YES];
  807. [findPanel searchStringWasFound:found];
  808. }
  809. - (IBAction)findPrevious:(id)sender
  810. {
  811. LPChatFindPanelController *findPanel = [LPChatFindPanelController sharedFindPanel];
  812. NSString *searchStr = [findPanel searchString];
  813. BOOL found = NO;
  814. if ([searchStr length] > 0)
  815. found = [m_chatWebView searchFor:searchStr direction:NO caseSensitive:NO wrap:YES];
  816. [findPanel searchStringWasFound:found];
  817. }
  818. - (IBAction)useSelectionForFind:(id)sender
  819. {
  820. NSString *selectedString = nil;
  821. id firstResponder = [[self window] firstResponder];
  822. if ([firstResponder isKindOfClass:[NSText class]])
  823. selectedString = [[firstResponder string] substringWithRange:[firstResponder selectedRange]];
  824. else if ([firstResponder isDescendantOf:m_chatWebView])
  825. selectedString = [[m_chatWebView selectedDOMRange] toString];
  826. if ([selectedString length] > 0)
  827. [[LPChatFindPanelController sharedFindPanel] setSearchString:selectedString];
  828. }
  829. #pragma mark Action Validation
  830. - (BOOL)p_validateAction:(SEL)action
  831. {
  832. // The sendSMS: action is not validated in here so that its menu item is always enabled. This makes it easier for the user to
  833. // get a window for sending SMS messages regardless of the current state of the GUI. If the contact supports sending SMS
  834. // it will be added automatically to the list of recipients for the message. Otherwise, the Send SMS window will show up
  835. // without any recipients. OTOH, the toolbar button for sending SMS messages is validated in the toolbar item validation method
  836. // and is disabled if the contact doesn't support sending SMS messages. This way we get an easy to check visual cue for the
  837. // capabilities of the contact.
  838. if (action == @selector(sendFile:)) {
  839. return ([[m_chat activeContactEntry] canDoFileTransfer] &&
  840. [[m_chat activeContactEntry] isOnline]);
  841. }
  842. else if (action == @selector(useSelectionForFind:)) {
  843. id firstResponder = [[self window] firstResponder];
  844. if ([firstResponder isKindOfClass:[NSText class]])
  845. return ([firstResponder selectedRange].length > 0);
  846. else if ([firstResponder isDescendantOf:m_chatWebView])
  847. return ([[[m_chatWebView selectedDOMRange] toString] length] > 0);
  848. else
  849. return NO;
  850. }
  851. else {
  852. return YES;
  853. }
  854. }
  855. - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
  856. {
  857. return [self p_validateAction:[menuItem action]];
  858. }
  859. #pragma mark Choose JID Panel
  860. - (void)p_reevaluateJIDPanelOKButtonEnabled
  861. {
  862. [m_chooseJIDPanelOKButton setEnabled:([[[m_chooseJIDPanelJIDEntryView JIDEntryTextField] stringValue] length] > 0
  863. && [[m_chooseJIDPanelJIDEntryView account] isOnline])];
  864. }
  865. - (IBAction)chooseJIDPanelOK:(id)sender
  866. {
  867. // Cleanup the sheet
  868. NSWindow *sheet = [[self window] attachedSheet];
  869. [NSApp endSheet:sheet];
  870. [sheet orderOut:nil];
  871. [m_chooseJIDPanelJIDEntryView removeObserver:self forKeyPath:@"account.online"];
  872. LPAccount *account = [m_chooseJIDPanelJIDEntryView account];
  873. NSString *jid = [m_chooseJIDPanelJIDEntryView enteredJID];
  874. LPContactEntry *contactEntry = [[LPRoster roster] contactEntryForAddress:jid
  875. account:account
  876. createNewHiddenWithNameIfNotFound:jid];
  877. LPChatsManager *chatsManager = [LPChatsManager chatsManager];
  878. LPChat *chat = [chatsManager chatForContact:[contactEntry contact]];
  879. if (chat == nil) {
  880. chat = [chatsManager startChatWithContactEntry:contactEntry];
  881. [self p_setChat:chat];
  882. [self setContact:[chat contact]];
  883. }
  884. else {
  885. if ([m_delegate respondsToSelector:@selector(chatController:orderChatWithContactEntryToFront:)]) {
  886. [m_delegate chatController:self orderChatWithContactEntryToFront:contactEntry];
  887. } else {
  888. NSBeep();
  889. NSLog(@"%@'s delegate should implement the method %@",
  890. NSStringFromClass([self class]), @"chatController:orderChatWithContactEntryToFront:");
  891. }
  892. [self close];
  893. }
  894. }
  895. - (IBAction)chooseJIDPanelCancel:(id)sender
  896. {
  897. NSWindow *sheet = [[self window] attachedSheet];
  898. [NSApp endSheet:sheet];
  899. [sheet orderOut:nil];
  900. [m_chooseJIDPanelJIDEntryView removeObserver:self forKeyPath:@"account.online"];
  901. [self close];
  902. }
  903. - (IBAction)copyStatusMessage:(id)sender
  904. {
  905. NSPasteboard *pboard = [NSPasteboard generalPasteboard];
  906. [pboard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] owner:nil];
  907. [pboard setString:[[self contact] statusMessage] forType:NSStringPboardType];
  908. }
  909. #pragma mark -
  910. #pragma mark LPChat Delegate Methods
  911. - (void)chat:(LPChat *)chat didReceiveErrorMessage:(NSString *)message
  912. {
  913. // Post a "system message"
  914. NSString *systemMessage = [NSString stringWithFormat:@"ERROR: %@", message];
  915. [m_chatViewsController appendDIVBlockToWebViewWithInnerHTML:[systemMessage stringByEscapingHTMLEntities]
  916. divClass:@"systemMessage"
  917. scrollToVisibleMode:LPScrollWithAnimationIfAtBottom];
  918. }
  919. - (void)chat:(LPChat *)chat didReceiveMessageFromNick:(NSString *)nick subject:(NSString *)subject plainTextVariant:(NSString *)plainTextMessage XHTMLVariant:(NSString *)XHTMLMessage URLs:(NSArray *)URLs
  920. {
  921. // DEBUG: this is useful for testing the code that handles the display of
  922. // received SMS messages without having to actually waste SMS messages.
  923. // if ([plainTextMessage hasPrefix:@"sms: "]) {
  924. // [self chat:chat didReceiveSMSFrom:@"00351964301673@phone.im.sapo.pt" withBody:plainTextMessage date:[NSDate date] newCredit:99 newFreeMessages:88 newTotalSentThisMonth:77];
  925. // return;
  926. // }
  927. // Add in the URLs
  928. NSString *messageBody = plainTextMessage;
  929. if (URLs && [URLs count] > 0) {
  930. NSMutableString *messageWithURLs = [NSMutableString stringWithString:messageBody];
  931. NSEnumerator *urlEnum = [URLs objectEnumerator];
  932. NSString *url;
  933. while (url = [urlEnum nextObject]) {
  934. [messageWithURLs appendFormat:@" | %@", url];
  935. }
  936. messageBody = messageWithURLs;
  937. }
  938. // Don't do everything at the same time. Allow the scroll animation to run first so that it doesn't appear choppy.
  939. [[m_chatViewsController grabMethodForAfterScrollingWithTarget:self]
  940. p_notifyUserAboutReceivedMessage:messageBody
  941. notificationsHandlerSelector:( !m_hasAlreadyProcessedSomeMessages ?
  942. @selector(notifyReceptionOfFirstMessage:fromContact:) :
  943. @selector(notifyReceptionOfMessage:fromContact:) )];
  944. [self p_appendMessageToWebView:messageBody subject:subject timestamp:[NSDate date] inbound:YES];
  945. m_hasAlreadyProcessedSomeMessages = YES;
  946. }
  947. - (void)chat:(LPChat *)chat didReceiveSystemMessage:(NSString *)message
  948. {
  949. // Post a "system message"
  950. NSString *systemMessage = [NSString stringWithFormat:@"System Message: %@", message];
  951. [m_chatViewsController appendDIVBlockToWebViewWithInnerHTML:[systemMessage stringByEscapingHTMLEntities]
  952. divClass:@"systemMessage"
  953. scrollToVisibleMode:LPScrollWithAnimationIfAtBottom];
  954. }
  955. - (void)chat:(LPChat *)chat didReceiveResultOfSMSSentTo:(NSString *)destinationPhoneNr withBody:(NSString *)msgBody resultCode:(int)result nrUsedMsgs:(int)nrUsedMsgs nrUsedChars:(int)nrUsedChars newCredit:(int)newCredit newFreeMessages:(int)newFreeMessages newTotalSentThisMonth:(int)newTotalSentThisMonth
  956. {
  957. // DEBUG:
  958. // NSString *text = [NSString stringWithFormat:@"SMS SENT to %@ (message: \"%@\"). Result: %d , %d msgs used, %d chars used, new credit: %d , new free msgs: %d , new total sent: %d", destinationPhoneNr, msgBody, result, nrUsedMsgs, nrUsedChars, newCredit, newFreeMessages, newTotalSentThisMonth];
  959. NSString *phoneNr = ( [destinationPhoneNr isPhoneJID] ?
  960. [destinationPhoneNr userPresentablePhoneNrRepresentation] :
  961. destinationPhoneNr );
  962. NSString *htmlText = nil;
  963. if (result == 1) {
  964. // Success
  965. htmlText = [NSString stringWithFormat:
  966. NSLocalizedString(@"SMS <b>sent</b> to \"%@\" at %@<br/>Used: %d message(s), total of %d characters.<p>\"<b>%@</b>\"</p>", @""),
  967. [phoneNr stringByEscapingHTMLEntities],
  968. [[NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil],
  969. nrUsedMsgs, nrUsedChars,
  970. [msgBody stringByEscapingHTMLEntities]
  971. //newCredit, newFreeMessages, newTotalSentThisMonth
  972. ];
  973. }
  974. else {
  975. // Failure
  976. htmlText = [NSString stringWithFormat:
  977. NSLocalizedString(@"<b>Failed</b> to send SMS to \"%@\" at %@.<p>\"<b>%@</b>\"</p>", @""),
  978. [phoneNr stringByEscapingHTMLEntities],
  979. [[NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil],
  980. [msgBody stringByEscapingHTMLEntities]];
  981. }
  982. [m_chatViewsController appendDIVBlockToWebViewWithInnerHTML:htmlText
  983. divClass:@"smsSentReplyBlock"
  984. scrollToVisibleMode:LPScrollWithAnimationIfAtBottom];
  985. }
  986. - (void)chat:(LPChat *)chat didReceiveSMSFrom:(NSString *)sourcePhoneNr withBody:(NSString *)msgBody date:(NSDate *)date newCredit:(int)newCredit newFreeMessages:(int)newFreeMessages newTotalSentThisMonth:(int)newTotalSentThisMonth
  987. {
  988. // DEBUG:
  989. // NSString *text = [NSString stringWithFormat:@"SMS RECEIVED on %@ from %@: \"%@\". New credit: %d , new free msgs: %d , new total sent: %d", date, sourcePhoneNr, msgBody, newCredit, newFreeMessages, newTotalSentThisMonth];
  990. NSString *phoneNr = ( [sourcePhoneNr isPhoneJID] ?
  991. [sourcePhoneNr userPresentablePhoneNrRepresentation] :
  992. sourcePhoneNr );
  993. NSString *htmlText = [NSString stringWithFormat:
  994. NSLocalizedString(@"SMS <b>received</b> from \"%@\" at %@<p>\"<b>%@</b>\"</p>", @""),
  995. // We don't use the date provided by the server because it is nil sometimes
  996. [phoneNr stringByEscapingHTMLEntities],
  997. [[NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:nil],
  998. [m_chatViewsController HTMLifyRawMessageString:msgBody]];
  999. // Don't do everything at the same time. Allow the scroll animation to run first so that it doesn't appear choppy.
  1000. [[m_chatViewsController grabMethodForAfterScrollingWithTarget:self]
  1001. p_notifyUserAboutReceivedMessage:msgBody
  1002. notificationsHandlerSelector:@selector(notifyReceptionOfSMSMessage:fromContact:)];
  1003. [m_chatViewsController appendDIVBlockToWebViewWithInnerHTML:htmlText
  1004. divClass:@"smsReceivedReplyBlock"
  1005. scrollToVisibleMode:LPScrollWithAnimationIfAtBottom];
  1006. LPContactEntry *activeEntry = [m_chat activeContactEntry];
  1007. [[LPRecentMessagesStore sharedMessagesStore] storeRawHTMLBlock:htmlText
  1008. withDIVClass:@"smsReceivedReplyBlock"
  1009. forJID:[activeEntry address]
  1010. thruAccountJID:[[activeEntry account] JID]];
  1011. }
  1012. - (void)chat:(LPChat *)chat didReceiveAudibleWithResourceName:(NSString *)resourceName msgBody:(NSString *)body msgHTMLBody:(NSString *)htmlBody
  1013. {
  1014. LPAudibleSet *set = [LPAudibleSet defaultAudibleSet];
  1015. if ([set isValidAudibleResourceName:resourceName]) {
  1016. NSString *localPath = [set filepathForAudibleWithName:resourceName];
  1017. if (localPath == nil) {
  1018. // We don't have this audible in local storage yet. Start loading it and insert it into the webview later.
  1019. [[self p_pendingAudiblesSet] addObject:resourceName];
  1020. [set startLoadingAudibleFromServer:resourceName];
  1021. } else {
  1022. [self p_appendAudibleWithResourceName:resourceName inbound:YES];
  1023. }
  1024. }
  1025. else {
  1026. [self chat:chat didReceiveErrorMessage:[NSString stringWithFormat:@"Received an unknown audible: \"%@\"",
  1027. resourceName]];
  1028. // Send an error back to the other contact
  1029. [m_chat sendInvalidAudibleErrorWithMessage:@"Bad Request: the audible that was sent is unknown!"
  1030. originalResourceName:resourceName
  1031. originalBody:body
  1032. originalHTMLBody:htmlBody];
  1033. }
  1034. }
  1035. #pragma mark -
  1036. #pragma mark LPJIDEntryView Notifications
  1037. - (void)JIDEntryViewEnteredJIDDidChange:(LPJIDEntryView *)view;
  1038. {
  1039. [self p_reevaluateJIDPanelOKButtonEnabled];
  1040. }
  1041. #pragma mark -
  1042. #pragma mark LPAudibleSet Notifications
  1043. - (void)audibleSetDidFinishLoadingAudible:(NSNotification *)notification
  1044. {
  1045. NSString *audibleResourceName = [[notification userInfo] objectForKey:@"LPAudibleName"];
  1046. if ([[self p_pendingAudiblesSet] containsObject:audibleResourceName]) {
  1047. [[self p_pendingAudiblesSet] removeObject:audibleResourceName];
  1048. [self p_appendAudibleWithResourceName:audibleResourceName inbound:YES];
  1049. }
  1050. }
  1051. #pragma mark -
  1052. #pragma mark LPFileTransfer Notifications
  1053. - (void)fileTransferStateDidChange:(NSNotification *)notification
  1054. {
  1055. LPFileTransfer *ft = [notification object];
  1056. LPContactEntry *peerContactEntry = [ft peerContactEntry];
  1057. if ([peerContactEntry contact] == [self contact]) {
  1058. [self updateInfoForFileTransfer:ft];
  1059. }
  1060. }
  1061. #pragma mark -
  1062. #pragma mark NSResponder Methods
  1063. - (void)keyDown:(NSEvent *)theEvent
  1064. {
  1065. /* If a keyDown event reaches this low in the responder chain then it means that no text field is
  1066. active to process the event. Activate the input text field and reroute the event that was received
  1067. back to it. */
  1068. if ([m_inputTextField canBecomeKeyView]) {
  1069. NSWindow *window = [self window];
  1070. [window makeFirstResponder:m_inputTextField];
  1071. [[window firstResponder] keyDown:theEvent];
  1072. } else {
  1073. [super keyDown:theEvent];
  1074. }
  1075. }
  1076. #pragma mark -
  1077. #pragma mark Private Methods
  1078. #pragma mark ** JIDs Popup Menu
  1079. - (NSAttributedString *)p_attributedTitleOfJIDMenuItemForContactEntry:(LPContactEntry *)entry withFont:(NSFont *)font
  1080. {
  1081. LPStatus entryStatus = [entry status];
  1082. NSString *menuItemTitle = ( (entryStatus == LPStatusInvisible || entryStatus == LPStatusOffline) ?
  1083. [NSString stringWithFormat:@"%@ %C %@",
  1084. [entry humanReadableAddress], 0x2014 /* em-dash */,
  1085. NSLocalizedStringFromTable(LPStatusStringFromStatus([entry status]), @"Status", @"")] :
  1086. [entry humanReadableAddress] );
  1087. NSDictionary *attribs = ( [entry isOnline] ?
  1088. [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName] :
  1089. [NSDictionary dictionaryWithObjectsAndKeys:
  1090. font, NSFontAttributeName,
  1091. [NSColor grayColor], NSForegroundColorAttributeName, nil] );
  1092. return ( (menuItemTitle != nil && attribs != nil) ?
  1093. [[[NSAttributedString alloc] initWithString:menuItemTitle attributes:attribs] autorelease] :
  1094. nil );
  1095. }
  1096. - (NSMenuItem *)p_popupMenuHeaderItemForAccount:(LPAccount *)account
  1097. {
  1098. id item = nil;
  1099. int idx = [m_addressesPopUp indexOfItemWithRepresentedObject:account];
  1100. if (idx >= 0) {
  1101. item = [m_addressesPopUp itemAtIndex:idx];
  1102. }
  1103. else {
  1104. item = [[NSMenuItem alloc] initWithTitle:@"" action:NULL keyEquivalent:@""];
  1105. [item setTitle:[NSString stringWithFormat:NSLocalizedString(@"Account \"%@\"", @"Chat and SMS window popup menu"), [account description]]];
  1106. [item setIndentationLevel:0];
  1107. [item setEnabled:NO];
  1108. [item setRepresentedObject:account];
  1109. [item autorelease];
  1110. }
  1111. return item;
  1112. }
  1113. - (NSMenuItem *)p_popupMenuItemForEntry:(LPContactEntry *)entry
  1114. {
  1115. id item = nil;
  1116. int idx = [m_addressesPopUp indexOfItemWithRepresentedObject:entry];
  1117. if (idx >= 0) {
  1118. item = [m_addressesPopUp itemAtIndex:idx];
  1119. }
  1120. else {
  1121. item = [[NSMenuItem alloc] initWithTitle:@"" action:@selector(selectChatAddress:) keyEquivalent:@""];
  1122. NSAttributedString *attributedTitle =
  1123. [self p_attributedTitleOfJIDMenuItemForContactEntry:entry withFont:[m_addressesPopUp font]];
  1124. [item setAttributedTitle:attributedTitle];
  1125. [item setIndentationLevel:1];
  1126. [item setRepresentedObject:entry];
  1127. [item setTarget:self];
  1128. [item autorelease];
  1129. }
  1130. return item;
  1131. }
  1132. - (void)p_moveJIDMenuItem:(NSMenuItem *)menuItem toIndex:(int)targetIndex inMenu:(NSMenu *)menu
  1133. {
  1134. int currentIndex = [menu indexOfItem:menuItem];
  1135. if (currentIndex != targetIndex) {
  1136. // Prevent it from being dealloced while we possibly take it out of the menu
  1137. [menuItem retain];
  1138. if (currentIndex >= 0)
  1139. [menu removeItemAtIndex:currentIndex];
  1140. [menu insertItem:menuItem atIndex:targetIndex];
  1141. [menuItem release];
  1142. }
  1143. }
  1144. - (void)p_syncJIDsPopupMenu
  1145. {
  1146. NSMenuItem *selectedItem = [m_addressesPopUp selectedItem];
  1147. NSPredicate *onlinePred = [NSPredicate predicateWithFormat:@"online == YES"];
  1148. NSPredicate *offlinePred = [NSPredicate predicateWithFormat:@"online == NO"];
  1149. int currentIndex = 0;
  1150. NSMenu *menu = [m_addressesPopUp menu];
  1151. NSFont *menuItemFont = [m_addressesPopUp font];
  1152. NSArray *accounts = [[LPAccountsController sharedAccountsController] accounts];
  1153. unsigned int nrOfAccounts = [accounts count];
  1154. NSEnumerator *accountEnumerator = [accounts objectEnumerator];
  1155. LPAccount *account;
  1156. while (account = [accountEnumerator nextObject]) {
  1157. if ([account isEnabled]) {
  1158. // Collect all the JIDs in this account into two lists: online JIDs and offline JIDs
  1159. NSPredicate *accountPred = [NSPredicate predicateWithFormat:@"account == %@", account];
  1160. NSPredicate *onlineInThisAccountPred = [NSCompoundPredicate andPredicateWithSubpredicates:
  1161. [NSArray arrayWithObjects:accountPred, onlinePred, nil]];
  1162. NSPredicate *offlineInThisAccountPred = [NSCompoundPredicate andPredicateWithSubpredicates:
  1163. [NSArray arrayWithObjects:accountPred, offlinePred, nil]];
  1164. NSArray *onlineEntries = [[m_contact chatContactEntries] filteredArrayUsingPredicate: onlineInThisAccountPred];
  1165. NSArray *offlineEntries = [[m_contact chatContactEntries] filteredArrayUsingPredicate: offlineInThisAccountPred];
  1166. if (([onlineEntries count] + [offlineEntries count]) > 0) {
  1167. // ---- Separator Item ----
  1168. if (currentIndex > 0) {
  1169. [[m_addressesPopUp menu] insertItem:[NSMenuItem separatorItem] atIndex:currentIndex];
  1170. ++currentIndex;
  1171. }
  1172. // Setup an account header in the menu, but only if there's more than one configured account
  1173. if (nrOfAccounts > 1) {
  1174. NSMenuItem *menuItem = [self p_popupMenuHeaderItemForAccount:account];
  1175. [self p_moveJIDMenuItem:menuItem toIndex:currentIndex inMenu:menu];
  1176. ++currentIndex;
  1177. }
  1178. NSEnumerator *entryEnum = nil;
  1179. LPContactEntry *entry = nil;
  1180. // Online Contact Entries
  1181. entryEnum = [onlineEntries objectEnumerator];
  1182. while (entry = [entryEnum nextObject]) {
  1183. NSMenuItem *menuItem = [self p_popupMenuItemForEntry:entry];
  1184. [self p_moveJIDMenuItem:menuItem toIndex:currentIndex inMenu:menu];
  1185. [menuItem setAttributedTitle:[self p_attributedTitleOfJIDMenuItemForContactEntry:entry withFont:menuItemFont]];
  1186. ++currentIndex;
  1187. }
  1188. // Offline Contact Entries
  1189. entryEnum = [offlineEntries objectEnumerator];
  1190. while (entry = [entryEnum nextObject]) {
  1191. NSMenuItem *menuItem = [self p_popupMenuItemForEntry:entry];
  1192. [self p_moveJIDMenuItem:menuItem toIndex:currentIndex inMenu:menu];
  1193. [menuItem setAttributedTitle:[self p_attributedTitleOfJIDMenuItemForContactEntry:entry withFont:menuItemFont]];
  1194. ++currentIndex;
  1195. }
  1196. }
  1197. }
  1198. }
  1199. // Remove the remaining items that were left in the menu
  1200. while ([m_addressesPopUp numberOfItems] > currentIndex) {
  1201. [m_addressesPopUp removeItemAtIndex:currentIndex];
  1202. }
  1203. // Re-select the saved selection if it's still in the menu
  1204. if (selectedItem != nil && [m_addressesPopUp indexOfItem:selectedItem] >= 0) {
  1205. [m_addressesPopUp selectItem:selectedItem];
  1206. }
  1207. else {
  1208. LPContactEntry *entry = [m_chat activeContactEntry];
  1209. int activeEntryIndex = (entry ? [m_addressesPopUp indexOfItemWithRepresentedObject:[m_chat activeContactEntry]] : -1);
  1210. if (activeEntryIndex >= 0) {
  1211. [m_addressesPopUp selectItemAtIndex:activeEntryIndex];
  1212. }
  1213. }
  1214. [m_addressesPopUp synchronizeTitleAndSelectedItem];
  1215. }
  1216. - (void)p_JIDsMenuWillPop:(NSNotification *)notif
  1217. {
  1218. [self p_syncJIDsPopupMenu];
  1219. }
  1220. #pragma mark ******
  1221. - (void)p_setSendFieldHidden:(BOOL)hideFlag animate:(BOOL)animateFlag
  1222. {
  1223. BOOL isInputHidden = (m_collapsedHeightWhenLastWentOffline >= 1.0);
  1224. if (hideFlag != isInputHidden) {
  1225. // The visibility of the text field doesn't match the state of the connection. We'll have to either show it or hide it.
  1226. unsigned int chatViewAutoresizingMask = [m_chatWebView autoresizingMask];
  1227. unsigned int inputBoxAutoresizingMask = [m_inputControlsBar autoresizingMask];
  1228. // Disable the autoresizing of the views and make them stay where they are when we resize the window vertically
  1229. [m_chatWebView setAutoresizingMask:NSViewMinYMargin];
  1230. [m_inputControlsBar setAutoresizingMask:NSViewMinYMargin];
  1231. float deltaY = 0.0;
  1232. BOOL mustBecomeVisible = (!hideFlag && isInputHidden);
  1233. if (mustBecomeVisible) {
  1234. deltaY = m_collapsedHeightWhenLastWentOffline;
  1235. m_collapsedHeightWhenLastWentOffline = 0.0;
  1236. } else {
  1237. m_collapsedHeightWhenLastWentOffline = NSHeight([m_inputControlsBar frame]);
  1238. deltaY = -m_collapsedHeightWhenLastWentOffline;
  1239. }
  1240. if (mustBecomeVisible == NO)
  1241. [[self window] makeFirstResponder:nil];
  1242. [m_inputTextField setEnabled:mustBecomeVisible];
  1243. [m_segmentedButton setEnabled:mustBecomeVisible];
  1244. if (mustBecomeVisible)
  1245. [[self window] makeFirstResponder:m_inputTextField];
  1246. NSWindow *win = [m_inputControlsBar window];
  1247. NSRect windowFrame = [win frame];
  1248. windowFrame.origin.y -= deltaY;
  1249. windowFrame.size.height += deltaY;
  1250. [win setFrame:windowFrame display:YES animate:animateFlag];
  1251. // Restore the autoresizing masks
  1252. [m_chatWebView setAutoresizingMask:chatViewAutoresizingMask];
  1253. [m_inputControlsBar setAutoresizingMask:inputBoxAutoresizingMask];
  1254. // Readjust the size of the text field in case the window was resized while the input bar was collapsed
  1255. if (mustBecomeVisible)
  1256. [m_inputTextField calcContentSize];
  1257. [self p_fixResizeIndicator];
  1258. }
  1259. }
  1260. - (void)p_fixResizeIndicator
  1261. {
  1262. // The resize indicator drawn on the corner of the window has some drawing issues when we resize the window
  1263. // and it gets moved to or from over a scroll bar. The only way I got it to get drawn correctly was by disabling
  1264. // the indicator immediatelly and reanabling it only on the next pass through the run loop.
  1265. NSWindow *win = [self window];
  1266. // Resize Indicator
  1267. [win setShowsResizeIndicator:NO];
  1268. NSMethodSignature *methodSig = [win methodSignatureForSelector:@selector(setShowsResizeIndicator:)];
  1269. NSInvocation *inv = [NSInvocation invocationWithMethodSignature:methodSig];
  1270. BOOL flag = YES;
  1271. [inv setTarget:win];
  1272. [inv setSelector:@selector(setShowsResizeIndicator:)];
  1273. [inv setArgument:&flag atIndex:2];
  1274. [inv retainArguments];
  1275. [inv performSelector:@selector(invoke) withObject:nil afterDelay:0.0];
  1276. }
  1277. - (NSMutableSet *)p_pendingAudiblesSet
  1278. {
  1279. if (m_audibleResourceNamesWaitingForLoadCompletion == nil) {
  1280. m_audibleResourceNamesWaitingForLoadCompletion = [[NSMutableSet alloc] init];
  1281. }
  1282. return m_audibleResourceNamesWaitingForLoadCompletion;
  1283. }
  1284. - (void)p_appendStandardMessageBlockWithInnerHTML:(NSString *)innerHTML timestamp:(NSDate *)timestamp inbound:(BOOL)isInbound saveInHistory:(BOOL)shouldSave scrollMode:(LPScrollToVisibleMode)scrollMode
  1285. {
  1286. NSString *authorName = nil;
  1287. if (isInbound) {
  1288. authorName = [m_contact name];
  1289. } else {
  1290. NSString *globalName = [[LPAccountsController sharedAccountsController] name];
  1291. authorName = ( [globalName length] > 0 ?
  1292. globalName :
  1293. [[[[self chat] activeContactEntry] account] JID] );
  1294. }
  1295. NSString *htmlString = [m_chatViewsController HTMLStringForStandardBlockWithInnerHTML:innerHTML timestamp:timestamp authorName:authorName];
  1296. // if it's an outbound message, also scroll down so that the user can see what he has just written
  1297. [m_chatViewsController appendDIVBlockToWebViewWithInnerHTML:htmlString divClass:@"messageBlock" scrollToVisibleMode:scrollMode];
  1298. // Save in the recent history log
  1299. if (shouldSave) {
  1300. LPContactEntry *activeEntry = [m_chat activeContactEntry];
  1301. if (isInbound) {
  1302. [[LPRecentMessagesStore sharedMessagesStore] storeMessage:innerHTML
  1303. receivedFromJID:[activeEntry address]
  1304. thruAccountJID:[[activeEntry account] JID]];
  1305. } else {
  1306. [[LPRecentMessagesStore sharedMessagesStore] storeMessage:innerHTML
  1307. sentToJID:[activeEntry address]
  1308. thruAccountJID:[[activeEntry account] JID]];
  1309. }
  1310. }
  1311. }
  1312. - (void)p_appendMessageToWebView:(NSString *)message subject:(NSString *)subject timestamp:(NSDate *)timestamp inbound:(BOOL)isInbound
  1313. {
  1314. NSString *messageHTML = [m_chatViewsController HTMLifyRawMessageString:message];
  1315. if ([subject length] > 0) {
  1316. NSString *subjectHTML = [m_chatViewsController HTMLifyRawMessageString:subject];
  1317. messageHTML = [NSString stringWithFormat:@"<b>%@:</b> %@", subjectHTML, messageHTML];
  1318. }
  1319. LPScrollToVisibleMode scrollMode = (isInbound ? LPScrollWithAnimationIfAtBottom : LPScrollWithJumpOrAnimationIfAtBottom);
  1320. [self p_appendStandardMessageBlockWithInnerHTML:messageHTML timestamp:timestamp inbound:isInbound saveInHistory:YES scrollMode:scrollMode];
  1321. }
  1322. - (void)p_appendAudibleWithResourceName:(NSString *)resourceName inbound:(BOOL)inbound
  1323. {
  1324. NSString *pathForHTMLFile = [[NSBundle mainBundle] pathForResource:@"AudibleObject" ofType:@"html" inDirectory:@"ChatView"];
  1325. NSMutableString *htmlCode = [NSMutableString stringWithContentsOfFile:pathForHTMLFile];
  1326. LPAudibleSet *audibleSet = [LPAudibleSet defaultAudibleSet];
  1327. NSString *audibleFilePath = [audibleSet filepathForAudibleWithName:resourceName];
  1328. NSString *audibleCaption = [audibleSet captionForAudibleWithName:resourceName];
  1329. NSString *audibleText = [audibleSet textForAudibleWithName:resourceName];
  1330. if (audibleFilePath) {
  1331. [htmlCode replaceOccurrencesOfString:@"%%AUDIBLE_URL%%"
  1332. withString:[[NSURL fileURLWithPath:audibleFilePath] absoluteString]
  1333. options:NSLiteralSearch
  1334. range:NSMakeRange(0, [htmlCode length])];
  1335. [htmlCode replaceOccurrencesOfString:@"%%AUDIBLE_CAPTION%%"
  1336. withString:[audibleCaption stringByEscapingHTMLEntities]
  1337. options:NSLiteralSearch
  1338. range:NSMakeRange(0, [htmlCode length])];
  1339. [htmlCode replaceOccurrencesOfString:@"%%AUDIBLE_TEXT%%"
  1340. withString:[audibleText stringByEscapingHTMLEntities]
  1341. options:NSLiteralSearch
  1342. range:NSMakeRange(0, [htmlCode length])];
  1343. }
  1344. else {
  1345. // We tried to load the audible and didn't get a file in the end. It's probably an invalid resource name.
  1346. htmlCode = [NSString stringWithFormat:@"Received an invalid audible (ref.: %@)",
  1347. [resourceName stringByEscapingHTMLEntities]];
  1348. }
  1349. if (inbound) {
  1350. // Don't do everything at the same time. Allow the scroll animation to run first so that it doesn't appear choppy.
  1351. [[m_chatViewsController grabMethodForAfterScrollingWithTarget:self]
  1352. p_notifyUserAboutReceivedMessage:audibleCaption
  1353. notificationsHandlerSelector:( !m_hasAlreadyProcessedSomeMessages ?
  1354. @selector(notifyReceptionOfFirstMessage:fromContact:) :
  1355. @selector(notifyReceptionOfMessage:fromContact:) )];
  1356. }
  1357. LPScrollToVisibleMode scrollMode = (inbound ? LPScrollWithAnimationIfAtBottom : LPScrollWithJumpOrAnimationIfAtBottom);
  1358. [self p_appendStandardMessageBlockWithInnerHTML:htmlCode timestamp:[NSDate date] inbound:inbound saveInHistory:YES scrollMode:scrollMode];
  1359. }
  1360. - (void)p_appendStoredRecentMessagesToWebView
  1361. {
  1362. LPRecentMessagesStore *recentMessagesStore = [LPRecentMessagesStore sharedMessagesStore];
  1363. NSArray *messagesList = [recentMessagesStore recentMessagesExchangedWithContact:[self contact]];
  1364. NSCalendarDate *prevDate = nil;
  1365. NSEnumerator *messageEnum = [messagesList objectEnumerator];
  1366. NSDictionary *messageRec;
  1367. while (messageRec = [messageEnum nextObject]) {
  1368. NSDate *timestamp = [messageRec objectForKey:@"Timestamp"];
  1369. NSString *message = [messageRec objectForKey:@"MessageText"];
  1370. NSString *kind = [messageRec objectForKey:@"Kind"];
  1371. NSCalendarDate *curDate = [timestamp dateWithCalendarFormat:NSLocalizedString(@"%b %e, %Y",
  1372. @"date format for chat recent messages")
  1373. timeZone:nil];
  1374. if (prevDate == nil || [prevDate dayOfCommonEra] != [curDate dayOfCommonEra]) {
  1375. // Post a "system message" to signal the change in the date
  1376. NSString *systemMessage = [NSString stringWithFormat:NSLocalizedString(@"Recent messages exchanged on %@",
  1377. @"chat recent messages"),
  1378. [curDate description]];
  1379. [m_chatViewsController appendDIVBlockToWebViewWithInnerHTML:[systemMessage stringByEscapingHTMLEntities]
  1380. divClass:@"systemMessage"
  1381. scrollToVisibleMode:LPScrollWithJump];
  1382. }
  1383. prevDate = curDate;
  1384. if ([kind isEqualToString:@"RawHTMLBlock"]) {
  1385. [m_chatViewsController appendDIVBlockToWebViewWithInnerHTML:message
  1386. divClass:[messageRec objectForKey:@"DIVClass"]
  1387. scrollToVisibleMode:LPScrollWithJump];
  1388. }
  1389. else {
  1390. [self p_appendStandardMessageBlockWithInnerHTML:message
  1391. timestamp:timestamp
  1392. inbound:[kind isEqualToString:@"Received"]
  1393. saveInHistory:NO
  1394. scrollMode:LPScrollWithJump ];
  1395. }
  1396. }
  1397. }
  1398. - (void)p_resizeInputFieldToContentsSize:(NSSize)newSize
  1399. {
  1400. // Determine the new window frame
  1401. float heightDifference = newSize.height - NSHeight([m_inputTextField bounds]);
  1402. BOOL inputBarIsCollapsed = (m_collapsedHeightWhenLastWentOffline >= 1.0);
  1403. if ((inputBarIsCollapsed == NO) && ((heightDifference > 0.5) || (heightDifference < -0.5))) {
  1404. NSRect newWindowFrame = [[self window] frame];
  1405. newWindowFrame.size.height += heightDifference;
  1406. newWindowFrame.origin.y -= heightDifference;
  1407. // Make sure the window is completely enclosed within the screen rect
  1408. NSRect screenRect = [[[self window] screen] visibleFrame];
  1409. if (NSContainsRect(screenRect, newWindowFrame) == NO) {
  1410. float dX = 0.0, dY = 0.0;
  1411. if (NSMinX(screenRect) > NSMinX(newWindowFrame)) {
  1412. dX = NSMinX(screenRect) - NSMinX(newWindowFrame);
  1413. }
  1414. else if (NSMaxX(screenRect) < NSMaxX(newWindowFrame)) {
  1415. dX = NSMaxX(screenRect) - NSMaxX(newWindowFrame);
  1416. }
  1417. if (NSMinY(screenRect) > NSMinY(newWindowFrame)) {
  1418. dY = NSMinY(screenRect) - NSMinY(newWindowFrame);
  1419. }
  1420. else if (NSMaxY(screenRect) < NSMaxY(newWindowFrame)) {
  1421. dY = NSMaxY(screenRect) - NSMaxY(newWindowFrame);
  1422. }
  1423. newWindowFrame = NSOffsetRect(newWindowFrame, dX, dY);
  1424. }
  1425. // Do the actual resizing
  1426. unsigned int webViewResizeMask = [m_chatWebView autoresizingMask];
  1427. unsigned int inputBoxResizeMask = [m_inputControlsBar autoresizingMask];
  1428. [m_chatWebView setAutoresizingMask:NSViewMinYMargin];
  1429. [m_inputControlsBar setAutoresizingMask:NSViewHeightSizable];
  1430. [[self window] setFrame:newWindowFrame display:YES animate:YES];
  1431. [m_inputControlsBar setAutoresizingMask:inputBoxResizeMask];
  1432. [m_chatWebView setAutoresizingMask:webViewResizeMask];
  1433. }
  1434. }
  1435. - (void)p_updateChatBackgroundColorFromDefaults
  1436. {
  1437. NSData *encodedChatBGColor = [[NSUserDefaults standardUserDefaults] objectForKey:@"ChatBackgroundColor"];
  1438. NSColor *backgroundColor = [NSUnarchiver unarchiveObjectWithData:encodedChatBGColor];
  1439. [m_chatWebView setBackgroundColor:backgroundColor];
  1440. }
  1441. - (void)p_setupChatDocumentTitle
  1442. {
  1443. NSString *timeFormat = NSLocalizedString(@"%Y-%m-%d %Hh%Mm%Ss",
  1444. @"time format for chat transcripts titles and filenames");
  1445. NSMutableString *mutableTimeFormat = [timeFormat mutableCopy];
  1446. // Make the timeFormat safe for filenames
  1447. [mutableTimeFormat replaceOccurrencesOfString:@":" withString:@"." options:0
  1448. range:NSMakeRange(0, [mutableTimeFormat length])];
  1449. [mutableTimeFormat replaceOccurrencesOfString:@"/" withString:@"-" options:0
  1450. range:NSMakeRange(0, [mutableTimeFormat length])];
  1451. NSString *newTitle = [NSString stringWithFormat:
  1452. NSLocalizedString(@"Chat with \"%@\" on %@", @"filename and title for saved chat transcripts"),
  1453. [[self contact] name],
  1454. [[NSDate date] descriptionWithCalendarFormat:mutableTimeFormat timeZone:nil locale:nil]];
  1455. [m_chatViewsController setChatDocumentTitle:newTitle];
  1456. [mutableTimeFormat release];
  1457. }
  1458. - (void)p_setSaveChatTranscriptEnabled:(BOOL)flag
  1459. {
  1460. if (flag && !m_isAutoSavingChatTranscript) {
  1461. [m_autoSaveChatTranscriptTimer invalidate];
  1462. [m_autoSaveChatTranscriptTimer release];
  1463. // Randomize the timer a bit so that they're not firing all at the same time.
  1464. // They are going to get delays in the interval [ 25.0 ; 35.0 ]
  1465. float randomCoef = 2.0 * ((float)rand() / (float)RAND_MAX) - 1.0;
  1466. float interval = 30.0 + (5.0 * randomCoef);
  1467. m_autoSaveChatTranscriptTimer = [[NSTimer scheduledTimerWithTimeInterval:interval
  1468. target:self
  1469. selector:@selector(p_autoSaveChatTranscript:)
  1470. userInfo:nil
  1471. repeats:YES] retain];
  1472. m_isAutoSavingChatTranscript = YES;
  1473. }
  1474. else if (!flag && m_isAutoSavingChatTranscript) {
  1475. // Save one last time
  1476. [m_autoSaveChatTranscriptTimer fire];
  1477. m_isAutoSavingChatTranscript = NO;
  1478. [m_autoSaveChatTranscriptTimer invalidate];
  1479. [m_autoSaveChatTranscriptTimer release];
  1480. m_autoSaveChatTranscriptTimer = nil;
  1481. }
  1482. }
  1483. - (void)p_autoSaveChatTranscript:(NSTimer *)timer
  1484. {
  1485. // Are there any messages worth saving?
  1486. if (m_hasAlreadyProcessedSomeMessages) {
  1487. NSError *error;
  1488. NSString *chatTranscriptFolderName = [[self contact] name];
  1489. NSString *chatTranscriptFolderPath = [LPChatTranscriptsFolderPath() stringByAppendingPathComponent:chatTranscriptFolderName];
  1490. [[NSFileManager defaultManager] createDirectoryAtPath:chatTranscriptFolderPath attributes:nil];
  1491. // Set the custom icon for the folder
  1492. @try {
  1493. IconFamily *iconFamily = [IconFamily iconFamilyWithThumbnailsOfImage:[[self contact] avatar]];
  1494. [iconFamily setAsCustomIconForDirectory:chatTranscriptFolderPath];
  1495. }
  1496. @catch (NSException *exception) {
  1497. // Nothing serious, really. The default folder icon is just as good.
  1498. }
  1499. NSString *chatTranscriptPath = [[chatTranscriptFolderPath stringByAppendingPathComponent:
  1500. [m_chatViewsController chatDocumentTitle]] stringByAppendingPathExtension:@"webarchive"];
  1501. [m_chatViewsController saveDocumentToFile:chatTranscriptPath hideExtension:YES error:&error];
  1502. }
  1503. }
  1504. - (void)p_displayAndReloadPubBannerIfNeeded
  1505. {
  1506. LPContactEntry *entryHavingPub = [[m_chat contact] firstContactEntryWithCapsFeature:@"http://messenger.sapo.pt/features/banners/chat"];
  1507. if (entryHavingPub != nil && [[[entryHavingPub account] pubManager] chatBotAdsBaseURL] != nil) {
  1508. NSWindow *win = [self window];
  1509. if (![m_pubElementsView isDescendantOf:[win contentView]]) {
  1510. // Insert Pub Elements in the window
  1511. NSSize minWinSize = [win minSize];
  1512. NSRect winFrame = [win frame];
  1513. float pubHeight = NSHeight([m_pubElementsView frame]);
  1514. // Start by shrinking the window to make room for the ads
  1515. winFrame.size.height = MAX(minWinSize.height, NSHeight(winFrame) - pubHeight);
  1516. winFrame.origin.y = NSMaxY([win frame]) - winFrame.size.height;
  1517. [win setFrame:winFrame display:YES animate:YES];
  1518. winFrame.size.height += pubHeight;
  1519. winFrame.origin.y -= pubHeight;
  1520. // Now expand it downwards
  1521. unsigned int savedChatElementsMask = [m_standardChatElementsView autoresizingMask];
  1522. [m_standardChatElementsView setAutoresizingMask:( NSViewWidthSizable | NSViewMinYMargin )];
  1523. [win setFrame:winFrame display:YES animate:YES];
  1524. [m_standardChatElementsView setAutoresizingMask:savedChatElementsMask];
  1525. // Resize and Insert the new view
  1526. [m_pubElementsView setFrame:NSMakeRect(0.0, 0.0, NSWidth(winFrame), pubHeight)];
  1527. [[win contentView] addSubview:m_pubElementsView];
  1528. }
  1529. // Load the content of the banner webview
  1530. NSURL *requestURL = [[[entryHavingPub account] pubManager] chatBotAdURLForBotWithJID:[entryHavingPub address]];
  1531. if (requestURL != nil) {
  1532. [[m_pubBannerWebView mainFrame] loadRequest:[NSURLRequest requestWithURL:requestURL]];
  1533. }
  1534. }
  1535. }
  1536. - (void)p_fetchHTMLforChatBotDidFinish:(NSString *)htmlCode
  1537. {
  1538. if (htmlCode)
  1539. [[m_pubBannerWebView mainFrame] loadHTMLString:htmlCode baseURL:nil];
  1540. }
  1541. - (void)p_incrementUnreadMessagesCount
  1542. {
  1543. [self willChangeValueForKey:@"numberOfUnreadMessages"];
  1544. ++m_nrUnreadMessages;
  1545. [self didChangeValueForKey:@"numberOfUnreadMessages"];
  1546. [m_unreadCountImageView setImage:[m_unreadMessagesBadge largeBadgeForValue:m_nrUnreadMessages]];
  1547. [self p_updateMiniwindowImage];
  1548. }
  1549. - (void)p_resetUnreadMessagesCount
  1550. {
  1551. [self willChangeValueForKey:@"numberOfUnreadMessages"];
  1552. m_nrUnreadMessages = 0;
  1553. [self didChangeValueForKey:@"numberOfUnreadMessages"];
  1554. [m_unreadCountImageView setImage:nil];
  1555. [self p_updateMiniwindowImage];
  1556. }
  1557. - (void)p_updateMiniwindowImage
  1558. {
  1559. if (m_unreadMessagesBadge == nil) {
  1560. m_unreadMessagesBadge = [[CTBadge alloc] init];
  1561. }
  1562. NSImage *badgedImage = ( m_nrUnreadMessages > 0 ?
  1563. [m_unreadMessagesBadge badgeOverlayImageForValue:m_nrUnreadMessages insetX:0.0 y:0.0] :
  1564. [[[NSImage alloc] initWithSize:NSMakeSize(128.0, 128.0)] autorelease] );
  1565. NSRect badgedImageRect = { { 0.0, 0.0 }, [badgedImage size] };
  1566. NSImage *avatarImage = [[self contact] avatar];
  1567. NSRect avatarImageRect = { { 0.0, 0.0 }, [avatarImage size] };
  1568. NSImage *appIcon = [NSImage imageNamed:@"NSApplicationIcon"];
  1569. NSRect appIconSrcRect = { { 0.0, 0.0 }, [appIcon size] };
  1570. float appIconSize = 48.0;
  1571. NSRect appIconDstRect = NSMakeRect(NSWidth(badgedImageRect) - appIconSize - 2.0, 2.0, appIconSize, appIconSize);
  1572. // Add the badges
  1573. [badgedImage lockFocus];
  1574. [avatarImage drawInRect:NSInsetRect(badgedImageRect, 4.0, 4.0)
  1575. fromRect:avatarImageRect
  1576. operation:NSCompositeDestinationOver
  1577. fraction:1.0];
  1578. [appIcon drawInRect:appIconDstRect
  1579. fromRect:appIconSrcRect
  1580. operation:NSCompositeSourceOver
  1581. fraction:1.0];
  1582. [badgedImage unlockFocus];
  1583. [[self window] setMiniwindowImage:badgedImage];
  1584. }
  1585. - (void)p_notifyUserAboutReceivedMessage:(NSString *)msgText notificationsHandlerSelector:(SEL)selector
  1586. {
  1587. NSWindow *win = [self window];
  1588. // Notifications
  1589. if (![NSApp isActive] || ![win isVisible]) {
  1590. [[LPEventNotificationsHandler defaultHandler] performSelector:selector withObject:msgText withObject:[self contact]];
  1591. }
  1592. // Unread message accounting
  1593. if (![win isKeyWindow]) {
  1594. [self p_incrementUnreadMessagesCount];
  1595. }
  1596. if ([m_delegate respondsToSelector:@selector(chatControllerDidReceiveNewMessage:)]) {
  1597. [m_delegate chatControllerDidReceiveNewMessage:self];
  1598. }
  1599. }
  1600. #pragma mark -
  1601. #pragma mark WebView Frame Load Delegate Methods
  1602. - (void)webView:(WebView *)sender windowScriptObjectAvailable:(WebScriptObject *)windowScriptObject
  1603. {
  1604. if (m_chatJSInterface == nil) {
  1605. m_chatJSInterface = [[LPChatJavaScriptInterface alloc] init];
  1606. [m_chatJSInterface setAccount:[[[self chat] activeContactEntry] account]];
  1607. }
  1608. /* Make it available to the WebView's JavaScript environment */
  1609. [windowScriptObject setValue:m_chatJSInterface forKey:@"chatJSInterface"];
  1610. }
  1611. - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
  1612. {
  1613. [self p_updateChatBackgroundColorFromDefaults];
  1614. [self p_setupChatDocumentTitle];
  1615. [m_chatViewsController dumpQueuedMessagesToWebView];
  1616. [m_chatViewsController showEmoticonsAsImages:[[NSUserDefaults standardUserDefaults] boolForKey:@"DisplayEmoticonImages"]];
  1617. }
  1618. #pragma mark WebView UI Delegate Methods
  1619. - (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
  1620. {
  1621. if (sender == m_chatWebView) {
  1622. NSMutableArray *itemsToReturn = [NSMutableArray array];
  1623. NSEnumerator *enumerator = [defaultMenuItems objectEnumerator];
  1624. id menuItem;
  1625. while ((menuItem = [enumerator nextObject]) != nil) {
  1626. switch ([menuItem tag]) {
  1627. case WebMenuItemTagCopyLinkToClipboard:
  1628. case WebMenuItemTagCopy:
  1629. case WebMenuItemTagSpellingGuess:
  1630. case WebMenuItemTagNoGuessesFound:
  1631. case WebMenuItemTagIgnoreSpelling:
  1632. case WebMenuItemTagLearnSpelling:
  1633. case WebMenuItemTagOther:
  1634. [itemsToReturn addObject:menuItem];
  1635. break;
  1636. }
  1637. }
  1638. return itemsToReturn;
  1639. }
  1640. else {
  1641. return [NSArray array];
  1642. }
  1643. }
  1644. - (unsigned)webView:(WebView *)sender dragDestinationActionMaskForDraggingInfo:(id <NSDraggingInfo>)draggingInfo
  1645. {
  1646. // We don't want the WebView to process anything dropped on it
  1647. return WebDragDestinationActionNone;
  1648. }
  1649. - (unsigned)webView:(WebView *)sender dragSourceActionMaskForPoint:(NSPoint)point
  1650. {
  1651. if (sender == m_pubBannerWebView)
  1652. return WebDragSourceActionNone;
  1653. else
  1654. return WebDragSourceActionAny;
  1655. }
  1656. #pragma mark WebView Delegate Methods (Pub Stuff)
  1657. - (WebView *)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request
  1658. {
  1659. if (sender == m_pubBannerWebView) {
  1660. /*
  1661. * We always get a nil request parameter in this method, it's probably a bug in WebKit or the Flash plug-in.
  1662. * In order to intercept the URL that WebKit is trying to open (so that we can redirect it to the system default
  1663. * web browser) we give it a dummy WebView if it wants to open a new window and make ourselves the WebPolicyDelegate
  1664. * for that dummy view in order to be able to intercept the URL being opened.
  1665. */
  1666. WebView *myDummyPubViewAux = [[WebView alloc] init];
  1667. [myDummyPubViewAux setPolicyDelegate:self];
  1668. return myDummyPubViewAux;
  1669. }
  1670. else {
  1671. return nil;
  1672. }
  1673. }
  1674. - (void)webView:(WebView *)sender decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener
  1675. {
  1676. if (sender == m_chatWebView) {
  1677. [[NSWorkspace sharedWorkspace] openURL:[request URL]];
  1678. [listener ignore];
  1679. }
  1680. else if (sender == m_pubBannerWebView) {
  1681. [listener use];
  1682. }
  1683. else {
  1684. [[NSWorkspace sharedWorkspace] openURL:[request URL]];
  1685. [listener ignore];
  1686. [sender autorelease];
  1687. }
  1688. }
  1689. #pragma mark -
  1690. #pragma mark NSWindow Delegate Methods
  1691. - (void)windowDidBecomeKey:(NSNotification *)aNotification
  1692. {
  1693. NSWindow *win = [self window];
  1694. if ([win level] > NSNormalWindowLevel) {
  1695. [win setLevel:NSNormalWindowLevel];
  1696. [win setAlphaValue:1.0];
  1697. }
  1698. [self p_resetUnreadMessagesCount];
  1699. }
  1700. - (void)windowDidMove:(NSNotification *)notification
  1701. {
  1702. NSWindow *win = [notification object];
  1703. NSString *contactName = [[self contact] name];
  1704. if ([win isVisible] && [contactName length] > 0) {
  1705. [[self class] saveWindowFrame:[win frame] forChatWithContactNamed:contactName];
  1706. }
  1707. }
  1708. - (void)windowDidResize:(NSNotification *)notification
  1709. {
  1710. NSWindow *win = [notification object];
  1711. NSString *contactName = [[self contact] name];
  1712. if ([win isVisible] && [contactName length] > 0) {
  1713. [[self class] saveWindowFrame:[win frame] forChatWithContactNamed:contactName];
  1714. }
  1715. }
  1716. - (void)windowWillClose:(NSNotification *)aNotification
  1717. {
  1718. [self p_resetUnreadMessagesCount];
  1719. // Undo the retain cycles we have established until now
  1720. [m_audiblesController setChatController:nil];
  1721. [m_chatWebView setChat:nil];
  1722. [[m_chatWebView windowScriptObject] setValue:[NSNull null] forKey:@"chatJSInterface"];
  1723. // If the WebView hasn't finished loading when the window is closed (extremely rare, but could happen), then we don't
  1724. // want to do any of the setup that is about to happen in our frame load delegate methods, since the window is going away
  1725. // anyway. If we allowed that setup to happen when the window is already closed it could originate some crashes, since
  1726. // most of the stuff was already released by the time the delegate methods get called.
  1727. [m_chatWebView setFrameLoadDelegate:nil];
  1728. [m_chatWebView setUIDelegate:nil];
  1729. [m_pubBannerWebView setFrameLoadDelegate:nil];
  1730. [m_pubBannerWebView setUIDelegate:nil];
  1731. // Make sure that the delayed perform of p_displayAndReloadPubBannerIfNeeded doesn't fire
  1732. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(p_displayAndReloadPubBannerIfNeeded) object:nil];
  1733. // Stop auto-saving our chat transcript
  1734. [self p_setSaveChatTranscriptEnabled:NO];
  1735. // Make sure that the content views of our drawers do not leak! (This is a known issue with Cocoa: drawers leak
  1736. // if their parent window is closed while they're open.)
  1737. [[[aNotification object] drawers] makeObjectsPerformSelector:@selector(setContentView:) withObject:nil];
  1738. [[[aNotification object] drawers] makeObjectsPerformSelector:@selector(close)];
  1739. // Cancel the pending chat typing notification if there was some text already entered but not yet sent
  1740. if (m_lastInputTextFieldStringLength > 0)
  1741. [m_chat setUserIsTyping:NO];
  1742. [m_chat endChat];
  1743. [m_chat setDelegate:nil];
  1744. if ([m_delegate respondsToSelector:@selector(chatControllerWindowWillClose:)]) {
  1745. [m_delegate chatControllerWindowWillClose:self];
  1746. }
  1747. }
  1748. - (id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)anObject
  1749. {
  1750. // This provides support for drag'n'drop to an active text entry field
  1751. if ([anObject isKindOfClass:[LPChatTextField class]])
  1752. return [anObject customFieldEditor];
  1753. else
  1754. return nil;
  1755. }
  1756. #pragma mark -
  1757. #pragma mark NSControl Delegate Methods
  1758. - (void)controlTextDidChange:(NSNotification *)aNotification
  1759. {
  1760. m_currentInputLineHistoryEntryIndex = 0;
  1761. // Chat typing events
  1762. NSUInteger currentStringLen = [[m_inputTextField stringValue] length];
  1763. if (m_lastInputTextFieldStringLength == 0 && currentStringLen > 0) {
  1764. // send composing event
  1765. [[self chat] setUserIsTyping:YES];
  1766. }
  1767. else if (m_lastInputTextFieldStringLength > 0 && currentStringLen == 0) {
  1768. // send cancellation of previous event
  1769. [[self chat] setUserIsTyping:NO];
  1770. }
  1771. m_lastInputTextFieldStringLength = currentStringLen;
  1772. }
  1773. - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
  1774. {
  1775. // NSLog(@"command: %@", NSStringFromSelector(command));
  1776. if (command == @selector(pageDown:) || command == @selector(pageUp:) ||
  1777. command == @selector(scrollPageDown:) || command == @selector(scrollPageUp:) ||
  1778. /* The following two selectors are undocumented. They're used by Cocoa to represent a Home or End key press. */
  1779. command == @selector(scrollToBeginningOfDocument:) || command == @selector(scrollToEndOfDocument:) )
  1780. {
  1781. [[[m_chatWebView mainFrame] frameView] doCommandBySelector:command];
  1782. return YES;
  1783. }
  1784. else if (command == @selector(moveToBeginningOfDocument:) || command == @selector(moveToEndOfDocument:)) {
  1785. if (m_currentInputLineHistoryEntryIndex == 0) {
  1786. if ([m_inputLineHistory count] > 0) {
  1787. [m_inputLineHistory replaceObjectAtIndex:0 withObject:[m_inputTextField attributedStringValue]];
  1788. }
  1789. else {
  1790. [m_inputLineHistory addObject:[m_inputTextField attributedStringValue]];
  1791. }
  1792. }
  1793. if (command == @selector(moveToBeginningOfDocument:))
  1794. m_currentInputLineHistoryEntryIndex = (m_currentInputLineHistoryEntryIndex + 1) % [m_inputLineHistory count];
  1795. else
  1796. m_currentInputLineHistoryEntryIndex = (m_currentInputLineHistoryEntryIndex > 0 ?
  1797. m_currentInputLineHistoryEntryIndex :
  1798. [m_inputLineHistory count]) - 1;
  1799. [m_inputTextField setAttributedStringValue:[m_inputLineHistory objectAtIndex:m_currentInputLineHistoryEntryIndex]];
  1800. [m_inputTextField performSelector:@selector(calcContentSize) withObject:nil afterDelay:0.0];
  1801. return YES;
  1802. }
  1803. else {
  1804. return NO;
  1805. }
  1806. }
  1807. #pragma mark LPGrowingTextField Delegate Methods
  1808. - (void)growingTextField:(LPGrowingTextField *)textField contentSizeDidChange:(NSSize)neededSize
  1809. {
  1810. [self p_resizeInputFieldToContentsSize:neededSize];
  1811. }
  1812. #pragma mark LPChatTextField Delegate Methods
  1813. - (BOOL)chatTextFieldShouldSupportFileDrops:(LPChatTextField *)tf
  1814. {
  1815. return [[[self chat] activeContactEntry] canDoFileTransfer];
  1816. }
  1817. - (BOOL)chatTextField:(LPChatTextField *)tf sendFileWithPathname:(NSString *)filepath
  1818. {
  1819. LPContactEntry *entry = [m_chat activeContactEntry];
  1820. if ([entry canDoFileTransfer]) {
  1821. [[LPFileTransfersManager fileTransfersManager] startSendingFile:filepath toContactEntry:entry];
  1822. return YES;
  1823. }
  1824. else {
  1825. return NO;
  1826. }
  1827. }
  1828. #pragma mark -
  1829. #pragma mark NSToolbar Methods
  1830. - (void)p_setupToolbar
  1831. {
  1832. // Create a new toolbar instance
  1833. NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"LPChatToolbar"];
  1834. [toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
  1835. [toolbar setSizeMode:NSToolbarSizeModeSmall];
  1836. // Set up toolbar properties: Allow customization, give a default display mode, and remember state in user defaults
  1837. [toolbar setAllowsUserCustomization:YES];
  1838. [toolbar setAutosavesConfiguration:YES];
  1839. // We are the delegate.
  1840. [toolbar setDelegate:self];
  1841. // Attach the toolbar to the window.
  1842. [[self window] setToolbar:toolbar];
  1843. [toolbar release];
  1844. }
  1845. - (IBAction)p_openChatTranscriptsFolder:(id)sender
  1846. {
  1847. NSString *folderPath = LPChatTranscriptsFolderPath();
  1848. if (folderPath == nil) {
  1849. NSBeep();
  1850. }
  1851. else {
  1852. [[NSWorkspace sharedWorkspace] openFile:folderPath];
  1853. }
  1854. }
  1855. - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)identifier willBeInsertedIntoToolbar:(BOOL)willBeInserted
  1856. {
  1857. // Create our toolbar items.
  1858. NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:identifier];
  1859. if ([identifier isEqualToString:ToolbarFileSendIdentifier])
  1860. {
  1861. [item setLabel:NSLocalizedString(@"Send File", @"toolbar button label")];
  1862. [item setPaletteLabel:NSLocalizedString(@"Send File", @"toolbar button label")];
  1863. [item setImage:[NSImage imageNamed:@"FileUpload"]];
  1864. [item setToolTip:NSLocalizedString(@"Send File", @"toolbar button")];
  1865. [item setAction:@selector(sendFile:)];
  1866. [item setTarget:self];
  1867. }
  1868. else if ([identifier isEqualToString:ToolbarSendSMSIdentifier])
  1869. {
  1870. [item setLabel:NSLocalizedString(@"Send SMS", @"toolbar button label")];
  1871. [item setPaletteLabel:NSLocalizedString(@"Send SMS", @"toolbar button label")];
  1872. [item setImage:[NSImage imageNamed:@"sendSMS"]];
  1873. [item setToolTip:NSLocalizedString(@"Send SMS", @"toolbar button")];
  1874. [item setAction:@selector(sendSMS:)];
  1875. [item setTarget:self];
  1876. }
  1877. else if ([identifier isEqualToString:ToolbarInfoIdentifier])
  1878. {
  1879. [item setLabel:NSLocalizedString(@"Get Info", @"toolbar button label")];
  1880. [item setPaletteLabel:NSLocalizedString(@"Get Info", @"toolbar button label")];
  1881. [item setImage:[NSImage imageNamed:@"info"]];
  1882. [item setToolTip:NSLocalizedString(@"Get Info", @"toolbar button")];
  1883. [item setAction:@selector(editContact:)];
  1884. [item setTarget:self];
  1885. }
  1886. else if ([identifier isEqualToString:ToolbarHistoryIdentifier])
  1887. {
  1888. [item setLabel:NSLocalizedString(@"History", @"toolbar button label")];
  1889. [item setPaletteLabel:NSLocalizedString(@"Chat History", @"toolbar button label")];
  1890. [item setImage:[NSImage imageNamed:@"HistoryFolder"]];
  1891. [item setToolTip:NSLocalizedString(@"Open chat history folder", @"toolbar button")];
  1892. [item setAction:@selector(p_openChatTranscriptsFolder:)];
  1893. [item setTarget:self];
  1894. }
  1895. else
  1896. {
  1897. // Invalid identifier!
  1898. NSLog(@"WARNING: Invalid toolbar item identifier: %@", identifier);
  1899. item = nil;
  1900. }
  1901. return [item autorelease];
  1902. }
  1903. - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar
  1904. {
  1905. return [NSArray arrayWithObjects:
  1906. ToolbarInfoIdentifier,
  1907. ToolbarFileSendIdentifier,
  1908. ToolbarSendSMSIdentifier,
  1909. // NSToolbarShowFontsItemIdentifier,
  1910. // NSToolbarShowColorsItemIdentifier,
  1911. NSToolbarFlexibleSpaceItemIdentifier,
  1912. ToolbarHistoryIdentifier,
  1913. nil];
  1914. }
  1915. - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar
  1916. {
  1917. return [NSArray arrayWithObjects:
  1918. ToolbarInfoIdentifier,
  1919. ToolbarFileSendIdentifier,
  1920. ToolbarSendSMSIdentifier,
  1921. ToolbarHistoryIdentifier,
  1922. NSToolbarCustomizeToolbarItemIdentifier,
  1923. NSToolbarFlexibleSpaceItemIdentifier,
  1924. NSToolbarSeparatorItemIdentifier,
  1925. // NSToolbarShowFontsItemIdentifier,
  1926. // NSToolbarShowColorsItemIdentifier,
  1927. NSToolbarSpaceItemIdentifier,
  1928. NSToolbarPrintItemIdentifier,
  1929. nil];
  1930. }
  1931. - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
  1932. {
  1933. SEL action = [theItem action];
  1934. if (action == @selector(sendSMS:)) {
  1935. return ([m_contact canDoSMS] && [[LPAccountsController sharedAccountsController] isOnline]);
  1936. }
  1937. else {
  1938. BOOL enabled = [self p_validateAction:action];
  1939. if (action == @selector(sendFile:)) {
  1940. if (enabled)
  1941. [theItem setToolTip:NSLocalizedString(@"Send File", @"toolbar button")];
  1942. else if ([m_contact canDoFileTransfer])
  1943. [theItem setToolTip:NSLocalizedString(@"The currently selected address doesn't support file transfers. You can send a file by selecting another address of this contact in the pop-up menu below.", @"\"Send File\" button tooltip")];
  1944. else
  1945. [theItem setToolTip:NSLocalizedString(@"This contact doesn't support file transfers.", @"\"Send File\" button tooltip")];
  1946. }
  1947. return enabled;
  1948. }
  1949. }
  1950. @end