Skip to content

Commit 54bcbfe

Browse files
committed
model: Add TypingNotifier.
The debugEnable setup is borrowed from debugEnableRegisterNotificationToken. This acts as a hook to temporarily disable typing notifications in later widget tests. The documentation and implementation are based on the web app: https://github.com/zulip/zulip/blob/52a9846cdf4abfbe937a94559690d508e95f4065/web/shared/src/typing_status.ts Signed-off-by: Zixuan James Li <[email protected]>
1 parent 06f4725 commit 54bcbfe

File tree

3 files changed

+402
-1
lines changed

3 files changed

+402
-1
lines changed

lib/model/store.dart

+7
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore {
249249
selfUserId: account.userId,
250250
typingStartedExpiryPeriod: Duration(milliseconds: initialSnapshot.serverTypingStartedExpiryPeriodMilliseconds),
251251
),
252+
typingNotifier: TypingNotifier(
253+
typingStoppedWaitPeriod: Duration(milliseconds: initialSnapshot.serverTypingStoppedWaitPeriodMilliseconds),
254+
typingStartedWaitPeriod: Duration(milliseconds: initialSnapshot.serverTypingStartedWaitPeriodMilliseconds),
255+
),
252256
channels: channels,
253257
messages: MessageStoreImpl(),
254258
unreads: Unreads(
@@ -276,6 +280,7 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore {
276280
required this.userSettings,
277281
required this.users,
278282
required this.typingStatus,
283+
required this.typingNotifier,
279284
required ChannelStoreImpl channels,
280285
required MessageStoreImpl messages,
281286
required this.unreads,
@@ -342,6 +347,7 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore {
342347
final Map<int, User> users;
343348

344349
final TypingStatus typingStatus;
350+
final TypingNotifier typingNotifier;
345351

346352
////////////////////////////////
347353
// Streams, topics, and stuff about them.
@@ -409,6 +415,7 @@ class PerAccountStore extends ChangeNotifier with ChannelStore, MessageStore {
409415
recentDmConversationsView.dispose();
410416
unreads.dispose();
411417
_messages.dispose();
418+
typingNotifier.dispose();
412419
typingStatus.dispose();
413420
super.dispose();
414421
}

lib/model/typing_status.dart

+125-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import 'dart:async';
22

3-
import 'package:flutter/foundation.dart';
3+
import 'package:clock/clock.dart';
4+
import 'package:flutter/widgets.dart';
45

56
import '../api/model/events.dart';
7+
import '../api/route/messages.dart';
8+
import '../api/route/typing.dart';
69
import 'narrow.dart';
10+
import 'store.dart';
711

812
/// The model for tracking the typing status organized by narrows.
913
///
@@ -84,3 +88,123 @@ class TypingStatus extends ChangeNotifier {
8488
}
8589
}
8690
}
91+
92+
/// Manages updates to the user's typing status.
93+
///
94+
/// See the server implementation:
95+
/// https://github.com/zulip/zulip/blob/52a9846cdf4abfbe937a94559690d508e95f4065/web/shared/src/typing_status.ts
96+
class TypingNotifier {
97+
TypingNotifier({
98+
required this.typingStoppedWaitPeriod,
99+
required this.typingStartedWaitPeriod,
100+
});
101+
102+
final Duration typingStoppedWaitPeriod;
103+
final Duration typingStartedWaitPeriod;
104+
105+
SendableNarrow? _currentDestination;
106+
Stopwatch? _lastNotifyStart;
107+
Timer? _idleTimer;
108+
109+
void dispose() {
110+
_idleTimer?.cancel();
111+
}
112+
113+
/// Update the store, and notify the server as needed, on the user's typing
114+
/// status.
115+
///
116+
/// This can and should be called frequently, on each keystroke. The
117+
/// implementation sends "still typing" notices at an appropriate throttled
118+
/// rate, and keeps a timer to send a "stopped typing" notice when the user
119+
/// hasn't typed for a few seconds.
120+
///
121+
/// Call with `destination` as `null` when the user actively stops composing a
122+
/// message. If the user switches from one destination to another, there's no
123+
/// need to call with `null` in between; the implementation tracks the change
124+
/// and behaves appropriately.
125+
///
126+
/// See docs/subsystems/typing-indicators.md for detailed background on the
127+
/// typing indicators system.
128+
void handleTypingStatusUpdate(PerAccountStore store, SendableNarrow? destination) {
129+
if (!DebugTypingNotifier.debugEnable) return;
130+
131+
if (_currentDestination != null) {
132+
if (destination == _currentDestination) {
133+
_startOrExtendIdleTimer(store);
134+
_maybePingServer(store);
135+
return;
136+
}
137+
138+
_stopLastNotification(store);
139+
}
140+
141+
if (destination == null) return;
142+
143+
_currentDestination = destination;
144+
_startOrExtendIdleTimer(store);
145+
_actuallyPingServer(store);
146+
}
147+
148+
void _startOrExtendIdleTimer(PerAccountStore store) {
149+
_idleTimer?.cancel();
150+
_idleTimer = Timer(typingStoppedWaitPeriod, () => _stopLastNotification(store));
151+
}
152+
153+
Future<void> _maybePingServer(PerAccountStore store) async {
154+
if (_lastNotifyStart == null || _lastNotifyStart!.elapsed > typingStartedWaitPeriod) {
155+
await _actuallyPingServer(store);
156+
}
157+
}
158+
159+
Future<void> _actuallyPingServer(PerAccountStore store) {
160+
_lastNotifyStart = clock.stopwatch()..start();
161+
162+
return setTypingStatus(store.connection,
163+
op: TypingOp.start,
164+
destination: MessageDestination.fromSendableNarrow(_currentDestination!),
165+
);
166+
}
167+
168+
Future<void> _stopLastNotification(PerAccountStore store) {
169+
assert(_currentDestination != null);
170+
final destination = _currentDestination!;
171+
172+
_idleTimer?.cancel();
173+
_currentDestination = null;
174+
_lastNotifyStart = null;
175+
176+
return setTypingStatus(store.connection,
177+
op: TypingOp.stop,
178+
destination: MessageDestination.fromSendableNarrow(destination),
179+
);
180+
}
181+
}
182+
183+
/// A namespace for [TypingNotifier]'s debugging symbols.
184+
// This is for uncluttering [TypingNotifier] while still offering some testing
185+
// capabilities.
186+
abstract final class DebugTypingNotifier {
187+
/// In debug mode, controls whether typing notifications should be sent.
188+
///
189+
/// Outside of debug mode, this is always true and the setter has no effect.
190+
static bool get debugEnable {
191+
bool result = true;
192+
assert(() {
193+
result = _debugEnable;
194+
return true;
195+
}());
196+
return result;
197+
}
198+
static bool _debugEnable = true;
199+
static set debugEnable(bool value) {
200+
assert(() {
201+
_debugEnable = value;
202+
return true;
203+
}());
204+
}
205+
206+
@visibleForTesting
207+
static void debugReset() {
208+
_debugEnable = true;
209+
}
210+
}

0 commit comments

Comments
 (0)