PageRenderTime 63ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/Plugins/WebKit Message View/AIWebkitMessageViewStyle.m

https://bitbucket.org/jsixface/adium
Objective C | 1355 lines | 1000 code | 218 blank | 137 comment | 235 complexity | d6034c101bb8e501b66f8f05c8980eb8 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.0, ISC, BSD-3-Clause

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

  1. /*
  2. * Adium is the legal property of its developers, whose names are listed in the copyright file included
  3. * with this source distribution.
  4. *
  5. * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
  6. * General Public License as published by the Free Software Foundation; either version 2 of the License,
  7. * or (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
  10. * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
  11. * Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along with this program; if not,
  14. * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  15. */
  16. #import "AIWebkitMessageViewStyle.h"
  17. #import <AIUtilities/AIColorAdditions.h>
  18. #import <AIUtilities/AIStringAdditions.h>
  19. #import <AIUtilities/AIDateFormatterAdditions.h>
  20. #import <AIUtilities/AIMutableStringAdditions.h>
  21. #import <Adium/AIAccount.h>
  22. #import <Adium/AIChat.h>
  23. #import <Adium/AIContentTopic.h>
  24. #import <Adium/AIContentContext.h>
  25. #import <Adium/AIContentMessage.h>
  26. #import <Adium/AIContentNotification.h>
  27. #import <Adium/AIContentObject.h>
  28. #import <Adium/AIContentStatus.h>
  29. #import <Adium/AIHTMLDecoder.h>
  30. #import <Adium/AIListObject.h>
  31. #import <Adium/AIListContact.h>
  32. #import <Adium/AIService.h>
  33. #import <Adium/ESFileTransfer.h>
  34. #import <Adium/AIServiceIcons.h>
  35. #import <Adium/AIContentControllerProtocol.h>
  36. #import <Adium/AIStatusIcons.h>
  37. //
  38. #define LEGACY_VERSION_THRESHOLD 3 //Styles older than this version are considered legacy
  39. #define MAX_KNOWN_WEBKIT_VERSION 4 //Styles newer than this version are unknown entities
  40. //
  41. #define KEY_WEBKIT_VERSION @"MessageViewVersion"
  42. #define KEY_WEBKIT_VERSION_MIN @"MessageViewVersion_MinimumCompatible"
  43. //BOM scripts for appending content.
  44. #define APPEND_MESSAGE_WITH_SCROLL @"checkIfScrollToBottomIsNeeded(); appendMessage(\"%@\"); scrollToBottomIfNeeded();"
  45. #define APPEND_NEXT_MESSAGE_WITH_SCROLL @"checkIfScrollToBottomIsNeeded(); appendNextMessage(\"%@\"); scrollToBottomIfNeeded();"
  46. #define APPEND_MESSAGE @"appendMessage(\"%@\");"
  47. #define APPEND_NEXT_MESSAGE @"appendNextMessage(\"%@\");"
  48. #define APPEND_MESSAGE_NO_SCROLL @"appendMessageNoScroll(\"%@\");"
  49. #define APPEND_NEXT_MESSAGE_NO_SCROLL @"appendNextMessageNoScroll(\"%@\");"
  50. #define REPLACE_LAST_MESSAGE @"replaceLastMessage(\"%@\");"
  51. #define TOPIC_MAIN_DIV @"<div id=\"topic\"></div>"
  52. // We set back, when the user finishes editing, the correct topic, which wipes out the existance of the span before. We don't need to undo the dbl click action.
  53. #define TOPIC_INDIVIDUAL_WRAPPER @"<span id=\"topicEdit\" ondblclick=\"this.setAttribute('contentEditable', true); this.focus();\">%@</span>"
  54. @interface NSString (NewSnowLeopardMethods)
  55. - (NSComparisonResult)localizedStandardCompare:(NSString *)string;
  56. @end
  57. @interface NSMutableString (AIKeywordReplacementAdditions)
  58. - (void) replaceKeyword:(NSString *)word withString:(NSString *)newWord;
  59. - (void) safeReplaceCharactersInRange:(NSRange)range withString:(NSString *)newWord;
  60. @end
  61. @implementation NSMutableString (AIKeywordReplacementAdditions)
  62. - (void) replaceKeyword:(NSString *)keyWord withString:(NSString *)newWord
  63. {
  64. if(!keyWord) return;
  65. if(!newWord) newWord = @"";
  66. [self replaceOccurrencesOfString:keyWord
  67. withString:newWord
  68. options:NSLiteralSearch
  69. range:NSMakeRange(0.0f, [self length])];
  70. }
  71. - (void) safeReplaceCharactersInRange:(NSRange)range withString:(NSString *)newWord
  72. {
  73. if (range.location == NSNotFound || range.length == 0) return;
  74. if (!newWord) [self deleteCharactersInRange:range];
  75. else [self replaceCharactersInRange:range withString:newWord];
  76. }
  77. @end
  78. //The old code built the paths itself, which follows the filesystem's case sensitivity, so some noobs named stuff wrong.
  79. //NSBundle is always case sensitive, so those styles broke (they were already broken on case sensitive hfsx)
  80. //These methods only check for the all-lowercase variant, so are not suitable for general purpose use.
  81. @interface NSBundle (StupidCompatibilityHack)
  82. - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type;
  83. - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type inDirectory:(NSString *)dirpath;
  84. @end
  85. @implementation NSBundle (StupidCompatibilityHack)
  86. - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type
  87. {
  88. NSString *path = [self pathForResource:res ofType:type];
  89. if(!path)
  90. path = [self pathForResource:[res lowercaseString] ofType:type];
  91. return path;
  92. }
  93. - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type inDirectory:(NSString *)dirpath
  94. {
  95. NSString *path = [self pathForResource:res ofType:type inDirectory:dirpath];
  96. if(!path)
  97. path = [self pathForResource:[res lowercaseString] ofType:type inDirectory:dirpath];
  98. return path;
  99. }
  100. @end
  101. @interface AIWebkitMessageViewStyle ()
  102. - (id)initWithBundle:(NSBundle *)inBundle;
  103. - (void)_loadTemplates;
  104. - (void)releaseResources;
  105. - (NSMutableString *)_escapeStringForPassingToScript:(NSMutableString *)inString;
  106. - (NSString *)noVariantName;
  107. - (NSString *)iconPathForFileTransfer:(ESFileTransfer *)inObject;
  108. - (NSString *)statusIconPathForListObject:(AIListObject *)inObject;
  109. @end
  110. @implementation AIWebkitMessageViewStyle
  111. @synthesize activeVariant;
  112. + (id)messageViewStyleFromBundle:(NSBundle *)inBundle
  113. {
  114. return [[self alloc] initWithBundle:inBundle];
  115. }
  116. + (id)messageViewStyleFromPath:(NSString *)path
  117. {
  118. NSBundle *styleBundle = [NSBundle bundleWithPath:[path stringByExpandingBundlePath]];
  119. if(styleBundle)
  120. return [[self alloc] initWithBundle:styleBundle];
  121. return nil;
  122. }
  123. /*!
  124. * @brief Initialize
  125. */
  126. - (id)initWithBundle:(NSBundle *)inBundle
  127. {
  128. if ((self = [super init])) {
  129. styleBundle = inBundle;
  130. stylePath = [styleBundle resourcePath];
  131. if ([self reloadStyle] == FALSE) {
  132. return nil;
  133. }
  134. }
  135. return self;
  136. }
  137. - (BOOL) reloadStyle
  138. {
  139. [self releaseResources];
  140. /* Our styles are versioned so we can change how they work without breaking compatibility.
  141. *
  142. * Version 0: Initial Webkit Version
  143. * Version 1: Template.html now handles all scroll-to-bottom functionality. It is no longer required to call the
  144. * scrollToBottom functions when inserting content.
  145. * Version 2: No significant changes
  146. * Version 3: main.css is no longer a separate style, it now serves as the base stylesheet and is imported by default.
  147. * The default variant is now a separate file in /variants like all other variants.
  148. * Template.html now includes appendMessageNoScroll() and appendNextMessageNoScroll() which behave
  149. * the same as appendMessage() and appendNextMessage() in Versions 1 and 2 but without scrolling.
  150. * Version 4: Template.html now includes replaceLastMessage()
  151. * Template.html now defines actionMessageUserName and actionMessageBody for display of /me (actions).
  152. * If the style provides a custom Template.html, these classes must be defined.
  153. * CSS can be used to customize the appearance of actions.
  154. * HTML filters in are now supported in Adium's content filter system; filters can assume Version 4 or later.
  155. */
  156. styleVersion = [[styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION] integerValue];
  157. /* Refuse to load a version whose minimum compatible version is greater than the latest version we know about; that
  158. * indicates this is a style FROM THE FUTURE, and we can't risk corrupting our own timeline.
  159. */
  160. NSInteger minimumCompatibleVersion = [[styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION_MIN] integerValue];
  161. if (minimumCompatibleVersion && (minimumCompatibleVersion > MAX_KNOWN_WEBKIT_VERSION)) {
  162. return NO;
  163. }
  164. //Default behavior
  165. allowTextBackgrounds = YES;
  166. //Pre-fetch our templates
  167. [self _loadTemplates];
  168. //Style flags
  169. allowsCustomBackground = ![[styleBundle objectForInfoDictionaryKey:@"DisableCustomBackground"] boolValue];
  170. transparentDefaultBackground = [[styleBundle objectForInfoDictionaryKey:@"DefaultBackgroundIsTransparent"] boolValue];
  171. combineConsecutive = ![[styleBundle objectForInfoDictionaryKey:@"DisableCombineConsecutive"] boolValue];
  172. NSNumber *tmpNum = [styleBundle objectForInfoDictionaryKey:@"ShowsUserIcons"];
  173. allowsUserIcons = (tmpNum ? [tmpNum boolValue] : YES);
  174. //User icon masking
  175. NSString *tmpName = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_USER_ICON_MASK];
  176. if (tmpName) userIconMask = [[NSImage alloc] initWithContentsOfFile:[stylePath stringByAppendingPathComponent:tmpName]];
  177. NSNumber *allowsColorsNumber = [styleBundle objectForInfoDictionaryKey:@"AllowTextColors"];
  178. allowsColors = (allowsColorsNumber ? [allowsColorsNumber boolValue] : YES);
  179. return YES;
  180. }
  181. /*!
  182. * @brief release everything we loaded from the style bundle
  183. */
  184. - (void)releaseResources
  185. {
  186. //Templates
  187. headerHTML = nil;
  188. footerHTML = nil;
  189. baseHTML = nil;
  190. contentHTML = nil;
  191. contentInHTML = nil;
  192. nextContentInHTML = nil;
  193. contextInHTML = nil;
  194. nextContextInHTML = nil;
  195. contentOutHTML = nil;
  196. nextContentOutHTML = nil;
  197. contextOutHTML = nil;
  198. nextContextOutHTML = nil;
  199. statusHTML = nil;
  200. fileTransferHTML = nil;
  201. topicHTML = nil;
  202. customBackgroundPath = nil;
  203. customBackgroundColor = nil;
  204. userIconMask = nil;
  205. }
  206. /*!
  207. * @brief Deallocate
  208. */
  209. - (void)dealloc
  210. {
  211. [self releaseResources];
  212. [[NSDistributedNotificationCenter defaultCenter] removeObserver: self];
  213. }
  214. @synthesize bundle = styleBundle;
  215. - (BOOL)isLegacy
  216. {
  217. return styleVersion < LEGACY_VERSION_THRESHOLD;
  218. }
  219. #pragma mark Settings
  220. @synthesize allowsCustomBackground, allowsUserIcons, allowsColors, userIconMask;
  221. - (NSArray *)validSenderColors
  222. {
  223. if(!checkedSenderColors) {
  224. NSURL *url = [NSURL fileURLWithPath:[stylePath stringByAppendingPathComponent:@"Incoming/SenderColors.txt"]];
  225. NSString *senderColorsFile = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:NULL];
  226. if(senderColorsFile)
  227. validSenderColors = [senderColorsFile componentsSeparatedByString:@":"];
  228. checkedSenderColors = YES;
  229. }
  230. return validSenderColors;
  231. }
  232. - (BOOL)isBackgroundTransparent
  233. {
  234. //Our custom background is only transparent if the user has set a custom color with an alpha component less than 1.0
  235. return ((!customBackgroundColor && transparentDefaultBackground) ||
  236. (customBackgroundColor && [customBackgroundColor alphaComponent] < 0.99));
  237. }
  238. - (NSString *)defaultFontFamily
  239. {
  240. NSString *defaultFontFamily = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_DEFAULT_FONT_FAMILY];
  241. if (!defaultFontFamily) defaultFontFamily = [[NSFont systemFontOfSize:0] familyName];
  242. return defaultFontFamily;
  243. }
  244. - (NSNumber *)defaultFontSize
  245. {
  246. NSNumber *defaultFontSize = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_DEFAULT_FONT_SIZE];
  247. if (!defaultFontSize) defaultFontSize = [NSNumber numberWithInteger:[[NSFont systemFontOfSize:0] pointSize]];
  248. return defaultFontSize;
  249. }
  250. - (BOOL)hasHeader
  251. {
  252. return headerHTML && [headerHTML length];
  253. }
  254. - (BOOL)hasTopic
  255. {
  256. return topicHTML && [topicHTML length];
  257. }
  258. #pragma mark Behavior
  259. - (void)setDateFormat:(NSString *)format
  260. {
  261. if (!format || [format length] == 0) {
  262. format = [NSDateFormatter localizedDateFormatStringShowingSeconds:NO showingAMorPM:NO];
  263. }
  264. if ([format rangeOfString:@"%"].location != NSNotFound) {
  265. /* Support strftime-style format strings, which old message styles may use */
  266. timeStampFormatter = [[NSDateFormatter alloc] initWithDateFormat:format allowNaturalLanguage:NO];
  267. } else {
  268. timeStampFormatter = [[NSDateFormatter alloc] init];
  269. [timeStampFormatter setDateFormat:format];
  270. }
  271. }
  272. - (void) flushTimeFormatterCache:(id)dummy {
  273. [timeFormatterCache removeAllObjects];
  274. }
  275. @synthesize allowTextBackgrounds, customBackgroundType, customBackgroundColor, showIncomingMessageColors=showIncomingColors, showIncomingMessageFonts=showIncomingFonts, customBackgroundPath, nameFormat, useCustomNameFormat, showHeader, showUserIcons;
  276. //Templates ------------------------------------------------------------------------------------------------------------
  277. #pragma mark Templates
  278. - (NSString *)baseTemplateForChat:(AIChat *)chat
  279. {
  280. NSMutableString *templateHTML;
  281. // If this is a group chat, we want to include a topic.
  282. // Otherwise, if the header is shown, use it.
  283. NSString *headerContent = @"";
  284. if (showHeader) {
  285. if (chat.isGroupChat) {
  286. headerContent = (chat.supportsTopic ? TOPIC_MAIN_DIV : @"");
  287. } else if (headerHTML) {
  288. headerContent = headerHTML;
  289. }
  290. }
  291. //Old styles may be using an old custom 4 parameter baseHTML. Styles version 3 and higher should
  292. //be using the bundled (or a custom) 5 parameter baseHTML.
  293. if ((styleVersion < 3) && usingCustomTemplateHTML) {
  294. templateHTML = [NSMutableString stringWithFormat:baseHTML, //Template
  295. [[NSURL fileURLWithPath:stylePath] absoluteString], //Base path
  296. [self pathForVariant:self.activeVariant], //Variant path
  297. headerContent,
  298. (footerHTML ? footerHTML : @"")];
  299. } else {
  300. templateHTML = [NSMutableString stringWithFormat:baseHTML, //Template
  301. [[NSURL fileURLWithPath:stylePath] absoluteString], //Base path
  302. styleVersion < 3 ? @"" : @"@import url( \"main.css\" );", //Import main.css for new enough styles
  303. [self pathForVariant:self.activeVariant], //Variant path
  304. headerContent,
  305. (footerHTML ? footerHTML : @"")];
  306. }
  307. return [self fillKeywordsForBaseTemplate:templateHTML chat:chat];
  308. }
  309. - (NSString *)templateForContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
  310. {
  311. NSString *template;
  312. //Get the correct template for what we're inserting
  313. if ([[content type] isEqualToString:CONTENT_MESSAGE_TYPE]) {
  314. if ([content isOutgoing]) {
  315. template = (contentIsSimilar ? nextContentOutHTML : contentOutHTML);
  316. } else {
  317. template = (contentIsSimilar ? nextContentInHTML : contentInHTML);
  318. }
  319. } else if ([[content type] isEqualToString:CONTENT_CONTEXT_TYPE]) {
  320. if ([content isOutgoing]) {
  321. template = (contentIsSimilar ? nextContextOutHTML : contextOutHTML);
  322. } else {
  323. template = (contentIsSimilar ? nextContextInHTML : contextInHTML);
  324. }
  325. } else if([[content type] isEqualToString:CONTENT_FILE_TRANSFER_TYPE]) {
  326. template = [fileTransferHTML mutableCopy];
  327. } else if ([[content type] isEqualToString:CONTENT_TOPIC_TYPE]) {
  328. template = topicHTML;
  329. }
  330. else {
  331. template = statusHTML;
  332. }
  333. return template;
  334. }
  335. - (NSString *)completedTemplateForContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
  336. {
  337. NSMutableString *mutableTemplate = [[self templateForContent:content similar:contentIsSimilar] mutableCopy];
  338. if (mutableTemplate)
  339. [self fillKeywords:mutableTemplate forContent:content similar:contentIsSimilar];
  340. return mutableTemplate;
  341. }
  342. /*!
  343. * @brief Pre-fetch all the style templates
  344. *
  345. * This needs to be called before either baseTemplate or templateForContent is called
  346. */
  347. - (void)_loadTemplates
  348. {
  349. //Load the style's templates
  350. //We can't use NSString's initWithContentsOfFile here. HTML files are interpreted in the defaultCEncoding
  351. //(which varies by system) when read that way. We want to always interpret the files as UTF8.
  352. headerHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Header" ofType:@"html"]];
  353. footerHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Footer" ofType:@"html"]];
  354. topicHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Topic" ofType:@"html"]];
  355. baseHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Template" ofType:@"html"]];
  356. //Starting with version 1, styles can choose to not include template.html. If the template is not included
  357. //Adium's default will be used. This is preferred since any future template updates will apply to the style
  358. if ((!baseHTML || [baseHTML length] == 0) && styleVersion >= 1) {
  359. baseHTML = [NSString stringWithContentsOfUTF8File:[[NSBundle bundleForClass:[self class]] semiCaseInsensitivePathForResource:@"Template" ofType:@"html"]];
  360. usingCustomTemplateHTML = NO;
  361. } else {
  362. usingCustomTemplateHTML = YES;
  363. NSAssert(baseHTML != nil, @"The impossible happened!");
  364. if ([baseHTML rangeOfString:@"function imageCheck()" options:NSLiteralSearch].location != NSNotFound) {
  365. /* This doesn't quite fix image swapping on styles with broken image swapping due to custom HTML templates,
  366. * but it improves it. For some reason, the result of using our normal template.html functions is that
  367. * clicking works once, then the text doesn't allow a return click. This is an improvement compared
  368. * to fully broken behavior in which the return click shows a missing-image placeholder.
  369. */
  370. NSMutableString *imageSwapFixedBaseHTML = [baseHTML mutableCopy];
  371. [imageSwapFixedBaseHTML replaceOccurrencesOfString:
  372. @" function imageCheck() {\n"
  373. " node = event.target;\n"
  374. " if(node.tagName == 'IMG' && node.alt) {\n"
  375. " a = document.createElement('a');\n"
  376. " a.setAttribute('onclick', 'imageSwap(this)');\n"
  377. " a.setAttribute('src', node.src);\n"
  378. " text = document.createTextNode(node.alt);\n"
  379. " a.appendChild(text);\n"
  380. " node.parentNode.replaceChild(a, node);\n"
  381. " }\n"
  382. " }"
  383. withString:
  384. @" function imageCheck() {\n"
  385. " var node = event.target;\n"
  386. " if(node.tagName.toLowerCase() == 'img' && !client.zoomImage(node) && node.alt) {\n"
  387. " var a = document.createElement('a');\n"
  388. " a.setAttribute('onclick', 'imageSwap(this)');\n"
  389. " a.setAttribute('src', node.getAttribute('src'));\n"
  390. " a.className = node.className;\n"
  391. " var text = document.createTextNode(node.alt);\n"
  392. " a.appendChild(text);\n"
  393. " node.parentNode.replaceChild(a, node);\n"
  394. " }\n"
  395. " }"
  396. options:NSLiteralSearch];
  397. [imageSwapFixedBaseHTML replaceOccurrencesOfString:
  398. @" function imageSwap(node) {\n"
  399. " img = document.createElement('img');\n"
  400. " img.setAttribute('src', node.src);\n"
  401. " img.setAttribute('alt', node.firstChild.nodeValue);\n"
  402. " node.parentNode.replaceChild(img, node);\n"
  403. " alignChat();\n"
  404. " }"
  405. withString:
  406. @" function imageSwap(node) {\n"
  407. " var shouldScroll = nearBottom();\n"
  408. " //Swap the image/text\n"
  409. " var img = document.createElement('img');\n"
  410. " img.setAttribute('src', node.getAttribute('src'));\n"
  411. " img.setAttribute('alt', node.firstChild.nodeValue);\n"
  412. " img.className = node.className;\n"
  413. " node.parentNode.replaceChild(img, node);\n"
  414. " \n"
  415. " alignChat(shouldScroll);\n"
  416. " }"
  417. options:NSLiteralSearch];
  418. /* Now for ones which don't call alignChat() */
  419. [imageSwapFixedBaseHTML replaceOccurrencesOfString:
  420. @" function imageSwap(node) {\n"
  421. " img = document.createElement('img');\n"
  422. " img.setAttribute('src', node.src);\n"
  423. " img.setAttribute('alt', node.firstChild.nodeValue);\n"
  424. " node.parentNode.replaceChild(img, node);\n"
  425. " }"
  426. withString:
  427. @" function imageSwap(node) {\n"
  428. " var shouldScroll = nearBottom();\n"
  429. " //Swap the image/text\n"
  430. " var img = document.createElement('img');\n"
  431. " img.setAttribute('src', node.getAttribute('src'));\n"
  432. " img.setAttribute('alt', node.firstChild.nodeValue);\n"
  433. " img.className = node.className;\n"
  434. " node.parentNode.replaceChild(img, node);\n"
  435. " }"
  436. options:NSLiteralSearch];
  437. baseHTML = imageSwapFixedBaseHTML;
  438. }
  439. }
  440. //Content Templates
  441. contentHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html"]];
  442. contentInHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html" inDirectory:@"Incoming"]];
  443. nextContentInHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContent" ofType:@"html" inDirectory:@"Incoming"]];
  444. contentOutHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html" inDirectory:@"Outgoing"]];
  445. nextContentOutHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContent" ofType:@"html" inDirectory:@"Outgoing"]];
  446. //Message history
  447. contextInHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Context" ofType:@"html" inDirectory:@"Incoming"]];
  448. nextContextInHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContext" ofType:@"html" inDirectory:@"Incoming"]];
  449. contextOutHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Context" ofType:@"html" inDirectory:@"Outgoing"]];
  450. nextContextOutHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContext" ofType:@"html" inDirectory:@"Outgoing"]];
  451. //Fall back to Resources/Content.html if Incoming isn't present
  452. if (!contentInHTML) contentInHTML = contentHTML;
  453. //Fall back to Content if NextContent doesn't need to use different HTML
  454. if (!nextContentInHTML) nextContentInHTML = contentInHTML;
  455. //Fall back to Content if Context isn't present
  456. if (!nextContextInHTML) nextContextInHTML = nextContentInHTML;
  457. if (!contextInHTML) contextInHTML = contentInHTML;
  458. //Fall back to Content if Context isn't present
  459. if (!nextContextOutHTML && nextContentOutHTML) nextContextOutHTML = nextContentOutHTML;
  460. if (!contextOutHTML && contentOutHTML) contextOutHTML = contentOutHTML;
  461. //Fall back to Content if Context isn't present
  462. if (!nextContextOutHTML) nextContextOutHTML = nextContextInHTML;
  463. if (!contextOutHTML) contextOutHTML = contextInHTML;
  464. //Fall back to Incoming if Outgoing doesn't need to be different
  465. if (!contentOutHTML) contentOutHTML = contentInHTML;
  466. if (!nextContentOutHTML) nextContentOutHTML = nextContentInHTML;
  467. //Status
  468. statusHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Status" ofType:@"html"]];
  469. //Fall back to Resources/Incoming/Content.html if Status isn't present
  470. if (!statusHTML) statusHTML = contentInHTML;
  471. //TODO: make a generic Request message, rather than having this ft specific one
  472. NSMutableString *fileTransferHTMLTemplate;
  473. fileTransferHTMLTemplate = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"FileTransferRequest" ofType:@"html"]] mutableCopy];
  474. if(!fileTransferHTMLTemplate) {
  475. fileTransferHTMLTemplate = [contentInHTML mutableCopy];
  476. [fileTransferHTMLTemplate replaceKeyword:@"%message%"
  477. withString:@"<p><img src=\"%fileIconPath%\" style=\"width:32px; height:32px; vertical-align:middle;\"></img><input type=\"button\" onclick=\"%saveFileAsHandler%\" value=\"Download %fileName%\"></p>"];
  478. }
  479. [fileTransferHTMLTemplate replaceKeyword:@"Download %fileName%"
  480. withString:[NSString stringWithFormat:AILocalizedString(@"Download %@", "%@ will be a file name"), @"%fileName%"]];
  481. fileTransferHTML = fileTransferHTMLTemplate;
  482. }
  483. #pragma mark Scripts
  484. - (NSString *)scriptForAppendingContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
  485. {
  486. NSMutableString *newHTML;
  487. NSString *script;
  488. //If combining of consecutive messages has been disabled, we treat all content as non-similar
  489. if (!combineConsecutive) contentIsSimilar = NO;
  490. //Fetch the correct template and substitute keywords for the passed content
  491. newHTML = [[self completedTemplateForContent:content similar:contentIsSimilar] mutableCopy];
  492. //BOM scripts vary by style version
  493. if (!usingCustomTemplateHTML && styleVersion >= 4) {
  494. /* If we're using the built-in template HTML, we know that it supports our most modern scripts */
  495. if (replaceLastContent)
  496. script = REPLACE_LAST_MESSAGE;
  497. else if (willAddMoreContentObjects) {
  498. script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_NO_SCROLL : APPEND_MESSAGE_NO_SCROLL);
  499. } else {
  500. script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
  501. }
  502. } else if (styleVersion >= 3) {
  503. if (willAddMoreContentObjects) {
  504. script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_NO_SCROLL : APPEND_MESSAGE_NO_SCROLL);
  505. } else {
  506. script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
  507. }
  508. } else if (styleVersion >= 1) {
  509. script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
  510. } else {
  511. if (usingCustomTemplateHTML && [content isKindOfClass:[AIContentStatus class]]) {
  512. /* Old styles with a custom template.html had Status.html files without 'insert' divs coupled
  513. * with a APPEND_NEXT_MESSAGE_WITH_SCROLL script which assumes one exists.
  514. */
  515. script = APPEND_MESSAGE_WITH_SCROLL;
  516. } else {
  517. script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_WITH_SCROLL : APPEND_MESSAGE_WITH_SCROLL);
  518. }
  519. }
  520. return [NSString stringWithFormat:script, [self _escapeStringForPassingToScript:newHTML]];
  521. }
  522. - (NSString *)scriptForChangingVariant
  523. {
  524. return [NSString stringWithFormat:@"setStylesheet(\"mainStyle\",\"%@\");",[self pathForVariant:self.activeVariant]];
  525. }
  526. - (NSString *)scriptForScrollingAfterAddingMultipleContentObjects
  527. {
  528. if ((styleVersion >= 3) || !usingCustomTemplateHTML) {
  529. return @"if (this.AI_viewScrolledOnLoad != undefined) {alignChat(nearBottom());} else {this.AI_viewScrolledOnLoad = true; alignChat(true);}";
  530. }
  531. return nil;
  532. }
  533. /*!
  534. * @brief Escape a string for passing to our BOM scripts
  535. */
  536. - (NSMutableString *)_escapeStringForPassingToScript:(NSMutableString *)inString
  537. {
  538. // We need to escape a few things to get our string to the javascript without trouble
  539. [inString replaceOccurrencesOfString:@"\\"
  540. withString:@"\\\\"
  541. options:NSLiteralSearch];
  542. [inString replaceOccurrencesOfString:@"\""
  543. withString:@"\\\""
  544. options:NSLiteralSearch];
  545. [inString replaceOccurrencesOfString:@"\n"
  546. withString:@""
  547. options:NSLiteralSearch];
  548. [inString replaceOccurrencesOfString:@"\r"
  549. withString:@"<br>"
  550. options:NSLiteralSearch];
  551. return inString;
  552. }
  553. #pragma mark Variants
  554. - (NSArray *)availableVariants
  555. {
  556. NSMutableArray *availableVariants = [NSMutableArray array];
  557. //Build an array of all variant names
  558. for (NSString *path in [styleBundle pathsForResourcesOfType:@"css" inDirectory:@"Variants"]) {
  559. [availableVariants addObject:[[path lastPathComponent] stringByDeletingPathExtension]];
  560. }
  561. //Style versions before 3 stored the default variant in a separate location. They also allowed for this
  562. //varient name to not be specified, and would substitute a localized string in its place.
  563. if (styleVersion < 3) {
  564. [availableVariants addObject:[self noVariantName]];
  565. }
  566. //Alphabetize the variants
  567. [availableVariants sortUsingSelector:@selector(localizedStandardCompare:)];
  568. return availableVariants;
  569. }
  570. - (NSString *)pathForVariant:(NSString *)variant
  571. {
  572. //Styles before version 3 stored the default variant in main.css, and not in the variants folder.
  573. if (styleVersion < 3 && [variant isEqualToString:[self noVariantName]]) {
  574. return @"main.css";
  575. } else {
  576. return [NSString stringWithFormat:@"Variants/%@.css",variant];
  577. }
  578. }
  579. /*!
  580. * @brief Base variant name for styles before version 2
  581. */
  582. - (NSString *)noVariantName
  583. {
  584. NSString *noVariantName = [styleBundle objectForInfoDictionaryKey:@"DisplayNameForNoVariant"];
  585. return noVariantName ? noVariantName : AILocalizedString(@"Normal","Normal style variant menu item");
  586. }
  587. + (NSString *)noVariantNameForBundle:(NSBundle *)inBundle
  588. {
  589. NSString *noVariantName = [inBundle objectForInfoDictionaryKey:@"DisplayNameForNoVariant"];
  590. return noVariantName ? noVariantName : AILocalizedString(@"Normal","Normal style variant menu item");
  591. }
  592. - (NSString *)defaultVariant
  593. {
  594. return styleVersion < 3 ? [self noVariantName] : [styleBundle objectForInfoDictionaryKey:@"DefaultVariant"];
  595. }
  596. + (NSString *)defaultVariantForBundle:(NSBundle *)inBundle
  597. {
  598. return [[inBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION] integerValue] < 3 ?
  599. [self noVariantNameForBundle:inBundle] :
  600. [inBundle objectForInfoDictionaryKey:@"DefaultVariant"];
  601. }
  602. #pragma mark Keyword replacement
  603. - (NSMutableString *)fillKeywords:(NSMutableString *)inString forContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
  604. {
  605. NSDate *date = nil;
  606. NSRange range;
  607. AIListObject *contentSource = [content source];
  608. AIListObject *theSource = ([contentSource isKindOfClass:[AIListContact class]] ?
  609. [(AIListContact *)contentSource parentContact] :
  610. contentSource);
  611. /*
  612. htmlEncodedMessage is only encoded correctly for AIContentMessages
  613. but we do it up here so that we can check for RTL/LTR text below without
  614. having to encode the message twice. This is less than ideal
  615. */
  616. NSString *htmlEncodedMessage = [AIHTMLDecoder encodeHTML:[content message]
  617. headers:NO
  618. fontTags:showIncomingFonts
  619. includingColorTags:(allowsColors && showIncomingColors)
  620. closeFontTags:YES
  621. styleTags:YES
  622. closeStyleTagsOnFontChange:YES
  623. encodeNonASCII:YES
  624. encodeSpaces:YES
  625. imagesPath:NSTemporaryDirectory()
  626. attachmentsAsText:NO
  627. onlyIncludeOutgoingImages:NO
  628. simpleTagsOnly:NO
  629. bodyBackground:NO
  630. allowJavascriptURLs:NO];
  631. if (styleVersion >= 4)
  632. htmlEncodedMessage = [adium.contentController filterHTMLString:htmlEncodedMessage
  633. direction:[content isOutgoing] ? AIFilterOutgoing : AIFilterIncoming
  634. content:content];
  635. //date
  636. if ([content respondsToSelector:@selector(date)])
  637. date = [(AIContentMessage *)content date];
  638. //Replacements applicable to any AIContentObject
  639. [inString replaceKeyword:@"%time%"
  640. withString:(date ? [timeStampFormatter stringFromDate:date] : @"")];
  641. __block NSString *shortTimeString;
  642. [NSDateFormatter withLocalizedDateFormatterShowingSeconds:NO showingAMorPM:NO perform:^(NSDateFormatter *dateFormatter){
  643. shortTimeString = (date ? [dateFormatter stringFromDate:date] : @"");
  644. }];
  645. [inString replaceKeyword:@"%shortTime%"
  646. withString:shortTimeString];
  647. if ([inString rangeOfString:@"%senderStatusIcon%"].location != NSNotFound) {
  648. //Only cache the status icon to disk if the message style will actually use it
  649. [inString replaceKeyword:@"%senderStatusIcon%"
  650. withString:[self statusIconPathForListObject:theSource]];
  651. }
  652. //Replaces %localized{x}% with a a localized version of x, searching the style's localizations, and then Adium's localizations
  653. do{
  654. range = [inString rangeOfString:@"%localized{"];
  655. if (range.location != NSNotFound) {
  656. NSRange endRange;
  657. endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
  658. if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
  659. NSString *untranslated = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
  660. NSString *translated = [styleBundle localizedStringForKey:untranslated
  661. value:untranslated
  662. table:nil];
  663. if (!translated || [translated length] == 0) {
  664. translated = [[NSBundle bundleForClass:[self class]] localizedStringForKey:untranslated
  665. value:untranslated
  666. table:nil];
  667. if (!translated || [translated length] == 0) {
  668. translated = [[NSBundle mainBundle] localizedStringForKey:untranslated
  669. value:untranslated
  670. table:nil];
  671. }
  672. }
  673. [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
  674. withString:translated];
  675. }
  676. }
  677. } while (range.location != NSNotFound);
  678. [inString replaceKeyword:@"%userIcons%"
  679. withString:(showUserIcons ? @"showIcons" : @"hideIcons")];
  680. [inString replaceKeyword:@"%messageClasses%"
  681. withString:[(contentIsSimilar ? @"consecutive " : @"") stringByAppendingString:[[content displayClasses] componentsJoinedByString:@" "]]];
  682. [inString replaceKeyword:@"%senderColor%"
  683. withString:[NSColor representedColorForObject:contentSource.UID withValidColors:self.validSenderColors]];
  684. //HAX. The odd conditional here detects the rtl html that our html parser spits out.
  685. BOOL isRTL = ([htmlEncodedMessage rangeOfString:@"<div dir=\"rtl\">"
  686. options:(NSCaseInsensitiveSearch | NSLiteralSearch)].location != NSNotFound);
  687. [inString replaceKeyword:@"%messageDirection%"
  688. withString:(isRTL ? @"rtl" : @"ltr")];
  689. //Replaces %time{x}% with a timestamp formatted like x (using NSDateFormatter)
  690. do{
  691. range = [inString rangeOfString:@"%time{"];
  692. if (range.location != NSNotFound) {
  693. NSRange endRange;
  694. endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
  695. if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
  696. if (date) {
  697. if (!timeFormatterCache) {
  698. timeFormatterCache = [[NSMutableDictionary alloc] init];
  699. [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(flushTimeFormatterCache:) name:@"AppleDatePreferencesChangedNotification" object:nil];
  700. [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(flushTimeFormatterCache:) name:@"AppleTimePreferencesChangedNotification" object:nil];
  701. }
  702. NSString *timeFormat = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
  703. NSDateFormatter *dateFormatter = [timeFormatterCache objectForKey:timeFormat];
  704. if (!dateFormatter) {
  705. if ([timeFormat rangeOfString:@"%"].location != NSNotFound) {
  706. /* Support strftime-style format strings, which old message styles may use */
  707. dateFormatter = [[NSDateFormatter alloc] initWithDateFormat:timeFormat allowNaturalLanguage:NO];
  708. } else {
  709. dateFormatter = [[NSDateFormatter alloc] init];
  710. [dateFormatter setDateFormat:timeFormat];
  711. }
  712. [timeFormatterCache setObject:dateFormatter forKey:timeFormat];
  713. }
  714. [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
  715. withString:[dateFormatter stringFromDate:date]];
  716. } else
  717. [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
  718. }
  719. }
  720. } while (range.location != NSNotFound);
  721. do{
  722. range = [inString rangeOfString:@"%userIconPath%"];
  723. if (range.location != NSNotFound) {
  724. NSString *userIconPath;
  725. NSString *replacementString;
  726. userIconPath = [theSource valueForProperty:KEY_WEBKIT_USER_ICON];
  727. if (!userIconPath) {
  728. userIconPath = [theSource valueForProperty:@"UserIconPath"];
  729. }
  730. if (showUserIcons && userIconPath) {
  731. replacementString = [NSString stringWithFormat:@"file://%@", userIconPath];
  732. } else {
  733. replacementString = ([content isOutgoing]
  734. ? @"Outgoing/buddy_icon.png"
  735. : @"Incoming/buddy_icon.png");
  736. }
  737. [inString safeReplaceCharactersInRange:range withString:replacementString];
  738. }
  739. } while (range.location != NSNotFound);
  740. [inString replaceKeyword:@"%service%"
  741. withString:[content.chat.account.service shortDescription]];
  742. [inString replaceKeyword:@"%serviceIconPath%"
  743. withString:[AIServiceIcons pathForServiceIconForServiceID:content.chat.account.service.serviceID
  744. type:AIServiceIconLarge]];
  745. if ([inString rangeOfString:@"%variant%"].location != NSNotFound) {
  746. /* Per #12702, don't allow spaces in the variant name, as otherwise it becomes multiple css classes */
  747. [inString replaceKeyword:@"%variant%"
  748. withString:[self.activeVariant stringByReplacingOccurrencesOfString:@" " withString:@"_"]];
  749. }
  750. //message stuff
  751. if ([content isKindOfClass:[AIContentMessage class]]) {
  752. //Use [content source] directly rather than the potentially-metaContact theSource
  753. NSString *formattedUID = nil;
  754. if ([content.chat aliasForContact:contentSource]) {
  755. formattedUID = [content.chat aliasForContact:contentSource];
  756. } else {
  757. formattedUID = contentSource.formattedUID;
  758. }
  759. NSString *displayName = [content.chat displayNameForContact:contentSource];
  760. [inString replaceKeyword:@"%status%"
  761. withString:@""];
  762. [inString replaceKeyword:@"%senderScreenName%"
  763. withString:[(formattedUID ?
  764. formattedUID :
  765. displayName) stringByEscapingForXMLWithEntities:nil]];
  766. [inString replaceKeyword:@"%senderPrefix%"
  767. withString:((AIContentMessage *)content).senderPrefix];
  768. do{
  769. range = [inString rangeOfString:@"%sender%"];
  770. if (range.location != NSNotFound) {
  771. NSString *senderDisplay = nil;
  772. if (useCustomNameFormat) {
  773. if (formattedUID && ![displayName isEqualToString:formattedUID]) {
  774. switch (nameFormat) {
  775. case AIDefaultName:
  776. break;
  777. case AIDisplayName:
  778. senderDisplay = displayName;
  779. break;
  780. case AIDisplayName_ScreenName:
  781. senderDisplay = [NSString stringWithFormat:@"%@ (%@)",displayName,formattedUID];
  782. break;
  783. case AIScreenName_DisplayName:
  784. senderDisplay = [NSString stringWithFormat:@"%@ (%@)",formattedUID,displayName];
  785. break;
  786. case AIScreenName:
  787. senderDisplay = formattedUID;
  788. break;
  789. }
  790. }
  791. //Test both displayName and formattedUID for nil-ness. If they're both nil, the assertion will trip.
  792. if (!senderDisplay) {
  793. senderDisplay = displayName;
  794. if (!senderDisplay) {
  795. senderDisplay = formattedUID;
  796. if (!senderDisplay) {
  797. AILog(@"XXX we don't have a sender for %@ (%@)", content, [content message]);
  798. NSLog(@"Enormous error: we don't have a sender for %@ (%@)", content, [content message]);
  799. // This shouldn't happen.
  800. senderDisplay = @"(unknown)";
  801. }
  802. }
  803. }
  804. } else {
  805. senderDisplay = displayName;
  806. }
  807. if ([(AIContentMessage *)content isAutoreply]) {
  808. senderDisplay = [NSString stringWithFormat:@"%@ %@",senderDisplay,AILocalizedString(@"(Autoreply)","Short word inserted after the sender's name when displaying a message which was an autoresponse")];
  809. }
  810. [inString safeReplaceCharactersInRange:range withString:[senderDisplay stringByEscapingForXMLWithEntities:nil]];
  811. }
  812. } while (range.location != NSNotFound);
  813. do {
  814. range = [inString rangeOfString:@"%senderDisplayName%"];
  815. if (range.location != NSNotFound) {
  816. NSString *serversideDisplayName = ([theSource isKindOfClass:[AIListContact class]] ?
  817. [(AIListContact *)theSource serversideDisplayName] :
  818. nil);
  819. if (!serversideDisplayName) {
  820. serversideDisplayName = theSource.displayName;
  821. }
  822. [inString safeReplaceCharactersInRange:range
  823. withString:[serversideDisplayName stringByEscapingForXMLWithEntities:nil]];
  824. }
  825. } while (range.location != NSNotFound);
  826. //Blatantly stealing the date code for the background color script.
  827. do{
  828. range = [inString rangeOfString:@"%textbackgroundcolor{"];
  829. if (range.location != NSNotFound) {
  830. NSRange endRange;
  831. endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
  832. if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
  833. NSString *transparency = [inString substringWithRange:NSMakeRange(NSMaxRange(range),
  834. (endRange.location - NSMaxRange(range)))];
  835. if (allowTextBackgrounds && showIncomingColors) {
  836. NSString *thisIsATemporaryString;
  837. unsigned rgb = 0, red, green, blue;
  838. NSScanner *hexcode;
  839. thisIsATemporaryString = [AIHTMLDecoder encodeHTML:[content message] headers:NO
  840. fontTags:NO
  841. includingColorTags:NO
  842. closeFontTags:NO
  843. styleTags:NO
  844. closeStyleTagsOnFontChange:NO
  845. encodeNonASCII:NO
  846. encodeSpaces:NO
  847. imagesPath:NSTemporaryDirectory()
  848. attachmentsAsText:NO
  849. onlyIncludeOutgoingImages:NO
  850. simpleTagsOnly:NO
  851. bodyBackground:YES
  852. allowJavascriptURLs:NO];
  853. hexcode = [NSScanner scannerWithString:thisIsATemporaryString];
  854. [hexcode scanHexInt:&rgb];
  855. if (![thisIsATemporaryString length] && rgb == 0) {
  856. [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
  857. } else {
  858. red = (rgb & 0xff0000) >> 16;
  859. green = (rgb & 0x00ff00) >> 8;
  860. blue = rgb & 0x0000ff;
  861. [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
  862. withString:[NSString stringWithFormat:@"rgba(%d, %d, %d, %@)", red, green, blue, transparency]];
  863. }
  864. } else {
  865. [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
  866. }
  867. } else if (endRange.location == NSMaxRange(range)) {
  868. if (allowTextBackgrounds && showIncomingColors) {
  869. NSString *thisIsATemporaryString;
  870. thisIsATemporaryString = [AIHTMLDecoder encodeHTML:[content message] headers:NO
  871. fontTags:NO
  872. includingColorTags:NO
  873. closeFontTags:NO
  874. styleTags:NO
  875. closeStyleTagsOnFontChange:NO
  876. encodeNonASCII:NO
  877. encodeSpaces:NO
  878. imagesPath:NSTemporaryDirectory()
  879. attachmentsAsText:NO
  880. onlyIncludeOutgoingImages:NO
  881. simpleTagsOnly:NO
  882. bodyBackground:YES
  883. allowJavascriptURLs:NO];
  884. [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
  885. withString:[NSString stringWithFormat:@"#%@", thisIsATemporaryString]];
  886. } else {
  887. [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
  888. }
  889. }
  890. }
  891. } while (range.location != NSNotFound);
  892. if ([content isKindOfClass:[ESFileTransfer class]]) { //file transfers are an AIContentMessage subclass
  893. ESFileTransfer *transfer = (ESFileTransfer *)content;
  894. NSString *fileName = [[transfer remoteFilename] stringByEscapingForXMLWithEntities:nil];
  895. NSString *fileTransferID = [[transfer uniqueID] stringByEscapingForXMLWithEntities:nil];
  896. range = [inString rangeOfString:@"%fileIconPath%"];
  897. if (range.location != NSNotFound) {
  898. NSString *iconPath = [self iconPathForFileTransfer:transfer];
  899. NSImage *icon = [transfer iconImage];
  900. do{
  901. [[icon TIFFRepresentation] writeToFile:iconPath atomically:YES];
  902. [inString safeReplaceCharactersInRange:range withString:iconPath];
  903. range = [inString rangeOfString:@"%fileIconPath%"];
  904. } while (range.location != NSNotFound);
  905. }
  906. [inString replaceKeyword:@"%fileName%"
  907. withString:fileName];
  908. [inString replaceKeyword:@"%saveFileHandler%"
  909. withString:[NSString stringWithFormat:@"client.handleFileTransfer('Save', '%@')", fileTransferID]];
  910. [inString replaceKeyword:@"%saveFileAsHandler%"
  911. withString:[NSString stringWithFormat:@"client.handleFileTransfer('SaveAs', '%@')", fileTransferID]];
  912. [inString replaceKeyword:@"%cancelRequestHandler%"
  913. withString:[NSString stringWithFormat:@"client.handleFileTransfer('Cancel', '%@')", fileTransferID]];
  914. }
  915. //Message (must do last)
  916. range = [inString rangeOfString:@"%message%"];
  917. while(range.location != NSNotFound) {
  918. [inString safeReplaceCharactersInRange:range withString:htmlEncodedMessage];
  919. range = [inString rangeOfString:@"%message%"
  920. options:NSLiteralSearch
  921. range:NSMakeRange(range.location + htmlEncodedMessage.length,
  922. inString.length - range.location - htmlEncodedMessage.length)];
  923. }
  924. // Topic replacement (if applicable)
  925. if ([content isKindOfClass:[AIContentTopic class]]) {
  926. range = [inString rangeOfString:@"%topic%"];
  927. if (range.location != NSNotFound) {
  928. [inString safeReplaceCharactersInRange:range withString:[NSString stringWithFormat:TOPIC_INDIVIDUAL_WRAPPER, htmlEncodedMessage]];
  929. }
  930. }
  931. } else if ([content isKindOfClass:[AIContentStatus class]]) {
  932. NSString *statusPhrase;
  933. BOOL replacedStatusPhrase = NO;
  934. [inString replaceKeyword:@"%status%"
  935. withString:[[(AIContentStatus *)content status] stringByEscapingForXMLWithEntities:nil]];
  936. [inString replaceKeyword:@"%statusSender%"
  937. withString:[theSource.displayName stringByEscapingForXMLWithEntities:nil]];
  938. [inString replaceKeyword:@"%senderScreenName%"
  939. withString:@""];
  940. [inString replaceKeyword:@"%senderPrefix%"
  941. withString:@""];
  942. [inString replaceKeyword:@"%sender%"
  943. withString:@""];
  944. if ((statusPhrase = [[content userInfo] objectForKey:@"Status Phrase"])) {
  945. do{
  946. range = [inString rangeOfString:@"%statusPhrase%"];
  947. if (range.location != NSNotFound) {
  948. [inString safeReplaceCharactersInRange:range
  949. withString:[statusPhrase stringByEscapingForXMLWithEntities:nil]];
  950. replacedStatusPhrase = YES;
  951. }
  952. } while (range.location != NSNotFound);
  953. }
  954. //Message (must do last)
  955. range = [inString rangeOfString:@"%message%"];
  956. if (range.location != NSNotFound) {
  957. NSString *messageString;
  958. if (replacedStatusPhrase) {
  959. //If the status phrase was used, clear the message tag
  960. messageString = @"";
  961. } else {
  962. messageString = [AIHTMLDecoder encodeHTML:[content message]
  963. headers:NO
  964. fontTags:NO
  965. includingColorTags:NO
  966. closeFontTags:YES
  967. styleTags:NO
  968. closeStyleTagsOnFontChange:YES
  969. encodeNonASCII:YES
  970. encodeSpaces:YES
  971. imagesPath:NSTemporaryDirectory()
  972. attachmentsAsText:NO
  973. onlyIncludeOutgoingImages:NO
  974. simpleTagsOnly:NO
  975. bodyBackground:NO
  976. allowJavascriptURLs:NO];
  977. }
  978. [inString safeReplaceCharactersInRange:range withString:messageString];
  979. }
  980. }
  981. return inString;
  982. }
  983. - (NSMutableString *)fillKeywordsForBaseTemplate:(NSMutableString *)inString chat:(AIChat *)chat
  984. {
  985. NSRange range;
  986. [inString replaceKeyword:@"%chatName%"
  987. withString:[chat.displayName stringByEscapingForXMLWithEntities:nil]];
  988. NSString * sourceName = [chat.account.displayName stringByEscapingForXMLWithEntities:nil];
  989. if(!sourceName) sourceName = @" ";
  990. [inString replaceKeyword:@"%sourceName%"
  991. withString:sourceName];
  992. NSString *destinationName = chat.listObject.displayName;
  993. if (!destinationName) destinationName = chat.displayName;
  994. [inString replaceKeyword:@"%destinationName%"
  995. withString:destinationName];
  996. NSString *serversideDisplayName = chat.listObject.serversideDisplayName;
  997. if (!serversideDisplayName) serversideDisplayName = chat.displayName;
  998. [inString replaceKeyword:@"%destinationDisplayName%"
  999. withString:[serversideDisplayName stringByEscapingForXMLWithEntities:nil]];
  1000. AIListContact *listObject = chat.listObject;
  1001. NSString *iconPath = nil;
  1002. [inString replaceKeyword:@"%incomingColor%"
  1003. withString:[NSColor representedColorForObject:listObject.UID withValidColors:self.validSenderColors]];
  1004. [inString replaceKeyword:@"%outgoingColor%"
  1005. withString:[NSColor representedColorForObject:chat.account.UID withValidColors:self.validSenderColors]];
  1006. if (listObject) {
  1007. iconPath = [listObject valueForProperty:KEY_WEBKIT_USER_ICON];
  1008. if (!iconPath)
  1009. iconPath = [listObject valueForProperty:@"UserIconPath"];
  1010. /* We couldn't get an icon... but perhaps we can for a parent contact */
  1011. if (!iconPath &&
  1012. [listObject isKindOfClass:[AIListContact class]] &&
  1013. ([(AIListContact *)listObject parentContact] != listObject)) {
  1014. iconPath = [[(AIListContact *)listObject parentContact] valueForProperty:KEY_WEBKIT_USER_ICON];
  1015. if (!iconPath)
  1016. iconPath = [[(AIListContact *)listObject parentContact] valueForProperty:@"UserIconPath"];
  1017. }
  1018. }
  1019. [inString replaceKeyword:@"%incomingIconPath%"
  1020. withString:(iconPath ? iconPath : @"incoming_icon.png")];
  1021. AIListObject *account = chat.account;
  1022. iconPath = nil;
  1023. if (account) {
  1024. iconPath = [account valueForProperty:KEY_WEBKIT_USER_ICON];
  1025. if (!iconPath)
  1026. iconPath = [account valueForProperty:@"UserIconPath"];
  1027. }
  1028. [inString replaceKeyword:@"%outgoingIconPath%"
  1029. withString:(iconPath ? iconPath : @"outgoing_icon.png")];
  1030. NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:account.service.serviceID
  1031. type:AIServiceIconLarge];
  1032. NSString *serviceIconTag = [NSString stringWithFormat:@"<img class=\"serviceIcon\" src=\"%@\" alt=\"%@\" title=\"%@\">", serviceIconPath ? serviceIconPath : @"outgoing_icon.png", [account.service shortDescription], [account.service shortDescription]];
  1033. [inString replaceKeyword:@"%service%"
  1034. withString:[account.service shortDescription]];
  1035. [inString replaceKeyword:@"%serviceIconImg%"
  1036. withString:serviceIconTag];
  1037. [inString replaceKeyword:@"%serviceIconPath%"
  1038. withString:serviceIconPath];
  1039. [inString replaceKeyword:@"%timeOpened%"
  1040. withString:[timeStampFormatter stringFromDate:[chat dateOpened]]];
  1041. //Replaces %time{x}% with a timestamp formatted like x (using NSDateFormatter)
  1042. do{
  1043. range = [inString rangeOfString:@"%timeOpened{"];
  1044. if (range.location != NSNotFound) {
  1045. NSRange endRange;

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