2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import <Adium/AIContentControllerProtocol.h>
18 #import <Adium/AIPreferenceControllerProtocol.h>
19 #import "DCMessageContextDisplayPlugin.h"
20 #import "DCMessageContextDisplayPreferences.h"
21 #import <AIUtilities/AIDictionaryAdditions.h>
22 #import <Adium/AIChat.h>
23 #import <Adium/AIContentContext.h>
24 //#import "SMSQLiteLoggerPlugin.h"
25 //#import "AICoreComponentLoader.h"
28 #import <Adium/AIListContact.h>
29 #import <AIUtilities/AIAttributedStringAdditions.h>
30 #import <Adium/AIAccountControllerProtocol.h>
33 #import "AILoggerPlugin.h"
36 #import <LMX/LMXParser.h>
37 #import <Adium/AIXMLElement.h>
38 #import <AIUtilities/AIStringAdditions.h>
40 #import <AIUtilities/NSCalendarDate+ISO8601Parsing.h>
41 #import <Adium/AIContactControllerProtocol.h>
42 #import <Adium/AIHTMLDecoder.h>
45 * @class DCMessageContextDisplayPlugin
46 * @brief Component to display in-window message history
48 * The amount of history, and criteria of when to display history, are determined in the Advanced->Message History preferences.
50 @interface DCMessageContextDisplayPlugin (PRIVATE)
51 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
52 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime;
53 - (BOOL)contextShouldBeDisplayed:(NSCalendarDate *)inDate;
54 - (NSArray *)contextForChat:(AIChat *)chat;
57 @implementation DCMessageContextDisplayPlugin
66 //Setup our preferences
67 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:CONTEXT_DISPLAY_DEFAULTS
68 forClass:[self class]]
69 forGroup:PREF_GROUP_CONTEXT_DISPLAY];
70 preferences = [[DCMessageContextDisplayPreferences preferencePane] retain];
72 //Observe preference changes
73 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_CONTEXT_DISPLAY];
79 - (void)uninstallPlugin
81 [[adium preferenceController] unregisterPreferenceObserver:self];
82 [[adium notificationCenter] removeObserver:self];
86 * @brief Preferences for when to display history changed
88 * Only change our preferences in response to global preference notifications; specific objects use this group as well.
90 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
91 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
94 haveTalkedDays = [[prefDict objectForKey:KEY_HAVE_TALKED_DAYS] intValue];
95 haveNotTalkedDays = [[prefDict objectForKey:KEY_HAVE_NOT_TALKED_DAYS] intValue];
96 displayMode = [[prefDict objectForKey:KEY_DISPLAY_MODE] intValue];
98 haveTalkedUnits = [[prefDict objectForKey:KEY_HAVE_TALKED_UNITS] intValue];
99 haveNotTalkedUnits = [[prefDict objectForKey:KEY_HAVE_NOT_TALKED_UNITS] intValue];
101 shouldDisplay = [[prefDict objectForKey:KEY_DISPLAY_CONTEXT] boolValue];
102 linesToDisplay = [[prefDict objectForKey:KEY_DISPLAY_LINES] intValue];
104 if (shouldDisplay && linesToDisplay > 0 && !isObserving) {
105 //Observe new message windows only if we aren't already observing them
107 [[adium notificationCenter] addObserver:self
108 selector:@selector(addContextDisplayToWindow:)
112 } else if (isObserving && (!shouldDisplay || linesToDisplay <= 0)) {
115 [[adium notificationCenter] removeObserver:self name:Chat_DidOpen object:nil];
122 * @brief Retrieve and display in-window message history
124 * Called in response to the Chat_DidOpen notification
126 - (void)addContextDisplayToWindow:(NSNotification *)notification
128 AIChat *chat = (AIChat *)[notification object];
129 NSArray *context = [self contextForChat:chat];
131 if (context && [context count] > 0 && shouldDisplay) {
132 //Check if the history fits the date restrictions
134 //The most recent message is what determines whether we have "chatted in the last X days", "not chatted in the last X days", etc.
135 NSCalendarDate *mostRecentMessage = [[(AIContentContext *)[context lastObject] date] dateWithCalendarFormat:nil timeZone:nil];
136 if ([self contextShouldBeDisplayed:mostRecentMessage]) {
137 NSEnumerator *enumerator;
138 AIContentContext *contextMessage;
140 enumerator = [context objectEnumerator];
141 while((contextMessage = [enumerator nextObject])) {
142 /* Don't display immediately, so the message view can aggregate multiple message history items.
143 * As required, we post Content_ChatDidFinishAddingUntrackedContent when finished adding. */
144 [contextMessage setDisplayContentImmediately:NO];
146 [[adium contentController] displayContentObject:contextMessage
147 usingContentFilters:YES
151 //We finished adding untracked content
152 [[adium notificationCenter] postNotificationName:Content_ChatDidFinishAddingUntrackedContent
160 * @brief Does a specified date match our criteria for display?
162 * The date passed should be the date of the _most recent_ stored message history item
164 * @result YES if the mesage history should be displayed
166 - (BOOL)contextShouldBeDisplayed:(NSCalendarDate *)inDate
168 BOOL dateIsGood = YES;
169 int thresholdDays = 0;
170 int thresholdHours = 0;
172 if (displayMode != MODE_ALWAYS) {
174 if (displayMode == MODE_HAVE_TALKED) {
175 if (haveTalkedUnits == UNIT_DAYS)
176 thresholdDays = haveTalkedDays;
178 else if (haveTalkedUnits == UNIT_HOURS)
179 thresholdHours = haveTalkedDays;
181 } else if (displayMode == MODE_HAVE_NOT_TALKED) {
182 if ( haveTalkedUnits == UNIT_DAYS )
183 thresholdDays = haveNotTalkedDays;
184 else if (haveTalkedUnits == UNIT_HOURS)
185 thresholdHours = haveNotTalkedDays;
188 // Take the most recent message's date, add our limits to it
189 // See if the new date is earlier or later than today's date
190 NSCalendarDate *newDate = [inDate dateByAddingYears:0 months:0 days:thresholdDays hours:thresholdHours minutes:0 seconds:0];
192 NSComparisonResult comparison = [newDate compare:[NSDate date]];
194 if (((displayMode == MODE_HAVE_TALKED) && (comparison == NSOrderedAscending)) ||
195 ((displayMode == MODE_HAVE_NOT_TALKED) && (comparison == NSOrderedDescending)) ) {
203 static int linesLeftToFind = 0;
205 * @brief Retrieve the message history for a particular chat
207 * Asks AILoggerPlugin for the path to the right file, and then uses LMX to parse that file backwards.
209 - (NSArray *)contextForChat:(AIChat *)chat
211 //If there's no log there, there's no message history. Bail out.
212 NSArray *logPaths = [AILoggerPlugin sortedArrayOfLogFilesForChat:chat];
213 if(!logPaths) return nil;
215 NSString *logObjectUID = [chat name];
216 if (!logObjectUID) logObjectUID = [[chat listObject] UID];
217 logObjectUID = [logObjectUID safeFilenameString];
219 NSString *baseLogPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:
220 [AILoggerPlugin relativePathForLogWithObject:logObjectUID onAccount:[chat account]]];
222 //Initialize a place to store found messages
223 NSMutableArray *outerFoundContentContexts = [NSMutableArray arrayWithCapacity:linesToDisplay];
225 //Set up the counter variable
226 linesLeftToFind = linesToDisplay;
228 //Iterate over the elements of the log path array.
229 NSEnumerator *pathsEnumerator = [logPaths objectEnumerator];
230 NSString *logPath = nil;
231 while (linesLeftToFind > 0 && (logPath = [pathsEnumerator nextObject])) {
232 //If it's not a .chatlog, ignore it.
233 if (![logPath hasSuffix:@".chatlog"])
236 //Stick the base path on to the beginning
237 logPath = [baseLogPath stringByAppendingPathComponent:logPath];
238 NSLog(@"Message History: Loading log file: %@", logPath);
240 //Initialize the found messages array and element stack for us-as-delegate
241 foundMessages = [NSMutableArray arrayWithCapacity:linesLeftToFind];
242 elementStack = [NSMutableArray array];
244 //Create the parser and set ourselves as the delegate
245 LMXParser *parser = [LMXParser parser];
246 [parser setDelegate:self];
248 //Set up info needed by elementStarted to create content objects.
249 NSMutableDictionary *contextInfo = nil;
251 //Get the service name from the path name
252 NSString *serviceName = [[[[[logPath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] lastPathComponent] componentsSeparatedByString:@"."] objectAtIndex:0U];
254 AIListObject *account = [chat account];
255 NSString *accountID = [NSString stringWithFormat:@"%@.%@", [account serviceID], [account UID]];
257 contextInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
258 serviceName, @"Service name",
260 accountID, @"Account ID",
263 [parser setContextInfo:(void *)contextInfo];
266 //Open up the file we need to read from, and seek to the end (this is a *backwards* parser, after all :)
267 NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:logPath];
268 [file seekToEndOfFile];
270 //Set up some more doohickeys and then start the parse loop
271 int readSize = 4 * getpagesize(); //Read 4 pages at a time.
272 NSMutableData *chunk = [NSMutableData dataWithLength:readSize];
273 int fd = [file fileDescriptor];
274 char *buf = [chunk mutableBytes];
275 off_t offset = [file offsetInFile];
276 enum LMXParseResult result = LMXParsedIncomplete;
278 //We use NSValue because autorelease pools cannot be retained.
279 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
280 NSValue *value = [NSValue valueWithNonretainedObject:pool];
281 [contextInfo setObject:value forKey:@"Autorelease pool"];
284 //Calculate the new offset
285 offset = (offset <= readSize) ? 0 : offset - readSize;
287 //Seek to it and read greedily until we hit readSize or run out of file.
289 for (ssize_t amountRead = 0; idx < readSize; idx += amountRead) {
290 amountRead = pread(fd, buf + idx, readSize, offset + idx);
291 if (amountRead <= 0) break;
296 result = [parser parseChunk:chunk];
298 //Continue to parse as long as we need more elements, we have data to read, and LMX doesn't think we're done.
299 } while ([foundMessages count] < linesLeftToFind && offset > 0 && result != LMXParsedCompletely);
301 //Pop our autorelease pool.
302 //It may be a different one from the one we started with, so get it from the dictionary.
303 value = [contextInfo objectForKey:@"Autorelease pool"];
304 [[value nonretainedObjectValue] release];
306 //Be a good citizen and close the file
309 //Add our locals to the outer array; we're probably looping again.
310 [outerFoundContentContexts replaceObjectsInRange:NSMakeRange(0, 0) withObjectsFromArray:foundMessages];
311 linesLeftToFind -= [outerFoundContentContexts count];
313 return outerFoundContentContexts;
316 #pragma mark LMX delegate
318 - (void)parser:(LMXParser *)parser elementEnded:(NSString *)elementName
320 if ([elementName isEqualToString:@"message"]) {
321 [elementStack insertObject:[AIXMLElement elementWithName:elementName] atIndex:0U];
323 else if ([elementStack count]) {
324 AIXMLElement *element = [AIXMLElement elementWithName:elementName];
325 [(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:element atIndex:0U];
326 [elementStack insertObject:element atIndex:0U];
330 - (void)parser:(LMXParser *)parser foundCharacters:(NSString *)string
332 if ([elementStack count])
333 [(AIXMLElement *)[elementStack objectAtIndex:0U] insertObject:string atIndex:0U];
336 - (void)parser:(LMXParser *)parser elementStarted:(NSString *)elementName attributes:(NSDictionary *)attributes
338 if ([elementStack count]) {
339 AIXMLElement *element = [elementStack objectAtIndex:0U];
341 [element setAttributeNames:[attributes allKeys] values:[attributes allValues]];
344 NSMutableDictionary *contextInfo = [parser contextInfo];
346 if ([elementName isEqualToString:@"message"]) {
347 //A message element has started!
348 //This means that we have all of this message now, and therefore can create a single content object from the AIXMLElement tree and then throw away that tree.
349 //This saves memory when a message element contains many elements (since each one is represented by an AIXMLElement sub-tree in the AIXMLElement tree, as opposed to a simple NSAttributeRun in the NSAttributedString of the content object).
351 NSString *serviceName = [contextInfo objectForKey:@"Service name"];
352 AIListObject *account = [contextInfo objectForKey:@"Account"];
353 NSString *accountID = [contextInfo objectForKey:@"Account ID"];
354 AIChat *chat = [contextInfo objectForKey:@"Chat"];
356 //Set up some doohickers.
357 NSDictionary *attributes = [element attributes];
358 NSString *timeString = [attributes objectForKey:@"time"];
359 //Create the context object
360 //http://www.visualdistortion.org/crash/view.jsp?crash=211821
362 NSLog(@"Message Context Display: Parsing message time attribute %@", timeString);
364 NSCalendarDate *time = [NSCalendarDate calendarDateWithString:timeString];
366 NSString *autoreplyAttribute = [attributes objectForKey:@"auto"];
367 NSString *sender = [NSString stringWithFormat:@"%@.%@", serviceName, [attributes objectForKey:@"sender"]];
368 BOOL sentByMe = ([sender isEqualToString:accountID]);
370 /*don't fade the messages if they're within the last 5 minutes
371 *since that will be resuming a conversation, not starting a new one.
372 *Why the class trickery? Less code duplication, clearer what is actually different between the two cases.
374 Class messageClass = (-[time timeIntervalSinceNow] > 300.0) ? [AIContentContext class] : [AIContentMessage class];
375 AIContentMessage *message = [messageClass messageInChat:chat
376 withSource:(sentByMe ? account : [chat listObject])
377 destination:(sentByMe ? [chat listObject] : account)
379 message:[[AIHTMLDecoder decoder] decodeHTML:[element contentsAsXMLString]]
380 autoreply:(autoreplyAttribute && [autoreplyAttribute caseInsensitiveCompare:@"true"] == NSOrderedSame)];
382 //Don't log this object
383 [message setPostProcessContent:NO];
384 [message setTrackContent:NO];
386 //Add it to the array (in front, since we're working backwards, and we want the array in forward order)
387 [foundMessages insertObject:message atIndex:0];
389 NSLog(@"Null message context display time for %@",element);
393 [elementStack removeObjectAtIndex:0U];
394 if ([foundMessages count] == linesLeftToFind) {
395 if ([elementStack count]) [elementStack removeAllObjects];
396 [parser abortParsing];
398 //We're still looking for more messages in this file.
399 //Pop the current autorelease pool and start a new one.
400 //This frees the most recent tree of autoreleased AIXMLElements.
401 //We use NSValue because autorelease pools cannot be retained.
402 NSValue *value = [contextInfo objectForKey:@"Autorelease pool"];
403 [[value nonretainedObjectValue] release];
404 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
405 value = [NSValue valueWithNonretainedObject:pool];
406 [contextInfo setObject:value forKey:@"Autorelease pool"];