Address a few font issues that have cropped up recently. Fixes #12906.
In previous releases of Adium, we would drop (in a few situations) the font tag information for outgoing messages if it was Helvetica sized 12. This, of course, was terribly confusing: if you specified any attributes, such as color, weight, decoration, etc., it forced Helvetica again.
In Snow Leopard, Apple started including the background color by default in our attributed strings, which has been causing us to, once again, force font tags in situations where we wouldn't before. Sometimes you'd see Helvetica, sometimes you wouldn't. Confusing.
Instead of trying to send plaintext, which we failed at in a good amount of situations (and all situations in SL), we now always send the font information. This was done by removing the forced Helvetica/12 assumptions in the HTML decoder.
We also respect the "show message fonts" preference for outgoing messages on top of incoming. This was another part of the confusing affects that a non-forced default of Helvetica was causing. Outgoing would appear Helvetica, incoming would be the non-allowed default WKMV font.
I tried to approach this as "strip the background color when foreground isn't specified, go back to the way the old things were" but its behavior was just honestly confusing. Let's be consistent in some way.
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
43 #define KEY_WEBKIT_VERSION @"MessageViewVersion"
45 //BOM scripts for appending content.
46 #define APPEND_MESSAGE_WITH_SCROLL @"checkIfScrollToBottomIsNeeded(); appendMessage(\"%@\"); scrollToBottomIfNeeded();"
47 #define APPEND_NEXT_MESSAGE_WITH_SCROLL @"checkIfScrollToBottomIsNeeded(); appendNextMessage(\"%@\"); scrollToBottomIfNeeded();"
48 #define APPEND_MESSAGE @"appendMessage(\"%@\");"
49 #define APPEND_NEXT_MESSAGE @"appendNextMessage(\"%@\");"
50 #define APPEND_MESSAGE_NO_SCROLL @"appendMessageNoScroll(\"%@\");"
51 #define APPEND_NEXT_MESSAGE_NO_SCROLL @"appendNextMessageNoScroll(\"%@\");"
52 #define REPLACE_LAST_MESSAGE @"replaceLastMessage(\"%@\");"
54 #define TOPIC_MAIN_DIV @"<div id=\"topic\"></div>"
55 // 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.
56 #define TOPIC_INDIVIDUAL_WRAPPER @"<span id=\"topicEdit\" ondblclick=\"this.setAttribute('contentEditable', true); this.focus();\">%@</span>"
58 static NSArray *validSenderColors;
60 @interface NSMutableString (AIKeywordReplacementAdditions)
61 - (void) replaceKeyword:(NSString *)word withString:(NSString *)newWord;
62 - (void) safeReplaceCharactersInRange:(NSRange)range withString:(NSString *)newWord;
65 @implementation NSMutableString (AIKeywordReplacementAdditions)
66 - (void) replaceKeyword:(NSString *)keyWord withString:(NSString *)newWord
69 if(!newWord) newWord = @"";
70 [self replaceOccurrencesOfString:keyWord
72 options:NSLiteralSearch
73 range:NSMakeRange(0.0, [self length])];
76 - (void) safeReplaceCharactersInRange:(NSRange)range withString:(NSString *)newWord
78 if (range.location == NSNotFound || range.length == 0) return;
79 if (!newWord) [self deleteCharactersInRange:range];
80 else [self replaceCharactersInRange:range withString:newWord];
84 //The old code built the paths itself, which follows the filesystem's case sensitivity, so some noobs named stuff wrong.
85 //NSBundle is always case sensitive, so those styles broke (they were already broken on case sensitive hfsx)
86 //These methods only check for the all-lowercase variant, so are not suitable for general purpose use.
87 @interface NSBundle (StupidCompatibilityHack)
88 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type;
89 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type inDirectory:(NSString *)dirpath;
92 @implementation NSBundle (StupidCompatibilityHack)
93 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type
95 NSString *path = [self pathForResource:res ofType:type];
97 path = [self pathForResource:[res lowercaseString] ofType:type];
101 - (NSString *)semiCaseInsensitivePathForResource:(NSString *)res ofType:(NSString *)type inDirectory:(NSString *)dirpath
103 NSString *path = [self pathForResource:res ofType:type inDirectory:dirpath];
105 path = [self pathForResource:[res lowercaseString] ofType:type inDirectory:dirpath];
111 @interface AIWebkitMessageViewStyle ()
112 - (id)initWithBundle:(NSBundle *)inBundle;
113 - (void)_loadTemplates;
114 - (void)releaseResources;
115 - (NSMutableString *)_escapeStringForPassingToScript:(NSMutableString *)inString;
116 - (NSString *)noVariantName;
117 - (NSString *)iconPathForFileTransfer:(ESFileTransfer *)inObject;
118 - (NSString *)statusIconPathForListObject:(AIListObject *)inObject;
121 @implementation AIWebkitMessageViewStyle
123 + (id)messageViewStyleFromBundle:(NSBundle *)inBundle
125 return [[[self alloc] initWithBundle:inBundle] autorelease];
128 + (id)messageViewStyleFromPath:(NSString *)path
130 NSBundle *styleBundle = [NSBundle bundleWithPath:path];
132 return [[[self alloc] initWithBundle:styleBundle] autorelease];
139 - (id)initWithBundle:(NSBundle *)inBundle
141 if ((self = [super init])) {
142 styleBundle = [inBundle retain];
143 stylePath = [[styleBundle resourcePath] retain];
152 [self releaseResources];
155 allowTextBackgrounds = YES;
157 /* Our styles are versioned so we can change how they work without breaking compatibility.
159 * Version 0: Initial Webkit Version
160 * Version 1: Template.html now handles all scroll-to-bottom functionality. It is no longer required to call the
161 * scrollToBottom functions when inserting content.
162 * Version 2: No significant changes
163 * Version 3: main.css is no longer a separate style, it now serves as the base stylesheet and is imported by default.
164 * The default variant is now a separate file in /variants like all other variants.
165 * Template.html now includes appendMessageNoScroll() and appendNextMessageNoScroll() which behave
166 * the same as appendMessage() and appendNextMessage() in Versions 1 and 2 but without scrolling.
167 * Version 4: Template.html now includes replaceLastMessage()
168 * Template.html now defines actionMessageUserName and actionMessageBody for display of /me (actions).
169 * If the style provides a custom Template.html, these classes must be defined.
170 * CSS can be used to customize the appearance of actions.
171 * HTML filters in are now supported in Adium's content filter system; filters can assume Version 4 or later.
173 styleVersion = [[styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION] integerValue];
175 //Pre-fetch our templates
176 [self _loadTemplates];
179 allowsCustomBackground = ![[styleBundle objectForInfoDictionaryKey:@"DisableCustomBackground"] boolValue];
180 transparentDefaultBackground = [[styleBundle objectForInfoDictionaryKey:@"DefaultBackgroundIsTransparent"] boolValue];
182 combineConsecutive = ![[styleBundle objectForInfoDictionaryKey:@"DisableCombineConsecutive"] boolValue];
184 NSNumber *tmpNum = [styleBundle objectForInfoDictionaryKey:@"ShowsUserIcons"];
185 allowsUserIcons = (tmpNum ? [tmpNum boolValue] : YES);
188 NSString *tmpName = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_USER_ICON_MASK];
189 if (tmpName) userIconMask = [[NSImage alloc] initWithContentsOfFile:[stylePath stringByAppendingPathComponent:tmpName]];
191 NSNumber *allowsColorsNumber = [styleBundle objectForInfoDictionaryKey:@"AllowTextColors"];
192 allowsColors = (allowsColorsNumber ? [allowsColorsNumber boolValue] : YES);
196 * @brief release everything we loaded from the style bundle
198 - (void)releaseResources
201 [headerHTML release];
202 [footerHTML release];
204 [contentHTML release];
205 [contentInHTML release];
206 [nextContentInHTML release];
207 [contextInHTML release];
208 [nextContextInHTML release];
209 [contentOutHTML release];
210 [nextContentOutHTML release];
211 [contextOutHTML release];
212 [nextContextOutHTML release];
213 [statusHTML release];
214 [fileTransferHTML release];
217 [customBackgroundPath release];
218 [customBackgroundColor release];
220 [userIconMask release];
228 [styleBundle release];
231 [self releaseResources];
232 [timeStampFormatter release];
234 [statusIconPathCache release];
239 @synthesize bundle = styleBundle;
243 return styleVersion < LEGACY_VERSION_THRESHOLD;
246 #pragma mark Settings
248 @synthesize allowsCustomBackground, allowsUserIcons, allowsColors, userIconMask;
250 - (BOOL)isBackgroundTransparent
252 //Our custom background is only transparent if the user has set a custom color with an alpha component less than 1.0
253 return ((!customBackgroundColor && transparentDefaultBackground) ||
254 (customBackgroundColor && [customBackgroundColor alphaComponent] < 0.99));
257 - (NSString *)defaultFontFamily
259 NSString *defaultFontFamily = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_DEFAULT_FONT_FAMILY];
260 if (!defaultFontFamily) defaultFontFamily = [[NSFont systemFontOfSize:0] familyName];
262 return defaultFontFamily;
265 - (NSNumber *)defaultFontSize
267 NSNumber *defaultFontSize = [styleBundle objectForInfoDictionaryKey:KEY_WEBKIT_DEFAULT_FONT_SIZE];
269 if (!defaultFontSize) defaultFontSize = [NSNumber numberWithInteger:[[NSFont systemFontOfSize:0] pointSize]];
271 return defaultFontSize;
276 return headerHTML && [headerHTML length];
281 return topicHTML && [topicHTML length];
284 #pragma mark Behavior
286 - (void)setDateFormat:(NSString *)format
288 if (!format || [format length] == 0) {
289 format = [NSDateFormatter localizedDateFormatStringShowingSeconds:NO showingAMorPM:NO];
292 [timeStampFormatter release];
294 if ([format rangeOfString:@"%"].location != NSNotFound) {
295 /* Support strftime-style format strings, which old message styles may use */
296 timeStampFormatter = [[NSDateFormatter alloc] initWithDateFormat:format allowNaturalLanguage:NO];
298 timeStampFormatter = [[NSDateFormatter alloc] init];
299 [timeStampFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
300 [timeStampFormatter setDateFormat:format];
304 @synthesize allowTextBackgrounds, customBackgroundType, customBackgroundColor, showIncomingMessageColors=showIncomingColors, showIncomingMessageFonts=showIncomingFonts, customBackgroundPath, nameFormat, useCustomNameFormat, showHeader, showUserIcons;
306 //Templates ------------------------------------------------------------------------------------------------------------
307 #pragma mark Templates
308 - (NSString *)baseTemplateWithVariant:(NSString *)variant chat:(AIChat *)chat
310 NSMutableString *templateHTML;
312 // If this is a group chat, we want to include a topic.
313 // Otherwise, if the header is shown, use it.
314 NSString *headerContent = @"";
316 if (chat.isGroupChat) {
317 headerContent = (chat.supportsTopic ? TOPIC_MAIN_DIV : @"");
318 } else if (headerHTML) {
319 headerContent = headerHTML;
323 //Old styles may be using an old custom 4 parameter baseHTML. Styles version 3 and higher should
324 //be using the bundled (or a custom) 5 parameter baseHTML.
325 if ((styleVersion < 3) && usingCustomTemplateHTML) {
326 templateHTML = [NSMutableString stringWithFormat:baseHTML, //Template
327 [[NSURL fileURLWithPath:stylePath] absoluteString], //Base path
328 [self pathForVariant:variant], //Variant path
330 (footerHTML ? footerHTML : @"")];
332 templateHTML = [NSMutableString stringWithFormat:baseHTML, //Template
333 [[NSURL fileURLWithPath:stylePath] absoluteString], //Base path
334 styleVersion < 3 ? @"" : @"@import url( \"main.css\" );", //Import main.css for new enough styles
335 [self pathForVariant:variant], //Variant path
337 (footerHTML ? footerHTML : @"")];
340 return [self fillKeywordsForBaseTemplate:templateHTML chat:chat];
343 - (NSString *)templateForContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
347 //Get the correct template for what we're inserting
348 if ([[content type] isEqualToString:CONTENT_MESSAGE_TYPE]) {
349 if ([content isOutgoing]) {
350 template = (contentIsSimilar ? nextContentOutHTML : contentOutHTML);
352 template = (contentIsSimilar ? nextContentInHTML : contentInHTML);
355 } else if ([[content type] isEqualToString:CONTENT_CONTEXT_TYPE]) {
356 if ([content isOutgoing]) {
357 template = (contentIsSimilar ? nextContextOutHTML : contextOutHTML);
359 template = (contentIsSimilar ? nextContextInHTML : contextInHTML);
362 } else if([[content type] isEqualToString:CONTENT_FILE_TRANSFER_TYPE]) {
363 template = [[fileTransferHTML mutableCopy] autorelease];
364 } else if ([[content type] isEqualToString:CONTENT_TOPIC_TYPE]) {
365 template = topicHTML;
368 template = statusHTML;
374 - (NSString *)completedTemplateForContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
376 NSMutableString *mutableTemplate = [[self templateForContent:content similar:contentIsSimilar] mutableCopy];
378 if (mutableTemplate) {
379 mutableTemplate = [self fillKeywords:mutableTemplate forContent:content similar:contentIsSimilar];
382 return [mutableTemplate autorelease];
386 * @brief Pre-fetch all the style templates
388 * This needs to be called before either baseTemplate or templateForContent is called
390 - (void)_loadTemplates
392 //Load the style's templates
393 //We can't use NSString's initWithContentsOfFile here. HTML files are interpreted in the defaultCEncoding
394 //(which varies by system) when read that way. We want to always interpret the files as UTF8.
395 headerHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Header" ofType:@"html"]] retain];
396 footerHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Footer" ofType:@"html"]] retain];
397 topicHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Topic" ofType:@"html"]] retain];
398 baseHTML = [NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Template" ofType:@"html"]];
400 //Starting with version 1, styles can choose to not include template.html. If the template is not included
401 //Adium's default will be used. This is preferred since any future template updates will apply to the style
402 if ((!baseHTML || [baseHTML length] == 0) && styleVersion >= 1) {
403 baseHTML = [NSString stringWithContentsOfUTF8File:[[NSBundle bundleForClass:[self class]] semiCaseInsensitivePathForResource:@"Template" ofType:@"html"]];
404 usingCustomTemplateHTML = NO;
406 usingCustomTemplateHTML = YES;
408 if ([baseHTML rangeOfString:@"function imageCheck()" options:NSLiteralSearch].location != NSNotFound) {
409 /* This doesn't quite fix image swapping on styles with broken image swapping due to custom HTML templates,
410 * but it improves it. For some reason, the result of using our normal template.html functions is that
411 * clicking works once, then the text doesn't allow a return click. This is an improvement compared
412 * to fully broken behavior in which the return click shows a missing-image placeholder.
414 NSMutableString *imageSwapFixedBaseHTML = [[baseHTML mutableCopy] autorelease];
415 [imageSwapFixedBaseHTML replaceOccurrencesOfString:
416 @" function imageCheck() {\n"
417 " node = event.target;\n"
418 " if(node.tagName == 'IMG' && node.alt) {\n"
419 " a = document.createElement('a');\n"
420 " a.setAttribute('onclick', 'imageSwap(this)');\n"
421 " a.setAttribute('src', node.src);\n"
422 " text = document.createTextNode(node.alt);\n"
423 " a.appendChild(text);\n"
424 " node.parentNode.replaceChild(a, node);\n"
428 @" function imageCheck() {\n"
429 " var node = event.target;\n"
430 " if(node.tagName.toLowerCase() == 'img' && !client.zoomImage(node) && node.alt) {\n"
431 " var a = document.createElement('a');\n"
432 " a.setAttribute('onclick', 'imageSwap(this)');\n"
433 " a.setAttribute('src', node.getAttribute('src'));\n"
434 " a.className = node.className;\n"
435 " var text = document.createTextNode(node.alt);\n"
436 " a.appendChild(text);\n"
437 " node.parentNode.replaceChild(a, node);\n"
440 options:NSLiteralSearch];
441 [imageSwapFixedBaseHTML replaceOccurrencesOfString:
442 @" function imageSwap(node) {\n"
443 " img = document.createElement('img');\n"
444 " img.setAttribute('src', node.src);\n"
445 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
446 " node.parentNode.replaceChild(img, node);\n"
450 @" function imageSwap(node) {\n"
451 " var shouldScroll = nearBottom();\n"
452 " //Swap the image/text\n"
453 " var img = document.createElement('img');\n"
454 " img.setAttribute('src', node.getAttribute('src'));\n"
455 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
456 " img.className = node.className;\n"
457 " node.parentNode.replaceChild(img, node);\n"
459 " alignChat(shouldScroll);\n"
461 options:NSLiteralSearch];
462 /* Now for ones which don't call alignChat() */
463 [imageSwapFixedBaseHTML replaceOccurrencesOfString:
464 @" function imageSwap(node) {\n"
465 " img = document.createElement('img');\n"
466 " img.setAttribute('src', node.src);\n"
467 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
468 " node.parentNode.replaceChild(img, node);\n"
471 @" function imageSwap(node) {\n"
472 " var shouldScroll = nearBottom();\n"
473 " //Swap the image/text\n"
474 " var img = document.createElement('img');\n"
475 " img.setAttribute('src', node.getAttribute('src'));\n"
476 " img.setAttribute('alt', node.firstChild.nodeValue);\n"
477 " img.className = node.className;\n"
478 " node.parentNode.replaceChild(img, node);\n"
480 options:NSLiteralSearch];
481 baseHTML = imageSwapFixedBaseHTML;
488 contentHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html"]] retain];
489 contentInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html" inDirectory:@"Incoming"]] retain];
490 nextContentInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContent" ofType:@"html" inDirectory:@"Incoming"]] retain];
491 contentOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Content" ofType:@"html" inDirectory:@"Outgoing"]] retain];
492 nextContentOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContent" ofType:@"html" inDirectory:@"Outgoing"]] retain];
495 contextInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Context" ofType:@"html" inDirectory:@"Incoming"]] retain];
496 nextContextInHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContext" ofType:@"html" inDirectory:@"Incoming"]] retain];
497 contextOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Context" ofType:@"html" inDirectory:@"Outgoing"]] retain];
498 nextContextOutHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"NextContext" ofType:@"html" inDirectory:@"Outgoing"]] retain];
500 //Fall back to Resources/Content.html if Incoming isn't present
501 if (!contentInHTML) contentInHTML = [contentHTML retain];
503 //Fall back to Content if NextContent doesn't need to use different HTML
504 if (!nextContentInHTML) nextContentInHTML = [contentInHTML retain];
506 //Fall back to Content if Context isn't present
507 if (!nextContextInHTML) nextContextInHTML = [nextContentInHTML retain];
508 if (!contextInHTML) contextInHTML = [contentInHTML retain];
510 //Fall back to Content if Context isn't present
511 if (!nextContextOutHTML && nextContentOutHTML) nextContextOutHTML = [nextContentOutHTML retain];
512 if (!contextOutHTML && contentOutHTML) contextOutHTML = [contentOutHTML retain];
514 //Fall back to Content if Context isn't present
515 if (!nextContextOutHTML) nextContextOutHTML = [nextContextInHTML retain];
516 if (!contextOutHTML) contextOutHTML = [contextInHTML retain];
518 //Fall back to Incoming if Outgoing doesn't need to be different
519 if (!contentOutHTML) contentOutHTML = [contentInHTML retain];
520 if (!nextContentOutHTML) nextContentOutHTML = [nextContentInHTML retain];
523 statusHTML = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"Status" ofType:@"html"]] retain];
525 //Fall back to Resources/Incoming/Content.html if Status isn't present
526 if (!statusHTML) statusHTML = [contentInHTML retain];
528 //TODO: make a generic Request message, rather than having this ft specific one
529 NSMutableString *fileTransferHTMLTemplate;
530 fileTransferHTMLTemplate = [[NSString stringWithContentsOfUTF8File:[styleBundle semiCaseInsensitivePathForResource:@"FileTransferRequest" ofType:@"html"]] mutableCopy];
531 if(!fileTransferHTMLTemplate) {
532 fileTransferHTMLTemplate = [contentInHTML mutableCopy];
533 [fileTransferHTMLTemplate replaceKeyword:@"%message%"
534 withString:@"<p><img src=\"%fileIconPath%\" style=\"width:32px; height:32px; vertical-align:middle;\"></img><input type=\"button\" onclick=\"%saveFileAsHandler%\" value=\"Download %fileName%\"></p>"];
536 [fileTransferHTMLTemplate replaceKeyword:@"Download %fileName%"
537 withString:[NSString stringWithFormat:AILocalizedString(@"Download %@", "%@ will be a file name"), @"%fileName%"]];
538 fileTransferHTML = fileTransferHTMLTemplate;
542 - (NSString *)scriptForAppendingContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
544 NSMutableString *newHTML;
547 //If combining of consecutive messages has been disabled, we treat all content as non-similar
548 if (!combineConsecutive) contentIsSimilar = NO;
550 //Fetch the correct template and substitute keywords for the passed content
551 newHTML = [[[self completedTemplateForContent:content similar:contentIsSimilar] mutableCopy] autorelease];
553 //BOM scripts vary by style version
554 if (!usingCustomTemplateHTML && styleVersion >= 4) {
555 /* If we're using the built-in template HTML, we know that it supports our most modern scripts */
556 if (replaceLastContent)
557 script = REPLACE_LAST_MESSAGE;
558 else if (willAddMoreContentObjects) {
559 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_NO_SCROLL : APPEND_MESSAGE_NO_SCROLL);
561 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
564 } else if (styleVersion >= 3) {
565 if (willAddMoreContentObjects) {
566 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_NO_SCROLL : APPEND_MESSAGE_NO_SCROLL);
568 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
570 } else if (styleVersion >= 1) {
571 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE : APPEND_MESSAGE);
574 if (usingCustomTemplateHTML && [content isKindOfClass:[AIContentStatus class]]) {
575 /* Old styles with a custom template.html had Status.html files without 'insert' divs coupled
576 * with a APPEND_NEXT_MESSAGE_WITH_SCROLL script which assumes one exists.
578 script = APPEND_MESSAGE_WITH_SCROLL;
580 script = (contentIsSimilar ? APPEND_NEXT_MESSAGE_WITH_SCROLL : APPEND_MESSAGE_WITH_SCROLL);
584 return [NSString stringWithFormat:script, [self _escapeStringForPassingToScript:newHTML]];
587 - (NSString *)scriptForChangingVariant:(NSString *)variant
589 AILogWithSignature(@"%@",[NSString stringWithFormat:@"setStylesheet(\"mainStyle\",\"%@\");",[self pathForVariant:variant]]);
591 return [NSString stringWithFormat:@"setStylesheet(\"mainStyle\",\"%@\");",[self pathForVariant:variant]];
594 - (NSString *)scriptForScrollingAfterAddingMultipleContentObjects
596 if ((styleVersion >= 3) || !usingCustomTemplateHTML) {
597 return @"alignChat(true);";
604 * @brief Escape a string for passing to our BOM scripts
606 - (NSMutableString *)_escapeStringForPassingToScript:(NSMutableString *)inString
608 //We need to escape a few things to get our string to the javascript without trouble
609 [inString replaceOccurrencesOfString:@"\\"
611 options:NSLiteralSearch];
613 [inString replaceOccurrencesOfString:@"\""
615 options:NSLiteralSearch];
617 [inString replaceOccurrencesOfString:@"\n"
619 options:NSLiteralSearch];
621 [inString replaceOccurrencesOfString:@"\r"
623 options:NSLiteralSearch];
628 #pragma mark Variants
630 - (NSArray *)availableVariants
632 NSMutableArray *availableVariants = [NSMutableArray array];
634 //Build an array of all variant names
635 for (NSString *path in [styleBundle pathsForResourcesOfType:@"css" inDirectory:@"Variants"]) {
636 [availableVariants addObject:[[path lastPathComponent] stringByDeletingPathExtension]];
639 //Style versions before 3 stored the default variant in a separate location. They also allowed for this
640 //varient name to not be specified, and would substitute a localized string in its place.
641 if (styleVersion < 3) {
642 [availableVariants addObject:[self noVariantName]];
645 //Alphabetize the variants
646 [availableVariants sortUsingSelector:@selector(compare:)];
648 return availableVariants;
651 - (NSString *)pathForVariant:(NSString *)variant
653 //Styles before version 3 stored the default variant in main.css, and not in the variants folder.
654 if (styleVersion < 3 && [variant isEqualToString:[self noVariantName]]) {
657 return [NSString stringWithFormat:@"Variants/%@.css",variant];
662 * @brief Base variant name for styles before version 2
664 - (NSString *)noVariantName
666 NSString *noVariantName = [styleBundle objectForInfoDictionaryKey:@"DisplayNameForNoVariant"];
667 return noVariantName ? noVariantName : AILocalizedString(@"Normal","Normal style variant menu item");
670 + (NSString *)noVariantNameForBundle:(NSBundle *)inBundle
672 NSString *noVariantName = [inBundle objectForInfoDictionaryKey:@"DisplayNameForNoVariant"];
673 return noVariantName ? noVariantName : AILocalizedString(@"Normal","Normal style variant menu item");
676 - (NSString *)defaultVariant
678 return styleVersion < 3 ? [self noVariantName] : [styleBundle objectForInfoDictionaryKey:@"DefaultVariant"];
681 + (NSString *)defaultVariantForBundle:(NSBundle *)inBundle
683 return [[inBundle objectForInfoDictionaryKey:KEY_WEBKIT_VERSION] integerValue] < 3 ?
684 [self noVariantNameForBundle:inBundle] :
685 [inBundle objectForInfoDictionaryKey:@"DefaultVariant"];
688 #pragma mark Keyword replacement
690 - (NSMutableString *)fillKeywords:(NSMutableString *)inString forContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar
694 AIListObject *contentSource = [content source];
695 AIListObject *theSource = ([contentSource isKindOfClass:[AIListContact class]] ?
696 [(AIListContact *)contentSource parentContact] :
700 htmlEncodedMessage is only encoded correctly for AIContentMessages
701 but we do it up here so that we can check for RTL/LTR text below without
702 having to encode the message twice. This is less than ideal
704 NSString *htmlEncodedMessage = [AIHTMLDecoder encodeHTML:[content message]
706 fontTags:showIncomingFonts
707 includingColorTags:showIncomingColors
710 closeStyleTagsOnFontChange:YES
713 imagesPath:NSTemporaryDirectory()
715 onlyIncludeOutgoingImages:NO
718 allowJavascriptURLs:NO];
720 if (styleVersion >= 4)
721 htmlEncodedMessage = [adium.contentController filterHTMLString:htmlEncodedMessage
722 direction:[content isOutgoing] ? AIFilterOutgoing : AIFilterIncoming
726 if ([content respondsToSelector:@selector(date)])
727 date = [(AIContentMessage *)content date];
729 //Replacements applicable to any AIContentObject
730 [inString replaceKeyword:@"%time%"
731 withString:(date ? [timeStampFormatter stringFromDate:date] : @"")];
733 NSString *shortTimeString = (date ? [[NSDateFormatter localizedDateFormatterShowingSeconds:NO showingAMorPM:NO] stringFromDate:date] : @"");
734 #warning working around http://trac.adium.im/ticket/11981
735 if ([shortTimeString hasSuffix:@" "])
736 shortTimeString = [shortTimeString substringToIndex:shortTimeString.length - 1];
737 [inString replaceKeyword:@"%shortTime%"
738 withString:shortTimeString];
740 if ([inString rangeOfString:@"%senderStatusIcon%"].location != NSNotFound) {
741 //Only cache the status icon to disk if the message style will actually use it
742 [inString replaceKeyword:@"%senderStatusIcon%"
743 withString:[self statusIconPathForListObject:theSource]];
746 //Replaces %localized{x}% with a a localized version of x, searching the style's localizations, and then Adium's localizations
748 range = [inString rangeOfString:@"%localized{"];
749 if (range.location != NSNotFound) {
751 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
752 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
753 NSString *untranslated = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
755 NSString *translated = [styleBundle localizedStringForKey:untranslated
758 if (!translated || [translated length] == 0) {
759 translated = [[NSBundle bundleForClass:[self class]] localizedStringForKey:untranslated
762 if (!translated || [translated length] == 0) {
763 translated = [[NSBundle mainBundle] localizedStringForKey:untranslated
770 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
771 withString:translated];
774 } while (range.location != NSNotFound);
776 [inString replaceKeyword:@"%userIcons%"
777 withString:(showUserIcons ? @"showIcons" : @"hideIcons")];
779 [inString replaceKeyword:@"%messageClasses%"
780 withString:[(contentIsSimilar ? @"consecutive " : @"") stringByAppendingString:[[content displayClasses] componentsJoinedByString:@" "]]];
782 if(!validSenderColors) {
783 NSURL *url = [NSURL fileURLWithPath:[stylePath stringByAppendingPathComponent:@"Incoming/SenderColors.txt"]];
784 NSString *senderColorsFile = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:NULL];
787 validSenderColors = [[senderColorsFile componentsSeparatedByString:@":"] retain];
789 [inString replaceKeyword:@"%senderColor%"
790 withString:[NSColor representedColorForObject:contentSource.UID withValidColors:validSenderColors]];
792 //HAX. The odd conditional here detects the rtl html that our html parser spits out.
793 [inString replaceKeyword:@"%messageDirection%"
794 withString:(([inString rangeOfString:@"<DIV dir=\"rtl\">"].location != NSNotFound) ? @"rtl" : @"ltr")];
796 //Replaces %time{x}% with a timestamp formatted like x (using NSDateFormatter)
798 range = [inString rangeOfString:@"%time{"];
799 if (range.location != NSNotFound) {
801 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
802 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
804 NSString *timeFormat = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
806 NSDateFormatter *dateFormatter;
807 if ([timeFormat rangeOfString:@"%"].location != NSNotFound) {
808 /* Support strftime-style format strings, which old message styles may use */
809 dateFormatter = [[NSDateFormatter alloc] initWithDateFormat:timeFormat allowNaturalLanguage:NO];
811 dateFormatter = [[NSDateFormatter alloc] init];
812 [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
813 [dateFormatter setDateFormat:timeFormat];
816 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
817 withString:[dateFormatter stringFromDate:date]];
819 [dateFormatter release];
822 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
826 } while (range.location != NSNotFound);
829 range = [inString rangeOfString:@"%userIconPath%"];
830 if (range.location != NSNotFound) {
831 NSString *userIconPath;
832 NSString *replacementString;
834 userIconPath = [theSource valueForProperty:KEY_WEBKIT_USER_ICON];
836 userIconPath = [theSource valueForProperty:@"UserIconPath"];
839 if (showUserIcons && userIconPath) {
840 replacementString = [NSString stringWithFormat:@"file://%@", userIconPath];
843 replacementString = ([content isOutgoing]
844 ? @"Outgoing/buddy_icon.png"
845 : @"Incoming/buddy_icon.png");
848 [inString safeReplaceCharactersInRange:range withString:replacementString];
850 } while (range.location != NSNotFound);
852 [inString replaceKeyword:@"%service%"
853 withString:[content.chat.account.service shortDescription]];
855 [inString replaceKeyword:@"%serviceIconPath%"
856 withString:[AIServiceIcons pathForServiceIconForServiceID:content.chat.account.service.serviceID
857 type:AIServiceIconLarge]];
860 if ([content isKindOfClass:[AIContentMessage class]]) {
862 //Use [content source] directly rather than the potentially-metaContact theSource
863 NSString *formattedUID = nil;
864 if ([content.chat aliasForContact:contentSource]) {
865 formattedUID = [content.chat aliasForContact:contentSource];
867 formattedUID = contentSource.formattedUID;
870 NSString *displayName = [content.chat displayNameForContact:contentSource];
872 [inString replaceKeyword:@"%status%"
875 [inString replaceKeyword:@"%senderScreenName%"
876 withString:[(formattedUID ?
878 displayName) stringByEscapingForXMLWithEntities:nil]];
881 [inString replaceKeyword:@"%senderPrefix%"
882 withString:((AIContentMessage *)content).senderPrefix];
885 range = [inString rangeOfString:@"%sender%"];
886 if (range.location != NSNotFound) {
887 NSString *senderDisplay = nil;
888 if (useCustomNameFormat) {
889 if (formattedUID && ![displayName isEqualToString:formattedUID]) {
890 switch (nameFormat) {
895 senderDisplay = displayName;
898 case AIDisplayName_ScreenName:
899 senderDisplay = [NSString stringWithFormat:@"%@ (%@)",displayName,formattedUID];
902 case AIScreenName_DisplayName:
903 senderDisplay = [NSString stringWithFormat:@"%@ (%@)",formattedUID,displayName];
907 senderDisplay = formattedUID;
912 //Test both displayName and formattedUID for nil-ness. If they're both nil, the assertion will trip.
913 if (!senderDisplay) {
914 senderDisplay = displayName;
915 if (!senderDisplay) {
916 senderDisplay = formattedUID;
917 if (!senderDisplay) {
918 AILog(@"wtf. we don't have a sender for %@ (%@)", content, [content message]);
919 NSAssert1(senderDisplay, @"Sender has no known display name that we can use! displayName and formattedUID were both nil for sender %@", contentSource);
924 senderDisplay = displayName;
927 if ([(AIContentMessage *)content isAutoreply]) {
928 senderDisplay = [NSString stringWithFormat:@"%@ %@",senderDisplay,AILocalizedString(@"(Autoreply)","Short word inserted after the sender's name when displaying a message which was an autoresponse")];
931 [inString safeReplaceCharactersInRange:range withString:[senderDisplay stringByEscapingForXMLWithEntities:nil]];
933 } while (range.location != NSNotFound);
936 range = [inString rangeOfString:@"%senderDisplayName%"];
937 if (range.location != NSNotFound) {
938 NSString *serversideDisplayName = ([theSource isKindOfClass:[AIListContact class]] ?
939 [(AIListContact *)theSource serversideDisplayName] :
941 if (!serversideDisplayName) {
942 serversideDisplayName = theSource.displayName;
945 [inString safeReplaceCharactersInRange:range
946 withString:[serversideDisplayName stringByEscapingForXMLWithEntities:nil]];
948 } while (range.location != NSNotFound);
950 //Blatantly stealing the date code for the background color script.
952 range = [inString rangeOfString:@"%textbackgroundcolor{"];
953 if (range.location != NSNotFound) {
955 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
956 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
957 NSString *transparency = [inString substringWithRange:NSMakeRange(NSMaxRange(range),
958 (endRange.location - NSMaxRange(range)))];
960 if (allowTextBackgrounds && showIncomingColors) {
961 NSString *thisIsATemporaryString;
962 unsigned rgb = 0, red, green, blue;
964 thisIsATemporaryString = [AIHTMLDecoder encodeHTML:[content message] headers:NO
966 includingColorTags:NO
969 closeStyleTagsOnFontChange:NO
972 imagesPath:NSTemporaryDirectory()
974 onlyIncludeOutgoingImages:NO
977 allowJavascriptURLs:NO];
978 hexcode = [NSScanner scannerWithString:thisIsATemporaryString];
979 [hexcode scanHexInt:&rgb];
980 if (![thisIsATemporaryString length] && rgb == 0) {
981 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
983 red = (rgb & 0xff0000) >> 16;
984 green = (rgb & 0x00ff00) >> 8;
985 blue = rgb & 0x0000ff;
986 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
987 withString:[NSString stringWithFormat:@"rgba(%d, %d, %d, %@)", red, green, blue, transparency]];
990 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
992 } else if (endRange.location == NSMaxRange(range)) {
993 if (allowTextBackgrounds && showIncomingColors) {
994 NSString *thisIsATemporaryString;
996 thisIsATemporaryString = [AIHTMLDecoder encodeHTML:[content message] headers:NO
998 includingColorTags:NO
1001 closeStyleTagsOnFontChange:NO
1004 imagesPath:NSTemporaryDirectory()
1005 attachmentsAsText:NO
1006 onlyIncludeOutgoingImages:NO
1009 allowJavascriptURLs:NO];
1010 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
1011 withString:[NSString stringWithFormat:@"#%@", thisIsATemporaryString]];
1013 [inString deleteCharactersInRange:NSUnionRange(range, endRange)];
1017 } while (range.location != NSNotFound);
1019 if ([content isKindOfClass:[ESFileTransfer class]]) { //file transfers are an AIContentMessage subclass
1021 ESFileTransfer *transfer = (ESFileTransfer *)content;
1022 NSString *fileName = [[transfer remoteFilename] stringByEscapingForXMLWithEntities:nil];
1023 NSString *fileTransferID = [[transfer uniqueID] stringByEscapingForXMLWithEntities:nil];
1025 range = [inString rangeOfString:@"%fileIconPath%"];
1026 if (range.location != NSNotFound) {
1027 NSString *iconPath = [self iconPathForFileTransfer:transfer];
1028 NSImage *icon = [transfer iconImage];
1030 [[icon TIFFRepresentation] writeToFile:iconPath atomically:YES];
1031 [inString safeReplaceCharactersInRange:range withString:iconPath];
1032 range = [inString rangeOfString:@"%fileIconPath%"];
1033 } while (range.location != NSNotFound);
1036 [inString replaceKeyword:@"%fileName%"
1037 withString:fileName];
1039 [inString replaceKeyword:@"%saveFileHandler%"
1040 withString:[NSString stringWithFormat:@"client.handleFileTransfer('Save', '%@')", fileTransferID]];
1042 [inString replaceKeyword:@"%saveFileAsHandler%"
1043 withString:[NSString stringWithFormat:@"client.handleFileTransfer('SaveAs', '%@')", fileTransferID]];
1045 [inString replaceKeyword:@"%cancelRequestHandler%"
1046 withString:[NSString stringWithFormat:@"client.handleFileTransfer('Cancel', '%@')", fileTransferID]];
1049 //Message (must do last)
1050 range = [inString rangeOfString:@"%message%"];
1051 while(range.location != NSNotFound) {
1052 [inString safeReplaceCharactersInRange:range withString:htmlEncodedMessage];
1053 range = [inString rangeOfString:@"%message%"
1054 options:NSLiteralSearch
1055 range:NSMakeRange(range.location + htmlEncodedMessage.length,
1056 inString.length - range.location - htmlEncodedMessage.length)];
1059 // Topic replacement (if applicable)
1060 if ([content isKindOfClass:[AIContentTopic class]]) {
1061 range = [inString rangeOfString:@"%topic%"];
1063 if (range.location != NSNotFound) {
1064 [inString safeReplaceCharactersInRange:range withString:[NSString stringWithFormat:TOPIC_INDIVIDUAL_WRAPPER, htmlEncodedMessage]];
1067 } else if ([content isKindOfClass:[AIContentStatus class]]) {
1068 NSString *statusPhrase;
1069 BOOL replacedStatusPhrase = NO;
1071 [inString replaceKeyword:@"%status%"
1072 withString:[[(AIContentStatus *)content status] stringByEscapingForXMLWithEntities:nil]];
1074 [inString replaceKeyword:@"%statusSender%"
1075 withString:[theSource.displayName stringByEscapingForXMLWithEntities:nil]];
1077 [inString replaceKeyword:@"%senderScreenName%"
1080 [inString replaceKeyword:@"%senderPrefix%"
1083 [inString replaceKeyword:@"%sender%"
1086 if ((statusPhrase = [[content userInfo] objectForKey:@"Status Phrase"])) {
1088 range = [inString rangeOfString:@"%statusPhrase%"];
1089 if (range.location != NSNotFound) {
1090 [inString safeReplaceCharactersInRange:range
1091 withString:[statusPhrase stringByEscapingForXMLWithEntities:nil]];
1092 replacedStatusPhrase = YES;
1094 } while (range.location != NSNotFound);
1097 //Message (must do last)
1098 range = [inString rangeOfString:@"%message%"];
1099 if (range.location != NSNotFound) {
1100 NSString *messageString;
1102 if (replacedStatusPhrase) {
1103 //If the status phrase was used, clear the message tag
1104 messageString = @"";
1106 messageString = [AIHTMLDecoder encodeHTML:[content message]
1109 includingColorTags:NO
1112 closeStyleTagsOnFontChange:YES
1115 imagesPath:NSTemporaryDirectory()
1116 attachmentsAsText:NO
1117 onlyIncludeOutgoingImages:NO
1120 allowJavascriptURLs:NO];
1123 [inString safeReplaceCharactersInRange:range withString:messageString];
1130 - (NSMutableString *)fillKeywordsForBaseTemplate:(NSMutableString *)inString chat:(AIChat *)chat
1134 [inString replaceKeyword:@"%chatName%"
1135 withString:[chat.displayName stringByEscapingForXMLWithEntities:nil]];
1137 NSString * sourceName = [chat.account.displayName stringByEscapingForXMLWithEntities:nil];
1138 if(!sourceName) sourceName = @" ";
1139 [inString replaceKeyword:@"%sourceName%"
1140 withString:sourceName];
1142 NSString *destinationName = chat.listObject.displayName;
1143 if (!destinationName) destinationName = chat.displayName;
1144 [inString replaceKeyword:@"%destinationName%"
1145 withString:destinationName];
1147 NSString *serversideDisplayName = chat.listObject.serversideDisplayName;
1148 if (!serversideDisplayName) serversideDisplayName = chat.displayName;
1149 [inString replaceKeyword:@"%destinationDisplayName%"
1150 withString:[serversideDisplayName stringByEscapingForXMLWithEntities:nil]];
1152 AIListContact *listObject = chat.listObject;
1153 NSString *iconPath = nil;
1155 [inString replaceKeyword:@"%senderColor%"
1156 withString:[NSColor representedColorForObject:listObject.UID withValidColors:validSenderColors]];
1159 iconPath = [listObject valueForProperty:KEY_WEBKIT_USER_ICON];
1161 iconPath = [listObject valueForProperty:@"UserIconPath"];
1164 [inString replaceKeyword:@"%incomingIconPath%"
1165 withString:(iconPath ? iconPath : @"incoming_icon.png")];
1167 AIListObject *account = chat.account;
1171 iconPath = [account valueForProperty:KEY_WEBKIT_USER_ICON];
1173 iconPath = [account valueForProperty:@"UserIconPath"];
1176 [inString replaceKeyword:@"%outgoingIconPath%"
1177 withString:(iconPath ? iconPath : @"outgoing_icon.png")];
1179 NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:account.service.serviceID
1180 type:AIServiceIconLarge];
1182 NSString *serviceIconTag = [NSString stringWithFormat:@"<img class=\"serviceIcon\" src=\"%@\" alt=\"%@\" title=\"%@\">", serviceIconPath ? serviceIconPath : @"outgoing_icon.png", [account.service shortDescription], [account.service shortDescription]];
1184 [inString replaceKeyword:@"%serviceIconImg%"
1185 withString:serviceIconTag];
1187 [inString replaceKeyword:@"%serviceIconPath%"
1188 withString:serviceIconPath];
1190 [inString replaceKeyword:@"%timeOpened%"
1191 withString:[timeStampFormatter stringFromDate:[chat dateOpened]]];
1193 //Replaces %time{x}% with a timestamp formatted like x (using NSDateFormatter)
1195 range = [inString rangeOfString:@"%timeOpened{"];
1196 if (range.location != NSNotFound) {
1198 endRange = [inString rangeOfString:@"}%" options:NSLiteralSearch range:NSMakeRange(NSMaxRange(range), [inString length] - NSMaxRange(range))];
1200 if (endRange.location != NSNotFound && endRange.location > NSMaxRange(range)) {
1201 NSString *timeFormat = [inString substringWithRange:NSMakeRange(NSMaxRange(range), (endRange.location - NSMaxRange(range)))];
1203 NSDateFormatter *dateFormatter;
1204 if ([timeFormat rangeOfString:@"%"].location != NSNotFound) {
1205 /* Support strftime-style format strings, which old message styles may use */
1206 dateFormatter = [[NSDateFormatter alloc] initWithDateFormat:timeFormat allowNaturalLanguage:NO];
1208 dateFormatter = [[NSDateFormatter alloc] init];
1209 [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
1210 [dateFormatter setDateFormat:timeFormat];
1213 [inString safeReplaceCharactersInRange:NSUnionRange(range, endRange)
1214 withString:[dateFormatter stringFromDate:[chat dateOpened]]];
1215 [dateFormatter release];
1219 } while (range.location != NSNotFound);
1221 [inString replaceKeyword:@"%dateOpened%"
1222 withString:[[NSDateFormatter localizedDateFormatter] stringFromDate:[chat dateOpened]]];
1226 range = [inString rangeOfString:@"==bodyBackground=="];
1228 if (range.location != NSNotFound) { //a backgroundImage tag is not required
1229 NSMutableString *bodyTag = nil;
1231 if (allowsCustomBackground && (customBackgroundPath || customBackgroundColor)) {
1232 bodyTag = [[[NSMutableString alloc] init] autorelease];
1234 if (customBackgroundPath) {
1235 if ([customBackgroundPath length]) {
1236 switch (customBackgroundType) {
1237 case BackgroundNormal:
1238 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: no-repeat; background-attachment:fixed;", customBackgroundPath]];
1240 case BackgroundCenter:
1241 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-position: center; background-repeat: no-repeat; background-attachment:fixed;", customBackgroundPath]];
1243 case BackgroundTile:
1244 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: repeat;", customBackgroundPath]];
1246 case BackgroundTileCenter:
1247 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); background-repeat: repeat; background-position: center;", customBackgroundPath]];
1249 case BackgroundScale:
1250 [bodyTag appendString:[NSString stringWithFormat:@"background-image: url('%@'); -webkit-background-size: 100%% 100%%; background-size: 100%% 100%%; background-attachment: fixed;", customBackgroundPath]];
1254 [bodyTag appendString:@"background-image: none; "];
1257 if (customBackgroundColor) {
1258 CGFloat red, green, blue, alpha;
1259 [customBackgroundColor getRed:&red green:&green blue:&blue alpha:&alpha];
1260 [bodyTag appendString:[NSString stringWithFormat:@"background-color: rgba(%ld, %ld, %ld, %f); ", (NSInteger)(red * 255.0), (NSInteger)(green * 255.0), (NSInteger)(blue * 255.0), alpha]];
1264 //Replace the body background tag
1265 [inString safeReplaceCharactersInRange:range withString:(bodyTag ? (NSString *)bodyTag : @"")];
1274 - (NSString *)iconPathForFileTransfer:(ESFileTransfer *)inObject
1276 NSString *filename = [NSString stringWithFormat:@"TEMP-%@%@.tiff", [inObject remoteFilename], [NSString randomStringOfLength:5]];
1277 return [[adium cachesPath] stringByAppendingPathComponent:filename];
1280 - (NSString *)statusIconPathForListObject:(AIListObject *)inObject
1282 if(!statusIconPathCache) statusIconPathCache = [[NSMutableDictionary alloc] init];
1283 NSImage *icon = [AIStatusIcons statusIconForListObject:inObject
1284 type:AIStatusIconTab
1285 direction:AIIconNormal];
1286 NSString *statusName = [AIStatusIcons statusNameForListObject:inObject];
1288 statusName = @"UnknownStatus";
1289 NSString *path = [statusIconPathCache objectForKey:statusName];
1292 path = [[adium cachesPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"TEMP-%@%@.tiff", statusName, [NSString randomStringOfLength:5]]];
1293 [[icon TIFFRepresentation] writeToFile:path atomically:YES];
1294 [statusIconPathCache setObject:path forKey:statusName];