Skip to content

Commit afb464b

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 b725b5e commit afb464b

File tree

3 files changed

+364
-0
lines changed

3 files changed

+364
-0
lines changed

lib/model/store.dart

Lines changed: 13 additions & 0 deletions
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

@@ -485,6 +495,9 @@ class PerAccountStore extends ChangeNotifier with StreamStore, MessageStore {
485495
assert(debugLog("server event: update_message_flags/${event.op} ${event.flag.toJson()}"));
486496
_messages.handleUpdateMessageFlagsEvent(event);
487497
unreads.handleUpdateMessageFlagsEvent(event);
498+
} else if (event is TypingEvent) {
499+
assert(debugLog("server event: typing/${event.op} ${event.messageType}"));
500+
typingStatus.handleTypingEvent(event);
488501
} else if (event is ReactionEvent) {
489502
assert(debugLog("server event: reaction/${event.op}"));
490503
_messages.handleReactionEvent(event);

lib/model/typing_status.dart

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
bool _addTypist(SendableNarrow narrow, int typistUserId, void Function() callback) {
26+
final narrowTimerMap = typistIdsByNarrow[narrow] ??= {};
27+
final typistTimer = narrowTimerMap[typistUserId];
28+
final isNewTypist = typistTimer == null;
29+
typistTimer?.cancel();
30+
narrowTimerMap[typistUserId] = Timer(typingStartedExpiryPeriod, callback);
31+
return isNewTypist;
32+
}
33+
34+
bool _removeTypist(SendableNarrow narrow, int typistUserId) {
35+
final narrowTimerMap = typistIdsByNarrow[narrow];
36+
final typistTimer = narrowTimerMap?[typistUserId];
37+
if (narrowTimerMap == null || typistTimer == null) {
38+
return false;
39+
}
40+
typistTimer.cancel();
41+
narrowTimerMap.remove(typistUserId);
42+
if (narrowTimerMap.isEmpty) typistIdsByNarrow.remove(narrow);
43+
return true;
44+
}
45+
46+
Iterable<int> getTypistIdsInNarrow(SendableNarrow narrow) {
47+
return typistIdsByNarrow[narrow]?.keys ?? [];
48+
}
49+
50+
void handleTypingEvent(TypingEvent event) {
51+
SendableNarrow narrow = switch (event.messageType) {
52+
MessageType.private => DmNarrow(
53+
allRecipientIds: event.recipientIds!..sort(), selfUserId: selfUserId),
54+
MessageType.stream => TopicNarrow(event.streamId!, event.topic!),
55+
};
56+
57+
bool hasUpdate = false;
58+
switch (event.op) {
59+
case TypingOp.start:
60+
hasUpdate = _addTypist(narrow, event.senderId, () {
61+
if (_removeTypist(narrow, event.senderId)) {
62+
notifyListeners();
63+
}
64+
});
65+
case TypingOp.stop:
66+
hasUpdate = _removeTypist(narrow, event.senderId);
67+
}
68+
69+
if (hasUpdate) {
70+
notifyListeners();
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)