Avoid a lot of false positives of the static analyzer.
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>
40 #define LEGACY_VERSION_THRESHOLD 3 //Styles older than this version are considered legacy
41 #define MAX_KNOWN_WEBKIT_VERSION 4 //Styles newer than this version are unknown entities
44 #define KEY_WEBKIT_VERSION @"MessageViewVersion"
45 #define KEY_WEBKIT_VERSION_MIN @"MessageViewVersion_MinimumCompatible"
47 //BOM scripts for appending content.
48 #define APPEND_MESSAGE_WITH_SCROLL @"checkIfScrollToBottomIsNeeded(); appendMessage(\"%@\"); scrollToBottomIfNeeded();"
49 #define APPEND_NEXT_MESSAGE_WITH_SCROLL @"checkIfScrollToBottomIsNeeded(); appendNextMessage(\"%@\"); scrollToBottomIfNeeded();"
50 #define APPEND_MESSAGE @"appendMessage(\"%@\");"
51 #define APPEND_NEXT_MESSAGE @"appendNextMessage(\"%@\");"
52 #define APPEND_MESSAGE_NO_SCROLL @"appendMessageNoScroll(\"%@\");"
53 #define APPEND_NEXT_MESSAGE_NO_SCROLL @"appendNextMessageNoScroll(\"%@\");"
54 #define REPLACE_LAST_MESSAGE @"replaceLastMessage(\"%@\");"
56 #define TOPIC_MAIN_DIV @"<div id=\"topic\"></div>"
57 // 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.
58 #define TOPIC_INDIVIDUAL_WRAPPER @"<span id=\"topicEdit\" ondblclick=\"this.setAttribute('contentEditable', true); this.focus();\">%@</span>"
60 @interface NSString (NewSnowLeopardMethods)
61 - (NSComparisonResult)localizedStandardCompare:(NSString *)string;
64 @interface NSMutableString (AIKeywordReplacementAdditions)
65 - (void) replaceKeyword:(NSString *)word withString:(NSString *)newWord;
66 - (void) safeReplaceCharactersInRange:(NSRange)range withString:(NSString *)newWord;
69 @implementation NSMutableString (AIKeywordReplacementAdditions)
70 - (void) replaceKeyword:(NSString *)keyWord withString:(NSString *)newWord
73 if(!newWord) newWord = @"";
74 [self replaceOccurrencesOfString:keyWord
76 options:NSLiteralSearch
77 range:NSMakeRange(0.0f, [self length])];
80 - (void) safeReplaceCharactersInRange:(NSRange)range withString:(NSString *)newWord
82 if (range.location == NSNotFound || range.length == 0) return;
83 if (!newWord) [self deleteCharactersInRange:range];
84 else [self replaceCharactersInRange:range withString:newWord];
88 //The old code built the paths itself, which follows the filesystem's case sensitivity, so some noobs named stuff wrong.
89 //NSBundle is always case sensitive, so those styles broke (they were already broken on case sensitive hfsx)
90 //These methods only check for the all-lowercase variant, so are not suitable for general purpose use.
91 @interface NSBundle (StupidCompatibilityHack)
92 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type;
93 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type inDirectory:(NSString *)dirpath;
96 @implementation NSBundle (StupidCompatibilityHack)
97 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type
99 NSString *path = [self pathForResource:res ofType:type];
101 path = [self pathForResource:[res lowercaseString] ofType:type];
105 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type inDirectory:(NSString *)dirpath
107 NSString *path = [self pathForResource:res ofType:type inDirectory:dirpath];
109 path = [self pathForResource:[res lowercaseString] ofType:type inDirectory:dirpath];
115 @interface AIWebkitMessageViewStyle ()
116 - (id)initWithBundle:(NSBundle *)inBundle;
117 - (void)_loadTemplates;
118 - (void)releaseResources;
119 - (NSMutableString *)_escapeStringForPassingToScript:(NSMutableString *)inString;
120 - (NSString *)noVariantName;
121 - (NSString *)iconPathForFileTransfer:(ESFileTransfer *)inObject;
122 - (NSString *)statusIconPathForListObject:(AIListObject *)inObject;
125 @implementation AIWebkitMessageViewStyle
127 @synthesize activeVariant;
129 + (id)messageViewStyleFromBundle:(NSBundle *)inBundle
131 return [[[self alloc] initWithBundle:inBundle] autorelease];
134 + (id)messageViewStyleFromPath:(NSString *)path
136 NSBundle *styleBundle = [NSBundle bundleWithPath:[path stringByExpandingBundlePath]];
138 return [[[self alloc] initWithBundle:styleBundle] autorelease];
145 - (id)initWithBundle:(NSBundle *)inBundle
147 if ((self = [super init])) {
148 styleBundle = [inBundle retain];
149 stylePath = [[styleBundle resourcePath] retain];
151 if ([self reloadStyle] == FALSE) {
162 [self releaseResources];
164 /* Our styles are versioned so we can change how they work without breaking compatibility.
166 * Version 0: Initial Webkit Version
167 * Version 1: Template.html now handles all scroll-to-bottom functionality. It is no longer required to call the
168 * scrollToBottom functions when inserting content.
169 * Version 2: No significant changes
170 * Version 3: main.css is no longer a separate style, it now serves as the base stylesheet and is imported by default.
171 * The default variant is now a separate file in /variants like all other variants.
172 * Template.html now includes appendMessageNoScroll() and appendNextMessageNoScroll() which behave
173 * the same as appendMessage() and appendNextMessage() in Versions 1 and 2 but without scrolling.
174 * Version 4: Template.html now includes replaceLastMessage()
175 * Template.html now defines actionMessageUserName and actionMessageBody for display of /me (actions).
176 * If the style provides a custom Template.html, these classes must be defined.
177 * CSS can be used to customize the appearance of actions.
178 * HTML filters in are now supported in Adium's content filter system; filters can assume Version 4 or later.
180 styleVersion = [[styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION] integerValue];
182 /* Refuse to load a version whose minimum compatible version is greater than the latest version we know about; that
183 * indicates this is a style FROM THE FUTURE, and we can't risk corrupting our own timeline.
185 NSInteger minimumCompatibleVersion = [[styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION_MIN] integerValue];
186 if (minimumCompatibleVersion && (minimumCompatibleVersion > MAX_KNOWN_WEBKIT_VERSION)) {
191 allowTextBackgrounds = YES;
193 //Pre-fetch our templates
194 [self _loadTemplates];
197 allowsCustomBackground = ![[styleBundle objectForInfoDictionaryKey:@"DisableCustomBackground"] boolValue];
198 transparentDefaultBackground = [[styleBundle objectForInfoDictionaryKey:@"DefaultBackgroundIsTransparent"] boolValue];
200 combineConsecutive = ![[styleBundle objectForInfoDictionaryKey:@"DisableCombineConsecutive"] boolValue];
202 NSNumber *tmpNum = [styleBundle objectForInfoDictionaryKey:@"ShowsUserIcons"];
203 allowsUserIcons = (tmpNum ? [tmpNum boolValue] : YES);
206 NSString *tmpName = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_USER_ICON_MASK];
207 if (tmpName) userIconMask = [[NSImage alloc] initWithContentsOfFile:[stylePath stringByAppendingPathComponent:tmpName]];
209 NSNumber *allowsColorsNumber = [styleBundle objectForInfoDictionaryKey:@"AllowTextColors"];
210 allowsColors = (allowsColorsNumber ? [allowsColorsNumber boolValue] : YES);
216 * @brief release everything we loaded from the style bundle
218 - (void)releaseResources
221 [headerHTML release];
222 [footerHTML release];
224 [contentHTML release];
225 [contentInHTML release];
226 [nextContentInHTML release];
227 [contextInHTML release];
228 [nextContextInHTML release];
229 [contentOutHTML release];
230 [nextContentOutHTML release];
231 [contextOutHTML release];
232 [nextContextOutHTML release];
233 [statusHTML release];
234 [fileTransferHTML release];
237 [customBackgroundPath release];
238 [customBackgroundColor release];
240 [userIconMask release];
248 [styleBundle release];
251 [self releaseResources];
252 [timeStampFormatter release];
254 [[NSDistributedNotificationCenter defaultCenter] removeObserver: self];
256 [statusIconPathCache release];
257 [timeFormatterCache release];
259 self.activeVariant = nil;
264 @synthesize bundle = styleBundle;
268 return styleVersion < LEGACY_VERSION_THRESHOLD;
271 #pragma mark Settings
273 @synthesize allowsCustomBackground, allowsUserIcons, allowsColors, userIconMask;
275 - (NSArray *)validSenderColors
277 if(!checkedSenderColors) {
278 NSURL *url = [NSURL fileURLWithPath:[stylePath stringByAppendingPathComponent:@"Incoming/SenderColors.txt"]];
279 NSString *senderColorsFile = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:NULL];
282 validSenderColors = [[senderColorsFile componentsSeparatedByString:@":"] retain];
284 checkedSenderColors = YES;
287 return validSenderColors;
290 - (BOOL)isBackgroundTransparent
292 //Our custom background is only transparent if the user has set a custom color with an alpha component less than 1.0
293 return ((!customBackgroundColor && transparentDefaultBackground) ||
294 (customBackgroundColor && [customBackgroundColor alphaComponent] < 0.99));
297 - (NSString *)defaultFontFamily
299 NSString *defaultFontFamily = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_DEFAULT_FONT_FAMILY];
300 if (!defaultFontFamily) defaultFontFamily = [[NSFont systemFontOfSize:0] familyName];
302 return defaultFontFamily;
305 - (NSNumber *)defaultFontSize
307 NSNumber *defaultFontSize = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_DEFAULT_FONT_SIZE];
309 if (!defaultFontSize) defaultFontSize = [NSNumber numberWithInteger:[[NSFont systemFontOfSize:0] pointSize]];
311 return defaultFontSize;
316 return headerHTML && [headerHTML length];
321 return topicHTML && [topicHTML length];
324 #pragma mark Behavior
326 - (void)setDateFormat:(NSString *)format
328 if (!format || [format length] == 0) {
329 format = [NSDateFormatter localizedDateFormatStringShowingSeconds:NO showingAMorPM:NO];
332 [timeStampFormatter release];
334 if ([format rangeOfString:@"%"].location != NSNotFound) {
335 /* Support strftime-style format strings, which old message styles may use */
336 timeStampFormatter = [[NSDateFormatter alloc] initWithDateFormat:format allowNaturalLanguage:NO];
338 timeStampFormatter = [[NSDateFormatter alloc] init];
339 [timeStampFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
340 [timeStampFormatter setDateFormat:format];
344 - (void) flushTimeFormatterCache:(id)dummy {
345 [timeFormatterCache removeAllObjects];
348 @synthesize allowTextBackgrounds, customBackgroundType, customBackgroundColor, showIncomingMessageColors=showIncomingColors, showIncomingMessageFonts=showIncomingFonts, customBackgroundPath, nameFormat, useCustomNameFormat, showHeader, showUserIcons;
350 //Templates ------------------------------------------------------------------------------------------------------------
351 #pragma mark Templates
352 - (NSString *)baseTemplateForChat:(AIChat *)chat
354 NSMutableString *templateHTML;
356 // If this is a group chat, we want to include a topic.
357 // Otherwise, if the header is shown, use it.
358 NSString *headerContent = @"";
360 if (chat.isGroupChat) {
361 headerContent = (chat.supportsTopic ? TOPIC_MAIN_DIV : @"");
362 } else if (headerHTML) {
363 headerContent = headerHTML;
367 //Old styles may be using an old custom 4 parameter baseHTML. Styles version 3 and higher should
368 //be using the bundled (or a custom) 5 parameter baseHTML.
369 if ((styleVersion < 3) && usingCustomTemplateHTML) {
370 templateHTML = [NSMutableString stringWithFormat:baseHTML, //Template
371 [[NSURL fileURLWithPath:stylePath] absoluteString], //Base path
372 [self pathForVariant:self.activeVariant], //Variant path
374 (footerHTML ? footerHTML : @"")];
376 templateHTML = [NSMutableString stringWithFormat:baseHTML, //Template
377 [[NSURL fileURLWithPath:stylePath] absoluteString], //Base path
378 styleVersion < 3 ? @"" : @"@import url( \"main.css\" );", //Import main.css for new enough styles
379 [self pathForVariant:self.activeVariant], //Variant path
381 (footerHTML ? footerHTML : @"")];
384 return [self fillKeywordsForBaseTemplate:templateHTML chat:chat];
387 - (NSString *)templateForContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
391 //Get the correct template for what we're inserting
392 if ([[content type] isEqualToString:CONTENT_MESSAGE_TYPE]) {
393 if ([content isOutgoing]) {
394 template = (contentIsSimilar ? nextContentOutHTML : contentOutHTML);
396 template = (contentIsSimilar ? nextContentInHTML : contentInHTML);
399 } else if ([[content type] isEqualToString:CONTENT_CONTEXT_TYPE]) {
400 if ([content isOutgoing]) {
401 template = (contentIsSimilar ? nextContextOutHTML : contextOutHTML);
403 template = (contentIsSimilar ? nextContextInHTML : contextInHTML);
406 } else if([[content type] isEqualToString:CONTENT_FILE_TRANSFER_TYPE]) {
407 template = [[fileTransferHTML mutableCopy] autorelease];
408 } else if ([[content type] isEqualToString:CONTENT_TOPIC_TYPE]) {
409 template = topicHTML;
412 template = statusHTML;
418 - (NSString *)completedTemplateForContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
420 NSMutableString *mutableTemplate = [[self templateForContent:content similar:contentIsSimilar] mutableCopy];
423 [self fillKeywords:mutableTemplate forContent:content similar:contentIsSimilar];
425 return [mutableTemplate autorelease];
429 * @brief Pre-fetch all the style templates
431 * This needs to be called before either baseTemplate or templateForContent is called
433 - (void)_loadTemplates
435 //Load the style's templates
436 //We can't use NSString's initWithContentsOfFile here. HTML files are interpreted in the defaultCEncoding
437 //(which varies by system) when read that way. We want to always interpret the files as UTF8.
438 headerHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Header" ofType:@"html"]] retain];
439 footerHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Footer" ofType:@"html"]] retain];
440 topicHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Topic" ofType:@"html"]] retain];
441 baseHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Template" ofType:@"html"]];
443 //Starting with version 1, styles can choose to not include template.html. If the template is not included
444 //Adium's default will be used. This is preferred since any future template updates will apply to the style
445 if ((!baseHTML || [baseHTML length] == 0) && styleVersion >= 1) {
446 baseHTML = [NSString stringWithContentsOfUTF8File:[[NSBundle bundleForClass:[self class]] semiCaseInsensitivePathForResource:@"Template" ofType:@"html"]];
447 usingCustomTemplateHTML = NO;
449 usingCustomTemplateHTML = YES;
451 NSAssert(baseHTML != nil, @"The impossible happened!");
453 if ([baseHTML rangeOfString:@"function imageCheck()" options:NSLiteralSearch].location != NSNotFound) {
454 /* This doesn't quite fix image swapping on styles with broken image swapping due to custom HTML templates,
455 * but it improves it. For some reason, the result of using our normal template.html functions is that
456 * clicking works once, then the text doesn't allow a return click. This is an improvement compared
457 * to fully broken behavior in which the return click shows a missing-image placeholder.
459 NSMutableString *imageSwapFixedBaseHTML = [[baseHTML mutableCopy] autorelease];
460 [imageSwapFixedBaseHTML replaceOccurrencesOfString:
461 @" function imageCheck() {\n"
462 " node = event.target;\n"
463 " if(node.tagName == 'IMG' && node.alt) {\n"
464 " a = document.createElement('a');\n"
465 " a.setAttribute('onclick', 'imageSwap(this)');\n"
466 " a.setAttribute('src', node.src);\n"
467 " text = document.createTextNode(node.alt);\n"
468 " a.appendChild(text);\n"
469 " node.parentNode.replaceChild(a, node);\n"
473 @" function imageCheck() {\n"
474 " var node = event.target;\n"
475 " if(node.tagName.toLowerCase() == 'img' && !client.zoomImage(node) && node.alt) {\n"
476 " var a = document.createElement('a');\n"
477 " a.setAttribute('onclick', 'imageSwap(this)');\n"
478 " a.setAttribute('src', node.getAttribute('src'));\n"
479 " a.className = node.className;\n"
480 " var text = document.createTextNode(node.alt);\n"
481 " a.appendChild(text);\n"
482 " node.parentNode.replaceChild(a, node);\n"
485 options:NSLiteralSearch];
486 [imageSwapFixedBaseHTML replaceOccurrencesOfString:
487 @" function imageSwap(node) {\n"
488 " img = document.createElement('img');\n"
489 " img.setAttribute('src', node.src);\n"
490 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
491 " node.parentNode.replaceChild(img, node);\n"
495 @" function imageSwap(node) {\n"
496 " var shouldScroll = nearBottom();\n"
497 " //Swap the image/text\n"
498 " var img = document.createElement('img');\n"
499 " img.setAttribute('src', node.getAttribute('src'));\n"
500 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
501 " img.className = node.className;\n"
502 " node.parentNode.replaceChild(img, node);\n"
504 " alignChat(shouldScroll);\n"
506 options:NSLiteralSearch];
507 /* Now for ones which don't call alignChat() */
508 [imageSwapFixedBaseHTML replaceOccurrencesOfString:
509 @" function imageSwap(node) {\n"
510 " img = document.createElement('img');\n"
511 " img.setAttribute('src', node.src);\n"
512 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
513 " node.parentNode.replaceChild(img, node);\n"
516 @" function imageSwap(node) {\n"
517 " var shouldScroll = nearBottom();\n"
518 " //Swap the image/text\n"
519 " var img = document.createElement('img');\n"
520 " img.setAttribute('src', node.getAttribute('src'));\n"
521 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
522 " img.className = node.className;\n"
523 " node.parentNode.replaceChild(img, node);\n"
525 options:NSLiteralSearch];
526 baseHTML = imageSwapFixedBaseHTML;
533 contentHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html"]] retain];
534 contentInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html" inDirectory:@"Incoming"]] retain];
535 nextContentInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContent" ofType:@"html" inDirectory:@"Incoming"]] retain];
536 contentOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html" inDirectory:@"Outgoing"]] retain];
537 nextContentOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContent" ofType:@"html" inDirectory:@"Outgoing"]] retain];
540 contextInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Context" ofType:@"html" inDirectory:@"Incoming"]] retain];
541 nextContextInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContext" ofType:@"html" inDirectory:@"Incoming"]] retain];
542 contextOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Context" ofType:@"html" inDirectory:@"Outgoing"]] retain];
543 nextContextOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContext" ofType:@"html" inDirectory:@"Outgoing"]] retain];
545 //Fall back to Resources/Content.html if Incoming isn't present
546 if (!contentInHTML) contentInHTML = [contentHTML retain];
548 //Fall back to Content if NextContent doesn't need to use different HTML
549 if (!nextContentInHTML) nextContentInHTML = [contentInHTML retain];
551 //Fall back to Content if Context isn't present
552 if (!nextContextInHTML) nextContextInHTML = [nextContentInHTML retain];
553 if (!contextInHTML) contextInHTML = [contentInHTML retain];
555 //Fall back to Content if Context isn't present
556 if (!nextContextOutHTML && nextContentOutHTML) nextContextOutHTML = [nextContentOutHTML retain];
557 if (!contextOutHTML && contentOutHTML) contextOutHTML = [contentOutHTML retain];
559 //Fall back to Content if Context isn't present
560 if (!nextContextOutHTML) nextContextOutHTML = [nextContextInHTML retain];
561 if (!contextOutHTML) contextOutHTML = [contextInHTML retain];
563 //Fall back to Incoming if Outgoing doesn't need to be different
564 if (!contentOutHTML) contentOutHTML = [contentInHTML retain];
565 if (!nextContentOutHTML) nextContentOutHTML = [nextContentInHTML retain];
568 statusHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Status" ofType:@"html"]] retain];
570 //Fall back to Resources/Incoming/Content.html if Status isn't present
571 if (!statusHTML) statusHTML = [contentInHTML retain];
573 //TODO: make a generic Request message, rather than having this ft specific one
574 NSMutableString *fileTransferHTMLTemplate;
575 fileTransferHTMLTemplate = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"FileTransferRequest" ofType:@"html"]] mutableCopy];
576 if(!fileTransferHTMLTemplate) {
577 fileTransferHTMLTemplate = [contentInHTML mutableCopy];
578 [fileTransferHTMLTemplate replaceKeyword:@"%message%"
579 withString:@"<p><img src=\"%fileIconPath%\" style=\"width:32px; height:32px; vertical-align:middle;\"></img><input type=\"button\" onclick=\"%saveFileAsHandler%\" value=\"Download %fileName%\"></p>"];
581 [fileTransferHTMLTemplate replaceKeyword:@"Download %fileName%"
582 withString:[NSString stringWithFormat:AILocalizedString(@"Download %@", "%@ will be a file name"), @"%fileName%"]];
583 fileTransferHTML = fileTransferHTMLTemplate;
587 - (NSString *)scriptForAppendingContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
589 NSMutableString *newHTML;
592 //If combining of consecutive messages has been disabled, we treat all content as non-similar
593 if (!combineConsecutive) contentIsSimilar = NO;
595 //Fetch the correct template and substitute keywords for the passed content
596 newHTML = [[[self completedTemplateForContent:content similar:contentIsSimilar] mutableCopy] autorelease];
598 //BOM scripts vary by style version
599 if (!usingCustomTemplateHTML && styleVersion >= 4) {
600 /* If we're using the built-in template HTML, we know that it supports our most modern scripts */
601 if (replaceLastContent)
602 script = REPLACE_LAST_MESSAGE;
603 else if (willAddMoreContentObjects) {
604 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_NO_SCROLL : APPEND_MESSAGE_NO_SCROLL);
606 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
609 } else if (styleVersion >= 3) {
610 if (willAddMoreContentObjects) {
611 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_NO_SCROLL : APPEND_MESSAGE_NO_SCROLL);
613 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
615 } else if (styleVersion >= 1) {
616 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
619 if (usingCustomTemplateHTML && [content isKindOfClass:[AIContentStatus class]]) {
620 /* Old styles with a custom template.html had Status.html files without 'insert' divs coupled
621 * with a APPEND_NEXT_MESSAGE_WITH_SCROLL script which assumes one exists.
623 script = APPEND_MESSAGE_WITH_SCROLL;
625 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_WITH_SCROLL : APPEND_MESSAGE_WITH_SCROLL);
629 return [NSString stringWithFormat:script, [self _escapeStringForPassingToScript:newHTML]];
632 - (NSString *)scriptForChangingVariant
634 return [NSString stringWithFormat:@"setStylesheet(\"mainStyle\",\"%@\");",[self pathForVariant:self.activeVariant]];
637 - (NSString *)scriptForScrollingAfterAddingMultipleContentObjects
639 if ((styleVersion >= 3) || !usingCustomTemplateHTML) {
640 return @"alignChat(true);";
647 * @brief Escape a string for passing to our BOM scripts
649 - (NSMutableString *)_escapeStringForPassingToScript:(NSMutableString *)inString
651 //We need to escape a few things to get our string to the javascript without trouble
652 [inString replaceOccurrencesOfString:@"\\"
654 options:NSLiteralSearch];
656 [inString replaceOccurrencesOfString:@"\""
658 options:NSLiteralSearch];
660 [inString replaceOccurrencesOfString:@"\n"
662 options:NSLiteralSearch];
664 [inString replaceOccurrencesOfString:@"\r"
666 options:NSLiteralSearch];
671 #pragma mark Variants
673 - (NSArray *)availableVariants
675 NSMutableArray *availableVariants = [NSMutableArray array];
677 //Build an array of all variant names
678 for (NSString *path in [styleBundle pathsForResourcesOfType:@"css" inDirectory:@"Variants"]) {
679 [availableVariants addObject:[[path lastPathComponent] stringByDeletingPathExtension]];
682 //Style versions before 3 stored the default variant in a separate location. They also allowed for this
683 //varient name to not be specified, and would substitute a localized string in its place.
684 if (styleVersion < 3) {
685 [availableVariants addObject:[self noVariantName]];
688 //Alphabetize the variants
689 [availableVariants sortUsingSelector:@selector(localizedStandardCompare:)];
691 return availableVariants;
694 - (NSString *)pathForVariant:(NSString *)variant
696 //Styles before version 3 stored the default variant in main.css, and not in the variants folder.
697 if (styleVersion < 3 && [variant isEqualToString:[self noVariantName]]) {
700 return [NSString stringWithFormat:@"Variants/%@.css",variant];
705 * @brief Base variant name for styles before version 2
707 - (NSString *)noVariantName
709 NSString *noVariantName = [styleBundle objectForInfoDictionaryKey:@"DisplayNameForNoVariant"];
710 return noVariantName ? noVariantName : AILocalizedString(@"Normal","Normal style variant menu item");
713 + (NSString *)noVariantNameForBundle:(NSBundle *)inBundle
715 NSString *noVariantName = [inBundle objectForInfoDictionaryKey:@"DisplayNameForNoVariant"];
716 return noVariantName ? noVariantName : AILocalizedString(@"Normal","Normal style variant menu item");
719 - (NSString *)defaultVariant
721 return styleVersion < 3 ? [self noVariantName] : [styleBundle objectForInfoDictionaryKey:@"DefaultVariant"];
724 + (NSString *)defaultVariantForBundle:(NSBundle *)inBundle
726 return [[inBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION] integerValue] < 3 ?
727 [self noVariantNameForBundle:inBundle] :
728 [inBundle objectForInfoDictionaryKey:@"DefaultVariant"];
731 #pragma mark Keyword replacement
733 - (NSMutableString *)fillKeywords:(NSMutableString *)inString forContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
737 AIListObject *contentSource = [content source];
738 AIListObject *theSource = ([contentSource isKindOfClass:[AIListContact class]] ?
739 [(AIListContact *)contentSource parentContact] :
743 htmlEncodedMessage is only encoded correctly for AIContentMessages
744 but we do it up here so that we can check for RTL/LTR text below without
745 having to encode the message twice. This is less than ideal
747 NSString *htmlEncodedMessage = [AIHTMLDecoder encodeHTML:[content message]
749 fontTags:showIncomingFonts
750 includingColorTags:(allowsColors && showIncomingColors)
753 closeStyleTagsOnFontChange:YES
756 imagesPath:NSTemporaryDirectory()
758 onlyIncludeOutgoingImages:NO
761 allowJavascriptURLs:NO];
763 if (styleVersion >= 4)
764 htmlEncodedMessage = [adium.contentController filterHTMLString:htmlEncodedMessage
765 direction:[content isOutgoing] ? AIFilterOutgoing : AIFilterIncoming
769 if ([content respondsToSelector:@selector(date)])
770 date = [(AIContentMessage *)content date];
772 //Replacements applicable to any AIContentObject
773 [inString replaceKeyword:@"%time%"
774 withString:(date ? [timeStampFormatter stringFromDate:date] : @"")];
776 __block NSString *shortTimeString;
777 [NSDateFormatter withLocalizedDateFormatterShowingSeconds:NO showingAMorPM:NO perform:^(NSDateFormatter *dateFormatter){
778 shortTimeString = (date ? [[dateFormatter stringFromDate:date] retain] : @"");
780 [shortTimeString autorelease];
782 [inString replaceKeyword:@"%shortTime%"
783 withString:shortTimeString];
785 if ([inString rangeOfString:@"%senderStatusIcon%"].location != NSNotFound) {
786 //Only cache the status icon to disk if the message style will actually use it
787 [inString replaceKeyword:@"%senderStatusIcon%"
788 withString:[self statusIconPathForListObject:theSource]];
791 //Replaces %localized{x}% with a a localized version of x, searching the style's localizations, and then Adium's localizations
793 range = [inString rangeOfString:@"%localized{"];
794 if (range.location != NSNotFound) {
796 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
797 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
798 NSString *untranslated = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
800 NSString *translated = [styleBundle localizedStringForKey:untranslated
803 if (!translated || [translated length] == 0) {
804 translated = [[NSBundle bundleForClass:[self class]] localizedStringForKey:untranslated
807 if (!translated || [translated length] == 0) {
808 translated = [[NSBundle mainBundle] localizedStringForKey:untranslated
815 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
816 withString:translated];
819 } while (range.location != NSNotFound);
821 [inString replaceKeyword:@"%userIcons%"
822 withString:(showUserIcons ? @"showIcons" : @"hideIcons")];
824 [inString replaceKeyword:@"%messageClasses%"
825 withString:[(contentIsSimilar ? @"consecutive " : @"") stringByAppendingString:[[content displayClasses] componentsJoinedByString:@" "]]];
827 [inString replaceKeyword:@"%senderColor%"
828 withString:[NSColor representedColorForObject:contentSource.UID withValidColors:self.validSenderColors]];
830 //HAX. The odd conditional here detects the rtl html that our html parser spits out.
831 BOOL isRTL = ([htmlEncodedMessage rangeOfString:@"<div dir=\"rtl\">"
832 options:(NSCaseInsensitiveSearch | NSLiteralSearch)].location != NSNotFound);
833 [inString replaceKeyword:@"%messageDirection%"
834 withString:(isRTL ? @"rtl" : @"ltr")];
836 //Replaces %time{x}% with a timestamp formatted like x (using NSDateFormatter)
838 range = [inString rangeOfString:@"%time{"];
839 if (range.location != NSNotFound) {
841 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
842 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
844 if (!timeFormatterCache) {
845 timeFormatterCache = [[NSMutableDictionary alloc] init];
846 [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(flushTimeFormatterCache:) name:@"AppleDatePreferencesChangedNotification" object:nil];
847 [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(flushTimeFormatterCache:) name:@"AppleTimePreferencesChangedNotification" object:nil];
849 NSString *timeFormat = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
851 NSDateFormatter *dateFormatter = [timeFormatterCache objectForKey:timeFormat];
852 if (!dateFormatter) {
853 if ([timeFormat rangeOfString:@"%"].location != NSNotFound) {
854 /* Support strftime-style format strings, which old message styles may use */
855 dateFormatter = [[NSDateFormatter alloc] initWithDateFormat:timeFormat allowNaturalLanguage:NO];
857 dateFormatter = [[NSDateFormatter alloc] init];
858 [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
859 [dateFormatter setDateFormat:timeFormat];
861 [timeFormatterCache setObject:dateFormatter forKey:timeFormat];
862 [dateFormatter release];
865 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
866 withString:[dateFormatter stringFromDate:date]];
869 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
873 } while (range.location != NSNotFound);
876 range = [inString rangeOfString:@"%userIconPath%"];
877 if (range.location != NSNotFound) {
878 NSString *userIconPath;
879 NSString *replacementString;
881 userIconPath = [theSource valueForProperty:KEY_WEBKIT_USER_ICON];
883 userIconPath = [theSource valueForProperty:@"UserIconPath"];
886 if (showUserIcons && userIconPath) {
887 replacementString = [NSString stringWithFormat:@"file://%@", userIconPath];
890 replacementString = ([content isOutgoing]
891 ? @"Outgoing/buddy_icon.png"
892 : @"Incoming/buddy_icon.png");
895 [inString safeReplaceCharactersInRange:range withString:replacementString];
897 } while (range.location != NSNotFound);
899 [inString replaceKeyword:@"%service%"
900 withString:[content.chat.account.service shortDescription]];
902 [inString replaceKeyword:@"%serviceIconPath%"
903 withString:[AIServiceIcons pathForServiceIconForServiceID:content.chat.account.service.serviceID
904 type:AIServiceIconLarge]];
906 if ([inString rangeOfString:@"%variant%"].location != NSNotFound) {
907 /* Per #12702, don't allow spaces in the variant name, as otherwise it becomes multiple css classes */
908 [inString replaceKeyword:@"%variant%"
909 withString:[self.activeVariant stringByReplacingOccurrencesOfString:@" " withString:@"_"]];
913 if ([content isKindOfClass:[AIContentMessage class]]) {
915 //Use [content source] directly rather than the potentially-metaContact theSource
916 NSString *formattedUID = nil;
917 if ([content.chat aliasForContact:contentSource]) {
918 formattedUID = [content.chat aliasForContact:contentSource];
920 formattedUID = contentSource.formattedUID;
923 NSString *displayName = [content.chat displayNameForContact:contentSource];
925 [inString replaceKeyword:@"%status%"
928 [inString replaceKeyword:@"%senderScreenName%"
929 withString:[(formattedUID ?
931 displayName) stringByEscapingForXMLWithEntities:nil]];
934 [inString replaceKeyword:@"%senderPrefix%"
935 withString:((AIContentMessage *)content).senderPrefix];
938 range = [inString rangeOfString:@"%sender%"];
939 if (range.location != NSNotFound) {
940 NSString *senderDisplay = nil;
941 if (useCustomNameFormat) {
942 if (formattedUID && ![displayName isEqualToString:formattedUID]) {
943 switch (nameFormat) {
948 senderDisplay = displayName;
951 case AIDisplayName_ScreenName:
952 senderDisplay = [NSString stringWithFormat:@"%@ (%@)",displayName,formattedUID];
955 case AIScreenName_DisplayName:
956 senderDisplay = [NSString stringWithFormat:@"%@ (%@)",formattedUID,displayName];
960 senderDisplay = formattedUID;
965 //Test both displayName and formattedUID for nil-ness. If they're both nil, the assertion will trip.
966 if (!senderDisplay) {
967 senderDisplay = displayName;
968 if (!senderDisplay) {
969 senderDisplay = formattedUID;
970 if (!senderDisplay) {
971 AILog(@"XXX we don't have a sender for %@ (%@)", content, [content message]);
972 NSLog(@"Enormous error: we don't have a sender for %@ (%@)", content, [content message]);
974 // This shouldn't happen.
975 senderDisplay = @"(unknown)";
980 senderDisplay = displayName;
983 if ([(AIContentMessage *)content isAutoreply]) {
984 senderDisplay = [NSString stringWithFormat:@"%@ %@",senderDisplay,AILocalizedString(@"(Autoreply)","Short word inserted after the sender's name when displaying a message which was an autoresponse")];
987 [inString safeReplaceCharactersInRange:range withString:[senderDisplay stringByEscapingForXMLWithEntities:nil]];
989 } while (range.location != NSNotFound);
992 range = [inString rangeOfString:@"%senderDisplayName%"];
993 if (range.location != NSNotFound) {
994 NSString *serversideDisplayName = ([theSource isKindOfClass:[AIListContact class]] ?
995 [(AIListContact *)theSource serversideDisplayName] :
997 if (!serversideDisplayName) {
998 serversideDisplayName = theSource.displayName;
1001 [inString safeReplaceCharactersInRange:range
1002 withString:[serversideDisplayName stringByEscapingForXMLWithEntities:nil]];
1004 } while (range.location != NSNotFound);
1006 //Blatantly stealing the date code for the background color script.
1008 range = [inString rangeOfString:@"%textbackgroundcolor{"];
1009 if (range.location != NSNotFound) {
1011 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
1012 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
1013 NSString *transparency = [inString substringWithRange:NSMakeRange(NSMaxRange(range),
1014 (endRange.location - NSMaxRange(range)))];
1016 if (allowTextBackgrounds && showIncomingColors) {
1017 NSString *thisIsATemporaryString;
1018 unsigned rgb = 0, red, green, blue;
1020 thisIsATemporaryString = [AIHTMLDecoder encodeHTML:[content message] headers:NO
1022 includingColorTags:NO
1025 closeStyleTagsOnFontChange:NO
1028 imagesPath:NSTemporaryDirectory()
1029 attachmentsAsText:NO
1030 onlyIncludeOutgoingImages:NO
1033 allowJavascriptURLs:NO];
1034 hexcode = [NSScanner scannerWithString:thisIsATemporaryString];
1035 [hexcode scanHexInt:&rgb];
1036 if (![thisIsATemporaryString length] && rgb == 0) {
1037 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
1039 red = (rgb & 0xff0000) >> 16;
1040 green = (rgb & 0x00ff00) >> 8;
1041 blue = rgb & 0x0000ff;
1042 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
1043 withString:[NSString stringWithFormat:@"rgba(%d, %d, %d, %@)", red, green, blue, transparency]];
1046 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
1048 } else if (endRange.location == NSMaxRange(range)) {
1049 if (allowTextBackgrounds && showIncomingColors) {
1050 NSString *thisIsATemporaryString;
1052 thisIsATemporaryString = [AIHTMLDecoder encodeHTML:[content message] headers:NO
1054 includingColorTags:NO
1057 closeStyleTagsOnFontChange:NO
1060 imagesPath:NSTemporaryDirectory()
1061 attachmentsAsText:NO
1062 onlyIncludeOutgoingImages:NO
1065 allowJavascriptURLs:NO];
1066 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
1067 withString:[NSString stringWithFormat:@"#%@", thisIsATemporaryString]];
1069 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
1073 } while (range.location != NSNotFound);
1075 if ([content isKindOfClass:[ESFileTransfer class]]) { //file transfers are an AIContentMessage subclass
1077 ESFileTransfer *transfer = (ESFileTransfer *)content;
1078 NSString *fileName = [[transfer remoteFilename] stringByEscapingForXMLWithEntities:nil];
1079 NSString *fileTransferID = [[transfer uniqueID] stringByEscapingForXMLWithEntities:nil];
1081 range = [inString rangeOfString:@"%fileIconPath%"];
1082 if (range.location != NSNotFound) {
1083 NSString *iconPath = [self iconPathForFileTransfer:transfer];
1084 NSImage *icon = [transfer iconImage];
1086 [[icon TIFFRepresentation] writeToFile:iconPath atomically:YES];
1087 [inString safeReplaceCharactersInRange:range withString:iconPath];
1088 range = [inString rangeOfString:@"%fileIconPath%"];
1089 } while (range.location != NSNotFound);
1092 [inString replaceKeyword:@"%fileName%"
1093 withString:fileName];
1095 [inString replaceKeyword:@"%saveFileHandler%"
1096 withString:[NSString stringWithFormat:@"client.handleFileTransfer('Save', '%@')", fileTransferID]];
1098 [inString replaceKeyword:@"%saveFileAsHandler%"
1099 withString:[NSString stringWithFormat:@"client.handleFileTransfer('SaveAs', '%@')", fileTransferID]];
1101 [inString replaceKeyword:@"%cancelRequestHandler%"
1102 withString:[NSString stringWithFormat:@"client.handleFileTransfer('Cancel', '%@')", fileTransferID]];
1105 //Message (must do last)
1106 range = [inString rangeOfString:@"%message%"];
1107 while(range.location != NSNotFound) {
1108 [inString safeReplaceCharactersInRange:range withString:htmlEncodedMessage];
1109 range = [inString rangeOfString:@"%message%"
1110 options:NSLiteralSearch
1111 range:NSMakeRange(range.location + htmlEncodedMessage.length,
1112 inString.length - range.location - htmlEncodedMessage.length)];
1115 // Topic replacement (if applicable)
1116 if ([content isKindOfClass:[AIContentTopic class]]) {
1117 range = [inString rangeOfString:@"%topic%"];
1119 if (range.location != NSNotFound) {
1120 [inString safeReplaceCharactersInRange:range withString:[NSString stringWithFormat:TOPIC_INDIVIDUAL_WRAPPER, htmlEncodedMessage]];
1123 } else if ([content isKindOfClass:[AIContentStatus class]]) {
1124 NSString *statusPhrase;
1125 BOOL replacedStatusPhrase = NO;
1127 [inString replaceKeyword:@"%status%"
1128 withString:[[(AIContentStatus *)content status] stringByEscapingForXMLWithEntities:nil]];
1130 [inString replaceKeyword:@"%statusSender%"
1131 withString:[theSource.displayName stringByEscapingForXMLWithEntities:nil]];
1133 [inString replaceKeyword:@"%senderScreenName%"
1136 [inString replaceKeyword:@"%senderPrefix%"
1139 [inString replaceKeyword:@"%sender%"
1142 if ((statusPhrase = [[content userInfo] objectForKey:@"Status Phrase"])) {
1144 range = [inString rangeOfString:@"%statusPhrase%"];
1145 if (range.location != NSNotFound) {
1146 [inString safeReplaceCharactersInRange:range
1147 withString:[statusPhrase stringByEscapingForXMLWithEntities:nil]];
1148 replacedStatusPhrase = YES;
1150 } while (range.location != NSNotFound);
1153 //Message (must do last)
1154 range = [inString rangeOfString:@"%message%"];
1155 if (range.location != NSNotFound) {
1156 NSString *messageString;
1158 if (replacedStatusPhrase) {
1159 //If the status phrase was used, clear the message tag
1160 messageString = @"";
1162 messageString = [AIHTMLDecoder encodeHTML:[content message]
1165 includingColorTags:NO
1168 closeStyleTagsOnFontChange:YES
1171 imagesPath:NSTemporaryDirectory()
1172 attachmentsAsText:NO
1173 onlyIncludeOutgoingImages:NO
1176 allowJavascriptURLs:NO];
1179 [inString safeReplaceCharactersInRange:range withString:messageString];
1186 - (NSMutableString *)fillKeywordsForBaseTemplate:(NSMutableString *)inString chat:(AIChat *)chat
1190 [inString replaceKeyword:@"%chatName%"
1191 withString:[chat.displayName stringByEscapingForXMLWithEntities:nil]];
1193 NSString * sourceName = [chat.account.displayName stringByEscapingForXMLWithEntities:nil];
1194 if(!sourceName) sourceName = @" ";
1195 [inString replaceKeyword:@"%sourceName%"
1196 withString:sourceName];
1198 NSString *destinationName = chat.listObject.displayName;
1199 if (!destinationName) destinationName = chat.displayName;
1200 [inString replaceKeyword:@"%destinationName%"
1201 withString:destinationName];
1203 NSString *serversideDisplayName = chat.listObject.serversideDisplayName;
1204 if (!serversideDisplayName) serversideDisplayName = chat.displayName;
1205 [inString replaceKeyword:@"%destinationDisplayName%"
1206 withString:[serversideDisplayName stringByEscapingForXMLWithEntities:nil]];
1208 AIListContact *listObject = chat.listObject;
1209 NSString *iconPath = nil;
1211 [inString replaceKeyword:@"%incomingColor%"
1212 withString:[NSColor representedColorForObject:listObject.UID withValidColors:self.validSenderColors]];
1214 [inString replaceKeyword:@"%outgoingColor%"
1215 withString:[NSColor representedColorForObject:chat.account.UID withValidColors:self.validSenderColors]];
1218 iconPath = [listObject valueForProperty:KEY_WEBKIT_USER_ICON];
1220 iconPath = [listObject valueForProperty:@"UserIconPath"];
1222 /* We couldn't get an icon... but perhaps we can for a parent contact */
1224 [listObject isKindOfClass:[AIListContact class]] &&
1225 ([(AIListContact *)listObject parentContact] != listObject)) {
1226 iconPath = [[(AIListContact *)listObject parentContact] valueForProperty:KEY_WEBKIT_USER_ICON];
1228 iconPath = [[(AIListContact *)listObject parentContact] valueForProperty:@"UserIconPath"];
1231 [inString replaceKeyword:@"%incomingIconPath%"
1232 withString:(iconPath ? iconPath : @"incoming_icon.png")];
1234 AIListObject *account = chat.account;
1238 iconPath = [account valueForProperty:KEY_WEBKIT_USER_ICON];
1240 iconPath = [account valueForProperty:@"UserIconPath"];
1242 [inString replaceKeyword:@"%outgoingIconPath%"
1243 withString:(iconPath ? iconPath : @"outgoing_icon.png")];
1245 NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:account.service.serviceID
1246 type:AIServiceIconLarge];
1248 NSString *serviceIconTag = [NSString stringWithFormat:@"<img class=\"serviceIcon\" src=\"%@\" alt=\"%@\" title=\"%@\">", serviceIconPath ? serviceIconPath : @"outgoing_icon.png", [account.service shortDescription], [account.service shortDescription]];
1250 [inString replaceKeyword:@"%service%"
1251 withString:[account.service shortDescription]];
1253 [inString replaceKeyword:@"%serviceIconImg%"
1254 withString:serviceIconTag];
1256 [inString replaceKeyword:@"%serviceIconPath%"
1257 withString:serviceIconPath];
1259 [inString replaceKeyword:@"%timeOpened%"
1260 withString:[timeStampFormatter stringFromDate:[chat dateOpened]]];
1262 //Replaces %time{x}% with a timestamp formatted like x (using NSDateFormatter)
1264 range = [inString rangeOfString:@"%timeOpened{"];
1265 if (range.location != NSNotFound) {
1267 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
1269 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
1270 NSString *timeFormat = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
1272 NSDateFormatter *dateFormatter;
1273 if ([timeFormat rangeOfString:@"%"].location != NSNotFound) {
1274 /* Support strftime-style format strings, which old message styles may use */
1275 dateFormatter = [[NSDateFormatter alloc] initWithDateFormat:timeFormat allowNaturalLanguage:NO];
1277 dateFormatter = [[NSDateFormatter alloc] init];
1278 [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
1279 [dateFormatter setDateFormat:timeFormat];
1282 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
1283 withString:[dateFormatter stringFromDate:[chat dateOpened]]];
1284 [dateFormatter release];
1288 } while (range.location != NSNotFound);
1290 [NSDateFormatter withLocalizedDateFormatterPerform:^(NSDateFormatter *dateFormatter){
1291 [inString replaceKeyword:@"%dateOpened%"
1292 withString:[dateFormatter stringFromDate:[chat dateOpened]]];
1297 range = [inString rangeOfString:@"==bodyBackground=="];
1299 if (range.location != NSNotFound) { //a backgroundImage tag is not required
1300 NSMutableString *bodyTag = nil;
1302 if (allowsCustomBackground && (customBackgroundPath || customBackgroundColor)) {
1303 bodyTag = [[[NSMutableString alloc] init] autorelease];
1305 if (customBackgroundPath) {
1306 if ([customBackgroundPath length]) {
1307 switch (customBackgroundType) {
1308 case BackgroundNormal:
1309 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: no-repeat; background-attachment:fixed;", customBackgroundPath]];
1311 case BackgroundCenter:
1312 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-position: center; background-repeat: no-repeat; background-attachment:fixed;", customBackgroundPath]];
1314 case BackgroundTile:
1315 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: repeat;", customBackgroundPath]];
1317 case BackgroundTileCenter:
1318 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: repeat; background-position: center;", customBackgroundPath]];
1320 case BackgroundScale:
1321 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); -webkit-background-size: 100%% 100%%; background-size: 100%% 100%%; background-attachment: fixed;", customBackgroundPath]];
1325 [bodyTag appendString:@"background-image: none; "];
1328 if (customBackgroundColor) {
1329 CGFloat red, green, blue, alpha;
1330 [customBackgroundColor getRed:&red green:&green blue:&blue alpha:&alpha];
1331 [bodyTag appendString:[NSString stringWithFormat:@"background-color: rgba(%ld, %ld, %ld, %f); ", (NSInteger)(red * 255.0), (NSInteger)(green * 255.0), (NSInteger)(blue * 255.0), alpha]];
1335 //Replace the body background tag
1336 [inString safeReplaceCharactersInRange:range withString:(bodyTag ? (NSString *)bodyTag : @"")];
1340 if ([inString rangeOfString:@"%variant%"].location != NSNotFound) {
1341 /* Per #12702, don't allow spaces in the variant name, as otherwise it becomes multiple css classes */
1342 [inString replaceKeyword:@"%variant%"
1343 withString:[self.activeVariant stringByReplacingOccurrencesOfString:@" " withString:@"_"]];
1351 - (NSString *)iconPathForFileTransfer:(ESFileTransfer *)inObject
1353 NSString *filename = [NSString stringWithFormat:@"TEMP-%@%@.tiff", [inObject uniqueID], [NSString randomStringOfLength:5]];
1354 return [[adium cachesPath] stringByAppendingPathComponent:filename];
1357 - (NSString *)statusIconPathForListObject:(AIListObject *)inObject
1359 if(!statusIconPathCache) statusIconPathCache = [[NSMutableDictionary alloc] init];
1360 NSImage *icon = [AIStatusIcons statusIconForListObject:inObject
1361 type:AIStatusIconTab
1362 direction:AIIconNormal];
1363 NSString *statusName = [AIStatusIcons statusNameForListObject:inObject];
1365 statusName = @"UnknownStatus";
1366 NSString *path = [statusIconPathCache objectForKey:statusName];
1369 path = [[adium cachesPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"TEMP-%@%@.tiff", statusName, [NSString randomStringOfLength:5]]];
1370 [[icon TIFFRepresentation] writeToFile:path atomically:YES];
1371 [statusIconPathCache setObject:path forKey:statusName];