Skip to content

Add and handle live-updates to saved snippets data #1511

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions lib/api/model/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ sealed class Event {
case 'update': return RealmUserUpdateEvent.fromJson(json);
default: return UnexpectedEvent.fromJson(json);
}
case 'saved_snippets':
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api: Add save_snippets events and handle live-updates

Commit-message nit: saved_snippets

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this is labeled as an "api:" commit, but it also adds to model code; how about separate api: and model: commits?

(Or perhaps just one model: commit with the API-binding additions lumped into that same commit—but if doing that, it would be logical to lump all the new API-binding code into that commit, including the initial-snapshot code.)

switch (json['op'] as String) {
case 'add': return SavedSnippetsAddEvent.fromJson(json);
case 'update': return SavedSnippetsUpdateEvent.fromJson(json);
case 'remove': return SavedSnippetsRemoveEvent.fromJson(json);
default: return UnexpectedEvent.fromJson(json);
}
case 'stream':
switch (json['op'] as String) {
case 'create': return ChannelCreateEvent.fromJson(json);
Expand Down Expand Up @@ -336,6 +343,68 @@ class RealmUserUpdateEvent extends RealmUserEvent {
Map<String, dynamic> toJson() => _$RealmUserUpdateEventToJson(this);
}

/// A Zulip event of type `saved_snippets`: https://zulip.com/api/get-events#saved_snippets-add
sealed class SavedSnippetsEvent extends Event {
@override
@JsonKey(includeToJson: true)
String get type => 'saved_snippets';

String get op;

SavedSnippetsEvent({required super.id});
}

/// A [SavedSnippetsEvent] with op `add`: https://zulip.com/api/get-events#saved_snippets-add
@JsonSerializable(fieldRename: FieldRename.snake)
class SavedSnippetsAddEvent extends SavedSnippetsEvent {
@override
String get op => 'add';

final SavedSnippet savedSnippet;

SavedSnippetsAddEvent({required super.id, required this.savedSnippet});

factory SavedSnippetsAddEvent.fromJson(Map<String, dynamic> json) =>
_$SavedSnippetsAddEventFromJson(json);

@override
Map<String, dynamic> toJson() => _$SavedSnippetsAddEventToJson(this);
}

/// A [SavedSnippetsEvent] with op `update`: https://zulip.com/api/get-events#saved_snippets-update
@JsonSerializable(fieldRename: FieldRename.snake)
class SavedSnippetsUpdateEvent extends SavedSnippetsEvent {
@override
String get op => 'update';

final SavedSnippet savedSnippet;

SavedSnippetsUpdateEvent({required super.id, required this.savedSnippet});

factory SavedSnippetsUpdateEvent.fromJson(Map<String, dynamic> json) =>
_$SavedSnippetsUpdateEventFromJson(json);

@override
Map<String, dynamic> toJson() => _$SavedSnippetsUpdateEventToJson(this);
}

/// A [SavedSnippetsEvent] with op `remove`: https://zulip.com/api/get-events#saved_snippets-remove
@JsonSerializable(fieldRename: FieldRename.snake)
class SavedSnippetsRemoveEvent extends SavedSnippetsEvent {
@override
String get op => 'remove';

final int savedSnippetId;

SavedSnippetsRemoveEvent({required super.id, required this.savedSnippetId});

factory SavedSnippetsRemoveEvent.fromJson(Map<String, dynamic> json) =>
_$SavedSnippetsRemoveEventFromJson(json);

@override
Map<String, dynamic> toJson() => _$SavedSnippetsRemoveEventToJson(this);
}

/// A Zulip event of type `stream`.
///
/// The corresponding API docs are in several places for
Expand Down
49 changes: 49 additions & 0 deletions lib/api/model/events.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions lib/api/model/initial_snapshot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class InitialSnapshot {

final List<RecentDmConversation> recentPrivateConversations;

final List<SavedSnippet>? savedSnippets; // TODO(server-10)

final List<Subscription> subscriptions;

final UnreadMessagesSnapshot unreadMsgs;
Expand Down Expand Up @@ -132,6 +134,7 @@ class InitialSnapshot {
required this.serverTypingStartedWaitPeriodMilliseconds,
required this.realmEmoji,
required this.recentPrivateConversations,
required this.savedSnippets,
required this.subscriptions,
required this.unreadMsgs,
required this.streams,
Expand Down
5 changes: 5 additions & 0 deletions lib/api/model/initial_snapshot.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions lib/api/model/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,30 @@ enum UserRole{
}
}

/// An item in `saved_snippets` from the initial snapshot.
///
/// For docs, search for "saved_snippets:"
/// in <https://zulip.com/api/register-queue>.
@JsonSerializable(fieldRename: FieldRename.snake)
class SavedSnippet {
SavedSnippet({
required this.id,
required this.title,
required this.content,
required this.dateCreated,
});

final int id;
final String title;
final String content;
final int dateCreated;

factory SavedSnippet.fromJson(Map<String, Object?> json) =>
_$SavedSnippetFromJson(json);

Map<String, dynamic> toJson() => _$SavedSnippetToJson(this);
}

/// As in `streams` in the initial snapshot.
///
/// Not called `Stream` because dart:async uses that name.
Expand Down
15 changes: 15 additions & 0 deletions lib/api/model/model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions lib/api/route/saved_snippets.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:json_annotation/json_annotation.dart';

import '../core.dart';

part 'saved_snippets.g.dart';

/// https://zulip.com/api/create-saved-snippet
Future<CreateSavedSnippetResult> createSavedSnippet(ApiConnection connection, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

api: Add createSavedSnippet route

This needs a simple smoke test in test/api/route/.

required String title,
required String content,
}) {
assert(connection.zulipFeatureLevel! >= 297); // TODO(server-10)
return connection.post('createSavedSnippet', CreateSavedSnippetResult.fromJson, 'saved_snippets', {
'title': RawParameter(title),
'content': RawParameter(content),
});
}

@JsonSerializable(fieldRename: FieldRename.snake)
class CreateSavedSnippetResult {
final int savedSnippetId;

CreateSavedSnippetResult({
required this.savedSnippetId,
});

factory CreateSavedSnippetResult.fromJson(Map<String, dynamic> json) =>
_$CreateSavedSnippetResultFromJson(json);

Map<String, dynamic> toJson() => _$CreateSavedSnippetResultToJson(this);
}
19 changes: 19 additions & 0 deletions lib/api/route/saved_snippets.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions lib/model/saved_snippet.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:collection/collection.dart';

import '../api/model/events.dart';
import '../api/model/model.dart';
import 'store.dart';

mixin SavedSnippetStore {
Map<int, SavedSnippet> get savedSnippets;
}

class SavedSnippetStoreImpl extends PerAccountStoreBase with SavedSnippetStore {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

model: Handle live-updates to savedSnippets

Commit-message nit: this commit does more than just handle live-updates to savedSnippets—it's what adds savedSnippets into the codebase in the first place :)

Could be:

model: Add PerAccountStore.savedSnippets, updating with events

or similar.

SavedSnippetStoreImpl({
required super.core,
required Iterable<SavedSnippet> savedSnippets,
}) : _savedSnippets = {
for (final savedSnippet in savedSnippets)
savedSnippet.id: savedSnippet,
};

@override
late Map<int, SavedSnippet> savedSnippets = UnmodifiableMapView(_savedSnippets);
final Map<int, SavedSnippet> _savedSnippets;

void handleSavedSnippetsEvent(SavedSnippetsEvent event) {
switch (event) {
case SavedSnippetsAddEvent(:final savedSnippet):
_savedSnippets[savedSnippet.id] = savedSnippet;

case SavedSnippetsUpdateEvent(:final savedSnippet):
assert(_savedSnippets[savedSnippet.id]!.dateCreated
== savedSnippet.dateCreated); // TODO(log)
_savedSnippets[savedSnippet.id] = savedSnippet;

case SavedSnippetsRemoveEvent(:final savedSnippetId):
_savedSnippets.remove(savedSnippetId);
}
}
}
16 changes: 15 additions & 1 deletion lib/model/store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'message_list.dart';
import 'recent_dm_conversations.dart';
import 'recent_senders.dart';
import 'channel.dart';
import 'saved_snippet.dart';
import 'settings.dart';
import 'typing_status.dart';
import 'unreads.dart';
Expand Down Expand Up @@ -431,7 +432,7 @@ Uri? tryResolveUrl(Uri baseUrl, String reference) {
/// This class does not attempt to poll an event queue
/// to keep the data up to date. For that behavior, see
/// [UpdateMachine].
class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStore, UserStore, ChannelStore, MessageStore {
class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStore, SavedSnippetStore, UserStore, ChannelStore, MessageStore {
/// Construct a store for the user's data, starting from the given snapshot.
///
/// The global store must already have been updated with
Expand Down Expand Up @@ -486,6 +487,8 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
emoji: EmojiStoreImpl(
core: core, allRealmEmoji: initialSnapshot.realmEmoji),
userSettings: initialSnapshot.userSettings,
savedSnippets: SavedSnippetStoreImpl(
core: core, savedSnippets: initialSnapshot.savedSnippets ?? []),
typingNotifier: TypingNotifier(
core: core,
typingStoppedWaitPeriod: Duration(
Expand Down Expand Up @@ -524,6 +527,7 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
required this.emailAddressVisibility,
required EmojiStoreImpl emoji,
required this.userSettings,
required SavedSnippetStoreImpl savedSnippets,
required this.typingNotifier,
required UserStoreImpl users,
required this.typingStatus,
Expand All @@ -534,6 +538,7 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
required this.recentSenders,
}) : _realmEmptyTopicDisplayName = realmEmptyTopicDisplayName,
_emoji = emoji,
_savedSnippets = savedSnippets,
_users = users,
_channels = channels,
_messages = messages;
Expand Down Expand Up @@ -621,6 +626,10 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor

final UserSettings? userSettings; // TODO(server-5)

@override
Map<int, SavedSnippet> get savedSnippets => _savedSnippets.savedSnippets;
final SavedSnippetStoreImpl _savedSnippets;

final TypingNotifier typingNotifier;

////////////////////////////////
Expand Down Expand Up @@ -868,6 +877,11 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
autocompleteViewManager.handleRealmUserUpdateEvent(event);
notifyListeners();

case SavedSnippetsEvent():
assert(debugLog('server event: saved_snippets/${event.op}'));
_savedSnippets.handleSavedSnippetsEvent(event);
notifyListeners();
Comment on lines +880 to +883
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I guess this case needs to be handled (as a no-op) in the earlier commit—

api: Add saved_snippets events

—to satisfy the analyzer there.


case ChannelEvent():
assert(debugLog("server event: stream/${event.op}"));
_channels.handleChannelEvent(event);
Expand Down
7 changes: 7 additions & 0 deletions test/api/model/model_checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ extension UserChecks on Subject<User> {
Subject<bool> get isSystemBot => has((x) => x.isSystemBot, 'isSystemBot');
}

extension SavedSnippetChecks on Subject<SavedSnippet> {
Subject<int> get id => has((x) => x.id, 'id');
Subject<String> get title => has((x) => x.title, 'title');
Subject<String> get content => has((x) => x.content, 'content');
Subject<int> get dateCreated => has((x) => x.dateCreated, 'dateCreated');
}

extension ZulipStreamChecks on Subject<ZulipStream> {
}

Expand Down
Loading