Skip to content

Support saved snippets #1391

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 10 commits into
base: main
Choose a base branch
from
Binary file modified assets/icons/ZulipIcons.ttf
Binary file not shown.
3 changes: 3 additions & 0 deletions assets/icons/message_square_text.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions assets/icons/plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,54 @@
"@composeBoxAttachFromCameraTooltip": {
"description": "Tooltip for compose box icon to attach an image from the camera to the message."
},
"composeBoxShowSavedSnippetsTooltip": "Show saved snippets",
"@composeBoxShowSavedSnippetsTooltip": {
"description": "Tooltip for compose box icon to show a list of saved snippets."
},
"noSavedSnippets": "No saved snippets",
"@noSavedSnippets": {
"description": "Text to show on the saved snippets bottom sheet when there are no saved snippets."
},
"savedSnippetsTitle": "Saved snippets",
"@savedSnippetsTitle": {
"description": "Title for the bottom sheet to display saved snippets."
},
"newSavedSnippetButton": "New",
"@newSavedSnippetButton": {
"description": "Label for adding a new saved snippet."
},
"newSavedSnippetTitle": "New snippet",
"@newSavedSnippetTitle": {
"description": "Title for the bottom sheet to add a new saved snippet."
},
"newSavedSnippetTitleHint": "Title",
"@newSavedSnippetTitleHint": {
"description": "Hint text for the title input when adding a new saved snippet."
},
"newSavedSnippetContentHint": "Content",
"@newSavedSnippetContentHint": {
"description": "Hint text for the content input when adding a new saved snippet."
},
"errorFailedToCreateSavedSnippetTitle": "Failed to create saved snippet",
"@errorFailedToCreateSavedSnippetTitle": {
"description": "Error title when the saved snippet failed to be created."
},
"savedSnippetTitleValidationErrorEmpty": "Title cannot be empty.",
"@savedSnippetTitleValidationErrorEmpty": {
"description": "Validation error message when the title of the saved snippet is empty."
},
"savedSnippetTitleValidationErrorTooLong": "Title length shouldn't be greater than 60 characters.",
"@savedSnippetTitleValidationErrorTooLong": {
"description": "Validation error message when the title of the saved snippet is too long."
},
"savedSnippetContentValidationErrorEmpty": "Content cannot be empty.",
"@savedSnippetContentValidationErrorEmpty": {
"description": "Validation error message when the content of the saved snippet is empty."
},
"savedSnippetContentValidationErrorTooLong": "Content length shouldn't be greater than 10000 characters.",
"@savedSnippetContentValidationErrorTooLong": {
"description": "Validation error message when the content of the saved snippet is too long."
},
"composeBoxGenericContentHint": "Type a message",
"@composeBoxGenericContentHint": {
"description": "Hint text for content input when sending a message."
Expand Down
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':
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, {
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.

Loading
Loading