Skip to content

Commit 654c03d

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

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';
@@ -267,7 +268,7 @@ class AccountNotFoundException implements Exception {}
267268
/// This class does not attempt to poll an event queue
268269
/// to keep the data up to date. For that behavior, see
269270
/// [UpdateMachine].
270-
class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, ChannelStore, MessageStore {
271+
class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, SavedSnippetStore, ChannelStore, MessageStore {
271272
/// Construct a store for the user's data, starting from the given snapshot.
272273
///
273274
/// The global store must already have been updated with
@@ -309,6 +310,8 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
309310
emoji: EmojiStoreImpl(
310311
realmUrl: realmUrl, allRealmEmoji: initialSnapshot.realmEmoji),
311312
accountId: accountId,
313+
savedSnippets: SavedSnippetStoreImpl(
314+
savedSnippets: initialSnapshot.savedSnippets ?? []),
312315
userSettings: initialSnapshot.userSettings,
313316
typingNotifier: TypingNotifier(
314317
connection: connection,
@@ -351,6 +354,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
351354
required this.emailAddressVisibility,
352355
required EmojiStoreImpl emoji,
353356
required this.accountId,
357+
required SavedSnippetStoreImpl savedSnippets,
354358
required this.userSettings,
355359
required this.typingNotifier,
356360
required UserStoreImpl users,
@@ -367,6 +371,7 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
367371
_realmEmptyTopicDisplayName = realmEmptyTopicDisplayName,
368372
_emoji = emoji,
369373
_users = users,
374+
_savedSnippets = savedSnippets,
370375
_channels = channels,
371376
_messages = messages;
372377

@@ -471,6 +476,10 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
471476
/// Will throw if called after [dispose] has been called.
472477
Account get account => _globalStore.getAccount(accountId)!;
473478

479+
@override
480+
Iterable<SavedSnippet> get savedSnippets => _savedSnippets.savedSnippets;
481+
final SavedSnippetStoreImpl _savedSnippets; // TODO(server-10)
482+
474483
final UserSettings? userSettings; // TODO(server-5)
475484

476485
final TypingNotifier typingNotifier;
@@ -698,6 +707,11 @@ class PerAccountStore extends ChangeNotifier with EmojiStore, UserStore, Channel
698707
autocompleteViewManager.handleRealmUserUpdateEvent(event);
699708
notifyListeners();
700709

710+
case SavedSnippetsEvent():
711+
assert(debugLog('server event: saved_snippet/${event.op}'));
712+
_savedSnippets.handleSavedSnippetsEvent(event);
713+
notifyListeners();
714+
701715
case ChannelEvent():
702716
assert(debugLog("server event: stream/${event.op}"));
703717
_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
@@ -231,6 +231,28 @@ final User thirdUser = user(fullName: 'Third User');
231231

232232
final User fourthUser = user(fullName: 'Fourth User');
233233

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

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
@@ -38,6 +38,7 @@ extension PerAccountStoreChecks on Subject<PerAccountStore> {
3838
Subject<int> get accountId => has((x) => x.accountId, 'accountId');
3939
Subject<Account> get account => has((x) => x.account, 'account');
4040
Subject<int> get selfUserId => has((x) => x.selfUserId, 'selfUserId');
41+
Subject<Iterable<SavedSnippet>> get savedSnippets => has((x) => x.savedSnippets, 'savedSnippets');
4142
Subject<UserSettings?> get userSettings => has((x) => x.userSettings, 'userSettings');
4243
Subject<Map<int, ZulipStream>> get streams => has((x) => x.streams, 'streams');
4344
Subject<Map<String, ZulipStream>> get streamsByName => has((x) => x.streamsByName, 'streamsByName');

0 commit comments

Comments
 (0)