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