Skip to content

Commit bbaf4cf

Browse files
committed
narrow: Add DmNarrow; handle in message list
This takes care of the bulk of zulip#142. The only thing missing is a way to navigate to one of these narrows, which we'll add next.
1 parent 281789d commit bbaf4cf

File tree

5 files changed

+188
-1
lines changed

5 files changed

+188
-1
lines changed

lib/model/narrow.dart

+97-1
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,100 @@ class TopicNarrow extends Narrow {
9090
int get hashCode => Object.hash('TopicNarrow', streamId, topic);
9191
}
9292

93-
// TODO other narrow types: DMs; starred, mentioned; searches; arbitrary
93+
bool _isSortedWithoutDuplicates(List<int> items) {
94+
final length = items.length;
95+
if (length == 0) {
96+
return true;
97+
}
98+
int lastItem = items[0];
99+
for (int i = 1; i < length; i++) {
100+
final item = items[i];
101+
if (item <= lastItem) {
102+
return false;
103+
}
104+
lastItem = item;
105+
}
106+
return true;
107+
}
108+
109+
/// The narrow for a direct-message conversation.
110+
// Zulip has many ways of representing a DM conversation; for example code
111+
// handling many of them, see zulip-mobile:src/utils/recipient.js .
112+
// Please add more constructors and getters here to handle any of those
113+
// as we turn out to need them.
114+
class DmNarrow extends Narrow {
115+
DmNarrow({required this.allRecipientIds, required int selfUserId})
116+
: assert(_isSortedWithoutDuplicates(allRecipientIds)),
117+
assert(allRecipientIds.contains(selfUserId)),
118+
_selfUserId = selfUserId;
119+
120+
/// The user IDs of everyone in the conversation, sorted.
121+
///
122+
/// Each message in the conversation is sent by one of these users
123+
/// and received by all the other users.
124+
///
125+
/// The self-user is always a member of this list.
126+
/// It has one element for the self-1:1 thread,
127+
/// two elements for other 1:1 threads,
128+
/// and three or more elements for a group DM thread.
129+
///
130+
/// See also:
131+
/// * [otherRecipientIds], an alternate way of identifying the conversation.
132+
/// * [DmMessage.allRecipientIds], which provides this same format.
133+
final List<int> allRecipientIds;
134+
135+
/// The user ID of the self-user.
136+
///
137+
/// The [DmNarrow] implementation needs this information
138+
/// for converting between different forms of referring to the narrow,
139+
/// such as [allRecipientIds] vs. [otherRecipientIds].
140+
final int _selfUserId;
141+
142+
/// The user IDs of everyone in the conversation except self, sorted.
143+
///
144+
/// This is empty for the self-1:1 thread,
145+
/// has one element for other 1:1 threads,
146+
/// and has two or more elements for a group DM thread.
147+
///
148+
/// See also:
149+
/// * [allRecipientIds], an alternate way of identifying the conversation.
150+
late final List<int> otherRecipientIds = allRecipientIds
151+
.where((userId) => userId != _selfUserId)
152+
.toList(growable: false);
153+
154+
/// A string that uniquely identifies the DM conversation (within the account).
155+
late final String _key = otherRecipientIds.join(',');
156+
157+
@override
158+
bool containsMessage(Message message) {
159+
if (message is! DmMessage) return false;
160+
if (message.allRecipientIds.length != allRecipientIds.length) return false;
161+
int i = 0;
162+
for (final userId in message.allRecipientIds) {
163+
if (userId != allRecipientIds[i]) return false;
164+
i++;
165+
}
166+
return true;
167+
}
168+
169+
// Not [otherRecipientIds], because for the self-1:1 thread that triggers
170+
// a server bug as of Zulip Server 7 (2023-05): an empty list here
171+
// causes a 5xx response from the server.
172+
@override
173+
ApiNarrow apiEncode() => [ApiNarrowDm(allRecipientIds)];
174+
175+
@override
176+
bool operator ==(Object other) {
177+
if (other is! DmNarrow) return false;
178+
assert(other._selfUserId == _selfUserId,
179+
'Two [Narrow]s belonging to different accounts were compared with `==`. '
180+
'This is a bug, because a [Narrow] does not contain information to '
181+
'reliably detect such a comparison, so it may produce false positives.');
182+
return other._key == _key;
183+
}
184+
185+
@override
186+
int get hashCode => Object.hash('DmNarrow', _key);
187+
}
188+
189+
// TODO other narrow types: starred, mentioned; searches; arbitrary

lib/widgets/compose_box.dart

+2
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,8 @@ class ComposeBox extends StatelessWidget {
695695
return StreamComposeBox(narrow: narrow, streamId: narrow.streamId);
696696
} else if (narrow is TopicNarrow) {
697697
return const SizedBox.shrink(); // TODO(#144): add a single-topic compose box
698+
} else if (narrow is DmNarrow) {
699+
return const SizedBox.shrink(); // TODO(#144): add a DM compose box
698700
} else if (narrow is AllMessagesNarrow) {
699701
return const SizedBox.shrink();
700702
} else {

lib/widgets/message_list.dart

+9
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ class MessageListAppBarTitle extends StatelessWidget {
6868
final store = PerAccountStoreWidget.of(context);
6969
final streamName = store.streams[streamId]?.name ?? '(unknown stream)';
7070
return Text("#$streamName > $topic"); // TODO show stream privacy icon; format on two lines
71+
72+
case DmNarrow(:var otherRecipientIds):
73+
final store = PerAccountStoreWidget.of(context);
74+
if (otherRecipientIds.isEmpty) {
75+
return const Text("DMs with yourself");
76+
} else {
77+
final names = otherRecipientIds.map((id) => store.users[id]?.fullName ?? '(unknown user)');
78+
return Text("DMs with ${names.join(", ")}"); // TODO show avatars
79+
}
7180
}
7281
}
7382
}

test/model/narrow_checks.dart

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
import 'package:checks/checks.dart';
3+
import 'package:zulip/api/model/narrow.dart';
4+
import 'package:zulip/model/narrow.dart';
5+
6+
extension NarrowChecks on Subject<Narrow> {
7+
Subject<ApiNarrow> get apiEncode => has((x) => x.apiEncode(), 'apiEncode()');
8+
}
9+
10+
extension DmNarrowChecks on Subject<DmNarrow> {
11+
Subject<List<int>> get allRecipientIds => has((x) => x.allRecipientIds, 'allRecipientIds');
12+
Subject<List<int>> get otherRecipientIds => has((x) => x.otherRecipientIds, 'otherRecipientIds');
13+
}

test/model/narrow_test.dart

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
2+
import 'package:checks/checks.dart';
3+
import 'package:test/scaffolding.dart';
4+
import 'package:zulip/api/model/model.dart';
5+
import 'package:zulip/model/narrow.dart';
6+
7+
import '../example_data.dart' as eg;
8+
import 'narrow_checks.dart';
9+
10+
void main() {
11+
group('DmNarrow', () {
12+
test('constructor assertions', () {
13+
check(() => DmNarrow(allRecipientIds: [2, 12], selfUserId: 2)).returnsNormally();
14+
check(() => DmNarrow(allRecipientIds: [2], selfUserId: 2)).returnsNormally();
15+
16+
check(() => DmNarrow(allRecipientIds: [12, 2], selfUserId: 2)).throws();
17+
check(() => DmNarrow(allRecipientIds: [2, 2], selfUserId: 2)).throws();
18+
check(() => DmNarrow(allRecipientIds: [2, 12], selfUserId: 1)).throws();
19+
check(() => DmNarrow(allRecipientIds: [], selfUserId: 2)).throws();
20+
});
21+
22+
test('otherRecipientIds', () {
23+
check(DmNarrow(allRecipientIds: [1, 2, 3], selfUserId: 2))
24+
.otherRecipientIds.deepEquals([1, 3]);
25+
check(DmNarrow(allRecipientIds: [1, 2], selfUserId: 2))
26+
.otherRecipientIds.deepEquals([1]);
27+
check(DmNarrow(allRecipientIds: [2], selfUserId: 2))
28+
.otherRecipientIds.deepEquals([]);
29+
});
30+
31+
test('containsMessage', () {
32+
final user1 = eg.user(userId: 1);
33+
final user2 = eg.user(userId: 2);
34+
final user3 = eg.user(userId: 3);
35+
final narrow2 = DmNarrow(allRecipientIds: [2], selfUserId: 2);
36+
final narrow12 = DmNarrow(allRecipientIds: [1, 2], selfUserId: 2);
37+
final narrow123 = DmNarrow(allRecipientIds: [1, 2, 3], selfUserId: 2);
38+
39+
Message dm(User from, List<User> to) => eg.dmMessage(from: from, to: to);
40+
final streamMessage = eg.streamMessage(sender: user2);
41+
42+
check(narrow2.containsMessage(streamMessage)).isFalse();
43+
check(narrow2.containsMessage(dm(user2, []))).isTrue();
44+
check(narrow2.containsMessage(dm(user1, [user2]))).isFalse();
45+
check(narrow2.containsMessage(dm(user2, [user1]))).isFalse();
46+
check(narrow2.containsMessage(dm(user1, [user2, user3]))).isFalse();
47+
check(narrow2.containsMessage(dm(user2, [user1, user3]))).isFalse();
48+
check(narrow2.containsMessage(dm(user3, [user1, user2]))).isFalse();
49+
50+
check(narrow12.containsMessage(streamMessage)).isFalse();
51+
check(narrow12.containsMessage(dm(user2, []))).isFalse();
52+
check(narrow12.containsMessage(dm(user1, [user2]))).isTrue();
53+
check(narrow12.containsMessage(dm(user2, [user1]))).isTrue();
54+
check(narrow12.containsMessage(dm(user1, [user2, user3]))).isFalse();
55+
check(narrow12.containsMessage(dm(user2, [user1, user3]))).isFalse();
56+
check(narrow12.containsMessage(dm(user3, [user1, user2]))).isFalse();
57+
58+
check(narrow123.containsMessage(streamMessage)).isFalse();
59+
check(narrow123.containsMessage(dm(user2, []))).isFalse();
60+
check(narrow123.containsMessage(dm(user1, [user2]))).isFalse();
61+
check(narrow123.containsMessage(dm(user2, [user1]))).isFalse();
62+
check(narrow123.containsMessage(dm(user1, [user2, user3]))).isTrue();
63+
check(narrow123.containsMessage(dm(user2, [user1, user3]))).isTrue();
64+
check(narrow123.containsMessage(dm(user3, [user1, user2]))).isTrue();
65+
});
66+
});
67+
}

0 commit comments

Comments
 (0)