* Adium is the legal property of its developers, whose names are listed in the copyright file included * with this source distribution. * This program is free software; you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * You should have received a copy of the GNU General Public License along with this program; if not, * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #import "NEHGrowlPlugin.h" #import "CBGrowlAlertDetailPane.h" #import "AIWebKitMessageViewPlugin.h" #import <Adium/AIChatControllerProtocol.h> #import <Adium/AIContactControllerProtocol.h> #import <Adium/AIContentControllerProtocol.h> #import <Adium/AIInterfaceControllerProtocol.h> #import <Adium/AIContactAlertsControllerProtocol.h> #import <Adium/AIStatusControllerProtocol.h> #import <Adium/AIAccount.h> #import <Adium/AIContentObject.h> #import <Adium/AIListContact.h> #import <Adium/AIListObject.h> #import <Adium/AIServiceIcons.h> #import <Adium/AIStatus.h> #import <Adium/ESFileTransfer.h> #import <AIUtilities/AIImageAdditions.h> #import <AIUtilities/AIStringAdditions.h> #import <AIUtilities/AIMutableStringAdditions.h> #import <AIUtilities/AIObjectAdditions.h> #define GROWL_ALERT AILocalizedString(@"Display a notification",nil) #define GROWL_STICKY_ALERT AILocalizedString(@"Display a notification until dismissed",nil) #define GROWL_STICKY_TIME_STAMP_ALERT AILocalizedString(@"Display a notification with a time stamp until dismissed", nil) #define GROWL_TIME_STAMP_ALERT AILocalizedString(@"Display a notification with a time stamp", nil) #define GROWL_TEXT_SIZE 11 #define GROWL_EVENT_ALERT_IDENTIFIER @"Growl" #define KEY_FILE_TRANSFER_ID @"fileTransferUniqueID" #define KEY_CHAT_ID @"uniqueChatID" #define KEY_LIST_OBJECT_ID @"internalObjectID" @interface NEHGrowlPlugin () - ( NSString * ) eventQueueKeyForEventID : ( NSString * ) eventID - ( void ) postSingleEventID: ( NSString * ) eventID forListObject :( AIListObject * ) listObject withDetails :( NSDictionary * ) details - ( void ) postMultipleEventID: ( NSString * ) eventID priority :( signed int ) priority forListObject :( AIListObject * ) listObject withCount :( NSUInteger ) count ; - ( void ) adiumFinishedLaunching: ( NSNotification * ) notification ; - ( void ) clearQueue: ( NSDictionary * ) callDict ; * @brief Implements Growl functionality in Adium * This class manages the Growl event type, and controls the display of Growl notifications that Adium generates. @implementation NEHGrowlPlugin * @brief Initialize the Growl plugin * Waits for Adium to finish launching before we perform further actions so all events are registered. [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( adiumFinishedLaunching : ) name : AIApplicationDidFinishLoadingNotification queuedEvents = [[ NSMutableDictionary alloc ] init ]; [ queuedEvents release ]; queuedEvents = nil ; * @brief Adium finished launching * Delays one more run loop so any events which are registered on this notification are guaranteed to be complete * regardless of the order in which the observers are called. - ( void ) adiumFinishedLaunching: ( NSNotification * ) notification [ self performSelector : @selector ( beginGrowling ) [[ NSNotificationCenter defaultCenter ] removeObserver : self name : AIApplicationDidFinishLoadingNotification * @brief Begin accepting Growl events [ GrowlApplicationBridge setGrowlDelegate : self ]; //Install our contact alert [ adium . contactAlertsController registerActionID : GROWL_EVENT_ALERT_IDENTIFIER withHandler : self ]; [ GrowlApplicationBridge notifyWithTitle : @"We have found a witch." description : @"May we burn her?" notificationName : CONTENT_MESSAGE_RECEIVED clickContext :[ NSDictionary dictionaryWithObjectsAndKeys : @"AIM.tekjew" , @"internalObjectID" , CONTENT_MESSAGE_RECEIVED , @"eventID" , #pragma mark AIActionHandler * @brief Returns a short description of Growl events - ( NSString * ) shortDescriptionForActionID: ( NSString * ) actionID * @brief Returns a long description of Growl events * The long description reflects the "sticky"-ness of the notification. - ( NSString * ) longDescriptionForActionID: ( NSString * ) actionID withDetails: ( NSDictionary * ) details if (([[ details objectForKey : KEY_GROWL_ALERT_TIME_STAMP ] boolValue ]) && ([[ details objectForKey : KEY_GROWL_ALERT_STICKY ] boolValue ])) { return GROWL_STICKY_TIME_STAMP_ALERT ; } else if ([[ details objectForKey : KEY_GROWL_ALERT_STICKY ] boolValue ]) { return GROWL_STICKY_ALERT ; } else if ([[ details objectForKey : KEY_GROWL_ALERT_TIME_STAMP ] boolValue ]) { return GROWL_TIME_STAMP_ALERT ; * @brief Returns the image associated with the Growl event - ( NSImage * ) imageForActionID: ( NSString * ) actionID return [ NSImage imageNamed : @"events-notification" forClass : [ self class ]]; * @brief Post a notification for Growl for display * This method is called when by Adium when a Growl alert is activated. It passes this information on to Growl, which displays a notificaion. * @param actionID The Action ID being performed, in this case the Growl plugin's Action ID. * @param listObject The list object the event is related to * @param details A dictionary containing additional information about the event * @param eventID The ID of the event (e.g. new message, contact went away, etc) * @param userInfo Any additional information - ( BOOL ) performActionID: ( NSString * ) actionID forListObject: ( AIListObject * ) listObject withDetails: ( NSDictionary * ) details triggeringEventID: ( NSString * ) eventID userInfo: ( id ) userInfo // Don't show growl notifications if we're silencing growl. if ([ adium . statusController . activeStatusState silencesGrowl ]) { // Get the chat if it's appropriate. if ([ userInfo respondsToSelector : @selector ( objectForKey : )]) { chat = [ userInfo objectForKey : @"AIChat" ]; AIContentObject * contentObject = [ userInfo objectForKey : @"AIContentObject" ]; if ( contentObject . source ) { listObject = contentObject . source ; // Add this event to the queue. NSString * queueKey = [ self eventQueueKeyForEventID : eventID inChat : chat ]; NSMutableArray * events = [ queuedEvents objectForKey : queueKey ]; events = [ NSMutableArray array ]; NSMutableDictionary * eventDetails = [ NSMutableDictionary dictionary ]; [ eventDetails setValue : listObject forKey : @"AIListObject" ]; [ eventDetails setValue : userInfo forKey : @"UserInfo" ]; [ eventDetails setValue : details forKey : @"Details" ]; [ events addObject : eventDetails ]; [ queuedEvents setValue : events forKey : queueKey ]; // wtb cancelPreviousPerformRequestsWithTarget:selector:object:object: NSDictionary * queueCall = [ NSDictionary dictionaryWithObjectsAndKeys : eventID , @"EventID" , chat , @"AIChat" , nil ]; // Trigger the queue to be cleared in GROWL_QUEUE_WAIT seconds. [ NSObject cancelPreviousPerformRequestsWithTarget : self selector : @selector ( clearQueue : ) [ self performSelector : @selector ( clearQueue : ) afterDelay : GROWL_QUEUE_WAIT ]; // If the queue has <GROWL_QUEUE_POST_COUNT entries already, post this one immediately. if ( events . count < GROWL_QUEUE_POST_COUNT ) { [ self postSingleEventID : eventID * @brief Returns our details pane, an instance of <tt>CBGrowlAlertDetailPane</tt> - ( AIActionDetailsPane * ) detailsPaneForActionID: ( NSString * ) actionID return [ CBGrowlAlertDetailPane actionDetailsPane ]; * @brief Allow multiple actions? * This action should not be performed multiple times for the same triggering event. - ( BOOL ) allowMultipleActionsWithID: ( NSString * ) actionID - ( NSString * ) eventQueueKeyForEventID: ( NSString * ) eventID return [ NSString stringWithFormat : @"%@-%@" , eventID , chat . internalObjectID ]; - ( void ) clearQueue: ( NSDictionary * ) callDict // Grab our actual arguments. NSString * eventID = [ callDict objectForKey : @"EventID" ]; AIChat * chat = [ callDict objectForKey : @"AIChat" ]; NSString * queueKey = [ self eventQueueKeyForEventID : eventID inChat : chat ]; NSMutableArray * events = [ queuedEvents objectForKey : queueKey ]; // If for some reason we don't have any events, bail. AILogWithSignature ( @"Called to clear queue with no events. EventID: %@ chat: %@" , eventID , chat ); // Remove the first GROWL_QUEUE_POST_COUNT entries, since we've already posted about them. NSRange removeRange = NSMakeRange ( 0 , ( events . count > GROWL_QUEUE_POST_COUNT ? GROWL_QUEUE_POST_COUNT : events . count )); [ events removeObjectsInRange : removeRange ]; // Seeing "1 message" is just silly! NSDictionary * event = [ events objectAtIndex : 0 ]; [ self postSingleEventID : eventID forListObject :[ event objectForKey : @"AIListObject" ] withDetails :[ event objectForKey : @"Details" ] userInfo :[ event objectForKey : @"UserInfo" ]]; } else if ( events . count ) { // We have a bunch of events; let's combine them. AIListObject * overallListObject = nil ; // If all events are from the same listObject, let's use that one in the message. NSArray * listObjects = [ events valueForKeyPath : @"@distinctUnionOfObjects.AIListObject" ]; if ( listObjects . count == 1 ) { overallListObject = [ listObjects objectAtIndex : 0 ]; AILog ( @"Posting multiple event - %@ %@ %@ %d" , eventID , overallListObject , chat , events . count ); // Use any random event for sticky. NSDictionary * anyEventDetails = [[ events objectAtIndex : 0 ] objectForKey : @"Details" ]; BOOL sticky = [[ anyEventDetails objectForKey : KEY_GROWL_ALERT_STICKY ] boolValue ]; unsigned priority = [[ anyEventDetails objectForKey : KEY_GROWL_PRIORITY ] unsignedIntValue ]; // Post the events combined. Use any random event to see if sticky. [ self postMultipleEventID : eventID forListObject : overallListObject // Clear our queue; we're done. [ queuedEvents setValue : nil forKey : queueKey ]; - ( void ) postSingleEventID: ( NSString * ) eventID forListObject :( AIListObject * ) listObject withDetails :( NSDictionary * ) details NSString * title , * description ; AIContentObject * contentObject = nil ; NSMutableDictionary * clickContext = [ NSMutableDictionary dictionary ]; NSString * identifier = nil ; //For a message event, listObject should become whoever sent the message if ([ adium . contactAlertsController isMessageEvent : eventID ] && [ userInfo respondsToSelector : @selector ( objectForKey : )] && [ userInfo objectForKey : @"AIContentObject" ]) { AIListObject * source = [ contentObject source ]; contentObject = [ userInfo objectForKey : @"AIContentObject" ]; chat = [ userInfo objectForKey : @"AIChat" ]; if ( source ) listObject = source ; [ clickContext setObject : eventID if ([ listObject isKindOfClass : [ AIListContact class ]]) { listObject = [( AIListContact * ) listObject parentContact ]; title = [ listObject longDisplayName ]; title = listObject . displayName ; iconData = [ listObject userIconData ]; iconData = [[ AIServiceIcons serviceIconForObject : listObject direction : AIIconNormal ] TIFFRepresentation ]; [ clickContext setObject : chat . uniqueChatID if ( chat && [ chat isGroupChat ]) { title = [ NSString stringWithFormat : @"%@ (%@)" , title , [ chat displayName ]]; if ([ userInfo isKindOfClass : [ ESFileTransfer class ]] && [ eventID isEqualToString : FILE_TRANSFER_COMPLETE ]) { [ clickContext setObject : [( ESFileTransfer * ) userInfo uniqueID ] forKey : KEY_FILE_TRANSFER_ID ]; [ clickContext setObject : listObject . internalObjectID forKey : KEY_LIST_OBJECT_ID ]; title = chat . displayName ; [ clickContext setObject : chat . uniqueChatID //If we have no listObject or we have a name, we are a group chat and //should use the account's service icon iconData = [[ AIServiceIcons serviceIconForObject : chat . account direction : AIIconNormal ] TIFFRepresentation ]; description = [[ adium contactAlertsController ] naturalLanguageDescriptionForEventID : eventID // Append event time stamp if preference is set if ([[ details objectForKey : KEY_GROWL_ALERT_TIME_STAMP ] boolValue ]) { NSDateFormatter * timeStampFormatter = [[ NSDateFormatter alloc ] init ]; [ timeStampFormatter setFormatterBehavior : NSDateFormatterBehaviorDefault ]; // Set the format to the user's system defined short style [ timeStampFormatter setTimeStyle : NSDateFormatterShortStyle ]; // For a message event use the contentObject's date otherwise use the current date NSDate * dateStamp = ( contentObject ) ? [ contentObject date ] : [ NSDate date ]; description = [ NSString stringWithFormat : AILocalizedString ( @"[%@] %@" , "A Growl notification with a timestamp. The first %@ is the timestamp, the second is the main string" ), [ timeStampFormatter stringFromDate : dateStamp ], description ]; [ timeStampFormatter release ]; if (([ eventID isEqualToString : CONTACT_STATUS_ONLINE_YES ] || [ eventID isEqualToString : CONTACT_STATUS_ONLINE_NO ] || [ eventID isEqualToString : CONTACT_STATUS_AWAY_YES ] || [ eventID isEqualToString : CONTACT_SEEN_ONLINE_YES ] || [ eventID isEqualToString : CONTACT_SEEN_ONLINE_NO ]) && [( AIListContact * ) listObject contactListStatusMessage ]) { NSString * statusMessage = [[ adium . contentController filterAttributedString : [( AIListContact * ) listObject contactListStatusMessage ] usingFilterType : AIFilterContactList direction : AIFilterIncoming context : listObject ] string ]; statusMessage = [[[ statusMessage stringByTrimmingCharactersInSet : [ NSCharacterSet whitespaceAndNewlineCharacterSet ]] mutableCopy ] autorelease ]; /* If the message contains line breaks, start it on a new line */ description = [ NSString stringWithFormat : @"%@:%@%@" , (([ statusMessage rangeOfLineBreakCharacter ]. location != NSNotFound ) ? @" \n " : @" " ), if ( listObject && [ adium . contactAlertsController isContactStatusEvent : eventID ]) { identifier = listObject . internalObjectID ; NSAssert5 (( title || description ), @"Growl notify error: EventID %@, listObject %@, userInfo %@ \n Gave Title \" %@ \" description \" %@ \" " , AILog ( @"Posting Growl notification: Event ID: %@, listObject: %@, chat: %@, description: %@" , eventID , listObject , chat , description ); [ GrowlApplicationBridge notifyWithTitle : title priority :[[ details objectForKey : KEY_GROWL_PRIORITY ] intValue ] isSticky :[[ details objectForKey : KEY_GROWL_ALERT_STICKY ] boolValue ] clickContext : clickContext - ( void ) postMultipleEventID: ( NSString * ) eventID priority :( signed int ) priority forListObject :( AIListObject * ) listObject withCount :( NSUInteger ) count NSString * title , * description ; NSMutableDictionary * clickContext = [ NSMutableDictionary dictionary ]; NSString * identifier = nil ; [ clickContext setObject : eventID if ([ listObject isKindOfClass : [ AIListContact class ]]) { listObject = [( AIListContact * ) listObject parentContact ]; title = [ listObject longDisplayName ]; title = listObject . displayName ; iconData = [ listObject userIconData ]; iconData = [[ AIServiceIcons serviceIconForObject : listObject direction : AIIconNormal ] TIFFRepresentation ]; [ clickContext setObject : chat . uniqueChatID [ clickContext setObject : listObject . internalObjectID forKey : KEY_LIST_OBJECT_ID ]; title = chat . displayName ; [ clickContext setObject : chat . uniqueChatID //If we have no listObject or we have a name, we are a group chat and //should use the account's service icon iconData = [[ AIServiceIcons serviceIconForObject : chat . account direction : AIIconNormal ] TIFFRepresentation ]; description = [ adium . contactAlertsController descriptionForCombinedEventID : eventID if ( listObject && [ adium . contactAlertsController isContactStatusEvent : eventID ]) { identifier = listObject . internalObjectID ; NSAssert5 (( title || description ), @"Growl notify error: EventID %@, listObject %@, chat %@ \n Gave Title \" %@ \" description \" %@ \" " , AILog ( @"Posting combined Growl notification: Event ID: %@, listObject: %@, chat: %@, description: %@" , eventID , listObject , chat , description ); [ GrowlApplicationBridge notifyWithTitle : title clickContext : clickContext * @brief Returns the application name Growl will use - ( NSString * ) applicationNameForGrowl * @brief Registration information for Growl * Returns information that Growl needs, like which notifications we will post and our application name. - ( NSDictionary * ) registrationDictionaryForGrowl id < AIContactAlertsController > contactAlertsController = adium . contactAlertsController ; NSArray * allNotes = [ contactAlertsController allEventIDs ]; NSMutableDictionary * humanReadableNames = [ NSMutableDictionary dictionary ]; NSMutableDictionary * descriptions = [ NSMutableDictionary dictionary ]; for ( eventID in allNotes ) { [ humanReadableNames setObject : [ contactAlertsController globalShortDescriptionForEventID : eventID ] [ descriptions setObject : [ contactAlertsController longDescriptionForEventID : eventID NSDictionary * growlReg = [ NSDictionary dictionaryWithObjectsAndKeys : allNotes , GROWL_NOTIFICATIONS_ALL , allNotes , GROWL_NOTIFICATIONS_DEFAULT , humanReadableNames , GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES , descriptions , GROWL_NOTIFICATIONS_DESCRIPTIONS , * @brief Called when Growl is ready * Currently, this is just used for debugging Growl. AILog ( @"Growl is go for launch." ); * @brief Called when a Growl notification is clicked * When a Growl notificaion is clicked, this method is called, allowing us to take action (e.g. open a new window, make * a conversation active, etc). * @param clickContext A dictionary that was passed to Growl when we installed the notification. - ( void ) growlNotificationWasClicked: ( NSDictionary * ) clickContext NSString * internalObjectID , * uniqueChatID ; AIListObject * listObject ; if (( internalObjectID = [ clickContext objectForKey : KEY_LIST_OBJECT_ID ])) { if (( listObject = [ adium . contactController existingListObjectWithUniqueID : internalObjectID ]) && ([ listObject isKindOfClass : [ AIListContact class ]])) { //First look for an existing chat to avoid changing anything if ( ! ( chat = [ adium . chatController existingChatWithContact : ( AIListContact * ) listObject ])) { //If we don't find one, create one chat = [ adium . chatController openChatWithContact : ( AIListContact * ) listObject } else if (( uniqueChatID = [ clickContext objectForKey : KEY_CHAT_ID ])) { chat = [ adium . chatController existingChatWithUniqueChatID : uniqueChatID ]; //If we didn't find a chat, it may have closed since the notification was posted. //If we have an appropriate existing list object, we can create a new chat. ( listObject = [ adium . contactController existingListObjectWithUniqueID : uniqueChatID ]) && ([ listObject isKindOfClass : [ AIListContact class ]])) { //If the uniqueChatID led us to an existing contact, create a chat with it chat = [ adium . chatController openChatWithContact : ( AIListContact * ) listObject NSString * fileTransferID ; if (( fileTransferID = [ clickContext objectForKey : KEY_FILE_TRANSFER_ID ])) { //If a file transfer notification is clicked, reveal the file [[ ESFileTransfer existingFileTransferWithID : fileTransferID ] reveal ]; [ adium . interfaceController setActiveChat : chat ]; //Make Adium active (needed if, for example, our notification was clicked with another app active) [ NSApp activateIgnoringOtherApps : YES ];