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/AIContactControllerProtocol.h>
18 #import "AIContactStatusEventsPlugin.h"
19 #import <Adium/AIContactAlertsControllerProtocol.h>
20 #import <AIUtilities/AIImageAdditions.h>
21 #import <Adium/AIAccount.h>
22 #import <Adium/AIListGroup.h>
23 #import <Adium/AIMetaContact.h>
25 @interface AIContactStatusEventsPlugin (PRIVATE)
26 - (BOOL)updateCache:(NSMutableDictionary *)cache
27 forKey:(NSString *)key
28 newValue:(id)newStatus
29 listObject:(AIListObject *)inObject
30 performCompare:(BOOL)performCompare;
34 * @class AIContactStatusEventsPlugin
35 * @brief Component to provide events for contact status changes (online, offline, away, idle, etc.)
37 @implementation AIContactStatusEventsPlugin
45 onlineCache = [[NSMutableDictionary alloc] init];
46 awayCache = [[NSMutableDictionary alloc] init];
47 idleCache = [[NSMutableDictionary alloc] init];
48 statusMessageCache = [[NSMutableDictionary alloc] init];
50 //Register the events we generate
51 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_ONLINE_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
52 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_ONLINE_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
53 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_AWAY_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
54 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_AWAY_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
55 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_IDLE_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
56 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_IDLE_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
57 [[adium contactAlertsController] registerEventID:CONTACT_SEEN_ONLINE_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
58 [[adium contactAlertsController] registerEventID:CONTACT_SEEN_ONLINE_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
60 //Observe status changes
61 [[adium contactController] registerListObjectObserver:self];
64 - (void)uninstallPlugin
66 [[adium contactController] unregisterListObjectObserver:self];
70 * @brief Short description
71 * @result A short localized description of the passed event
73 - (NSString *)shortDescriptionForEventID:(NSString *)eventID
75 NSString *description;
77 if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]) {
78 description = AILocalizedString(@"Signs on",nil);
79 } else if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]) {
80 description = AILocalizedString(@"Signs off",nil);
81 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]) {
82 description = AILocalizedString(@"Goes away",nil);
83 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]) {
84 description = AILocalizedString(@"Returns from away",nil);
85 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]) {
86 description = AILocalizedString(@"Becomes idle",nil);
87 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]) {
88 description = AILocalizedString(@"Returns from idle",nil);
89 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]) {
90 description = AILocalizedString(@"Is seen",nil);
91 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]) {
92 description = AILocalizedString(@"Is no longer seen",nil);
101 * @brief Global short description for an event
103 - (NSString *)globalShortDescriptionForEventID:(NSString *)eventID
105 NSString *description;
107 if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]) {
108 description = AILocalizedString(@"Contact signs on",nil);
109 } else if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]) {
110 description = AILocalizedString(@"Contact signs off",nil);
111 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]) {
112 description = AILocalizedString(@"Contact goes away",nil);
113 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]) {
114 description = AILocalizedString(@"Contact returns from away",nil);
115 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]) {
116 description = AILocalizedString(@"Contact becomes idle",nil);
117 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]) {
118 description = AILocalizedString(@"Contact returns from idle",nil);
119 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]) {
120 description = AILocalizedString(@"Contact is seen",nil);
121 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]) {
122 description = AILocalizedString(@"Contact is no longer seen",nil);
131 * @brief English, non-translated global short description for an event
133 * This exists because old X(tras) relied upon matching the description of event IDs, and I don't feel like making
134 * a converter for old packs. If anyone wants to fix this situation, please feel free :)
136 * @result English global short description which should only be used internally
138 - (NSString *)englishGlobalShortDescriptionForEventID:(NSString *)eventID
140 NSString *description;
142 if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]) {
143 description = @"Contact Signed On";
144 } else if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]) {
145 description = @"Contact Signed Off";
146 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]) {
147 description = @"Contact Went Away";
148 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]) {
149 description = @"Contact Returned from Away";
150 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]) {
151 description = @"Contact Went Idle";
152 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]) {
153 description = @"Contact Returned from Idle";
154 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]) {
155 description = @"Contact is seen";
156 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]) {
157 description = @"Contact is no longer seen";
166 * @brief Long description for an event
168 - (NSString *)longDescriptionForEventID:(NSString *)eventID forListObject:(AIListObject *)listObject
173 if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]) {
174 format = AILocalizedString(@"When %@ connects",nil);
175 } else if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]) {
176 format = AILocalizedString(@"When %@ disconnects",nil);
177 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]) {
178 format = AILocalizedString(@"When %@ goes away",nil);
179 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]) {
180 format = AILocalizedString(@"When %@ returns from away",nil);
181 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]) {
182 format = AILocalizedString(@"When %@ goes idle",nil);
183 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]) {
184 format = AILocalizedString(@"When %@ returns from idle",nil);
185 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]) {
186 format = AILocalizedString(@"When you see %@",nil);
187 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]) {
188 format = AILocalizedString(@"When you no longer see %@",nil);
194 name = ([listObject isKindOfClass:[AIListGroup class]] ?
195 [NSString stringWithFormat:AILocalizedString(@"a member of %@",nil),[listObject displayName]] :
196 [listObject displayName]);
198 name = AILocalizedString(@"a contact",nil);
201 return [NSString stringWithFormat:format,name];
205 * @brief Natural language description for an event
207 * @param eventID The event identifier
208 * @param listObject The listObject triggering the event
209 * @param userInfo Event-specific userInfo
210 * @param includeSubject If YES, return a full sentence. If not, return a fragment.
211 * @result The natural language description.
213 - (NSString *)naturalLanguageDescriptionForEventID:(NSString *)eventID
214 listObject:(AIListObject *)listObject
215 userInfo:(id)userInfo
216 includeSubject:(BOOL)includeSubject
218 NSString *description = nil;
220 if (includeSubject) {
221 NSString *format = nil;
223 if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]) {
224 format = AILocalizedString(@"%@ connected", "Event: <A contact's name> connected");
225 } else if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]) {
226 format = AILocalizedString(@"%@ disconnected","Event: <A contact's name> disconnected");
227 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]) {
228 format = AILocalizedString(@"%@ went away","Event: <A contact's name> went away (is no longer available but is still online)");
229 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]) {
230 format = AILocalizedString(@"%@ came back","Event: <A contact's name> came back (is now available)");
231 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]) {
232 format = AILocalizedString(@"%@ went idle",nil"Event: <A contact's name> went idle");
233 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]) {
234 format = AILocalizedString(@"%@ became active","Event: <A contact's name> became active (is no longer idle)");
235 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]) {
236 format = AILocalizedString(@"%@ is seen","Event: <A contact's name> is seen (which can be 'came online' or 'was online when you connected')");
237 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]) {
238 format = AILocalizedString(@"%@ is no longer seen","Event: <A contact's name> is no longer seen (went offline, or you went offline)");
242 description = [NSString stringWithFormat:format,[listObject displayName]];
245 if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]) {
246 description = AILocalizedString(@"connected","Event: connected (follows a contact's name displayed as a header)");
247 } else if ([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]) {
248 description = AILocalizedString(@"disconnected","Event: disconnected (follows a contact's name displayed as a header)");
249 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]) {
250 description = AILocalizedString(@"went away","Event: went away (follows a contact's name displayed as a header)");
251 } else if ([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]) {
252 description = AILocalizedString(@"came back","Event: came back (follows a contact's name displayed as a header)");
253 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]) {
254 description = AILocalizedString(@"went idle","Event: went idle (follows a contact's name displayed as a header)");
255 } else if ([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]) {
256 description = AILocalizedString(@"became active","Event: became active (follows a contact's name displayed as a header)");
257 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]) {
258 description = AILocalizedString(@"is seen","Event: is seen (follows a contact's name displayed as a header)");
259 } else if ([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]) {
260 description = AILocalizedString(@"is no longer seen","Event: is no longer seen (follows a contact's name displayed as a header)");
267 - (NSImage *)imageForEventID:(NSString *)eventID
269 static NSImage *eventImage = nil;
270 if (!eventImage) eventImage = [[NSImage imageNamed:@"DefaultIcon" forClass:[self class]] retain];
274 #pragma mark Caching and event generation
276 * @brief Cache list object updates
278 * We cache list object updates so we can avoid generating the same event for the same contact on two accounts
279 * or for multiple identical changes within a metaContact.
281 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
284 * Ignore meta contact children since the actual meta contact provides a better event. The best way to check this is to verify that the contact's parentContact is itself.*/
285 if (([inObject isKindOfClass:[AIListContact class]]) &&
286 ([(AIListContact *)inObject parentContact] == (AIListContact *)inObject)) {
288 if ([inModifiedKeys containsObject:@"Online"]) {
289 id newValue = [inObject numberStatusObjectForKey:@"Online" fromAnyContainedObject:NO];
291 if ([self updateCache:onlineCache
295 performCompare:YES]) {
297 NSString *event = ([newValue boolValue] ? CONTACT_STATUS_ONLINE_YES : CONTACT_STATUS_ONLINE_NO);
298 [[adium contactAlertsController] generateEvent:event
299 forListObject:inObject
301 previouslyPerformedActionIDs:nil];
304 NSString *event = ([newValue boolValue] ? CONTACT_SEEN_ONLINE_YES : CONTACT_SEEN_ONLINE_NO);
305 [[adium contactAlertsController] generateEvent:event
306 forListObject:inObject
308 previouslyPerformedActionIDs:nil];
312 /* Events which are irrelevent if the contact is not online - these changes occur when we are
313 * just doing bookkeeping e.g. an away contact signs off, we clear the away flag, but they didn't actually
314 * come back from away. */
315 if ([[inObject numberStatusObjectForKey:@"Online"] boolValue]) {
316 if ([inModifiedKeys containsObject:@"StatusMessage"] || [inModifiedKeys containsObject:@"StatusType"]) {
317 NSNumber *newAwayNumber;
318 NSString *newStatusMessage;
319 BOOL awayChanged, statusMessageChanged;
320 NSSet *previouslyPerformedActionIDs = nil;
322 //Update away/not-away
323 newAwayNumber = [NSNumber numberWithBool:([inObject statusType] == AIAwayStatusType)];
325 awayChanged = [self updateCache:awayCache
327 newValue:newAwayNumber
331 //Update status message
332 newStatusMessage = [[inObject statusMessage] string];
333 statusMessageChanged = [self updateCache:statusMessageCache
334 forKey:@"StatusMessage"
335 newValue:newStatusMessage
338 if (statusMessageChanged && !silent) {
339 if (newStatusMessage != nil) {
340 //Evan: Not yet a contact alert, but we use the notification - how could/should we use this?
341 previouslyPerformedActionIDs = [[adium contactAlertsController] generateEvent:CONTACT_STATUS_MESSAGE
342 forListObject:inObject
344 previouslyPerformedActionIDs:nil];
348 //Don't repeat notifications for the away change which the status message already covered
349 if (awayChanged && !silent) {
350 NSString *event = ([newAwayNumber boolValue] ? CONTACT_STATUS_AWAY_YES : CONTACT_STATUS_AWAY_NO);
351 NSDictionary *userInfo = nil;
353 if ([event isEqualToString:CONTACT_STATUS_AWAY_YES] &&
354 (statusMessageChanged && (newStatusMessage != nil))) {
355 userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
356 forKey:@"Already Posted StatusMessage"];
359 [[adium contactAlertsController] generateEvent:event
360 forListObject:inObject
362 previouslyPerformedActionIDs:previouslyPerformedActionIDs];
366 if ([inModifiedKeys containsObject:@"IsIdle"]) {
367 id newValue = [inObject numberStatusObjectForKey:@"IsIdle" fromAnyContainedObject:NO];
368 if ([self updateCache:idleCache
372 performCompare:YES] && !silent) {
373 NSString *event = ([newValue boolValue] ? CONTACT_STATUS_IDLE_YES : CONTACT_STATUS_IDLE_NO);
374 [[adium contactAlertsController] generateEvent:event
375 forListObject:inObject
377 previouslyPerformedActionIDs:nil];
387 * @brief Update the cache
389 * @param cache The cache
391 * @param newStatus The new value
392 * @param inObject The list object
393 * @param performCompare If NO, we are only concerned about whether any object exists. If YES, a change from one value to another means we've updated.
395 * @result YES if the cache changed; NO if it remained the same (event has already occurred on another associated contact)
397 - (BOOL)updateCache:(NSMutableDictionary *)cache forKey:(NSString *)key newValue:(id)newStatus listObject:(AIListObject *)inObject performCompare:(BOOL)performCompare
399 id oldStatus = [cache objectForKey:[inObject internalObjectID]];
401 if ((newStatus && !oldStatus) ||
402 (oldStatus && !newStatus) ||
403 ((performCompare && newStatus && oldStatus && ![newStatus performSelector:@selector(compare:) withObject:oldStatus] == 0))) {
406 [cache setObject:newStatus forKey:[inObject internalObjectID]];
408 [cache removeObjectForKey:[inObject internalObjectID]];