Skip to content

Commit f1c68fd

Browse files
committed
model: Add view-model for typing status.
Using SendableNarrow as the key covers the narrows where typing notifications are supported (topics and dms). Consumers of the typing status will only be notified if a typist has been added or removed from any of the narrows. At the moment, getTypistIdsInNarrow is unused. It will get exercised by the UI code that implements the typing indicator. Signed-off-by: Zixuan James Li <[email protected]>
1 parent eddb1ba commit f1c68fd

File tree

3 files changed

+375
-0
lines changed

3 files changed

+375
-0
lines changed

lib/model/store.dart

+14
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import 'message.dart';
2424
import 'message_list.dart';
2525
import 'recent_dm_conversations.dart';
2626
import 'stream.dart';
27+
import 'typing_status.dart';
2728
import 'unreads.dart';
2829

2930
export 'package:drift/drift.dart' show Value;
@@ -251,6 +252,12 @@ class PerAccountStore extends ChangeNotifier with StreamStore, MessageStore {
251252
),
252253
recentDmConversationsView: RecentDmConversationsView(
253254
initial: initialSnapshot.recentPrivateConversations, selfUserId: account.userId),
255+
typingStatus: TypingStatus(
256+
selfUserId: account.userId,
257+
typingStartedExpiryPeriod: Duration(milliseconds: initialSnapshot.serverTypingStartedExpiryPeriodMilliseconds),
258+
typingStoppedWaitPeriod: Duration(milliseconds: initialSnapshot.serverTypingStoppedWaitPeriodMilliseconds),
259+
typingStartedWaitPeriod: Duration(milliseconds: initialSnapshot.serverTypingStartedWaitPeriodMilliseconds),
260+
)
254261
);
255262
}
256263

@@ -270,6 +277,7 @@ class PerAccountStore extends ChangeNotifier with StreamStore, MessageStore {
270277
required MessageStoreImpl messages,
271278
required this.unreads,
272279
required this.recentDmConversationsView,
280+
required this.typingStatus,
273281
}) : assert(selfUserId == globalStore.getAccount(accountId)!.userId),
274282
assert(realmUrl == globalStore.getAccount(accountId)!.realmUrl),
275283
assert(realmUrl == connection.realmUrl),
@@ -361,6 +369,8 @@ class PerAccountStore extends ChangeNotifier with StreamStore, MessageStore {
361369

362370
final RecentDmConversationsView recentDmConversationsView;
363371

372+
final TypingStatus typingStatus;
373+
364374
////////////////////////////////
365375
// Other digests of data.
366376

@@ -380,6 +390,7 @@ class PerAccountStore extends ChangeNotifier with StreamStore, MessageStore {
380390

381391
@override
382392
void dispose() {
393+
typingStatus.dispose();
383394
recentDmConversationsView.dispose();
384395
unreads.dispose();
385396
_messages.dispose();
@@ -485,6 +496,9 @@ class PerAccountStore extends ChangeNotifier with StreamStore, MessageStore {
485496
assert(debugLog("server event: update_message_flags/${event.op} ${event.flag.toJson()}"));
486497
_messages.handleUpdateMessageFlagsEvent(event);
487498
unreads.handleUpdateMessageFlagsEvent(event);
499+
} else if (event is TypingEvent) {
500+
assert(debugLog("server event: typing/${event.op} ${event.messageType}"));
501+
typingStatus.handleTypingEvent(event);
488502
} else if (event is ReactionEvent) {
489503
assert(debugLog("server event: reaction/${event.op}"));
490504
_messages.handleReactionEvent(event);

lib/model/typing_status.dart

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter/foundation.dart';
4+
5+
import '../api/model/events.dart';
6+
import 'narrow.dart';
7+
8+
/// The view-model for tracking the typing status organized by narrows.
9+
class TypingStatus extends ChangeNotifier {
10+
@visibleForTesting
11+
final Map<SendableNarrow, Map<int, Timer>> typistIdsByNarrow = {};
12+
13+
final int selfUserId;
14+
final Duration typingStartedExpiryPeriod;
15+
final Duration typingStoppedWaitPeriod;
16+
final Duration typingStartedWaitPeriod;
17+
18+
TypingStatus({
19+
required this.selfUserId,
20+
required this.typingStartedExpiryPeriod,
21+
required this.typingStoppedWaitPeriod,
22+
required this.typingStartedWaitPeriod,
23+
});
24+
25+
@override
26+
void dispose() {
27+
for (final typistTimer in typistIdsByNarrow.values) {
28+
for (final timer in typistTimer.values) {
29+
timer.cancel();
30+
}
31+
}
32+
super.dispose();
33+
}
34+
35+
bool _addTypist(SendableNarrow narrow, int typistUserId, void Function() callback) {
36+
final narrowTimerMap = typistIdsByNarrow[narrow] ??= {};
37+
final typistTimer = narrowTimerMap[typistUserId];
38+
final isNewTypist = typistTimer == null;
39+
typistTimer?.cancel();
40+
narrowTimerMap[typistUserId] = Timer(typingStartedExpiryPeriod, callback);
41+
return isNewTypist;
42+
}
43+
44+
bool _removeTypist(SendableNarrow narrow, int typistUserId) {
45+
final narrowTimerMap = typistIdsByNarrow[narrow];
46+
final typistTimer = narrowTimerMap?[typistUserId];
47+
if (narrowTimerMap == null || typistTimer == null) {
48+
return false;
49+
}
50+
typistTimer.cancel();
51+
narrowTimerMap.remove(typistUserId);
52+
if (narrowTimerMap.isEmpty) typistIdsByNarrow.remove(narrow);
53+
return true;
54+
}
55+
56+
Iterable<int> getTypistIdsInNarrow(SendableNarrow narrow) {
57+
return typistIdsByNarrow[narrow]?.keys ?? [];
58+
}
59+
60+
void handleTypingEvent(TypingEvent event) {
61+
SendableNarrow narrow = switch (event.messageType) {
62+
MessageType.private => DmNarrow(
63+
allRecipientIds: event.recipientIds!..sort(), selfUserId: selfUserId),
64+
MessageType.stream => TopicNarrow(event.streamId!, event.topic!),
65+
};
66+
67+
bool hasUpdate = false;
68+
switch (event.op) {
69+
case TypingOp.start:
70+
hasUpdate = _addTypist(narrow, event.senderId, () {
71+
if (_removeTypist(narrow, event.senderId)) {
72+
notifyListeners();
73+
}
74+
});
75+
case TypingOp.stop:
76+
hasUpdate = _removeTypist(narrow, event.senderId);
77+
}
78+
79+
if (hasUpdate) {
80+
notifyListeners();
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)