Skip to content

Commit d36bcff

Browse files
committed
api: Add save_snippet events and handle live-updates
Signed-off-by: Zixuan James Li <[email protected]>
1 parent 88a0440 commit d36bcff

File tree

8 files changed

+181
-1
lines changed

8 files changed

+181
-1
lines changed

lib/api/model/events.dart

+54
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ 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 'remove': return SavedSnippetsRemoveEvent.fromJson(json);
44+
default: return UnexpectedEvent.fromJson(json);
45+
}
4046
case 'stream':
4147
switch (json['op'] as String) {
4248
case 'create': return ChannelCreateEvent.fromJson(json);
@@ -336,6 +342,54 @@ class RealmUserUpdateEvent extends RealmUserEvent {
336342
Map<String, dynamic> toJson() => _$RealmUserUpdateEventToJson(this);
337343
}
338344

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

lib/api/model/events.g.dart

+32
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/model/saved_snippet.dart

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import '../api/model/events.dart';
2+
import '../api/model/model.dart';
3+
4+
mixin SavedSnippetStore {
5+
Iterable<SavedSnippet> get savedSnippets;
6+
}
7+
8+
class SavedSnippetStoreImpl with SavedSnippetStore {
9+
SavedSnippetStoreImpl({required Iterable<SavedSnippet> savedSnippets})
10+
: _savedSnippets = Map.fromIterable(
11+
savedSnippets, key: (x) => (x as SavedSnippet).id);
12+
13+
@override
14+
Iterable<SavedSnippet> get savedSnippets => _savedSnippets.values;
15+
16+
final Map<int, SavedSnippet> _savedSnippets;
17+
18+
void handleSavedSnippetsEvent(SavedSnippetsEvent event) {
19+
switch (event) {
20+
case SavedSnippetsAddEvent(:final savedSnippet):
21+
_savedSnippets[savedSnippet.id] = savedSnippet;
22+
23+
case SavedSnippetsRemoveEvent(:final savedSnippetId):
24+
_savedSnippets.remove(savedSnippetId);
25+
}
26+
}
27+
}

lib/model/store.dart

+15-1
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 'typing_status.dart';
3334
import 'unreads.dart';
3435
import 'user.dart';
@@ -295,7 +296,7 @@ class AccountNotFoundException implements Exception {}
295296
/// This class does not attempt to poll an event queue
296297
/// to keep the data up to date. For that behavior, see
297298
/// [UpdateMachine].
298-
class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, ChannelStore, MessageStore {
299+
class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, SavedSnippetStore, ChannelStore, MessageStore {
299300
/// Construct a store for the user's data, starting from the given snapshot.
300301
///
301302
/// The global store must already have been updated with
@@ -337,6 +338,8 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
337338
emoji: EmojiStoreImpl(
338339
realmUrl: realmUrl, allRealmEmoji: initialSnapshot.realmEmoji),
339340
accountId: accountId,
341+
savedSnippets: SavedSnippetStoreImpl(
342+
savedSnippets: initialSnapshot.savedSnippets ?? []),
340343
userSettings: initialSnapshot.userSettings,
341344
typingNotifier: TypingNotifier(
342345
connection: connection,
@@ -379,6 +382,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
379382
required this.emailAddressVisibility,
380383
required EmojiStoreImpl emoji,
381384
required this.accountId,
385+
required SavedSnippetStoreImpl savedSnippets,
382386
required this.userSettings,
383387
required this.typingNotifier,
384388
required UserStoreImpl users,
@@ -395,6 +399,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
395399
_realmEmptyTopicDisplayName = realmEmptyTopicDisplayName,
396400
_emoji = emoji,
397401
_users = users,
402+
_savedSnippets = savedSnippets,
398403
_channels = channels,
399404
_messages = messages;
400405

@@ -499,6 +504,10 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
499504
/// Will throw if called after [dispose] has been called.
500505
Account get account => _globalStore.getAccount(accountId)!;
501506

507+
@override
508+
Iterable<SavedSnippet> get savedSnippets => _savedSnippets.savedSnippets;
509+
final SavedSnippetStoreImpl _savedSnippets; // TODO(server-10)
510+
502511
final UserSettings? userSettings; // TODO(server-5)
503512

504513
final TypingNotifier typingNotifier;
@@ -726,6 +735,11 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
726735
autocompleteViewManager.handleRealmUserUpdateEvent(event);
727736
notifyListeners();
728737

738+
case SavedSnippetsEvent():
739+
assert(debugLog('server event: saved_snippet/${event.op}'));
740+
_savedSnippets.handleSavedSnippetsEvent(event);
741+
notifyListeners();
742+
729743
case ChannelEvent():
730744
assert(debugLog("server event: stream/${event.op}"));
731745
_channels.handleChannelEvent(event);

test/api/model/model_checks.dart

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ 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+
}
27+
2428
extension ZulipStreamChecks on Subject<ZulipStream> {
2529
}
2630

test/example_data.dart

+22
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,28 @@ final User thirdUser = user(fullName: 'Third User');
233233

234234
final User fourthUser = user(fullName: 'Fourth User');
235235

236+
////////////////////////////////////////////////////////////////
237+
// Data attached to the self-account on the realm
238+
//
239+
240+
int _nextSavedSnippetId() => _lastSavedSnippetId++;
241+
int _lastSavedSnippetId = 1;
242+
243+
SavedSnippet savedSnippet({
244+
int? id,
245+
String? title,
246+
String? content,
247+
int? dateCreated,
248+
}) {
249+
_checkPositive(id, 'saved snippet ID');
250+
return SavedSnippet(
251+
id: id ?? _nextSavedSnippetId(),
252+
title: title ?? 'A saved snippet',
253+
content: content ?? 'foo bar baz',
254+
dateCreated: dateCreated ?? 1741390853,
255+
);
256+
}
257+
236258
////////////////////////////////////////////////////////////////
237259
// Streams and subscriptions.
238260
//

test/model/saved_snippet.dart

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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('handleSavedSnippetEvent', () {
12+
final store = eg.store(initialSnapshot: eg.initialSnapshot(
13+
savedSnippets: [eg.savedSnippet(id: 101)]));
14+
15+
store.handleEvent(SavedSnippetsAddEvent(
16+
id: 1, savedSnippet: eg.savedSnippet(id: 102)));
17+
check(store).savedSnippets.deepEquals(<Condition<Object?>>[
18+
(it) => it.isA<SavedSnippet>().id.equals(101),
19+
(it) => it.isA<SavedSnippet>().id.equals(102),
20+
]);
21+
22+
store.handleEvent(SavedSnippetsRemoveEvent(
23+
id: 2, savedSnippetId: 101));
24+
check(store).savedSnippets.single.id.equals(102);
25+
});
26+
}

test/model/store_checks.dart

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ extension PerAccountStoreChecks on Subject<PerAccountStore> {
4545
Subject<int> get accountId => has((x) => x.accountId, 'accountId');
4646
Subject<Account> get account => has((x) => x.account, 'account');
4747
Subject<int> get selfUserId => has((x) => x.selfUserId, 'selfUserId');
48+
Subject<Iterable<SavedSnippet>> get savedSnippets => has((x) => x.savedSnippets, 'savedSnippets');
4849
Subject<UserSettings?> get userSettings => has((x) => x.userSettings, 'userSettings');
4950
Subject<Map<int, ZulipStream>> get streams => has((x) => x.streams, 'streams');
5051
Subject<Map<String, ZulipStream>> get streamsByName => has((x) => x.streamsByName, 'streamsByName');

0 commit comments

Comments
 (0)