Merged [13645] and [13646]: Fixed one of the longest-standing Adium bugs: The Color...
[adiumx.git] / Source / AITypingNotificationPlugin.m
bloba54228af7c9a4f15710a61febf671ebc4e996434
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 "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;
31 @end
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
40 /*!
41  * @class AITypingNotificationPlugin
42  * @brief Component to send typing notifications in open chats
43  *
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.
46  */
47 @implementation AITypingNotificationPlugin
49 /*!
50  * @brief Install
51  */
52 - (void)installPlugin
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
61                                                                          object:nil];
64 //Text entry -----------------------------------------------------------------------------------------------------------
65 /*!
66  * @brief Text entry view was opened
67  *
68  * Sent because we are a text entry view filter; ignored.
69  */
70 - (void)didOpenTextEntryView:(NSText<AITextEntryView> *)inTextEntryView {};
72 /*!
73  * @brief Text entry view will close
74  *
75  * Be sure to clear the typing state of a chat when its text entry view closes
76  */
77 - (void)willCloseTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
79     AIChat              *chat = [inTextEntryView chat];
80         
81     //Send a 'not-typing' message to this chat
82     if([chat integerStatusObjectForKey:WE_ARE_TYPING] != AINotTyping){
83         [self _sendTypingState:AINotTyping toChat:chat];
84     }
85         
86         //Remove our typing timer
87         [self _removeTypingTimer:[chat statusObjectForKey:ENTERED_TEXT_TIMER] forChat:chat];
90 /*!
91  * @brief A string was added to a text entry view
92  */
93 - (void)stringAdded:(NSString *)inString toTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
95     [self _processTypingInView:inTextEntryView];
98 /*!
99  * @brief The contents of a text entry view changed
100  */
101 - (void)contentsChangedInTextEntryView:(NSText<AITextEntryView> *)inTextEntryView
103         if([inTextEntryView isSendingContent]){
104                 [self _processTypingInView:inTextEntryView];
105         }else{
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];
115         }
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.
125  */
126 - (void)_processTypingInView:(NSText<AITextEntryView> *)inTextEntryView
128     AIChat              *chat = [inTextEntryView chat];
129         
130     if(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];
137                 
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];
145                         }else{
146                                 [self _addTypingTimerForChat:chat];
147                         }
149                 }else{ //User is not typing
150                         currentTypingState = AINotTyping;
151                         [self _removeTypingTimer:enteredTextTimer forChat:chat];
152                 }
153                 
154                 //We don't want to send the same typing value more than once
155         if(previousTypingState != currentTypingState){
156                         [self _sendTypingState:currentTypingState toChat:chat];
157                 }
158     }    
161 /* 
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.
169  */
170 - (void)didSendMessage:(NSNotification *)notification
172         AIChat  *chat = [notification object];
173         
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
178                                            notify:NotifyNever];
179                 
180                 //Clear the flag after a short delay
181                 [self performSelector:@selector(_removeSuppressFlagFromChat:) withObject:chat afterDelay:0.0000001];
182         }
186  * @brief Remove the typing suppression for a chat
187  */
188 - (void)_removeSuppressFlagFromChat:(AIChat *)chat
190         [chat setStatusObject:nil
191                                    forKey:KEY_TEMP_SUPPRESS_TYPING_NOTIFICATIONS
192                                    notify:NotifyNever];
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.
201  */
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
210                                                                                                   withSource:account
211                                                                                                  destination:nil
212                                                                                                  typingState:typingState];
213                 [[adium contentController] sendContentObject:contentObject];
214     }
216     //Remember the state
217         [chat setStatusObject:(typingState != AINotTyping ? [NSNumber numberWithInt:typingState] : nil)
218                                    forKey:WE_ARE_TYPING
219                                    notify:NotifyNever];
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
228  */
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
240  */
241 - (void)_addTypingTimerForChat:(AIChat *)chat
243         NSTimer *enteredTextTimer = [NSTimer scheduledTimerWithTimeInterval:ENTERED_TEXT_INTERVAL
244                                                                                                                                  target:self
245                                                                                                                            selector:@selector(_switchToEnteredText:)
246                                                                                                                            userInfo:chat
247                                                                                                                                 repeats:NO];
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
255  */
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
263  */
264 - (void)_removeTypingTimer:(NSTimer *)enteredTextTimer forChat:(AIChat *)chat
266         [enteredTextTimer invalidate];
267         [chat setStatusObject:nil forKey:ENTERED_TEXT_TIMER notify:NotifyNever];
270 @end