Skip to content

Commit 2f0d1b4

Browse files
committed
api: Add save_snippets events and handle live-updates
1 parent b2d935e commit 2f0d1b4

File tree

8 files changed

+244
-1
lines changed

8 files changed

+244
-1
lines changed

lib/api/model/events.dart

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ sealed class Event {
3737
case 'update': return RealmUserUpdateEvent.fromJson(json);
3838
default: return UnexpectedEvent.fromJson(json);
3939
}
40+
case 'saved_snippets':
41+
switch (json['op'] as String) {
42+
case 'add': return SavedSnippetsAddEvent.fromJson(json);
43+
case 'update': return SavedSnippetsUpdateEvent.fromJson(json);
44+
case 'remove': return SavedSnippetsRemoveEvent.fromJson(json);
45+
default: return UnexpectedEvent.fromJson(json);
46+
}
4047
case 'stream':
4148
switch (json['op'] as String) {
4249
case 'create': return ChannelCreateEvent.fromJson(json);
@@ -336,6 +343,71 @@ class RealmUserUpdateEvent extends RealmUserEvent {
336343
Map<String, dynamic> toJson() => _$RealmUserUpdateEventToJson(this);
337344
}
338345

346+
/// A Zulip event of type `saved_snippets`.
347+
///
348+
/// The corresponding API docs are in several places for
349+
/// different values of `op`; see subclasses.
350+
sealed class SavedSnippetsEvent extends Event {
351+
@override
352+
@JsonKey(includeToJson: true)
353+
String get type => 'saved_snippets';
354+
355+
String get op;
356+
357+
SavedSnippetsEvent({required super.id});
358+
}
359+
360+
/// A [SavedSnippetsEvent] with op `add`: https://zulip.com/api/get-events#saved_snippets-add
361+
@JsonSerializable(fieldRename: FieldRename.snake)
362+
class SavedSnippetsAddEvent extends SavedSnippetsEvent {
363+
@override
364+
String get op => 'add';
365+
366+
final SavedSnippet savedSnippet;
367+
368+
SavedSnippetsAddEvent({required super.id, required this.savedSnippet});
369+
370+
factory SavedSnippetsAddEvent.fromJson(Map<String, dynamic> json) =>
371+
_$SavedSnippetsAddEventFromJson(json);
372+
373+
@override
374+
Map<String, dynamic> toJson() => _$SavedSnippetsAddEventToJson(this);
375+
}
376+
377+
/// A [SavedSnippetsEvent] with op `update`: https://zulip.com/api/get-events#saved_snippets-update
378+
@JsonSerializable(fieldRename: FieldRename.snake)
379+
class SavedSnippetsUpdateEvent extends SavedSnippetsEvent {
380+
@override
381+
String get op => 'update';
382+
383+
final SavedSnippet savedSnippet;
384+
385+
SavedSnippetsUpdateEvent({required super.id, required this.savedSnippet});
386+
387+
factory SavedSnippetsUpdateEvent.fromJson(Map<String, dynamic> json) =>
388+
_$SavedSnippetsUpdateEventFromJson(json);
389+
390+
@override
391+
Map<String, dynamic> toJson() => _$SavedSnippetsUpdateEventToJson(this);
392+
}
393+
394+
/// A [SavedSnippetsEvent] with op `remove`: https://zulip.com/api/get-events#saved_snippets-remove
395+
@JsonSerializable(fieldRename: FieldRename.snake)
396+
class SavedSnippetsRemoveEvent extends SavedSnippetsEvent {
397+
@override
398+
String get op => 'remove';
399+
400+
final int savedSnippetId;
401+
402+
SavedSnippetsRemoveEvent({required super.id, required this.savedSnippetId});
403+
404+
factory SavedSnippetsRemoveEvent.fromJson(Map<String, dynamic> json) =>
405+
_$SavedSnippetsRemoveEventFromJson(json);
406+
407+
@override
408+
Map<String, dynamic> toJson() => _$SavedSnippetsRemoveEventToJson(this);
409+
}
410+
339411
/// A Zulip event of type `stream`.
340412
///
341413
/// The corresponding API docs are in several places for

lib/api/model/events.g.dart

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/model/saved_snippet.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import 'package:collection/collection.dart';
2+
3+
import '../api/model/events.dart';
4+
import '../api/model/model.dart';
5+
6+
mixin SavedSnippetStore {
7+
Map<int, SavedSnippet> get savedSnippets;
8+
}
9+
10+
class SavedSnippetStoreImpl with SavedSnippetStore {
11+
SavedSnippetStoreImpl({required Iterable<SavedSnippet> savedSnippets})
12+
: _savedSnippets = {
13+
for (final savedSnippet in savedSnippets)
14+
savedSnippet.id: savedSnippet,
15+
};
16+
17+
@override
18+
late Map<int, SavedSnippet> savedSnippets = UnmodifiableMapView(_savedSnippets);
19+
final Map<int, SavedSnippet> _savedSnippets;
20+
21+
void handleSavedSnippetsEvent(SavedSnippetsEvent event) {
22+
switch (event) {
23+
case SavedSnippetsAddEvent(:final savedSnippet):
24+
_savedSnippets[savedSnippet.id] = savedSnippet;
25+
26+
case SavedSnippetsUpdateEvent(:final savedSnippet):
27+
assert(_savedSnippets[savedSnippet.id]!.dateCreated
28+
== savedSnippet.dateCreated); // TODO(log)
29+
_savedSnippets[savedSnippet.id] = savedSnippet;
30+
31+
case SavedSnippetsRemoveEvent(:final savedSnippetId):
32+
_savedSnippets.remove(savedSnippetId);
33+
}
34+
}
35+
}

lib/model/store.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import 'message_list.dart';
2929
import 'recent_dm_conversations.dart';
3030
import 'recent_senders.dart';
3131
import 'channel.dart';
32+
import 'saved_snippet.dart';
3233
import 'settings.dart';
3334
import 'typing_status.dart';
3435
import 'unreads.dart';
@@ -431,7 +432,7 @@ Uri? tryResolveUrl(Uri baseUrl, String reference) {
431432
/// This class does not attempt to poll an event queue
432433
/// to keep the data up to date. For that behavior, see
433434
/// [UpdateMachine].
434-
class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStore, UserStore, ChannelStore, MessageStore {
435+
class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStore, SavedSnippetStore, UserStore, ChannelStore, MessageStore {
435436
/// Construct a store for the user's data, starting from the given snapshot.
436437
///
437438
/// The global store must already have been updated with
@@ -485,6 +486,8 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
485486
emailAddressVisibility: initialSnapshot.emailAddressVisibility,
486487
emoji: EmojiStoreImpl(
487488
core: core, allRealmEmoji: initialSnapshot.realmEmoji),
489+
savedSnippets: SavedSnippetStoreImpl(
490+
savedSnippets: initialSnapshot.savedSnippets ?? []),
488491
userSettings: initialSnapshot.userSettings,
489492
typingNotifier: TypingNotifier(
490493
core: core,
@@ -523,6 +526,7 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
523526
required this.customProfileFields,
524527
required this.emailAddressVisibility,
525528
required EmojiStoreImpl emoji,
529+
required SavedSnippetStoreImpl savedSnippets,
526530
required this.userSettings,
527531
required this.typingNotifier,
528532
required UserStoreImpl users,
@@ -534,6 +538,7 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
534538
required this.recentSenders,
535539
}) : _realmEmptyTopicDisplayName = realmEmptyTopicDisplayName,
536540
_emoji = emoji,
541+
_savedSnippets = savedSnippets,
537542
_users = users,
538543
_channels = channels,
539544
_messages = messages;
@@ -619,6 +624,10 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
619624
////////////////////////////////
620625
// Data attached to the self-account on the realm.
621626

627+
@override
628+
Map<int, SavedSnippet> get savedSnippets => _savedSnippets.savedSnippets;
629+
final SavedSnippetStoreImpl _savedSnippets;
630+
622631
final UserSettings? userSettings; // TODO(server-5)
623632

624633
final TypingNotifier typingNotifier;
@@ -868,6 +877,11 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
868877
autocompleteViewManager.handleRealmUserUpdateEvent(event);
869878
notifyListeners();
870879

880+
case SavedSnippetsEvent():
881+
assert(debugLog('server event: saved_snippets/${event.op}'));
882+
_savedSnippets.handleSavedSnippetsEvent(event);
883+
notifyListeners();
884+
871885
case ChannelEvent():
872886
assert(debugLog("server event: stream/${event.op}"));
873887
_channels.handleChannelEvent(event);

test/api/model/model_checks.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ extension UserChecks on Subject<User> {
2121
Subject<bool> get isSystemBot => has((x) => x.isSystemBot, 'isSystemBot');
2222
}
2323

24+
extension SavedSnippetChecks on Subject<SavedSnippet> {
25+
Subject<int> get id => has((x) => x.id, 'id');
26+
Subject<String> get title => has((x) => x.title, 'title');
27+
Subject<String> get content => has((x) => x.content, 'content');
28+
}
29+
2430
extension ZulipStreamChecks on Subject<ZulipStream> {
2531
}
2632

test/example_data.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,28 @@ final User thirdUser = user(fullName: 'Third User');
250250

251251
final User fourthUser = user(fullName: 'Fourth User');
252252

253+
////////////////////////////////////////////////////////////////
254+
// Data attached to the self-account on the realm
255+
//
256+
257+
int _nextSavedSnippetId() => _lastSavedSnippetId++;
258+
int _lastSavedSnippetId = 1;
259+
260+
SavedSnippet savedSnippet({
261+
int? id,
262+
String? title,
263+
String? content,
264+
int? dateCreated,
265+
}) {
266+
_checkPositive(id, 'saved snippet ID');
267+
return SavedSnippet(
268+
id: id ?? _nextSavedSnippetId(),
269+
title: title ?? 'A saved snippet',
270+
content: content ?? 'foo bar baz',
271+
dateCreated: dateCreated ?? 1234567890, // TODO generate timestamp
272+
);
273+
}
274+
253275
////////////////////////////////////////////////////////////////
254276
// Streams and subscriptions.
255277
//

test/model/saved_snippet.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:zulip/api/model/events.dart';
4+
import 'package:zulip/api/model/model.dart';
5+
6+
import '../api/model/model_checks.dart';
7+
import '../example_data.dart' as eg;
8+
import 'store_checks.dart';
9+
10+
void main() {
11+
test('handleSavedSnippetsEvent', () async {
12+
final store = eg.store(initialSnapshot: eg.initialSnapshot(
13+
savedSnippets: [eg.savedSnippet(id: 101)]));
14+
check(store).savedSnippets.values.single.id.equals(101);
15+
16+
await store.handleEvent(SavedSnippetsAddEvent(id: 1,savedSnippet:
17+
eg.savedSnippet(
18+
id: 102,
19+
title: 'foo title',
20+
content: 'foo content',
21+
)));
22+
check(store).savedSnippets.values.deepEquals(<Condition<Object?>>[
23+
(it) => it.isA<SavedSnippet>().id.equals(101),
24+
(it) => it.isA<SavedSnippet>()..id.equals(102)
25+
..title.equals('foo title')
26+
..content.equals('foo content')
27+
]);
28+
29+
await store.handleEvent(SavedSnippetsRemoveEvent(id: 1, savedSnippetId: 101));
30+
check(store).savedSnippets.values.single.id.equals(102);
31+
32+
await store.handleEvent(SavedSnippetsUpdateEvent(id: 1, savedSnippet:
33+
eg.savedSnippet(
34+
id: 102,
35+
title: 'bar title',
36+
content: 'bar content',
37+
dateCreated: store.savedSnippets.values.single.dateCreated,
38+
)));
39+
check(store).savedSnippets.values.single
40+
..id.equals(102)
41+
..title.equals('bar title')
42+
..content.equals('bar content');
43+
});
44+
}

test/model/store_checks.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ extension PerAccountStoreChecks on Subject<PerAccountStore> {
5555
Subject<int> get accountId => has((x) => x.accountId, 'accountId');
5656
Subject<Account> get account => has((x) => x.account, 'account');
5757
Subject<int> get selfUserId => has((x) => x.selfUserId, 'selfUserId');
58+
Subject<Map<int, SavedSnippet>> get savedSnippets => has((x) => x.savedSnippets, 'savedSnippets');
5859
Subject<UserSettings?> get userSettings => has((x) => x.userSettings, 'userSettings');
5960
Subject<Map<int, ZulipStream>> get streams => has((x) => x.streams, 'streams');
6061
Subject<Map<String, ZulipStream>> get streamsByName => has((x) => x.streamsByName, 'streamsByName');

0 commit comments

Comments
 (0)