Plugins/WebKit Message View/AIWebkitMessageViewStyle.m
author Evan Schoenberg
Sun Nov 22 20:06:12 2009 -0600 (2009-11-22)
changeset 2939 9f11b96baf1d
parent 2892 ab4ed45fa4b8
child 2954 31f6f4c62d78
permissions -rw-r--r--
If we try to update icons while the webview doesn't have an accessible documentFrame (e.g. while it is still working on loading its initial HTML), try again after a second. This fixes icons which are part of the initial bolus of information presented by a webview that change shortly after it loads, which can most noticeably happen when restoring a chat using a webview with icons in its header but could also occur if a user icon changed as we opened a chat window.

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