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 3 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
72 changes: 72 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':
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,71 @@ class RealmUserUpdateEvent extends RealmUserEvent {
Map<String, dynamic> toJson() => _$RealmUserUpdateEventToJson(this);
}

/// A Zulip event of type `saved_snippets`.
///
/// The corresponding API docs are in several places for
/// different values of `op`; see subclasses.
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.

30 changes: 30 additions & 0 deletions lib/api/route/saved_snippets.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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, {
required String title,
required String content,
}) {
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.

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

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

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

class SavedSnippetStoreImpl with SavedSnippetStore {
SavedSnippetStoreImpl({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 @@ -485,6 +486,8 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
emailAddressVisibility: initialSnapshot.emailAddressVisibility,
emoji: EmojiStoreImpl(
core: core, allRealmEmoji: initialSnapshot.realmEmoji),
savedSnippets: SavedSnippetStoreImpl(
savedSnippets: initialSnapshot.savedSnippets ?? []),
userSettings: initialSnapshot.userSettings,
typingNotifier: TypingNotifier(
core: core,
Expand Down Expand Up @@ -523,6 +526,7 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
required this.customProfileFields,
required this.emailAddressVisibility,
required EmojiStoreImpl emoji,
required SavedSnippetStoreImpl savedSnippets,
required this.userSettings,
required this.typingNotifier,
required UserStoreImpl users,
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 @@ -619,6 +624,10 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor
////////////////////////////////
// Data attached to the self-account on the realm.

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

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

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();

case ChannelEvent():
assert(debugLog("server event: stream/${event.op}"));
_channels.handleChannelEvent(event);
Expand Down
6 changes: 6 additions & 0 deletions test/api/model/model_checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ 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');
}

extension ZulipStreamChecks on Subject<ZulipStream> {
}

Expand Down
Loading