PageRenderTime 65ms CodeModel.GetById 24ms RepoModel.GetById 1ms 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

Large files files are truncated, but you can click here to view the full file

  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_attribut

Large files files are truncated, but you can click here to view the full file