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 "AIContactController.h"
18 #import "AIContactStatusEventsPlugin.h"
19 #import "ESContactAlertsController.h"
20 #import <AIUtilities/ESImageAdditions.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 forKey:(NSString *)key newValue:(id)newStatus listObject:(AIListObject *)inObject performCompare:(BOOL)performCompare;
30 * @class AIContactStatusEventsPlugin
31 * @brief Component to provide events for contact status changes (online, offline, away, idle, etc.)
33 @implementation AIContactStatusEventsPlugin
41 onlineCache = [[NSMutableDictionary alloc] init];
42 awayCache = [[NSMutableDictionary alloc] init];
43 idleCache = [[NSMutableDictionary alloc] init];
44 statusMessageCache = [[NSMutableDictionary alloc] init];
46 //Register the events we generate
47 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_ONLINE_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
48 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_ONLINE_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
49 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_AWAY_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
50 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_AWAY_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
51 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_IDLE_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
52 [[adium contactAlertsController] registerEventID:CONTACT_STATUS_IDLE_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
53 [[adium contactAlertsController] registerEventID:CONTACT_SEEN_ONLINE_YES withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
54 [[adium contactAlertsController] registerEventID:CONTACT_SEEN_ONLINE_NO withHandler:self inGroup:AIContactsEventHandlerGroup globalOnly:NO];
56 //Observe status changes
57 [[adium contactController] registerListObjectObserver:self];
60 - (void)uninstallPlugin
62 [[adium contactController] unregisterListObjectObserver:self];
66 * @brief Short description
67 * @result A short localized description of the passed event
69 - (NSString *)shortDescriptionForEventID:(NSString *)eventID
71 NSString *description;
73 if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
74 description = AILocalizedString(@"Signs on",nil);
75 }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
76 description = AILocalizedString(@"Signs off",nil);
77 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
78 description = AILocalizedString(@"Goes away",nil);
79 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
80 description = AILocalizedString(@"Returns from away",nil);
81 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
82 description = AILocalizedString(@"Becomes idle",nil);
83 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
84 description = AILocalizedString(@"Returns from idle",nil);
85 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
86 description = AILocalizedString(@"Is seen",nil);
87 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
88 description = AILocalizedString(@"Is no longer seen",nil);
97 * @brief Global short description for an event
99 - (NSString *)globalShortDescriptionForEventID:(NSString *)eventID
101 NSString *description;
103 if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
104 description = AILocalizedString(@"Contact signs on",nil);
105 }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
106 description = AILocalizedString(@"Contact signs off",nil);
107 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
108 description = AILocalizedString(@"Contact goes away",nil);
109 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
110 description = AILocalizedString(@"Contact returns from away",nil);
111 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
112 description = AILocalizedString(@"Contact becomes idle",nil);
113 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
114 description = AILocalizedString(@"Contact returns from idle",nil);
115 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
116 description = AILocalizedString(@"Contact is seen",nil);
117 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
118 description = AILocalizedString(@"Contact is no longer seen",nil);
127 * @brief English, non-translated global short description for an event
129 * This exists because old X(tras) relied upon matching the description of event IDs, and I don't feel like making
130 * a converter for old packs. If anyone wants to fix this situation, please feel free :)
132 * @result English global short description which should only be used internally
134 - (NSString *)englishGlobalShortDescriptionForEventID:(NSString *)eventID
136 NSString *description;
138 if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
139 description = @"Contact Signed On";
140 }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
141 description = @"Contact Signed Off";
142 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
143 description = @"Contact Went Away";
144 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
145 description = @"Contact Returned from Away";
146 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
147 description = @"Contact Went Idle";
148 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
149 description = @"Contact Returned from Idle";
150 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
151 description = @"Contact is seen";
152 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
153 description = @"Contact is no longer seen";
162 * @brief Long description for an event
164 - (NSString *)longDescriptionForEventID:(NSString *)eventID forListObject:(AIListObject *)listObject
169 if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
170 format = AILocalizedString(@"When %@ connects",nil);
171 }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
172 format = AILocalizedString(@"When %@ disconnects",nil);
173 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
174 format = AILocalizedString(@"When %@ goes away",nil);
175 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
176 format = AILocalizedString(@"When %@ returns from away",nil);
177 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
178 format = AILocalizedString(@"When %@ goes idle",nil);
179 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
180 format = AILocalizedString(@"When %@ returns from idle",nil);
181 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
182 format = AILocalizedString(@"When you see %@",nil);
183 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
184 format = AILocalizedString(@"When you no longer see %@",nil);
190 name = ([listObject isKindOfClass:[AIListGroup class]] ?
191 [NSString stringWithFormat:AILocalizedString(@"a member of %@",nil),[listObject displayName]] :
192 [listObject displayName]);
194 name = AILocalizedString(@"a contact",nil);
197 return([NSString stringWithFormat:format,name]);
201 * @brief Natural language description for an event
203 * @param eventID The event identifier
204 * @param listObject The listObject triggering the event
205 * @param userInfo Event-specific userInfo
206 * @param includeSubject If YES, return a full sentence. If not, return a fragment.
207 * @result The natural language description.
209 - (NSString *)naturalLanguageDescriptionForEventID:(NSString *)eventID
210 listObject:(AIListObject *)listObject
211 userInfo:(id)userInfo
212 includeSubject:(BOOL)includeSubject
214 NSString *description = nil;
217 NSString *format = nil;
219 if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
220 format = AILocalizedString(@"%@ connected",nil);
221 }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
222 format = AILocalizedString(@"%@ disconnected",nil);
223 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
224 format = AILocalizedString(@"%@ went away",nil);
225 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
226 format = AILocalizedString(@"%@ came back",nil);
227 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
228 format = AILocalizedString(@"%@ went idle",nil);
229 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
230 format = AILocalizedString(@"%@ became active",nil);
231 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
232 format = AILocalizedString(@"%@ is seen",nil);
233 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
234 format = AILocalizedString(@"%@ is no longer seen",nil);
238 description = [NSString stringWithFormat:format,[listObject displayName]];
241 if([eventID isEqualToString:CONTACT_STATUS_ONLINE_YES]){
242 description = AILocalizedString(@"connected",nil);
243 }else if([eventID isEqualToString:CONTACT_STATUS_ONLINE_NO]){
244 description = AILocalizedString(@"disconnected",nil);
245 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_YES]){
246 description = AILocalizedString(@"went away",nil);
247 }else if([eventID isEqualToString:CONTACT_STATUS_AWAY_NO]){
248 description = AILocalizedString(@"came back",nil);
249 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_YES]){
250 description = AILocalizedString(@"went idle",nil);
251 }else if([eventID isEqualToString:CONTACT_STATUS_IDLE_NO]){
252 description = AILocalizedString(@"became active",nil);
253 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_YES]){
254 description = AILocalizedString(@"is seen",nil);
255 }else if([eventID isEqualToString:CONTACT_SEEN_ONLINE_NO]){
256 description = AILocalizedString(@"is no longer seen",nil);
263 - (NSImage *)imageForEventID:(NSString *)eventID
265 static NSImage *eventImage = nil;
266 if(!eventImage) eventImage = [[NSImage imageNamed:@"DefaultIcon" forClass:[self class]] retain];
270 #pragma mark Caching and event generation
272 * @brief Cache list object updates
274 * We cache list object updates so we can avoid generating the same event for the same contact on two accounts
275 * or for multiple identical changes within a metaContact.
277 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
280 * Ignore meta contact children since the actual meta contact provides a better event. */
281 if((![inObject isKindOfClass:[AIAccount class]]) && //Ignore accounts
282 (![[inObject containingObject] isKindOfClass:[AIMetaContact class]])){
284 if([inModifiedKeys containsObject:@"Online"]){
285 id newValue = [inObject numberStatusObjectForKey:@"Online" fromAnyContainedObject:NO];
286 if([self updateCache:onlineCache
290 performCompare:YES]){
292 NSString *event = ([newValue boolValue] ? CONTACT_STATUS_ONLINE_YES : CONTACT_STATUS_ONLINE_NO);
293 [[adium contactAlertsController] generateEvent:event
294 forListObject:inObject
296 previouslyPerformedActionIDs:nil];
299 NSString *event = ([newValue boolValue] ? CONTACT_SEEN_ONLINE_YES : CONTACT_SEEN_ONLINE_NO);
300 [[adium contactAlertsController] generateEvent:event
301 forListObject:inObject
303 previouslyPerformedActionIDs:nil];
307 /* Events which are irrelevent if the contact is not online - these changes occur when we are
308 * just doing bookkeeping e.g. an away contact signs off, we clear the away flag, but they didn't actually
309 * come back from away. */
310 if ([[inObject numberStatusObjectForKey:@"Online"] boolValue]){
311 if([inModifiedKeys containsObject:@"StatusMessage"] || [inModifiedKeys containsObject:@"StatusType"]){
312 NSNumber *newAwayNumber;
313 NSString *newStatusMessage;
314 BOOL awayChanged, statusMessageChanged;
315 NSSet *previouslyPerformedActionIDs = nil;
317 //Update away/not-away
318 newAwayNumber = [NSNumber numberWithBool:([inObject statusType] == AIAwayStatusType)];
320 awayChanged = [self updateCache:awayCache
322 newValue:newAwayNumber
326 //Update status message
327 newStatusMessage = [[inObject statusMessage] string];
328 statusMessageChanged = [self updateCache:statusMessageCache
329 forKey:@"StatusMessage"
330 newValue:newStatusMessage
334 if(statusMessageChanged && !silent){
335 if (newStatusMessage != nil){
336 //Evan: Not yet a contact alert, but we use the notification - how could/should we use this?
337 previouslyPerformedActionIDs = [[adium contactAlertsController] generateEvent:CONTACT_STATUS_MESSAGE
338 forListObject:inObject
340 previouslyPerformedActionIDs:nil];
344 //Don't repeat notifications for the away change which the status message already covered
345 if(awayChanged && !silent){
346 NSString *event = ([newAwayNumber boolValue] ? CONTACT_STATUS_AWAY_YES : CONTACT_STATUS_AWAY_NO);
347 NSDictionary *userInfo;
349 userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:(statusMessageChanged && (newStatusMessage != nil))]
350 forKey:@"Already Posted StatusMessage"];
352 [[adium contactAlertsController] generateEvent:event
353 forListObject:inObject
355 previouslyPerformedActionIDs:previouslyPerformedActionIDs];
359 if([inModifiedKeys containsObject:@"IsIdle"]){
360 id newValue = [inObject numberStatusObjectForKey:@"IsIdle" fromAnyContainedObject:NO];
361 if([self updateCache:idleCache
365 performCompare:YES] && !silent){
366 NSString *event = ([newValue boolValue] ? CONTACT_STATUS_IDLE_YES : CONTACT_STATUS_IDLE_NO);
367 [[adium contactAlertsController] generateEvent:event
368 forListObject:inObject
370 previouslyPerformedActionIDs:nil];
380 * @brief Update the cache
382 * @param cache The cache
384 * @param newStatus The new value
385 * @param inObject The list object
386 * @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.
388 * @result YES if the cache changed; NO if it remained the same (event has already occurred on another associated contact)
390 - (BOOL)updateCache:(NSMutableDictionary *)cache forKey:(NSString *)key newValue:(id)newStatus listObject:(AIListObject *)inObject performCompare:(BOOL)performCompare
392 id oldStatus = [cache objectForKey:[inObject internalObjectID]];
393 if((newStatus && !oldStatus) ||
394 (oldStatus && !newStatus) ||
395 ((performCompare && newStatus && oldStatus && ![newStatus performSelector:@selector(compare:) withObject:oldStatus] == 0))){
398 [cache setObject:newStatus forKey:[inObject internalObjectID]];
400 [cache removeObjectForKey:[inObject internalObjectID]];