If we try to update icons while the webview doesn't have an accessible documentFrame (e.g. while it is still working on loading its initial HTML), try again after a second. This fixes icons which are part of the initial bolus of information presented by a webview that change shortly after it loads, which can most noticeably happen when restoring a chat using a webview with icons in its header but could also occur if a user icon changed as we opened a chat window.
Fixes #12696
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
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.
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.
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.
17 #import "AIWebkitMessageViewStyle.h"
18 #import <AIUtilities/AIColorAdditions.h>
19 #import <AIUtilities/AIStringAdditions.h>
20 #import <AIUtilities/AIDateFormatterAdditions.h>
21 #import <AIUtilities/AIMutableStringAdditions.h>
22 #import <Adium/AIAccount.h>
23 #import <Adium/AIChat.h>
24 #import <Adium/AIContentTopic.h>
25 #import <Adium/AIContentContext.h>
26 #import <Adium/AIContentMessage.h>
27 #import <Adium/AIContentNotification.h>
28 #import <Adium/AIContentObject.h>
29 #import <Adium/AIContentStatus.h>
30 #import <Adium/AIHTMLDecoder.h>
31 #import <Adium/AIListObject.h>
32 #import <Adium/AIListContact.h>
33 #import <Adium/AIService.h>
34 #import <Adium/ESFileTransfer.h>
35 #import <Adium/AIServiceIcons.h>
36 #import <Adium/AIContentControllerProtocol.h>
37 #import <Adium/AIStatusIcons.h>
38 #import <AIUtilities/AIApplicationAdditions.h>
41 #define LEGACY_VERSION_THRESHOLD 3 //Styles older than this version are considered legacy
44 #define KEY_WEBKIT_VERSION @"MessageViewVersion"
46 //BOM scripts for appending content.
47 #define APPEND_MESSAGE_WITH_SCROLL @"checkIfScrollToBottomIsNeeded(); appendMessage(\"%@\"); scrollToBottomIfNeeded();"
48 #define APPEND_NEXT_MESSAGE_WITH_SCROLL @"checkIfScrollToBottomIsNeeded(); appendNextMessage(\"%@\"); scrollToBottomIfNeeded();"
49 #define APPEND_MESSAGE @"appendMessage(\"%@\");"
50 #define APPEND_NEXT_MESSAGE @"appendNextMessage(\"%@\");"
51 #define APPEND_MESSAGE_NO_SCROLL @"appendMessageNoScroll(\"%@\");"
52 #define APPEND_NEXT_MESSAGE_NO_SCROLL @"appendNextMessageNoScroll(\"%@\");"
53 #define REPLACE_LAST_MESSAGE @"replaceLastMessage(\"%@\");"
55 #define TOPIC_MAIN_DIV @"<div id=\"topic\"></div>"
56 // 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.
57 #define TOPIC_INDIVIDUAL_WRAPPER @"<span id=\"topicEdit\" ondblclick=\"this.setAttribute('contentEditable', true); this.focus();\">%@</span>"
59 @interface NSMutableString (AIKeywordReplacementAdditions)
60 - (void) replaceKeyword:(NSString *)word withString:(NSString *)newWord;
61 - (void) safeReplaceCharactersInRange:(NSRange)range withString:(NSString *)newWord;
64 @implementation NSMutableString (AIKeywordReplacementAdditions)
65 - (void) replaceKeyword:(NSString *)keyWord withString:(NSString *)newWord
68 if(!newWord) newWord = @"";
69 [self replaceOccurrencesOfString:keyWord
71 options:NSLiteralSearch
72 range:NSMakeRange(0.0, [self length])];
75 - (void) safeReplaceCharactersInRange:(NSRange)range withString:(NSString *)newWord
77 if (range.location == NSNotFound || range.length == 0) return;
78 if (!newWord) [self deleteCharactersInRange:range];
79 else [self replaceCharactersInRange:range withString:newWord];
83 //The old code built the paths itself, which follows the filesystem's case sensitivity, so some noobs named stuff wrong.
84 //NSBundle is always case sensitive, so those styles broke (they were already broken on case sensitive hfsx)
85 //These methods only check for the all-lowercase variant, so are not suitable for general purpose use.
86 @interface NSBundle (StupidCompatibilityHack)
87 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type;
88 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type inDirectory:(NSString *)dirpath;
91 @implementation NSBundle (StupidCompatibilityHack)
92 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type
94 NSString *path = [self pathForResource:res ofType:type];
96 path = [self pathForResource:[res lowercaseString] ofType:type];
100 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type inDirectory:(NSString *)dirpath
102 NSString *path = [self pathForResource:res ofType:type inDirectory:dirpath];
104 path = [self pathForResource:[res lowercaseString] ofType:type inDirectory:dirpath];
110 @interface AIWebkitMessageViewStyle ()
111 - (id)initWithBundle:(NSBundle *)inBundle;
112 - (void)_loadTemplates;
113 - (void)releaseResources;
114 - (NSMutableString *)_escapeStringForPassingToScript:(NSMutableString *)inString;
115 - (NSString *)noVariantName;
116 - (NSString *)iconPathForFileTransfer:(ESFileTransfer *)inObject;
117 - (NSString *)statusIconPathForListObject:(AIListObject *)inObject;
120 @implementation AIWebkitMessageViewStyle
122 + (id)messageViewStyleFromBundle:(NSBundle *)inBundle
124 return [[[self alloc] initWithBundle:inBundle] autorelease];
127 + (id)messageViewStyleFromPath:(NSString *)path
129 NSBundle *styleBundle = [NSBundle bundleWithPath:path];
131 return [[[self alloc] initWithBundle:styleBundle] autorelease];
138 - (id)initWithBundle:(NSBundle *)inBundle
140 if ((self = [super init])) {
141 styleBundle = [inBundle retain];
142 stylePath = [[styleBundle resourcePath] retain];
151 [self releaseResources];
154 allowTextBackgrounds = YES;
156 /* Our styles are versioned so we can change how they work without breaking compatibility.
158 * Version 0: Initial Webkit Version
159 * Version 1: Template.html now handles all scroll-to-bottom functionality. It is no longer required to call the
160 * scrollToBottom functions when inserting content.
161 * Version 2: No significant changes
162 * Version 3: main.css is no longer a separate style, it now serves as the base stylesheet and is imported by default.
163 * The default variant is now a separate file in /variants like all other variants.
164 * Template.html now includes appendMessageNoScroll() and appendNextMessageNoScroll() which behave
165 * the same as appendMessage() and appendNextMessage() in Versions 1 and 2 but without scrolling.
166 * Version 4: Template.html now includes replaceLastMessage()
167 * Template.html now defines actionMessageUserName and actionMessageBody for display of /me (actions).
168 * If the style provides a custom Template.html, these classes must be defined.
169 * CSS can be used to customize the appearance of actions.
170 * HTML filters in are now supported in Adium's content filter system; filters can assume Version 4 or later.
172 styleVersion = [[styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION] integerValue];
174 //Pre-fetch our templates
175 [self _loadTemplates];
178 allowsCustomBackground = ![[styleBundle objectForInfoDictionaryKey:@"DisableCustomBackground"] boolValue];
179 transparentDefaultBackground = [[styleBundle objectForInfoDictionaryKey:@"DefaultBackgroundIsTransparent"] boolValue];
181 combineConsecutive = ![[styleBundle objectForInfoDictionaryKey:@"DisableCombineConsecutive"] boolValue];
183 NSNumber *tmpNum = [styleBundle objectForInfoDictionaryKey:@"ShowsUserIcons"];
184 allowsUserIcons = (tmpNum ? [tmpNum boolValue] : YES);
187 NSString *tmpName = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_USER_ICON_MASK];
188 if (tmpName) userIconMask = [[NSImage alloc] initWithContentsOfFile:[stylePath stringByAppendingPathComponent:tmpName]];
190 NSNumber *allowsColorsNumber = [styleBundle objectForInfoDictionaryKey:@"AllowTextColors"];
191 allowsColors = (allowsColorsNumber ? [allowsColorsNumber boolValue] : YES);
195 * @brief release everything we loaded from the style bundle
197 - (void)releaseResources
200 [headerHTML release];
201 [footerHTML release];
203 [contentHTML release];
204 [contentInHTML release];
205 [nextContentInHTML release];
206 [contextInHTML release];
207 [nextContextInHTML release];
208 [contentOutHTML release];
209 [nextContentOutHTML release];
210 [contextOutHTML release];
211 [nextContextOutHTML release];
212 [statusHTML release];
213 [fileTransferHTML release];
216 [customBackgroundPath release];
217 [customBackgroundColor release];
219 [userIconMask release];
227 [styleBundle release];
230 [self releaseResources];
231 [timeStampFormatter release];
233 [statusIconPathCache release];
238 @synthesize bundle = styleBundle;
242 return styleVersion < LEGACY_VERSION_THRESHOLD;
245 #pragma mark Settings
247 @synthesize allowsCustomBackground, allowsUserIcons, allowsColors, userIconMask;
249 - (NSArray *)validSenderColors
251 if(!checkedSenderColors) {
252 NSURL *url = [NSURL fileURLWithPath:[stylePath stringByAppendingPathComponent:@"Incoming/SenderColors.txt"]];
253 NSString *senderColorsFile = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:NULL];
256 validSenderColors = [[senderColorsFile componentsSeparatedByString:@":"] retain];
258 checkedSenderColors = YES;
261 return validSenderColors;
264 - (BOOL)isBackgroundTransparent
266 //Our custom background is only transparent if the user has set a custom color with an alpha component less than 1.0
267 return ((!customBackgroundColor && transparentDefaultBackground) ||
268 (customBackgroundColor && [customBackgroundColor alphaComponent] < 0.99));
271 - (NSString *)defaultFontFamily
273 NSString *defaultFontFamily = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_DEFAULT_FONT_FAMILY];
274 if (!defaultFontFamily) defaultFontFamily = [[NSFont systemFontOfSize:0] familyName];
276 return defaultFontFamily;
279 - (NSNumber *)defaultFontSize
281 NSNumber *defaultFontSize = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_DEFAULT_FONT_SIZE];
283 if (!defaultFontSize) defaultFontSize = [NSNumber numberWithInteger:[[NSFont systemFontOfSize:0] pointSize]];
285 return defaultFontSize;
290 return headerHTML && [headerHTML length];
295 return topicHTML && [topicHTML length];
298 #pragma mark Behavior
300 - (void)setDateFormat:(NSString *)format
302 if (!format || [format length] == 0) {
303 format = [NSDateFormatter localizedDateFormatStringShowingSeconds:NO showingAMorPM:NO];
306 [timeStampFormatter release];
308 if ([format rangeOfString:@"%"].location != NSNotFound) {
309 /* Support strftime-style format strings, which old message styles may use */
310 timeStampFormatter = [[NSDateFormatter alloc] initWithDateFormat:format allowNaturalLanguage:NO];
312 timeStampFormatter = [[NSDateFormatter alloc] init];
313 [timeStampFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
314 [timeStampFormatter setDateFormat:format];
318 @synthesize allowTextBackgrounds, customBackgroundType, customBackgroundColor, showIncomingMessageColors=showIncomingColors, showIncomingMessageFonts=showIncomingFonts, customBackgroundPath, nameFormat, useCustomNameFormat, showHeader, showUserIcons;
320 //Templates ------------------------------------------------------------------------------------------------------------
321 #pragma mark Templates
322 - (NSString *)baseTemplateWithVariant:(NSString *)variant chat:(AIChat *)chat
324 NSMutableString *templateHTML;
326 // If this is a group chat, we want to include a topic.
327 // Otherwise, if the header is shown, use it.
328 NSString *headerContent = @"";
330 if (chat.isGroupChat) {
331 headerContent = (chat.supportsTopic ? TOPIC_MAIN_DIV : @"");
332 } else if (headerHTML) {
333 headerContent = headerHTML;
337 //Old styles may be using an old custom 4 parameter baseHTML. Styles version 3 and higher should
338 //be using the bundled (or a custom) 5 parameter baseHTML.
339 if ((styleVersion < 3) && usingCustomTemplateHTML) {
340 templateHTML = [NSMutableString stringWithFormat:baseHTML, //Template
341 [[NSURL fileURLWithPath:stylePath] absoluteString], //Base path
342 [self pathForVariant:variant], //Variant path
344 (footerHTML ? footerHTML : @"")];
346 templateHTML = [NSMutableString stringWithFormat:baseHTML, //Template
347 [[NSURL fileURLWithPath:stylePath] absoluteString], //Base path
348 styleVersion < 3 ? @"" : @"@import url( \"main.css\" );", //Import main.css for new enough styles
349 [self pathForVariant:variant], //Variant path
351 (footerHTML ? footerHTML : @"")];
354 return [self fillKeywordsForBaseTemplate:templateHTML chat:chat];
357 - (NSString *)templateForContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
361 //Get the correct template for what we're inserting
362 if ([[content type] isEqualToString:CONTENT_MESSAGE_TYPE]) {
363 if ([content isOutgoing]) {
364 template = (contentIsSimilar ? nextContentOutHTML : contentOutHTML);
366 template = (contentIsSimilar ? nextContentInHTML : contentInHTML);
369 } else if ([[content type] isEqualToString:CONTENT_CONTEXT_TYPE]) {
370 if ([content isOutgoing]) {
371 template = (contentIsSimilar ? nextContextOutHTML : contextOutHTML);
373 template = (contentIsSimilar ? nextContextInHTML : contextInHTML);
376 } else if([[content type] isEqualToString:CONTENT_FILE_TRANSFER_TYPE]) {
377 template = [[fileTransferHTML mutableCopy] autorelease];
378 } else if ([[content type] isEqualToString:CONTENT_TOPIC_TYPE]) {
379 template = topicHTML;
382 template = statusHTML;
388 - (NSString *)completedTemplateForContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
390 NSMutableString *mutableTemplate = [[self templateForContent:content similar:contentIsSimilar] mutableCopy];
392 if (mutableTemplate) {
393 mutableTemplate = [self fillKeywords:mutableTemplate forContent:content similar:contentIsSimilar];
396 return [mutableTemplate autorelease];
400 * @brief Pre-fetch all the style templates
402 * This needs to be called before either baseTemplate or templateForContent is called
404 - (void)_loadTemplates
406 //Load the style's templates
407 //We can't use NSString's initWithContentsOfFile here. HTML files are interpreted in the defaultCEncoding
408 //(which varies by system) when read that way. We want to always interpret the files as UTF8.
409 headerHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Header" ofType:@"html"]] retain];
410 footerHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Footer" ofType:@"html"]] retain];
411 topicHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Topic" ofType:@"html"]] retain];
412 baseHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Template" ofType:@"html"]];
414 //Starting with version 1, styles can choose to not include template.html. If the template is not included
415 //Adium's default will be used. This is preferred since any future template updates will apply to the style
416 if ((!baseHTML || [baseHTML length] == 0) && styleVersion >= 1) {
417 baseHTML = [NSString stringWithContentsOfUTF8File:[[NSBundle bundleForClass:[self class]] semiCaseInsensitivePathForResource:@"Template" ofType:@"html"]];
418 usingCustomTemplateHTML = NO;
420 usingCustomTemplateHTML = YES;
422 if ([baseHTML rangeOfString:@"function imageCheck()" options:NSLiteralSearch].location != NSNotFound) {
423 /* This doesn't quite fix image swapping on styles with broken image swapping due to custom HTML templates,
424 * but it improves it. For some reason, the result of using our normal template.html functions is that
425 * clicking works once, then the text doesn't allow a return click. This is an improvement compared
426 * to fully broken behavior in which the return click shows a missing-image placeholder.
428 NSMutableString *imageSwapFixedBaseHTML = [[baseHTML mutableCopy] autorelease];
429 [imageSwapFixedBaseHTML replaceOccurrencesOfString:
430 @" function imageCheck() {\n"
431 " node = event.target;\n"
432 " if(node.tagName == 'IMG' && node.alt) {\n"
433 " a = document.createElement('a');\n"
434 " a.setAttribute('onclick', 'imageSwap(this)');\n"
435 " a.setAttribute('src', node.src);\n"
436 " text = document.createTextNode(node.alt);\n"
437 " a.appendChild(text);\n"
438 " node.parentNode.replaceChild(a, node);\n"
442 @" function imageCheck() {\n"
443 " var node = event.target;\n"
444 " if(node.tagName.toLowerCase() == 'img' && !client.zoomImage(node) && node.alt) {\n"
445 " var a = document.createElement('a');\n"
446 " a.setAttribute('onclick', 'imageSwap(this)');\n"
447 " a.setAttribute('src', node.getAttribute('src'));\n"
448 " a.className = node.className;\n"
449 " var text = document.createTextNode(node.alt);\n"
450 " a.appendChild(text);\n"
451 " node.parentNode.replaceChild(a, node);\n"
454 options:NSLiteralSearch];
455 [imageSwapFixedBaseHTML replaceOccurrencesOfString:
456 @" function imageSwap(node) {\n"
457 " img = document.createElement('img');\n"
458 " img.setAttribute('src', node.src);\n"
459 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
460 " node.parentNode.replaceChild(img, node);\n"
464 @" function imageSwap(node) {\n"
465 " var shouldScroll = nearBottom();\n"
466 " //Swap the image/text\n"
467 " var img = document.createElement('img');\n"
468 " img.setAttribute('src', node.getAttribute('src'));\n"
469 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
470 " img.className = node.className;\n"
471 " node.parentNode.replaceChild(img, node);\n"
473 " alignChat(shouldScroll);\n"
475 options:NSLiteralSearch];
476 /* Now for ones which don't call alignChat() */
477 [imageSwapFixedBaseHTML replaceOccurrencesOfString:
478 @" function imageSwap(node) {\n"
479 " img = document.createElement('img');\n"
480 " img.setAttribute('src', node.src);\n"
481 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
482 " node.parentNode.replaceChild(img, node);\n"
485 @" function imageSwap(node) {\n"
486 " var shouldScroll = nearBottom();\n"
487 " //Swap the image/text\n"
488 " var img = document.createElement('img');\n"
489 " img.setAttribute('src', node.getAttribute('src'));\n"
490 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
491 " img.className = node.className;\n"
492 " node.parentNode.replaceChild(img, node);\n"
494 options:NSLiteralSearch];
495 baseHTML = imageSwapFixedBaseHTML;
502 contentHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html"]] retain];
503 contentInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html" inDirectory:@"Incoming"]] retain];
504 nextContentInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContent" ofType:@"html" inDirectory:@"Incoming"]] retain];
505 contentOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html" inDirectory:@"Outgoing"]] retain];
506 nextContentOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContent" ofType:@"html" inDirectory:@"Outgoing"]] retain];
509 contextInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Context" ofType:@"html" inDirectory:@"Incoming"]] retain];
510 nextContextInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContext" ofType:@"html" inDirectory:@"Incoming"]] retain];
511 contextOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Context" ofType:@"html" inDirectory:@"Outgoing"]] retain];
512 nextContextOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContext" ofType:@"html" inDirectory:@"Outgoing"]] retain];
514 //Fall back to Resources/Content.html if Incoming isn't present
515 if (!contentInHTML) contentInHTML = [contentHTML retain];
517 //Fall back to Content if NextContent doesn't need to use different HTML
518 if (!nextContentInHTML) nextContentInHTML = [contentInHTML retain];
520 //Fall back to Content if Context isn't present
521 if (!nextContextInHTML) nextContextInHTML = [nextContentInHTML retain];
522 if (!contextInHTML) contextInHTML = [contentInHTML retain];
524 //Fall back to Content if Context isn't present
525 if (!nextContextOutHTML && nextContentOutHTML) nextContextOutHTML = [nextContentOutHTML retain];
526 if (!contextOutHTML && contentOutHTML) contextOutHTML = [contentOutHTML retain];
528 //Fall back to Content if Context isn't present
529 if (!nextContextOutHTML) nextContextOutHTML = [nextContextInHTML retain];
530 if (!contextOutHTML) contextOutHTML = [contextInHTML retain];
532 //Fall back to Incoming if Outgoing doesn't need to be different
533 if (!contentOutHTML) contentOutHTML = [contentInHTML retain];
534 if (!nextContentOutHTML) nextContentOutHTML = [nextContentInHTML retain];
537 statusHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Status" ofType:@"html"]] retain];
539 //Fall back to Resources/Incoming/Content.html if Status isn't present
540 if (!statusHTML) statusHTML = [contentInHTML retain];
542 //TODO: make a generic Request message, rather than having this ft specific one
543 NSMutableString *fileTransferHTMLTemplate;
544 fileTransferHTMLTemplate = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"FileTransferRequest" ofType:@"html"]] mutableCopy];
545 if(!fileTransferHTMLTemplate) {
546 fileTransferHTMLTemplate = [contentInHTML mutableCopy];
547 [fileTransferHTMLTemplate replaceKeyword:@"%message%"
548 withString:@"<p><img src=\"%fileIconPath%\" style=\"width:32px; height:32px; vertical-align:middle;\"></img><input type=\"button\" onclick=\"%saveFileAsHandler%\" value=\"Download %fileName%\"></p>"];
550 [fileTransferHTMLTemplate replaceKeyword:@"Download %fileName%"
551 withString:[NSString stringWithFormat:AILocalizedString(@"Download %@", "%@ will be a file name"), @"%fileName%"]];
552 fileTransferHTML = fileTransferHTMLTemplate;
556 - (NSString *)scriptForAppendingContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
558 NSMutableString *newHTML;
561 //If combining of consecutive messages has been disabled, we treat all content as non-similar
562 if (!combineConsecutive) contentIsSimilar = NO;
564 //Fetch the correct template and substitute keywords for the passed content
565 newHTML = [[[self completedTemplateForContent:content similar:contentIsSimilar] mutableCopy] autorelease];
567 //BOM scripts vary by style version
568 if (!usingCustomTemplateHTML && styleVersion >= 4) {
569 /* If we're using the built-in template HTML, we know that it supports our most modern scripts */
570 if (replaceLastContent)
571 script = REPLACE_LAST_MESSAGE;
572 else if (willAddMoreContentObjects) {
573 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_NO_SCROLL : APPEND_MESSAGE_NO_SCROLL);
575 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
578 } else if (styleVersion >= 3) {
579 if (willAddMoreContentObjects) {
580 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_NO_SCROLL : APPEND_MESSAGE_NO_SCROLL);
582 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
584 } else if (styleVersion >= 1) {
585 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
588 if (usingCustomTemplateHTML && [content isKindOfClass:[AIContentStatus class]]) {
589 /* Old styles with a custom template.html had Status.html files without 'insert' divs coupled
590 * with a APPEND_NEXT_MESSAGE_WITH_SCROLL script which assumes one exists.
592 script = APPEND_MESSAGE_WITH_SCROLL;
594 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_WITH_SCROLL : APPEND_MESSAGE_WITH_SCROLL);
598 return [NSString stringWithFormat:script, [self _escapeStringForPassingToScript:newHTML]];
601 - (NSString *)scriptForChangingVariant:(NSString *)variant
603 AILogWithSignature(@"%@",[NSString stringWithFormat:@"setStylesheet(\"mainStyle\",\"%@\");",[self pathForVariant:variant]]);
605 return [NSString stringWithFormat:@"setStylesheet(\"mainStyle\",\"%@\");",[self pathForVariant:variant]];
608 - (NSString *)scriptForScrollingAfterAddingMultipleContentObjects
610 if ((styleVersion >= 3) || !usingCustomTemplateHTML) {
611 return @"alignChat(true);";
618 * @brief Escape a string for passing to our BOM scripts
620 - (NSMutableString *)_escapeStringForPassingToScript:(NSMutableString *)inString
622 //We need to escape a few things to get our string to the javascript without trouble
623 [inString replaceOccurrencesOfString:@"\\"
625 options:NSLiteralSearch];
627 [inString replaceOccurrencesOfString:@"\""
629 options:NSLiteralSearch];
631 [inString replaceOccurrencesOfString:@"\n"
633 options:NSLiteralSearch];
635 [inString replaceOccurrencesOfString:@"\r"
637 options:NSLiteralSearch];
642 #pragma mark Variants
644 - (NSArray *)availableVariants
646 NSMutableArray *availableVariants = [NSMutableArray array];
648 //Build an array of all variant names
649 for (NSString *path in [styleBundle pathsForResourcesOfType:@"css" inDirectory:@"Variants"]) {
650 [availableVariants addObject:[[path lastPathComponent] stringByDeletingPathExtension]];
653 //Style versions before 3 stored the default variant in a separate location. They also allowed for this
654 //varient name to not be specified, and would substitute a localized string in its place.
655 if (styleVersion < 3) {
656 [availableVariants addObject:[self noVariantName]];
659 //Alphabetize the variants
660 if ([NSApp isOnSnowLeopardOrBetter]) {
661 [availableVariants sortUsingSelector:@selector(localizedStandardCompare:)];
663 [availableVariants sortUsingSelector:@selector(compare:)];
666 return availableVariants;
669 - (NSString *)pathForVariant:(NSString *)variant
671 //Styles before version 3 stored the default variant in main.css, and not in the variants folder.
672 if (styleVersion < 3 && [variant isEqualToString:[self noVariantName]]) {
675 return [NSString stringWithFormat:@"Variants/%@.css",variant];
680 * @brief Base variant name for styles before version 2
682 - (NSString *)noVariantName
684 NSString *noVariantName = [styleBundle objectForInfoDictionaryKey:@"DisplayNameForNoVariant"];
685 return noVariantName ? noVariantName : AILocalizedString(@"Normal","Normal style variant menu item");
688 + (NSString *)noVariantNameForBundle:(NSBundle *)inBundle
690 NSString *noVariantName = [inBundle objectForInfoDictionaryKey:@"DisplayNameForNoVariant"];
691 return noVariantName ? noVariantName : AILocalizedString(@"Normal","Normal style variant menu item");
694 - (NSString *)defaultVariant
696 return styleVersion < 3 ? [self noVariantName] : [styleBundle objectForInfoDictionaryKey:@"DefaultVariant"];
699 + (NSString *)defaultVariantForBundle:(NSBundle *)inBundle
701 return [[inBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION] integerValue] < 3 ?
702 [self noVariantNameForBundle:inBundle] :
703 [inBundle objectForInfoDictionaryKey:@"DefaultVariant"];
706 #pragma mark Keyword replacement
708 - (NSMutableString *)fillKeywords:(NSMutableString *)inString forContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
712 AIListObject *contentSource = [content source];
713 AIListObject *theSource = ([contentSource isKindOfClass:[AIListContact class]] ?
714 [(AIListContact *)contentSource parentContact] :
718 htmlEncodedMessage is only encoded correctly for AIContentMessages
719 but we do it up here so that we can check for RTL/LTR text below without
720 having to encode the message twice. This is less than ideal
722 NSString *htmlEncodedMessage = [AIHTMLDecoder encodeHTML:[content message]
724 fontTags:showIncomingFonts
725 includingColorTags:(allowsColors && showIncomingColors)
728 closeStyleTagsOnFontChange:YES
731 imagesPath:NSTemporaryDirectory()
733 onlyIncludeOutgoingImages:NO
736 allowJavascriptURLs:NO];
738 if (styleVersion >= 4)
739 htmlEncodedMessage = [adium.contentController filterHTMLString:htmlEncodedMessage
740 direction:[content isOutgoing] ? AIFilterOutgoing : AIFilterIncoming
744 if ([content respondsToSelector:@selector(date)])
745 date = [(AIContentMessage *)content date];
747 //Replacements applicable to any AIContentObject
748 [inString replaceKeyword:@"%time%"
749 withString:(date ? [timeStampFormatter stringFromDate:date] : @"")];
751 NSString *shortTimeString = (date ? [[NSDateFormatter localizedDateFormatterShowingSeconds:NO showingAMorPM:NO] stringFromDate:date] : @"");
752 [inString replaceKeyword:@"%shortTime%"
753 withString:shortTimeString];
755 if ([inString rangeOfString:@"%senderStatusIcon%"].location != NSNotFound) {
756 //Only cache the status icon to disk if the message style will actually use it
757 [inString replaceKeyword:@"%senderStatusIcon%"
758 withString:[self statusIconPathForListObject:theSource]];
761 //Replaces %localized{x}% with a a localized version of x, searching the style's localizations, and then Adium's localizations
763 range = [inString rangeOfString:@"%localized{"];
764 if (range.location != NSNotFound) {
766 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
767 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
768 NSString *untranslated = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
770 NSString *translated = [styleBundle localizedStringForKey:untranslated
773 if (!translated || [translated length] == 0) {
774 translated = [[NSBundle bundleForClass:[self class]] localizedStringForKey:untranslated
777 if (!translated || [translated length] == 0) {
778 translated = [[NSBundle mainBundle] localizedStringForKey:untranslated
785 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
786 withString:translated];
789 } while (range.location != NSNotFound);
791 [inString replaceKeyword:@"%userIcons%"
792 withString:(showUserIcons ? @"showIcons" : @"hideIcons")];
794 [inString replaceKeyword:@"%messageClasses%"
795 withString:[(contentIsSimilar ? @"consecutive " : @"") stringByAppendingString:[[content displayClasses] componentsJoinedByString:@" "]]];
797 [inString replaceKeyword:@"%senderColor%"
798 withString:[NSColor representedColorForObject:contentSource.UID withValidColors:self.validSenderColors]];
800 //HAX. The odd conditional here detects the rtl html that our html parser spits out.
801 [inString replaceKeyword:@"%messageDirection%"
802 withString:(([inString rangeOfString:@"<DIV dir=\"rtl\">"].location != NSNotFound) ? @"rtl" : @"ltr")];
804 //Replaces %time{x}% with a timestamp formatted like x (using NSDateFormatter)
806 range = [inString rangeOfString:@"%time{"];
807 if (range.location != NSNotFound) {
809 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
810 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
812 NSString *timeFormat = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
814 NSDateFormatter *dateFormatter;
815 if ([timeFormat rangeOfString:@"%"].location != NSNotFound) {
816 /* Support strftime-style format strings, which old message styles may use */
817 dateFormatter = [[NSDateFormatter alloc] initWithDateFormat:timeFormat allowNaturalLanguage:NO];
819 dateFormatter = [[NSDateFormatter alloc] init];
820 [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
821 [dateFormatter setDateFormat:timeFormat];
824 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
825 withString:[dateFormatter stringFromDate:date]];
827 [dateFormatter release];
830 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
834 } while (range.location != NSNotFound);
837 range = [inString rangeOfString:@"%userIconPath%"];
838 if (range.location != NSNotFound) {
839 NSString *userIconPath;
840 NSString *replacementString;
842 userIconPath = [theSource valueForProperty:KEY_WEBKIT_USER_ICON];
844 userIconPath = [theSource valueForProperty:@"UserIconPath"];
847 if (showUserIcons && userIconPath) {
848 replacementString = [NSString stringWithFormat:@"file://%@", userIconPath];
851 replacementString = ([content isOutgoing]
852 ? @"Outgoing/buddy_icon.png"
853 : @"Incoming/buddy_icon.png");
856 [inString safeReplaceCharactersInRange:range withString:replacementString];
858 } while (range.location != NSNotFound);
860 [inString replaceKeyword:@"%service%"
861 withString:[content.chat.account.service shortDescription]];
863 [inString replaceKeyword:@"%serviceIconPath%"
864 withString:[AIServiceIcons pathForServiceIconForServiceID:content.chat.account.service.serviceID
865 type:AIServiceIconLarge]];
868 if ([content isKindOfClass:[AIContentMessage class]]) {
870 //Use [content source] directly rather than the potentially-metaContact theSource
871 NSString *formattedUID = nil;
872 if ([content.chat aliasForContact:contentSource]) {
873 formattedUID = [content.chat aliasForContact:contentSource];
875 formattedUID = contentSource.formattedUID;
878 NSString *displayName = [content.chat displayNameForContact:contentSource];
880 [inString replaceKeyword:@"%status%"
883 [inString replaceKeyword:@"%senderScreenName%"
884 withString:[(formattedUID ?
886 displayName) stringByEscapingForXMLWithEntities:nil]];
889 [inString replaceKeyword:@"%senderPrefix%"
890 withString:((AIContentMessage *)content).senderPrefix];
893 range = [inString rangeOfString:@"%sender%"];
894 if (range.location != NSNotFound) {
895 NSString *senderDisplay = nil;
896 if (useCustomNameFormat) {
897 if (formattedUID && ![displayName isEqualToString:formattedUID]) {
898 switch (nameFormat) {
903 senderDisplay = displayName;
906 case AIDisplayName_ScreenName:
907 senderDisplay = [NSString stringWithFormat:@"%@ (%@)",displayName,formattedUID];
910 case AIScreenName_DisplayName:
911 senderDisplay = [NSString stringWithFormat:@"%@ (%@)",formattedUID,displayName];
915 senderDisplay = formattedUID;
920 //Test both displayName and formattedUID for nil-ness. If they're both nil, the assertion will trip.
921 if (!senderDisplay) {
922 senderDisplay = displayName;
923 if (!senderDisplay) {
924 senderDisplay = formattedUID;
925 if (!senderDisplay) {
926 AILog(@"wtf. we don't have a sender for %@ (%@)", content, [content message]);
927 NSAssert1(senderDisplay, @"Sender has no known display name that we can use! displayName and formattedUID were both nil for sender %@", contentSource);
932 senderDisplay = displayName;
935 if ([(AIContentMessage *)content isAutoreply]) {
936 senderDisplay = [NSString stringWithFormat:@"%@ %@",senderDisplay,AILocalizedString(@"(Autoreply)","Short word inserted after the sender's name when displaying a message which was an autoresponse")];
939 [inString safeReplaceCharactersInRange:range withString:[senderDisplay stringByEscapingForXMLWithEntities:nil]];
941 } while (range.location != NSNotFound);
944 range = [inString rangeOfString:@"%senderDisplayName%"];
945 if (range.location != NSNotFound) {
946 NSString *serversideDisplayName = ([theSource isKindOfClass:[AIListContact class]] ?
947 [(AIListContact *)theSource serversideDisplayName] :
949 if (!serversideDisplayName) {
950 serversideDisplayName = theSource.displayName;
953 [inString safeReplaceCharactersInRange:range
954 withString:[serversideDisplayName stringByEscapingForXMLWithEntities:nil]];
956 } while (range.location != NSNotFound);
958 //Blatantly stealing the date code for the background color script.
960 range = [inString rangeOfString:@"%textbackgroundcolor{"];
961 if (range.location != NSNotFound) {
963 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
964 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
965 NSString *transparency = [inString substringWithRange:NSMakeRange(NSMaxRange(range),
966 (endRange.location - NSMaxRange(range)))];
968 if (allowTextBackgrounds && showIncomingColors) {
969 NSString *thisIsATemporaryString;
970 unsigned rgb = 0, red, green, blue;
972 thisIsATemporaryString = [AIHTMLDecoder encodeHTML:[content message] headers:NO
974 includingColorTags:NO
977 closeStyleTagsOnFontChange:NO
980 imagesPath:NSTemporaryDirectory()
982 onlyIncludeOutgoingImages:NO
985 allowJavascriptURLs:NO];
986 hexcode = [NSScanner scannerWithString:thisIsATemporaryString];
987 [hexcode scanHexInt:&rgb];
988 if (![thisIsATemporaryString length] && rgb == 0) {
989 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
991 red = (rgb & 0xff0000) >> 16;
992 green = (rgb & 0x00ff00) >> 8;
993 blue = rgb & 0x0000ff;
994 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
995 withString:[NSString stringWithFormat:@"rgba(%d, %d, %d, %@)", red, green, blue, transparency]];
998 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
1000 } else if (endRange.location == NSMaxRange(range)) {
1001 if (allowTextBackgrounds && showIncomingColors) {
1002 NSString *thisIsATemporaryString;
1004 thisIsATemporaryString = [AIHTMLDecoder encodeHTML:[content message] headers:NO
1006 includingColorTags:NO
1009 closeStyleTagsOnFontChange:NO
1012 imagesPath:NSTemporaryDirectory()
1013 attachmentsAsText:NO
1014 onlyIncludeOutgoingImages:NO
1017 allowJavascriptURLs:NO];
1018 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
1019 withString:[NSString stringWithFormat:@"#%@", thisIsATemporaryString]];
1021 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
1025 } while (range.location != NSNotFound);
1027 if ([content isKindOfClass:[ESFileTransfer class]]) { //file transfers are an AIContentMessage subclass
1029 ESFileTransfer *transfer = (ESFileTransfer *)content;
1030 NSString *fileName = [[transfer remoteFilename] stringByEscapingForXMLWithEntities:nil];
1031 NSString *fileTransferID = [[transfer uniqueID] stringByEscapingForXMLWithEntities:nil];
1033 range = [inString rangeOfString:@"%fileIconPath%"];
1034 if (range.location != NSNotFound) {
1035 NSString *iconPath = [self iconPathForFileTransfer:transfer];
1036 NSImage *icon = [transfer iconImage];
1038 [[icon TIFFRepresentation] writeToFile:iconPath atomically:YES];
1039 [inString safeReplaceCharactersInRange:range withString:iconPath];
1040 range = [inString rangeOfString:@"%fileIconPath%"];
1041 } while (range.location != NSNotFound);
1044 [inString replaceKeyword:@"%fileName%"
1045 withString:fileName];
1047 [inString replaceKeyword:@"%saveFileHandler%"
1048 withString:[NSString stringWithFormat:@"client.handleFileTransfer('Save', '%@')", fileTransferID]];
1050 [inString replaceKeyword:@"%saveFileAsHandler%"
1051 withString:[NSString stringWithFormat:@"client.handleFileTransfer('SaveAs', '%@')", fileTransferID]];
1053 [inString replaceKeyword:@"%cancelRequestHandler%"
1054 withString:[NSString stringWithFormat:@"client.handleFileTransfer('Cancel', '%@')", fileTransferID]];
1057 //Message (must do last)
1058 range = [inString rangeOfString:@"%message%"];
1059 while(range.location != NSNotFound) {
1060 [inString safeReplaceCharactersInRange:range withString:htmlEncodedMessage];
1061 range = [inString rangeOfString:@"%message%"
1062 options:NSLiteralSearch
1063 range:NSMakeRange(range.location + htmlEncodedMessage.length,
1064 inString.length - range.location - htmlEncodedMessage.length)];
1067 // Topic replacement (if applicable)
1068 if ([content isKindOfClass:[AIContentTopic class]]) {
1069 range = [inString rangeOfString:@"%topic%"];
1071 if (range.location != NSNotFound) {
1072 [inString safeReplaceCharactersInRange:range withString:[NSString stringWithFormat:TOPIC_INDIVIDUAL_WRAPPER, htmlEncodedMessage]];
1075 } else if ([content isKindOfClass:[AIContentStatus class]]) {
1076 NSString *statusPhrase;
1077 BOOL replacedStatusPhrase = NO;
1079 [inString replaceKeyword:@"%status%"
1080 withString:[[(AIContentStatus *)content status] stringByEscapingForXMLWithEntities:nil]];
1082 [inString replaceKeyword:@"%statusSender%"
1083 withString:[theSource.displayName stringByEscapingForXMLWithEntities:nil]];
1085 [inString replaceKeyword:@"%senderScreenName%"
1088 [inString replaceKeyword:@"%senderPrefix%"
1091 [inString replaceKeyword:@"%sender%"
1094 if ((statusPhrase = [[content userInfo] objectForKey:@"Status Phrase"])) {
1096 range = [inString rangeOfString:@"%statusPhrase%"];
1097 if (range.location != NSNotFound) {
1098 [inString safeReplaceCharactersInRange:range
1099 withString:[statusPhrase stringByEscapingForXMLWithEntities:nil]];
1100 replacedStatusPhrase = YES;
1102 } while (range.location != NSNotFound);
1105 //Message (must do last)
1106 range = [inString rangeOfString:@"%message%"];
1107 if (range.location != NSNotFound) {
1108 NSString *messageString;
1110 if (replacedStatusPhrase) {
1111 //If the status phrase was used, clear the message tag
1112 messageString = @"";
1114 messageString = [AIHTMLDecoder encodeHTML:[content message]
1117 includingColorTags:NO
1120 closeStyleTagsOnFontChange:YES
1123 imagesPath:NSTemporaryDirectory()
1124 attachmentsAsText:NO
1125 onlyIncludeOutgoingImages:NO
1128 allowJavascriptURLs:NO];
1131 [inString safeReplaceCharactersInRange:range withString:messageString];
1138 - (NSMutableString *)fillKeywordsForBaseTemplate:(NSMutableString *)inString chat:(AIChat *)chat
1142 [inString replaceKeyword:@"%chatName%"
1143 withString:[chat.displayName stringByEscapingForXMLWithEntities:nil]];
1145 NSString * sourceName = [chat.account.displayName stringByEscapingForXMLWithEntities:nil];
1146 if(!sourceName) sourceName = @" ";
1147 [inString replaceKeyword:@"%sourceName%"
1148 withString:sourceName];
1150 NSString *destinationName = chat.listObject.displayName;
1151 if (!destinationName) destinationName = chat.displayName;
1152 [inString replaceKeyword:@"%destinationName%"
1153 withString:destinationName];
1155 NSString *serversideDisplayName = chat.listObject.serversideDisplayName;
1156 if (!serversideDisplayName) serversideDisplayName = chat.displayName;
1157 [inString replaceKeyword:@"%destinationDisplayName%"
1158 withString:[serversideDisplayName stringByEscapingForXMLWithEntities:nil]];
1160 AIListContact *listObject = chat.listObject;
1161 NSString *iconPath = nil;
1163 [inString replaceKeyword:@"%incomingColor%"
1164 withString:[NSColor representedColorForObject:listObject.UID withValidColors:self.validSenderColors]];
1166 [inString replaceKeyword:@"%outgoingColor%"
1167 withString:[NSColor representedColorForObject:chat.account.UID withValidColors:self.validSenderColors]];
1170 iconPath = [listObject valueForProperty:KEY_WEBKIT_USER_ICON];
1172 iconPath = [listObject valueForProperty:@"UserIconPath"];
1174 /* We couldn't get an icon... but perhaps we can for a parent contact */
1176 [listObject isKindOfClass:[AIListContact class]] &&
1177 ([(AIListContact *)listObject parentContact] != listObject)) {
1178 iconPath = [[(AIListContact *)listObject parentContact] valueForProperty:KEY_WEBKIT_USER_ICON];
1180 iconPath = [[(AIListContact *)listObject parentContact] valueForProperty:@"UserIconPath"];
1183 [inString replaceKeyword:@"%incomingIconPath%"
1184 withString:(iconPath ? iconPath : @"incoming_icon.png")];
1186 AIListObject *account = chat.account;
1190 iconPath = [account valueForProperty:KEY_WEBKIT_USER_ICON];
1192 iconPath = [account valueForProperty:@"UserIconPath"];
1194 [inString replaceKeyword:@"%outgoingIconPath%"
1195 withString:(iconPath ? iconPath : @"outgoing_icon.png")];
1197 NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:account.service.serviceID
1198 type:AIServiceIconLarge];
1200 NSString *serviceIconTag = [NSString stringWithFormat:@"<img class=\"serviceIcon\" src=\"%@\" alt=\"%@\" title=\"%@\">", serviceIconPath ? serviceIconPath : @"outgoing_icon.png", [account.service shortDescription], [account.service shortDescription]];
1202 [inString replaceKeyword:@"%serviceIconImg%"
1203 withString:serviceIconTag];
1205 [inString replaceKeyword:@"%serviceIconPath%"
1206 withString:serviceIconPath];
1208 [inString replaceKeyword:@"%timeOpened%"
1209 withString:[timeStampFormatter stringFromDate:[chat dateOpened]]];
1211 //Replaces %time{x}% with a timestamp formatted like x (using NSDateFormatter)
1213 range = [inString rangeOfString:@"%timeOpened{"];
1214 if (range.location != NSNotFound) {
1216 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
1218 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
1219 NSString *timeFormat = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
1221 NSDateFormatter *dateFormatter;
1222 if ([timeFormat rangeOfString:@"%"].location != NSNotFound) {
1223 /* Support strftime-style format strings, which old message styles may use */
1224 dateFormatter = [[NSDateFormatter alloc] initWithDateFormat:timeFormat allowNaturalLanguage:NO];
1226 dateFormatter = [[NSDateFormatter alloc] init];
1227 [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
1228 [dateFormatter setDateFormat:timeFormat];
1231 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
1232 withString:[dateFormatter stringFromDate:[chat dateOpened]]];
1233 [dateFormatter release];
1237 } while (range.location != NSNotFound);
1239 [inString replaceKeyword:@"%dateOpened%"
1240 withString:[[NSDateFormatter localizedDateFormatter] stringFromDate:[chat dateOpened]]];
1244 range = [inString rangeOfString:@"==bodyBackground=="];
1246 if (range.location != NSNotFound) { //a backgroundImage tag is not required
1247 NSMutableString *bodyTag = nil;
1249 if (allowsCustomBackground && (customBackgroundPath || customBackgroundColor)) {
1250 bodyTag = [[[NSMutableString alloc] init] autorelease];
1252 if (customBackgroundPath) {
1253 if ([customBackgroundPath length]) {
1254 switch (customBackgroundType) {
1255 case BackgroundNormal:
1256 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: no-repeat; background-attachment:fixed;", customBackgroundPath]];
1258 case BackgroundCenter:
1259 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-position: center; background-repeat: no-repeat; background-attachment:fixed;", customBackgroundPath]];
1261 case BackgroundTile:
1262 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: repeat;", customBackgroundPath]];
1264 case BackgroundTileCenter:
1265 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: repeat; background-position: center;", customBackgroundPath]];
1267 case BackgroundScale:
1268 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); -webkit-background-size: 100%% 100%%; background-size: 100%% 100%%; background-attachment: fixed;", customBackgroundPath]];
1272 [bodyTag appendString:@"background-image: none; "];
1275 if (customBackgroundColor) {
1276 CGFloat red, green, blue, alpha;
1277 [customBackgroundColor getRed:&red green:&green blue:&blue alpha:&alpha];
1278 [bodyTag appendString:[NSString stringWithFormat:@"background-color: rgba(%ld, %ld, %ld, %f); ", (NSInteger)(red * 255.0), (NSInteger)(green * 255.0), (NSInteger)(blue * 255.0), alpha]];
1282 //Replace the body background tag
1283 [inString safeReplaceCharactersInRange:range withString:(bodyTag ? (NSString *)bodyTag : @"")];
1292 - (NSString *)iconPathForFileTransfer:(ESFileTransfer *)inObject
1294 NSString *filename = [NSString stringWithFormat:@"TEMP-%@%@.tiff", [inObject remoteFilename], [NSString randomStringOfLength:5]];
1295 return [[adium cachesPath] stringByAppendingPathComponent:filename];
1298 - (NSString *)statusIconPathForListObject:(AIListObject *)inObject
1300 if(!statusIconPathCache) statusIconPathCache = [[NSMutableDictionary alloc] init];
1301 NSImage *icon = [AIStatusIcons statusIconForListObject:inObject
1302 type:AIStatusIconTab
1303 direction:AIIconNormal];
1304 NSString *statusName = [AIStatusIcons statusNameForListObject:inObject];
1306 statusName = @"UnknownStatus";
1307 NSString *path = [statusIconPathCache objectForKey:statusName];
1310 path = [[adium cachesPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"TEMP-%@%@.tiff", statusName, [NSString randomStringOfLength:5]]];
1311 [[icon TIFFRepresentation] writeToFile:path atomically:YES];
1312 [statusIconPathCache setObject:path forKey:statusName];