Skip to content

Commit 359f2ef

Browse files
committed
recent-senders: Add the new MessageIdTracker and RecentSenders data structures
These data structures are used to keep track of user messages in topics and streams.
1 parent 9b75e44 commit 359f2ef

File tree

2 files changed

+291
-0
lines changed

2 files changed

+291
-0
lines changed

lib/model/recent_senders.dart

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import 'package:collection/collection.dart';
2+
import 'package:flutter/foundation.dart';
3+
4+
import '../api/model/model.dart';
5+
6+
class MessageIdTracker {
7+
MessageIdTracker();
8+
9+
@visibleForTesting
10+
MessageIdTracker.fromIds(List<int> ids) {
11+
_ids.addAll([...{...ids}]..sort());
12+
}
13+
14+
final List<int> _ids = [];
15+
16+
void add(int id) {
17+
if (_ids.isEmpty) {
18+
_ids.add(id);
19+
} else {
20+
int i = lowerBound(_ids, id);
21+
if (i < _ids.length && _ids[i] == id) return; // the [id] already exists, so do not add it.
22+
_ids.insert(i, id);
23+
}
24+
}
25+
26+
// TODO: remove
27+
28+
/// The maximum id in the tracker list.
29+
///
30+
/// Returns -1 if the tracker list is empty.
31+
int get maxId => _ids.isNotEmpty ? _ids.last : -1;
32+
33+
/// Getter for the tracked message IDs.
34+
@visibleForTesting
35+
List<int> get debugIds => _ids;
36+
37+
@override
38+
bool operator == (covariant MessageIdTracker other) {
39+
if (identical(this, other)) return true;
40+
41+
return _ids.equals(other._ids);
42+
}
43+
44+
@override
45+
int get hashCode => Object.hashAll(_ids);
46+
47+
@override
48+
String toString() => _ids.toString();
49+
}
50+
51+
/// A data structure to keep track of stream and topic messages.
52+
///
53+
/// The owner should call [clear] in order to free resources.
54+
class RecentSenders {
55+
// streamSenders[streamId][senderId] = IdTracker
56+
final Map<int, Map<int, MessageIdTracker>> _streamSenders = {};
57+
58+
// topicSenders[streamId][topic][senderId] = IdTracker
59+
final Map<int, Map<String, Map<int, MessageIdTracker>>> _topicSenders = {};
60+
61+
/// Getter for the stream senders.
62+
@visibleForTesting
63+
Map<int, Map<int, MessageIdTracker>> get debugStreamSenders => _streamSenders;
64+
65+
/// Getter for the topic senders.
66+
@visibleForTesting
67+
Map<int, Map<String, Map<int, MessageIdTracker>>> get debugTopicSenders => _topicSenders;
68+
69+
/// Whether stream senders and topic senders are both empty.
70+
@visibleForTesting
71+
bool get debugIsEmpty => _streamSenders.isEmpty && _topicSenders.isEmpty;
72+
73+
/// Whether stream senders and topic senders are both not empty.
74+
@visibleForTesting
75+
bool get debugIsNotEmpty => _streamSenders.isNotEmpty && _topicSenders.isNotEmpty;
76+
77+
void clear() {
78+
_streamSenders.clear();
79+
_topicSenders.clear();
80+
}
81+
82+
int latestMessageIdOfSenderInStream({required int streamId, required int senderId}) {
83+
return _streamSenders[streamId]?[senderId]?.maxId ?? -1;
84+
}
85+
86+
int latestMessageIdOfSenderInTopic({
87+
required int streamId,
88+
required String topic,
89+
required int senderId,
90+
}) {
91+
return _topicSenders[streamId]?[topic]?[senderId]?.maxId ?? -1;
92+
}
93+
94+
void _addMessageInStream({
95+
required int streamId,
96+
required int senderId,
97+
required int messageId,
98+
}) {
99+
final sendersMap = _streamSenders[streamId] ??= {};
100+
final idTracker = sendersMap[senderId] ??= MessageIdTracker();
101+
idTracker.add(messageId);
102+
}
103+
104+
void _addMessageInTopic({
105+
required int streamId,
106+
required String topic,
107+
required int senderId,
108+
required int messageId,
109+
}) {
110+
final topicsMap = _topicSenders[streamId] ??= {};
111+
final sendersMap = topicsMap[topic] ??= {};
112+
final idTracker = sendersMap[senderId] ??= MessageIdTracker();
113+
idTracker.add(messageId);
114+
}
115+
116+
/// Extracts and keeps track of the necessary data from a [message] only
117+
/// if it is a stream message.
118+
void handleMessage(Message message) {
119+
if (message is! StreamMessage) {
120+
return;
121+
}
122+
123+
final streamId = message.streamId;
124+
final topic = message.subject;
125+
final senderId = message.senderId;
126+
final messageId = message.id;
127+
128+
_addMessageInStream(
129+
streamId: streamId, senderId: senderId, messageId: messageId);
130+
_addMessageInTopic(
131+
streamId: streamId,
132+
topic: topic,
133+
senderId: senderId,
134+
messageId: messageId);
135+
}
136+
137+
// TODO: removeMessageInTopic
138+
}

test/model/recent_senders_test.dart

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:test/scaffolding.dart';
3+
import 'package:zulip/model/recent_senders.dart';
4+
import '../example_data.dart' as eg;
5+
6+
void main() {
7+
group('MessageIdTracker', () {
8+
late MessageIdTracker idTracker;
9+
10+
void prepare() {
11+
idTracker = MessageIdTracker();
12+
}
13+
14+
test('starts with no ids', () {
15+
prepare();
16+
check(idTracker.debugIds).isEmpty();
17+
});
18+
19+
test('calling add(id) adds the same id to the tracker just one time', () {
20+
prepare();
21+
check(idTracker.debugIds).isEmpty();
22+
idTracker.add(1);
23+
idTracker.add(1);
24+
check(idTracker.debugIds.singleOrNull).equals(1);
25+
});
26+
27+
test('ids are sorted ascendingly, with maxId pointing to the last element', () {
28+
prepare();
29+
check(idTracker.debugIds).isEmpty();
30+
idTracker.add(1);
31+
idTracker.add(9);
32+
idTracker.add(-1);
33+
idTracker.add(5);
34+
check(idTracker.debugIds).deepEquals([-1, 1, 5, 9]);
35+
check(idTracker.maxId).equals(idTracker.debugIds.last);
36+
});
37+
});
38+
39+
group('RecentSenders', () {
40+
late RecentSenders recentSenders;
41+
42+
void prepare() {
43+
recentSenders = RecentSenders();
44+
}
45+
46+
test('starts with no stream or topic senders', () {
47+
prepare();
48+
check(recentSenders.debugIsEmpty).equals(true);
49+
});
50+
51+
test('only processes a stream message', () {
52+
prepare();
53+
final dmMessage = eg.dmMessage(from: eg.selfUser, to: [eg.otherUser]);
54+
recentSenders.handleMessage(dmMessage);
55+
check(recentSenders.debugIsEmpty).equals(true);
56+
57+
final streamMessage = eg.streamMessage(
58+
id: 100,
59+
sender: eg.user(userId: 10),
60+
stream: eg.stream(streamId: 1),
61+
topic: 'topic',
62+
);
63+
recentSenders.handleMessage(streamMessage);
64+
65+
final expectedStreamSenders = {
66+
1: {
67+
10: MessageIdTracker.fromIds([100])
68+
}
69+
};
70+
check(recentSenders.debugStreamSenders).deepEquals(expectedStreamSenders);
71+
72+
final expectedTopicSenders = {
73+
1: {
74+
'topic': {
75+
10: MessageIdTracker.fromIds([100])
76+
},
77+
},
78+
};
79+
check(recentSenders.debugTopicSenders).deepEquals(expectedTopicSenders);
80+
});
81+
82+
test('adding multiple messages', () {
83+
prepare();
84+
final message1 = eg.streamMessage(
85+
stream: eg.stream(streamId: 1),
86+
topic: 'topic1',
87+
sender: eg.user(userId: 10),
88+
id: 300,
89+
);
90+
final message2 = eg.streamMessage(
91+
stream: eg.stream(streamId: 1),
92+
topic: 'topic2',
93+
sender: eg.user(userId: 10),
94+
id: 100,
95+
);
96+
final message3 = eg.streamMessage(
97+
stream: eg.stream(streamId: 1),
98+
topic: 'topic1',
99+
sender: eg.user(userId: 20),
100+
id: 200,
101+
);
102+
final message4 = eg.streamMessage(
103+
stream: eg.stream(streamId: 1),
104+
topic: 'topic2',
105+
sender: eg.user(userId: 20),
106+
id: 400,
107+
);
108+
final message5 = eg.streamMessage(
109+
id: 500,
110+
stream: eg.stream(streamId: 2),
111+
topic: 'topic3',
112+
sender: eg.user(userId: 20),
113+
);
114+
115+
recentSenders.handleMessage(message1);
116+
recentSenders.handleMessage(message2);
117+
recentSenders.handleMessage(message3);
118+
recentSenders.handleMessage(message4);
119+
recentSenders.handleMessage(message5);
120+
121+
final expectedStreamSenders = {
122+
1: {
123+
10: MessageIdTracker.fromIds([100, 300]),
124+
20: MessageIdTracker.fromIds([200, 400]),
125+
},
126+
2: {
127+
20: MessageIdTracker.fromIds([500]),
128+
},
129+
};
130+
131+
final expectedTopicSenders = {
132+
1: {
133+
'topic1': {
134+
10: MessageIdTracker.fromIds([300]),
135+
20: MessageIdTracker.fromIds([200]),
136+
},
137+
'topic2': {
138+
10: MessageIdTracker.fromIds([100]),
139+
20: MessageIdTracker.fromIds([400]),
140+
},
141+
},
142+
2: {
143+
'topic3': {
144+
20: MessageIdTracker.fromIds([500]),
145+
}
146+
}
147+
};
148+
149+
check(recentSenders.debugStreamSenders).deepEquals(expectedStreamSenders);
150+
check(recentSenders.debugTopicSenders).deepEquals(expectedTopicSenders);
151+
});
152+
});
153+
}

0 commit comments

Comments
 (0)