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 "AIContentController.h"
18 #import "AIInterfaceController.h"
19 #import "AIPreferenceController.h"
20 #import "AITypingNotificationPlugin.h"
21 #import <Adium/AIAccount.h>
22 #import <Adium/AIChat.h>
23 #import <Adium/AIContentTyping.h>
25 @interface AITypingNotificationPlugin (PRIVATE)
26 - (void)_sendTypingState:(AITypingState)typingState toChat:(AIChat *)chat;
27 - (void)_processTypingInView:(NSText<AITextEntryView> *)inTextEntryView;
28 - (void)_addTypingTimerForChat:(AIChat *)chat;
29 - (void)_resetTypingTimer:(NSTimer *)enteredTextTimer forChat:(AIChat *)chat;
30 - (void)_removeTypingTimer:(NSTimer *)enteredTextTimer forChat:(AIChat *)chat;
33 #define WE_ARE_TYPING @"WeAreTyping"
35 #define ENTERED_TEXT_TIMER @"EnteredTextTimer"
36 #define ENTERED_TEXT_INTERVAL 3.0
38 #define CONTENT_CHANGED_PROCESS_DELAY 2.0
41 * @class AITypingNotificationPlugin
42 * @brief Component to send typing notifications in open chats
44 * The possible typing notifications are 'actively typing', 'entered text', and 'not typing'.
45 * Not all protocols will support the 'entered text' notification; it may be treated as actively typing as appropriate.
47 @implementation AITypingNotificationPlugin
54 //Register as an entry filter and observe content
55 [[adium contentController] registerTextEntryFilter:self];
57 //Observe message sending
58 [[adium notificationCenter] addObserver:self
59 selector:@selector(didSendMessage:)
60 name:Interface_DidSendEnteredMessage
64 //Text entry -----------------------------------------------------------------------------------------------------------
66 * @brief Text entry view was opened
68 * Sent because we are a text entry view filter; ignored.
70 - (void)didOpenTextEntryView:(NSText<AITextEntryView> *)inTextEntryView {};
73 * @brief Text entry view will close
75 * Be sure to clear the typing state of a chat when its text entry view closes
77 - (void)willCloseTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
79 AIChat *chat = [inTextEntryView chat];
81 //Send a 'not-typing' message to this chat
82 if([chat integerStatusObjectForKey:WE_ARE_TYPING] != AINotTyping){
83 [self _sendTypingState:AINotTyping toChat:chat];
86 //Remove our typing timer
87 [self _removeTypingTimer:[chat statusObjectForKey:ENTERED_TEXT_TIMER] forChat:chat];
91 * @brief A string was added to a text entry view
93 - (void)stringAdded:(NSString *)inString toTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
95 [self _processTypingInView:inTextEntryView];
99 * @brief The contents of a text entry view changed
101 - (void)contentsChangedInTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
103 if([inTextEntryView isSendingContent]){
104 [self _processTypingInView:inTextEntryView];
106 /* Delay one second before processing the large typing change, to prevent 'flickering' if the view is cleared
107 * and then typing begins again. Cancel previous delayed changes before queuing this one. */
108 [NSObject cancelPreviousPerformRequestsWithTarget:self
109 selector:@selector(_processTypingInView:)
110 object:inTextEntryView];
112 [self performSelector:@selector(_processTypingInView:)
113 withObject:inTextEntryView
114 afterDelay:CONTENT_CHANGED_PROCESS_DELAY];
119 * @brief Process the current typing state in a text entry view
121 * When the user makes a change or adds text, mark the chat with an AITyping state.
122 * After a timeout with no changes, change that state to AIEnteredText.
124 * When the user makes a change resulting in an empty text view, however, clear the typing state.
126 - (void)_processTypingInView:(NSText<AITextEntryView> *)inTextEntryView
128 AIChat *chat = [inTextEntryView chat];
131 NSTimer *enteredTextTimer;
132 NSNumber *previousTypingNumber = [chat statusObjectForKey:WE_ARE_TYPING];
133 AITypingState previousTypingState = (previousTypingNumber ? [previousTypingNumber intValue] : AINotTyping);
134 AITypingState currentTypingState;
136 enteredTextTimer = [chat statusObjectForKey:ENTERED_TEXT_TIMER];
138 //Determine if this change indicated the user was typing or indicated the user had no longer entered text
139 if([[inTextEntryView textStorage] length] != 0){ //User is typing
141 currentTypingState = AITyping;
143 if(enteredTextTimer){
144 [self _resetTypingTimer:enteredTextTimer forChat:chat];
146 [self _addTypingTimerForChat:chat];
149 }else{ //User is not typing
150 currentTypingState = AINotTyping;
151 [self _removeTypingTimer:enteredTextTimer forChat:chat];
154 //We don't want to send the same typing value more than once
155 if(previousTypingState != currentTypingState){
156 [self _sendTypingState:currentTypingState toChat:chat];
162 * @brief Suppress typing notifications when sending a message
164 * Some protocols require a 'Stopped typing' notification to be sent along with an instant message. Other protocols
165 * implicitly assume that typing has stopped with an incoming message and the extraneous typing notification may cause
166 * strange behavior. To prevent this, we allow accounts to suppress these typing notifications.
168 * This notification will be received before the text entry view is cleared after sending.
170 - (void)didSendMessage:(NSNotification *)notification
172 AIChat *chat = [notification object];
174 if([[chat account] suppressTypingNotificationChangesAfterSend]){
175 //Set the suppress typing flag for this chat
176 [chat setStatusObject:[NSNumber numberWithBool:YES]
177 forKey:KEY_TEMP_SUPPRESS_TYPING_NOTIFICATIONS
180 //Clear the flag after a short delay
181 [self performSelector:@selector(_removeSuppressFlagFromChat:) withObject:chat afterDelay:0.0000001];
186 * @brief Remove the typing suppression for a chat
188 - (void)_removeSuppressFlagFromChat:(AIChat *)chat
190 [chat setStatusObject:nil
191 forKey:KEY_TEMP_SUPPRESS_TYPING_NOTIFICATIONS
195 //Typing state ---------------------------------------------------------------------------------------------------------
197 * @brief Send an AIContentTyping object for an AITypingState on a given chat
199 * The chat determines whether the notification should be sent or not, based on the account preference and, possibly,
200 * the temporary suppression status object.
202 - (void)_sendTypingState:(AITypingState)typingState toChat:(AIChat *)chat
204 if([chat sendTypingNotificationsForNewTypingState:typingState]){
205 AIAccount *account = [chat account];
206 AIContentTyping *contentObject;
208 //Send typing content object (It will go directly to the account since typing content isn't tracked or filtered)
209 contentObject = [AIContentTyping typingContentInChat:chat
212 typingState:typingState];
213 [[adium contentController] sendContentObject:contentObject];
217 [chat setStatusObject:(typingState != AINotTyping ? [NSNumber numberWithInt:typingState] : nil)
223 * @brief Switch the typing state to AIEnteredText
225 * Called after a timeout when the user has entered text but is not actively typing
227 * @param inTimer An NSTimer whose userInfo is an AIChat
229 - (void)_switchToEnteredText:(NSTimer *)inTimer
231 AIChat *chat = [inTimer userInfo];
232 [self _sendTypingState:AIEnteredText toChat:chat];
233 [self _removeTypingTimer:[chat statusObjectForKey:ENTERED_TEXT_TIMER] forChat:chat];
237 //Typing timer ---------------------------------------------------------------------------------------------------------
239 * @brief Add the timer responsible for detecting when the user stops typing
241 - (void)_addTypingTimerForChat:(AIChat *)chat
243 NSTimer *enteredTextTimer = [NSTimer scheduledTimerWithTimeInterval:ENTERED_TEXT_INTERVAL
245 selector:@selector(_switchToEnteredText:)
248 [chat setStatusObject:enteredTextTimer forKey:ENTERED_TEXT_TIMER notify:NotifyNever];
252 * @brief Reset the timer responsible for detecting when the user stops typing
254 * This is done because it is cheaper than removing the old timer and adding a new one
256 - (void)_resetTypingTimer:(NSTimer *)enteredTextTimer forChat:(AIChat *)chat
258 [enteredTextTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:ENTERED_TEXT_INTERVAL]];
262 * @brief Remove the timer responsible for detecting when the user stops typing
264 - (void)_removeTypingTimer:(NSTimer *)enteredTextTimer forChat:(AIChat *)chat
266 [enteredTextTimer invalidate];
267 [chat setStatusObject:nil forKey:ENTERED_TEXT_TIMER notify:NotifyNever];