Skip to content

Commit 3ff64be

Browse files
committed
store: Add channelFolders
1 parent 3da70bc commit 3ff64be

File tree

7 files changed

+184
-6
lines changed

7 files changed

+184
-6
lines changed

lib/api/model/model.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -854,13 +854,13 @@ class Subscription extends ZulipStream {
854854
@JsonSerializable(fieldRename: FieldRename.snake)
855855
class ChannelFolder {
856856
final int id;
857-
final String name;
858-
final int? order; // TODO(server-11); added in a later FL than the rest
857+
String name;
858+
int? order; // TODO(server-11); added in a later FL than the rest
859859
final int? dateCreated;
860860
final int? creatorId;
861-
final String description;
862-
final String renderedDescription;
863-
final bool isArchived;
861+
String description;
862+
String renderedDescription;
863+
bool isArchived;
864864

865865
ChannelFolder({
866866
required this.id,

lib/model/channel.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ mixin ChannelStore on UserStore {
4242
/// and [streamsByName].
4343
Map<int, Subscription> get subscriptions;
4444

45+
/// All the channel folders, including archived ones, indexed by ID.
46+
Map<int, ChannelFolder> get channelFolders;
47+
4548
static int compareChannelsByName(ZulipStream a, ZulipStream b) {
4649
// A user gave feedback wanting zulip-flutter to match web in putting
4750
// emoji-prefixed channels first; see #1202.
@@ -267,6 +270,9 @@ mixin ProxyChannelStore on ChannelStore {
267270
@override
268271
Map<int, Subscription> get subscriptions => channelStore.subscriptions;
269272

273+
@override
274+
Map<int, ChannelFolder> get channelFolders => channelStore.channelFolders;
275+
270276
@override
271277
UserTopicVisibilityPolicy topicVisibilityPolicy(int streamId, TopicName topic) =>
272278
channelStore.topicVisibilityPolicy(streamId, topic);
@@ -306,6 +312,9 @@ class ChannelStoreImpl extends HasUserStore with ChannelStore {
306312
streams.putIfAbsent(stream.streamId, () => stream);
307313
}
308314

315+
final channelFolders = Map.fromEntries((initialSnapshot.channelFolders ?? [])
316+
.map((channelFolder) => MapEntry(channelFolder.id, channelFolder)));
317+
309318
final topicVisibility = <int, TopicKeyedMap<UserTopicVisibilityPolicy>>{};
310319
for (final item in initialSnapshot.userTopics ?? const <UserTopicItem>[]) {
311320
if (_warnInvalidVisibilityPolicy(item.visibilityPolicy)) {
@@ -321,6 +330,7 @@ class ChannelStoreImpl extends HasUserStore with ChannelStore {
321330
streams: streams,
322331
streamsByName: streams.map((_, stream) => MapEntry(stream.name, stream)),
323332
subscriptions: subscriptions,
333+
channelFolders: channelFolders,
324334
topicVisibility: topicVisibility,
325335
);
326336
}
@@ -330,6 +340,7 @@ class ChannelStoreImpl extends HasUserStore with ChannelStore {
330340
required this.streams,
331341
required this.streamsByName,
332342
required this.subscriptions,
343+
required this.channelFolders,
333344
required this.topicVisibility,
334345
});
335346

@@ -339,6 +350,8 @@ class ChannelStoreImpl extends HasUserStore with ChannelStore {
339350
final Map<String, ZulipStream> streamsByName;
340351
@override
341352
final Map<int, Subscription> subscriptions;
353+
@override
354+
final Map<int, ChannelFolder> channelFolders;
342355

343356
@override
344357
Map<int, TopicKeyedMap<UserTopicVisibilityPolicy>> get debugTopicVisibility => topicVisibility;
@@ -500,6 +513,31 @@ class ChannelStoreImpl extends HasUserStore with ChannelStore {
500513
}
501514
}
502515

516+
void handleChannelFolderEvent(ChannelFolderEvent event) {
517+
switch (event) {
518+
case ChannelFolderAddEvent():
519+
final newChannelFolder = event.channelFolder;
520+
channelFolders[newChannelFolder.id] = newChannelFolder;
521+
case ChannelFolderUpdateEvent():
522+
final change = event.data;
523+
final channelFolder = channelFolders[event.channelFolderId];
524+
if (channelFolder == null) return; // TODO(log)
525+
526+
if (change.name != null) channelFolder.name = change.name!;
527+
if (change.description != null) channelFolder.description = change.description!;
528+
if (change.renderedDescription != null) channelFolder.renderedDescription = change.renderedDescription!;
529+
if (change.isArchived != null) channelFolder.isArchived = change.isArchived!;
530+
case ChannelFolderReorderEvent():
531+
final order = event.order;
532+
for (int i = 0; i < order.length; i++) {
533+
final id = order[i];
534+
final channelFolder = channelFolders[id];
535+
if (channelFolder == null) continue; // TODO(log)
536+
channelFolder.order = i;
537+
}
538+
}
539+
}
540+
503541
void handleUserTopicEvent(UserTopicEvent event) {
504542
UserTopicVisibilityPolicy visibilityPolicy = event.visibilityPolicy;
505543
if (_warnInvalidVisibilityPolicy(visibilityPolicy)) {

lib/model/store.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,7 @@ class PerAccountStore extends PerAccountStoreBase with
805805

806806
case ChannelFolderEvent():
807807
assert(debugLog("server event: channel_folder/${event.op}"));
808-
// TODO handle
808+
_channels.handleChannelFolderEvent(event);
809809
break;
810810

811811
case UserStatusEvent():

test/api/model/model_checks.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ extension ZulipStreamChecks on Subject<ZulipStream> {
5656
Subject<int> get streamId => has((x) => x.streamId, 'streamId');
5757
}
5858

59+
extension ChannelFolderChecks on Subject<ChannelFolder> {
60+
Subject<int> get id => has((x) => x.id, 'id');
61+
Subject<String> get name => has((x) => x.name, 'name');
62+
Subject<int?> get order => has((x) => x.order, 'order');
63+
Subject<int?> get dateCreated => has((x) => x.dateCreated, 'dateCreated');
64+
Subject<int?> get creatorId => has((x) => x.creatorId, 'creatorId');
65+
Subject<String> get description => has((x) => x.description, 'description');
66+
Subject<String> get renderedDescription => has((x) => x.renderedDescription, 'renderedDescription');
67+
Subject<bool> get isArchived => has((x) => x.isArchived, 'isArchived');
68+
}
69+
5970
extension TopicNameChecks on Subject<TopicName> {
6071
Subject<String> get apiName => has((x) => x.apiName, 'apiName');
6172
Subject<String?> get displayName => has((x) => x.displayName, 'displayName');

test/example_data.dart

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,49 @@ Subscription subscription(
553553
);
554554
}
555555

556+
/// A fresh channel folder ID,
557+
/// from a random but always strictly increasing sequence.
558+
int _nextChannelFolderId() => (_lastChannelFolderId += 1 + Random().nextInt(100));
559+
int _lastChannelFolderId = 1000;
560+
561+
ChannelFolder channelFolder({
562+
int? id,
563+
String? name,
564+
int? order,
565+
int? dateCreated,
566+
int? creatorId,
567+
String? description,
568+
String? renderedDescription,
569+
bool? isArchived,
570+
}) {
571+
final effectiveId = id ?? _nextChannelFolderId();
572+
final effectiveDescription = description ?? 'An example channel folder.';
573+
return ChannelFolder(
574+
id: effectiveId,
575+
name: name ?? 'channel folder $effectiveId',
576+
order: order,
577+
dateCreated: dateCreated ?? utcTimestamp(),
578+
creatorId: creatorId ?? selfUser.userId,
579+
description: effectiveDescription,
580+
renderedDescription: renderedDescription ?? '<p>$effectiveDescription</p>',
581+
isArchived: isArchived ?? false,
582+
);
583+
}
584+
585+
ChannelFolderChange channelFolderChange({
586+
String? name,
587+
String? description,
588+
String? renderedDescription,
589+
bool? isArchived,
590+
}) {
591+
return ChannelFolderChange(
592+
name: name,
593+
description: description,
594+
renderedDescription: renderedDescription,
595+
isArchived: isArchived,
596+
);
597+
}
598+
556599
/// The [TopicName] constructor, but shorter.
557600
///
558601
/// Useful in test code that mentions a lot of topics in a compact format.

test/model/channel_test.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,88 @@ void main() {
137137
});
138138
});
139139

140+
group('ChannelFolderEvent', () {
141+
group('add', () {
142+
test('smoke', () async {
143+
final folder1 = eg.channelFolder(id: 1);
144+
final folder2 = eg.channelFolder(id: 2);
145+
final store = eg.store(initialSnapshot: eg.initialSnapshot(
146+
channelFolders: [folder1]));
147+
148+
await store.handleEvent(ChannelFolderAddEvent(
149+
id: 1, channelFolder: folder2));
150+
check(store.channelFolders).deepEquals({1: folder1, 2: folder2});
151+
});
152+
});
153+
154+
group('update', () {
155+
void doTest(ChannelFolderChange change) {
156+
final ChannelFolderChange(
157+
:name, :description, :renderedDescription, :isArchived) = change;
158+
final testDescription = [
159+
if (name != null) 'name: $name',
160+
if (description != null) 'description: $description',
161+
if (renderedDescription != null) 'renderedDescription: $renderedDescription',
162+
if (isArchived != null) 'isArchived: $isArchived',
163+
].join(', ');
164+
test(testDescription, () async {
165+
final channelFolder = eg.channelFolder();
166+
assert(name == null || name != channelFolder.name);
167+
assert(description == null || description != channelFolder.description);
168+
assert(renderedDescription == null || renderedDescription != channelFolder.renderedDescription);
169+
assert(isArchived == null || isArchived != channelFolder.isArchived);
170+
171+
final store = eg.store(initialSnapshot: eg.initialSnapshot(
172+
channelFolders: [channelFolder]));
173+
await store.handleEvent(ChannelFolderUpdateEvent(id: 1,
174+
channelFolderId: channelFolder.id, data: change));
175+
check(store.channelFolders.values.single)
176+
..name.equals(name ?? channelFolder.name)
177+
..description.equals(description ?? channelFolder.description)
178+
..renderedDescription.equals(renderedDescription ?? channelFolder.renderedDescription)
179+
..isArchived.equals(isArchived ?? channelFolder.isArchived);
180+
});
181+
}
182+
183+
doTest(eg.channelFolderChange(name: 'new name'));
184+
doTest(eg.channelFolderChange(description: 'new description'));
185+
doTest(eg.channelFolderChange(renderedDescription: '<p>new description</p>'));
186+
doTest(eg.channelFolderChange(isArchived: true));
187+
188+
doTest(eg.channelFolderChange(
189+
name: 'new name',
190+
description: 'new description',
191+
renderedDescription: '<p>new description</p>',
192+
isArchived: true,
193+
));
194+
});
195+
196+
group('reorder', () {
197+
List<ChannelFolder> foldersFromStoreInOrder(PerAccountStore store) {
198+
return store.channelFolders.values.toList()
199+
..sort((a, b) => a.order!.compareTo(b.order!));
200+
}
201+
202+
test('smoke', () async {
203+
final folderA = eg.channelFolder(order: 0);
204+
final folderB = eg.channelFolder(order: 1);
205+
final folderC = eg.channelFolder(order: 2);
206+
207+
final store = eg.store(initialSnapshot: eg.initialSnapshot(
208+
channelFolders: [folderA, folderB, folderC]));
209+
check(foldersFromStoreInOrder(store)).deepEquals([folderA, folderB, folderC]);
210+
211+
await store.handleEvent(ChannelFolderReorderEvent(id: 1,
212+
order: [folderA.id, folderC.id, folderB.id]));
213+
check(foldersFromStoreInOrder(store)).deepEquals([folderA, folderC, folderB]);
214+
215+
await store.handleEvent(ChannelFolderReorderEvent(id: 1,
216+
order: [folderC.id, folderB.id, folderA.id]));
217+
check(foldersFromStoreInOrder(store)).deepEquals([folderC, folderB, folderA]);
218+
});
219+
});
220+
});
221+
140222
group('topic visibility', () {
141223
final stream1 = eg.stream();
142224
final stream2 = eg.stream();

test/model/test_store.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,10 @@ extension PerAccountStoreTestExtension on PerAccountStore {
335335
await handleEvent(SubscriptionRemoveEvent(id: 1, streamIds: channelIds));
336336
}
337337

338+
Future<void> addChannelFolder(ChannelFolder channelFolder) async {
339+
await handleEvent(ChannelFolderAddEvent(id: 1, channelFolder: channelFolder));
340+
}
341+
338342
Future<void> setUserTopic(ZulipStream stream, String topic, UserTopicVisibilityPolicy visibilityPolicy) async {
339343
await handleEvent(eg.userTopicEvent(stream.streamId, topic, visibilityPolicy));
340344
}

0 commit comments

Comments
 (0)