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 16 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
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.
102 changes: 99 additions & 3 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@
"@actionSheetOptionUnstarMessage": {
"description": "Label for unstar button on action sheet."
},
"actionSheetOptionEditMessage": "Edit message",
"@actionSheetOptionEditMessage": {
"description": "Label for the 'Edit message' button in the message action sheet."
},
"actionSheetOptionMarkTopicAsRead": "Mark topic as read",
"@actionSheetOptionMarkTopicAsRead": {
"description": "Option to mark a specific topic as read in the action sheet."
Expand All @@ -168,7 +172,7 @@
"server": {"type": "String", "example": "https://example.com"}
}
},
"errorCouldNotFetchMessageSource": "Could not fetch message source",
"errorCouldNotFetchMessageSource": "Could not fetch message source.",
"@errorCouldNotFetchMessageSource": {
"description": "Error message when the source of a message could not be fetched."
},
Expand Down Expand Up @@ -219,6 +223,10 @@
"@errorMessageNotSent": {
"description": "Error message for compose box when a message could not be sent."
},
"errorMessageEditNotSaved": "Message not saved",
"@errorMessageEditNotSaved": {
"description": "Error message for compose box when a message edit could not be saved."
},
"errorLoginCouldNotConnect": "Failed to connect to server:\n{url}",
"@errorLoginCouldNotConnect": {
"description": "Error message when the app could not connect to the server.",
Expand Down Expand Up @@ -309,6 +317,10 @@
"@errorUnstarMessageFailedTitle": {
"description": "Error title when unstarring a message failed."
},
"errorCouldNotEditMessageTitle": "Could not edit message",
"@errorCouldNotEditMessageTitle": {
"description": "Error title when an exception prevented us from opening the compose box for editing a message."
},
"successLinkCopied": "Link copied",
"@successLinkCopied": {
"description": "Success message after copy link action completed."
Expand All @@ -329,6 +341,38 @@
"@errorBannerCannotPostInChannelLabel": {
"description": "Error-banner text replacing the compose box when you do not have permission to send a message to the channel."
},
"composeBoxBannerLabelEditMessage": "Edit message",
"@composeBoxBannerLabelEditMessage": {
"description": "Label text for the compose-box banner when you are editing a message."
},
"composeBoxBannerButtonCancel": "Cancel",
"@composeBoxBannerButtonCancel": {
"description": "Label text for the 'Cancel' button in the compose-box banner when you are editing a message."
},
"composeBoxBannerButtonSave": "Save",
"@composeBoxBannerButtonSave": {
"description": "Label text for the 'Save' button in the compose-box banner when you are editing a message."
},
"savingMessageEditLabel": "SAVING EDIT…",
"@savingMessageEditLabel": {
"description": "Text on a message in the message list saying that a message edit request is processing. (Use ALL CAPS for cased alphabets: Latin, Greek, Cyrillic, etc.)"
},
"savingMessageEditFailedLabel": "EDIT NOT SAVED",
"@savingMessageEditFailedLabel": {
"description": "Text on a message in the message list saying that a message edit request failed. (Use ALL CAPS for cased alphabets: Latin, Greek, Cyrillic, etc.)"
},
"discardDraftConfirmationDialogTitle": "Discard the message you’re writing?",
"@discardDraftConfirmationDialogTitle": {
"description": "Title for a confirmation dialog for discarding message text that was typed into the compose box."
},
"discardDraftConfirmationDialogMessage": "When you edit a message, the content that was previously in the compose box is discarded.",
"@discardDraftConfirmationDialogMessage": {
"description": "Message for a confirmation dialog for discarding message text that was typed into the compose box."
},
"discardDraftConfirmationDialogConfirmButton": "Discard",
"@discardDraftConfirmationDialogConfirmButton": {
"description": "Label for the 'Discard' button on a confirmation dialog for discarding message text that was typed into the compose box."
},
"composeBoxAttachFilesTooltip": "Attach files",
"@composeBoxAttachFilesTooltip": {
"description": "Tooltip for compose box icon to attach a file to the message."
Expand All @@ -341,6 +385,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 All @@ -367,6 +459,10 @@
"destination": {"type": "String", "example": "#channel name > topic name"}
}
},
"composeBoxEditMessageHint": "Message content",
"@composeBoxEditMessageHint": {
"description": "Hint text for content input when editing a message."
},
"composeBoxSendTooltip": "Send",
"@composeBoxSendTooltip": {
"description": "Tooltip for send button in compose box."
Expand Down Expand Up @@ -561,7 +657,7 @@
"url": {"type": "String", "example": "http://chat.example.com/"}
}
},
"errorInvalidResponse": "The server sent an invalid response",
"errorInvalidResponse": "The server sent an invalid response.",
"@errorInvalidResponse": {
"description": "Error message when an API call returned an invalid response."
},
Expand Down Expand Up @@ -591,7 +687,7 @@
"httpStatus": {"type": "int", "example": "500"}
}
},
"errorVideoPlayerFailed": "Unable to play the video",
"errorVideoPlayerFailed": "Unable to play the video.",
"@errorVideoPlayerFailed": {
"description": "Error message when a video fails to play."
},
Expand Down
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
Loading