Skip to content

Commit b658af6

Browse files
chrisbobbegnprice
authored andcommitted
store: Add channelFolders
1 parent 1c52fdf commit b658af6

File tree

7 files changed

+186
-6
lines changed

7 files changed

+186
-6
lines changed

lib/api/model/model.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -852,13 +852,13 @@ class Subscription extends ZulipStream {
852852
@JsonSerializable(fieldRename: FieldRename.snake)
853853
class ChannelFolder {
854854
final int id;
855-
final String name;
856-
final int? order; // TODO(server-11); added in a later FL than the rest
855+
String name;
856+
int? order; // TODO(server-11); added in a later FL than the rest
857857
final int? dateCreated;
858858
final int? creatorId;
859-
final String description;
860-
final String renderedDescription;
861-
final bool isArchived;
859+
String description;
860+
String renderedDescription;
861+
bool isArchived;
862862

863863
ChannelFolder({
864864
required this.id,

lib/model/channel.dart

Lines changed: 40 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) {
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;
@@ -498,6 +511,33 @@ class ChannelStoreImpl extends HasUserStore with ChannelStore {
498511
}
499512
}
500513

514+
void handleChannelFolderEvent(ChannelFolderEvent event) {
515+
switch (event) {
516+
case ChannelFolderAddEvent():
517+
final newChannelFolder = event.channelFolder;
518+
channelFolders[newChannelFolder.id] = newChannelFolder;
519+
520+
case ChannelFolderUpdateEvent():
521+
final change = event.data;
522+
final channelFolder = channelFolders[event.channelFolderId];
523+
if (channelFolder == null) return; // TODO(log)
524+
525+
if (change.name != null) channelFolder.name = change.name!;
526+
if (change.description != null) channelFolder.description = change.description!;
527+
if (change.renderedDescription != null) channelFolder.renderedDescription = change.renderedDescription!;
528+
if (change.isArchived != null) channelFolder.isArchived = change.isArchived!;
529+
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+
501541
void handleUserTopicEvent(UserTopicEvent event) {
502542
UserTopicVisibilityPolicy visibilityPolicy = event.visibilityPolicy;
503543
if (_warnInvalidVisibilityPolicy(visibilityPolicy)) {

lib/model/store.dart

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

832832
case ChannelFolderEvent():
833833
assert(debugLog("server event: channel_folder/${event.op}"));
834-
// TODO handle
834+
_channels.handleChannelFolderEvent(event);
835835
break;
836836

837837
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
@@ -560,6 +560,49 @@ Subscription subscription(
560560
);
561561
}
562562

563+
/// A fresh channel folder ID,
564+
/// from a random but always strictly increasing sequence.
565+
int _nextChannelFolderId() => (_lastChannelFolderId += 1 + Random().nextInt(100));
566+
int _lastChannelFolderId = 1000;
567+
568+
ChannelFolder channelFolder({
569+
int? id,
570+
String? name,
571+
int? order,
572+
int? dateCreated,
573+
int? creatorId,
574+
String? description,
575+
String? renderedDescription,
576+
bool? isArchived,
577+
}) {
578+
final effectiveId = id ?? _nextChannelFolderId();
579+
final effectiveDescription = description ?? 'An example channel folder.';
580+
return ChannelFolder(
581+
id: effectiveId,
582+
name: name ?? 'channel folder $effectiveId',
583+
order: order,
584+
dateCreated: dateCreated ?? utcTimestamp(),
585+
creatorId: creatorId ?? selfUser.userId,
586+
description: effectiveDescription,
587+
renderedDescription: renderedDescription ?? '<p>$effectiveDescription</p>',
588+
isArchived: isArchived ?? false,
589+
);
590+
}
591+
592+
ChannelFolderChange channelFolderChange({
593+
String? name,
594+
String? description,
595+
String? renderedDescription,
596+
bool? isArchived,
597+
}) {
598+
return ChannelFolderChange(
599+
name: name,
600+
description: description,
601+
renderedDescription: renderedDescription,
602+
isArchived: isArchived,
603+
);
604+
}
605+
563606
/// The [TopicName] constructor, but shorter.
564607
///
565608
/// 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
@@ -123,6 +123,88 @@ void main() {
123123
});
124124
});
125125

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

test/model/test_store.dart

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

353+
Future<void> addChannelFolder(ChannelFolder channelFolder) async {
354+
await handleEvent(ChannelFolderAddEvent(id: 1, channelFolder: channelFolder));
355+
}
356+
353357
Future<void> setUserTopic(ZulipStream stream, String topic, UserTopicVisibilityPolicy visibilityPolicy) async {
354358
await handleEvent(eg.userTopicEvent(stream.streamId, topic, visibilityPolicy));
355359
}

0 commit comments

Comments
 (0)