|
1 | 1 | import 'dart:async';
|
2 | 2 |
|
3 |
| -import 'package:flutter/foundation.dart'; |
| 3 | +import 'package:clock/clock.dart'; |
| 4 | +import 'package:flutter/widgets.dart'; |
4 | 5 |
|
5 | 6 | import '../api/model/events.dart';
|
| 7 | +import '../api/route/messages.dart'; |
| 8 | +import '../api/route/typing.dart'; |
6 | 9 | import 'narrow.dart';
|
| 10 | +import 'store.dart'; |
7 | 11 |
|
8 | 12 | /// The model for tracking the typing status organized by narrows.
|
9 | 13 | ///
|
@@ -84,3 +88,123 @@ class TypingStatus extends ChangeNotifier {
|
84 | 88 | }
|
85 | 89 | }
|
86 | 90 | }
|
| 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