Skip to content

Commit

Permalink
narrow: Add DmNarrow; handle in message list
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gnprice committed Jun 8, 2023
1 parent 621869d commit baa44cb
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 1 deletion.
98 changes: 97 additions & 1 deletion lib/model/narrow.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,100 @@ class TopicNarrow extends Narrow {
int get hashCode => Object.hash('TopicNarrow', streamId, topic);
}

// TODO other narrow types: DMs; starred, mentioned; searches; arbitrary
bool _isSortedWithoutDuplicates(List<int> items) {
final length = items.length;
if (length == 0) {
return true;
}
int lastItem = items[0];
for (int i = 1; i < length; i++) {
final item = items[i];
if (item <= lastItem) {
return false;
}
lastItem = item;
}
return true;
}

/// The narrow for a direct-message conversation.
// Zulip has many ways of representing a DM conversation; for example code
// handling many of them, see zulip-mobile:src/utils/recipient.js .
// Please add more constructors and getters here to handle any of those
// as we turn out to need them.
class DmNarrow extends Narrow {
DmNarrow({required this.allRecipientIds, required int selfUserId})
: assert(_isSortedWithoutDuplicates(allRecipientIds)),
assert(allRecipientIds.contains(selfUserId)),
_selfUserId = selfUserId;

/// The user IDs of everyone in the conversation, sorted.
///
/// Each message in the conversation is sent by one of these users
/// and received by all the other users.
///
/// The self-user is always a member of this list.
/// It has one element for the self-1:1 thread,
/// two elements for other 1:1 threads,
/// and three or more elements for a group DM thread.
///
/// See also:
/// * [otherRecipientIds], an alternate way of identifying the conversation.
/// * [DmMessage.allRecipientIds], which provides this same format.
final List<int> allRecipientIds;

/// The user ID of the self-user.
///
/// The [DmNarrow] implementation needs this information
/// for converting between different forms of referring to the narrow,
/// such as [allRecipientIds] vs. [otherRecipientIds].
final int _selfUserId;

/// The user IDs of everyone in the conversation except self, sorted.
///
/// This is empty for the self-1:1 thread,
/// has one element for other 1:1 threads,
/// and has two or more elements for a group DM thread.
///
/// See also:
/// * [allRecipientIds], an alternate way of identifying the conversation.
late final List<int> otherRecipientIds = allRecipientIds
.where((userId) => userId != _selfUserId)
.toList(growable: false);

/// A string that uniquely identifies the DM conversation (within the account).
late final String _key = otherRecipientIds.join(',');

@override
bool containsMessage(Message message) {
if (message is! DmMessage) return false;
if (message.allRecipientIds.length != allRecipientIds.length) return false;
int i = 0;
for (final userId in message.allRecipientIds) {
if (userId != allRecipientIds[i]) return false;
i++;
}
return true;
}

// Not [otherRecipientIds], because for the self-1:1 thread that triggers
// a server bug as of Zulip Server 7 (2023-05): an empty list here
// causes a 5xx response from the server.
@override
ApiNarrow apiEncode() => [ApiNarrowDm(allRecipientIds)];

@override
bool operator ==(Object other) {
if (other is! DmNarrow) return false;
assert(other._selfUserId == _selfUserId,
'Two [Narrow]s belonging to different accounts were compared with `==`. '
'This is a bug, because a [Narrow] does not contain information to '
'reliably detect such a comparison, so it may produce false positives.');
return other._key == _key;
}

@override
int get hashCode => Object.hash('DmNarrow', _key);
}

// TODO other narrow types: starred, mentioned; searches; arbitrary
2 changes: 2 additions & 0 deletions lib/widgets/compose_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,8 @@ class ComposeBox extends StatelessWidget {
return StreamComposeBox(narrow: narrow, streamId: narrow.streamId);
} else if (narrow is TopicNarrow) {
return const SizedBox.shrink(); // TODO(#144): add a single-topic compose box
} else if (narrow is DmNarrow) {
return const SizedBox.shrink(); // TODO(#144): add a DM compose box
} else if (narrow is AllMessagesNarrow) {
return const SizedBox.shrink();
} else {
Expand Down
9 changes: 9 additions & 0 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ class MessageListAppBarTitle extends StatelessWidget {
final store = PerAccountStoreWidget.of(context);
final streamName = store.streams[streamId]?.name ?? '(unknown stream)';
return Text("#$streamName > $topic"); // TODO show stream privacy icon; format on two lines

case DmNarrow(:var otherRecipientIds):
final store = PerAccountStoreWidget.of(context);
if (otherRecipientIds.isEmpty) {
return const Text("DMs with yourself");
} else {
final names = otherRecipientIds.map((id) => store.users[id]?.fullName ?? '(unknown user)');
return Text("DMs with ${names.join(", ")}"); // TODO show avatars
}
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions test/model/narrow_checks.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

import 'package:checks/checks.dart';
import 'package:zulip/api/model/narrow.dart';
import 'package:zulip/model/narrow.dart';

extension NarrowChecks on Subject<Narrow> {
Subject<ApiNarrow> get apiEncode => has((x) => x.apiEncode(), 'apiEncode()');
}

extension DmNarrowChecks on Subject<DmNarrow> {
Subject<List<int>> get allRecipientIds => has((x) => x.allRecipientIds, 'allRecipientIds');
Subject<List<int>> get otherRecipientIds => has((x) => x.otherRecipientIds, 'otherRecipientIds');
}
67 changes: 67 additions & 0 deletions test/model/narrow_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@

import 'package:checks/checks.dart';
import 'package:test/scaffolding.dart';
import 'package:zulip/api/model/model.dart';
import 'package:zulip/model/narrow.dart';

import '../example_data.dart' as eg;
import 'narrow_checks.dart';

void main() {
group('DmNarrow', () {
test('constructor assertions', () {
check(() => DmNarrow(allRecipientIds: [2, 12], selfUserId: 2)).returnsNormally();
check(() => DmNarrow(allRecipientIds: [2], selfUserId: 2)).returnsNormally();

check(() => DmNarrow(allRecipientIds: [12, 2], selfUserId: 2)).throws();
check(() => DmNarrow(allRecipientIds: [2, 2], selfUserId: 2)).throws();
check(() => DmNarrow(allRecipientIds: [2, 12], selfUserId: 1)).throws();
check(() => DmNarrow(allRecipientIds: [], selfUserId: 2)).throws();
});

test('otherRecipientIds', () {
check(DmNarrow(allRecipientIds: [1, 2, 3], selfUserId: 2))
.otherRecipientIds.deepEquals([1, 3]);
check(DmNarrow(allRecipientIds: [1, 2], selfUserId: 2))
.otherRecipientIds.deepEquals([1]);
check(DmNarrow(allRecipientIds: [2], selfUserId: 2))
.otherRecipientIds.deepEquals([]);
});

test('containsMessage', () {
final user1 = eg.user(userId: 1);
final user2 = eg.user(userId: 2);
final user3 = eg.user(userId: 3);
final narrow2 = DmNarrow(allRecipientIds: [2], selfUserId: 2);
final narrow12 = DmNarrow(allRecipientIds: [1, 2], selfUserId: 2);
final narrow123 = DmNarrow(allRecipientIds: [1, 2, 3], selfUserId: 2);

Message dm(User from, List<User> to) => eg.dmMessage(from: from, to: to);
final streamMessage = eg.streamMessage(sender: user2);

check(narrow2.containsMessage(streamMessage)).isFalse();
check(narrow2.containsMessage(dm(user2, []))).isTrue();
check(narrow2.containsMessage(dm(user1, [user2]))).isFalse();
check(narrow2.containsMessage(dm(user2, [user1]))).isFalse();
check(narrow2.containsMessage(dm(user1, [user2, user3]))).isFalse();
check(narrow2.containsMessage(dm(user2, [user1, user3]))).isFalse();
check(narrow2.containsMessage(dm(user3, [user1, user2]))).isFalse();

check(narrow12.containsMessage(streamMessage)).isFalse();
check(narrow12.containsMessage(dm(user2, []))).isFalse();
check(narrow12.containsMessage(dm(user1, [user2]))).isTrue();
check(narrow12.containsMessage(dm(user2, [user1]))).isTrue();
check(narrow12.containsMessage(dm(user1, [user2, user3]))).isFalse();
check(narrow12.containsMessage(dm(user2, [user1, user3]))).isFalse();
check(narrow12.containsMessage(dm(user3, [user1, user2]))).isFalse();

check(narrow123.containsMessage(streamMessage)).isFalse();
check(narrow123.containsMessage(dm(user2, []))).isFalse();
check(narrow123.containsMessage(dm(user1, [user2]))).isFalse();
check(narrow123.containsMessage(dm(user2, [user1]))).isFalse();
check(narrow123.containsMessage(dm(user1, [user2, user3]))).isTrue();
check(narrow123.containsMessage(dm(user2, [user1, user3]))).isTrue();
check(narrow123.containsMessage(dm(user3, [user1, user2]))).isTrue();
});
});
}

0 comments on commit baa44cb

Please sign in to comment.