|
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 |