Minor improvements to the localization updating script to hand a missing Scripts...
[adiumx.git] / Source / AIContactStatusEventsPlugin.m
blobab553d6f816c32cd79faa3c7aae557c7e7614ec2
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
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.
8  * 
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.
12  * 
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.
15  */
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;
31 @end
33 /*!
34  * @class AIContactStatusEventsPlugin
35  * @brief Component to provide events for contact status changes (online, offline, away, idle, etc.)
36  */
37 @implementation AIContactStatusEventsPlugin
39 /*!
40  * @brief Install
41  */
42 - (void)installPlugin
44         //
45     onlineCache = [[NSMutableDictionary alloc] init];
46     awayCache = [[NSMutableDictionary alloc] init];
47     idleCache = [[NSMutableDictionary alloc] init];
48         statusMessageCache = [[NSMutableDictionary alloc] init];
49         
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];
59         
60         //Observe status changes
61     [[adium contactController] registerListObjectObserver:self];
64 - (void)uninstallPlugin
66         [[adium contactController] unregisterListObjectObserver:self];
69 /*!
70  * @brief Short description
71  * @result A short localized description of the passed event
72  */
73 - (NSString *)shortDescriptionForEventID:(NSString *)eventID
75         NSString        *description;
76         
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);
93         } else {
94                 description = @"";
95         }
96         
97         return description;
101  * @brief Global short description for an event
102  */
103 - (NSString *)globalShortDescriptionForEventID:(NSString *)eventID
105         NSString        *description;
106         
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);
123         } else {
124                 description = @"";
125         }
126         
127         return description;
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
137  */
138 - (NSString *)englishGlobalShortDescriptionForEventID:(NSString *)eventID
140         NSString        *description;
141         
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";
158         } else {
159                 description = @"";      
160         }
161         
162         return description;
166  * @brief Long description for an event
167  */
168 - (NSString *)longDescriptionForEventID:(NSString *)eventID forListObject:(AIListObject *)listObject
170         NSString        *format;
171         NSString        *name;
172         
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);
189         } else {
190                 format = @"";
191         }
192         
193         if (listObject) {
194                 name = ([listObject isKindOfClass:[AIListGroup class]] ?
195                                 [NSString stringWithFormat:AILocalizedString(@"a member of %@",nil),[listObject displayName]] :
196                                 [listObject displayName]);
197         } else {
198                 name = AILocalizedString(@"a contact",nil);
199         }
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.
212  */
213 - (NSString *)naturalLanguageDescriptionForEventID:(NSString *)eventID
214                                                                                 listObject:(AIListObject *)listObject
215                                                                                   userInfo:(id)userInfo
216                                                                         includeSubject:(BOOL)includeSubject
218         NSString        *description = nil;
219         
220         if (includeSubject) {
221                 NSString        *format = nil;
222                 
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)");
239                 }
240                 
241                 if (format) {
242                         description = [NSString stringWithFormat:format,[listObject displayName]];
243                 }
244         } else {
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)");
261                 }
262         }
263         
264         return description;
267 - (NSImage *)imageForEventID:(NSString *)eventID
269         static NSImage  *eventImage = nil;
270         if (!eventImage) eventImage = [[NSImage imageNamed:@"DefaultIcon" forClass:[self class]] retain];
271         return eventImage;
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.
280  */
281 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
283         /* Ignore accounts.
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
292                                                    forKey:@"Online"
293                                                  newValue:newValue
294                                            listObject:inObject
295                                    performCompare:YES]) {
296                                 if (!silent) {
297                                         NSString        *event = ([newValue boolValue] ? CONTACT_STATUS_ONLINE_YES : CONTACT_STATUS_ONLINE_NO);
298                                         [[adium contactAlertsController] generateEvent:event
299                                                                                                          forListObject:inObject
300                                                                                                                   userInfo:nil
301                                                                           previouslyPerformedActionIDs:nil];
302                                 }
303                                                                         
304                                 NSString        *event = ([newValue boolValue] ? CONTACT_SEEN_ONLINE_YES : CONTACT_SEEN_ONLINE_NO);
305                                 [[adium contactAlertsController] generateEvent:event
306                                                                                                  forListObject:inObject
307                                                                                                           userInfo:nil
308                                                                   previouslyPerformedActionIDs:nil];
309                         }
310                 }
311                 
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)];
324                                 
325                                 awayChanged = [self updateCache:awayCache
326                                                                                  forKey:@"Away"
327                                                                            newValue:newAwayNumber
328                                                                          listObject:inObject
329                                                                  performCompare:YES];
330                                 
331                                 //Update status message
332                                 newStatusMessage = [[inObject statusMessage] string];
333                                 statusMessageChanged = [self updateCache:statusMessageCache 
334                                                                                                  forKey:@"StatusMessage"
335                                                                                            newValue:newStatusMessage
336                                                                                          listObject:inObject
337                                                                                  performCompare:YES];                           
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
343                                                                                                                                                                                          userInfo:nil
344                                                                                                                                                  previouslyPerformedActionIDs:nil];
345                                         }
346                                 }
347                                 
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;
352                                         
353                                         if ([event isEqualToString:CONTACT_STATUS_AWAY_YES] &&
354                                                 (statusMessageChanged && (newStatusMessage != nil))) {
355                                                 userInfo = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
356                                                                                                                            forKey:@"Already Posted StatusMessage"];
357                                         }
359                                         [[adium contactAlertsController] generateEvent:event
360                                                                                                          forListObject:inObject
361                                                                                                                   userInfo:userInfo
362                                                                           previouslyPerformedActionIDs:previouslyPerformedActionIDs];
363                                 }
364                         }
366                         if ([inModifiedKeys containsObject:@"IsIdle"]) {
367                                 id newValue = [inObject numberStatusObjectForKey:@"IsIdle" fromAnyContainedObject:NO];
368                                 if ([self updateCache:idleCache
369                                                            forKey:@"IsIdle"
370                                                          newValue:newValue
371                                                    listObject:inObject
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
376                                                                                                                   userInfo:nil
377                                                                           previouslyPerformedActionIDs:nil];
378                                 }
379                         }
380                 }
381         }
383         return nil;     
387  * @brief Update the cache
389  * @param cache The cache
390  * @param key The key
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)
396  */
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))) {
404                 
405                 if (newStatus) {
406                         [cache setObject:newStatus forKey:[inObject internalObjectID]];
407                 } else {
408                         [cache removeObjectForKey:[inObject internalObjectID]];
409                 }
411                 return YES;
412         } else {
413                 return NO;
414         }
417 @end