@@ -2,7 +2,10 @@ import 'dart:async';
2
2
3
3
import 'package:flutter/foundation.dart' ;
4
4
5
+ import '../api/core.dart' ;
5
6
import '../api/model/events.dart' ;
7
+ import '../api/route/typing.dart' ;
8
+ import 'binding.dart' ;
6
9
import 'narrow.dart' ;
7
10
8
11
/// The model for tracking the typing status organized by narrows.
@@ -84,3 +87,148 @@ class TypingStatus extends ChangeNotifier {
84
87
}
85
88
}
86
89
}
90
+
91
+ /// Sends the self-user's typing-status updates.
92
+ ///
93
+ /// See also:
94
+ /// * https://github.com/zulip/zulip/blob/52a9846cdf4abfbe937a94559690d508e95f4065/web/shared/src/typing_status.ts
95
+ /// * https://zulip.readthedocs.io/en/latest/subsystems/typing-indicators.html
96
+ class TypingNotifier {
97
+ TypingNotifier ({
98
+ required this .connection,
99
+ required this .typingStoppedWaitPeriod,
100
+ required this .typingStartedWaitPeriod,
101
+ });
102
+
103
+ final ApiConnection connection;
104
+ final Duration typingStoppedWaitPeriod;
105
+ final Duration typingStartedWaitPeriod;
106
+
107
+ SendableNarrow ? _currentDestination;
108
+
109
+ /// Records time elapsed since the last time we notify the server;
110
+ /// this is `null` when the user is not actively typing.
111
+ Stopwatch ? _sinceLastPing;
112
+
113
+ /// A timer that resets on every [keystroke] .
114
+ ///
115
+ /// Upon its expiry, the user is considered idle and
116
+ /// a "typing stopped" notice will be sent.
117
+ Timer ? _idleTimer;
118
+
119
+ void dispose () {
120
+ _idleTimer? .cancel ();
121
+ }
122
+
123
+ /// Updates the server, if needed, that a keystroke was made when
124
+ /// composing a new message to [destination] .
125
+ ///
126
+ /// To be called on all keystrokes in the composing session.
127
+ /// Sends "typing started" notices, throttled appropriately,
128
+ /// for repeated calls to the same [destination] .
129
+ ///
130
+ /// If [destination] differs from the previous call, such as after a topic
131
+ /// input change, sends a "typing stopped" notice for the old destination.
132
+ ///
133
+ /// Keeps a timer to send a "typing stopped" notice when this and
134
+ /// [stoppedComposing] haven't been called in some time.
135
+ void keystroke (SendableNarrow destination) {
136
+ if (! debugEnable) return ;
137
+
138
+ if (_currentDestination != null ) {
139
+ if (destination == _currentDestination) {
140
+ // Nothing has really changed, except we may need
141
+ // to send a ping to the server and extend out our idle time.
142
+ if (_sinceLastPing! .elapsed > typingStartedWaitPeriod) {
143
+ _actuallyPingServer ();
144
+ }
145
+ _startOrExtendIdleTimer ();
146
+ return ;
147
+ }
148
+
149
+ _stopLastNotification ();
150
+ }
151
+
152
+ // We just started typing to this destination, so notify the server.
153
+ _currentDestination = destination;
154
+ _startOrExtendIdleTimer ();
155
+ _actuallyPingServer ();
156
+ }
157
+
158
+ /// Sends the server a "typing stopped" notice for the destination of
159
+ /// the current composing session, if there is one.
160
+ ///
161
+ /// To be called on cues that the user has exited a new-message composing session,
162
+ /// e.g., send button tapped, compose box unfocused, nav changed, app quit.
163
+ ///
164
+ /// If [keystroke] hasn't been called in some time, does nothing.
165
+ ///
166
+ /// Otherwise:
167
+ /// - Users will see our user's typing indicator disappear immediately
168
+ /// instead of after [keystroke] 's timer.
169
+ /// - [keystroke] 's timer is canceled.
170
+ ///
171
+ /// (This has no "destination" param because the user can really only compose
172
+ /// to one destination at a time. This function acts on the current session
173
+ /// regardless of its destination.)
174
+ void stoppedComposing () {
175
+ if (! debugEnable) return ;
176
+
177
+ if (_currentDestination != null ) {
178
+ _stopLastNotification ();
179
+ }
180
+ }
181
+
182
+ void _startOrExtendIdleTimer () {
183
+ _idleTimer? .cancel ();
184
+ _idleTimer = Timer (typingStoppedWaitPeriod, _stopLastNotification);
185
+ }
186
+
187
+ void _actuallyPingServer () {
188
+ // This allows us to use [clock.stopwatch] only when testing.
189
+ _sinceLastPing = ZulipBinding .instance.stopwatch ()..start ();
190
+
191
+ unawaited (setTypingStatus (
192
+ connection,
193
+ op: TypingOp .start,
194
+ destination: _currentDestination! .destination));
195
+ }
196
+
197
+ void _stopLastNotification () {
198
+ assert (_currentDestination != null );
199
+ final destination = _currentDestination! ;
200
+
201
+ _idleTimer! .cancel ();
202
+ _currentDestination = null ;
203
+ _sinceLastPing = null ;
204
+
205
+ unawaited (setTypingStatus (
206
+ connection,
207
+ op: TypingOp .stop,
208
+ destination: destination.destination));
209
+ }
210
+
211
+ /// In debug mode, controls whether typing notices should be sent.
212
+ ///
213
+ /// Outside of debug mode, this is always true and the setter has no effect.
214
+ static bool get debugEnable {
215
+ bool result = true ;
216
+ assert (() {
217
+ result = _debugEnable;
218
+ return true ;
219
+ }());
220
+ return result;
221
+ }
222
+ static bool _debugEnable = true ;
223
+ static set debugEnable (bool value) {
224
+ assert (() {
225
+ _debugEnable = value;
226
+ return true ;
227
+ }());
228
+ }
229
+
230
+ @visibleForTesting
231
+ static void debugReset () {
232
+ _debugEnable = true ;
233
+ }
234
+ }
0 commit comments