From 63f712aaa9a9e66c4c3d87779dd88e31284bd9a3 Mon Sep 17 00:00:00 2001 From: Zixuan James Li Date: Tue, 20 May 2025 19:08:07 +0200 Subject: [PATCH 1/8] l10n: Update (squashed) translations from Weblate and resolve conflicts This squashes all the pending commits from hosted Weblate that are not in our GitHub repo. This also resolves the merge conflicts between the two remotes, and has `tools/check --fix l10n` rerun. CZO discussion: https://chat.zulip.org/#narrow/channel/58-translation/topic/Weblate.20conflict.20resolution/near/2178307 --- assets/l10n/app_pl.arb | 62 ++++++++++++++++++- .../l10n/zulip_localizations_pl.dart | 34 +++++----- 2 files changed, 76 insertions(+), 20 deletions(-) diff --git a/assets/l10n/app_pl.arb b/assets/l10n/app_pl.arb index af51066e19..8de9527def 100644 --- a/assets/l10n/app_pl.arb +++ b/assets/l10n/app_pl.arb @@ -107,7 +107,7 @@ } } }, - "errorCouldNotFetchMessageSource": "Nie można uzyskać źródłowej wiadomości", + "errorCouldNotFetchMessageSource": "Nie można uzyskać źródłowej wiadomości.", "@errorCouldNotFetchMessageSource": { "description": "Error message when the source of a message could not be fetched." }, @@ -377,11 +377,11 @@ "@topicValidationErrorMandatoryButEmpty": { "description": "Topic validation error when topic is required but was empty." }, - "errorInvalidResponse": "Nieprawidłowa odpowiedź serwera", + "errorInvalidResponse": "Nieprawidłowa odpowiedź serwera.", "@errorInvalidResponse": { "description": "Error message when an API call returned an invalid response." }, - "errorVideoPlayerFailed": "Nie da rady odtworzyć wideo", + "errorVideoPlayerFailed": "Nie da rady odtworzyć wideo.", "@errorVideoPlayerFailed": { "description": "Error message when a video fails to play." }, @@ -1016,5 +1016,61 @@ "example": "general chat" } } + }, + "actionSheetOptionEditMessage": "Zmień wiadomość", + "@actionSheetOptionEditMessage": { + "description": "Label for the 'Edit message' button in the message action sheet." + }, + "errorMessageEditNotSaved": "Nie zapisano wiadomości", + "@errorMessageEditNotSaved": { + "description": "Error message for compose box when a message edit could not be saved." + }, + "errorCouldNotEditMessageTitle": "Nie można zmienić wiadomości", + "@errorCouldNotEditMessageTitle": { + "description": "Error title when an exception prevented us from opening the compose box for editing a message." + }, + "composeBoxBannerLabelEditMessage": "Zmień wiadomość", + "@composeBoxBannerLabelEditMessage": { + "description": "Label text for the compose-box banner when you are editing a message." + }, + "editAlreadyInProgressMessage": "Operacja zmiany w toku. Zaczekaj na jej zakończenie.", + "@editAlreadyInProgressMessage": { + "description": "Error message when a message edit cannot be saved because there is another edit already in progress." + }, + "savingMessageEditLabel": "ZAPIS ZMIANY…", + "@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": "NIE ZAPISANO ZMIANY", + "@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": "Czy chcesz przerwać szykowanie wpisu?", + "@discardDraftConfirmationDialogTitle": { + "description": "Title for a confirmation dialog for discarding message text that was typed into the compose box." + }, + "discardDraftConfirmationDialogMessage": "Miej na uwadze, że przechodząc do zmiany wiadomości wyczyścisz okno nowej wiadomości.", + "@discardDraftConfirmationDialogMessage": { + "description": "Message for a confirmation dialog for discarding message text that was typed into the compose box." + }, + "discardDraftConfirmationDialogConfirmButton": "Odrzuć", + "@discardDraftConfirmationDialogConfirmButton": { + "description": "Label for the 'Discard' button on a confirmation dialog for discarding message text that was typed into the compose box." + }, + "composeBoxBannerButtonCancel": "Anuluj", + "@composeBoxBannerButtonCancel": { + "description": "Label text for the 'Cancel' button in the compose-box banner when you are editing a message." + }, + "preparingEditMessageContentInput": "Przygotowywanie…", + "@preparingEditMessageContentInput": { + "description": "Hint text for content input when the compose box is preparing to edit a message." + }, + "editAlreadyInProgressTitle": "Nie udało się zapisać zmiany", + "@editAlreadyInProgressTitle": { + "description": "Error title when a message edit cannot be saved because there is another edit already in progress." + }, + "composeBoxBannerButtonSave": "Zapisz", + "@composeBoxBannerButtonSave": { + "description": "Label text for the 'Save' button in the compose-box banner when you are editing a message." } } diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart index a3cb2ee66b..26b4b7e306 100644 --- a/lib/generated/l10n/zulip_localizations_pl.dart +++ b/lib/generated/l10n/zulip_localizations_pl.dart @@ -128,7 +128,7 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get actionSheetOptionUnstarMessage => 'Odbierz gwiazdkę'; @override - String get actionSheetOptionEditMessage => 'Edit message'; + String get actionSheetOptionEditMessage => 'Zmień wiadomość'; @override String get actionSheetOptionMarkTopicAsRead => @@ -150,7 +150,7 @@ class ZulipLocalizationsPl extends ZulipLocalizations { @override String get errorCouldNotFetchMessageSource => - 'Nie można uzyskać źródłowej wiadomości'; + 'Nie można uzyskać źródłowej wiadomości.'; @override String get errorCopyingFailed => 'Nie udało się skopiować'; @@ -201,7 +201,7 @@ class ZulipLocalizationsPl extends ZulipLocalizations { String get errorMessageNotSent => 'Nie wysłano wiadomości'; @override - String get errorMessageEditNotSaved => 'Message not saved'; + String get errorMessageEditNotSaved => 'Nie zapisano wiadomości'; @override String errorLoginCouldNotConnect(String url) { @@ -276,7 +276,7 @@ class ZulipLocalizationsPl extends ZulipLocalizations { 'Odebranie gwiazdki bez powodzenia'; @override - String get errorCouldNotEditMessageTitle => 'Could not edit message'; + String get errorCouldNotEditMessageTitle => 'Nie można zmienić wiadomości'; @override String get successLinkCopied => 'Skopiowano odnośnik'; @@ -296,37 +296,37 @@ class ZulipLocalizationsPl extends ZulipLocalizations { 'Nie masz uprawnień do dodawania wpisów w tym kanale.'; @override - String get composeBoxBannerLabelEditMessage => 'Edit message'; + String get composeBoxBannerLabelEditMessage => 'Zmień wiadomość'; @override - String get composeBoxBannerButtonCancel => 'Cancel'; + String get composeBoxBannerButtonCancel => 'Anuluj'; @override - String get composeBoxBannerButtonSave => 'Save'; + String get composeBoxBannerButtonSave => 'Zapisz'; @override - String get editAlreadyInProgressTitle => 'Cannot edit message'; + String get editAlreadyInProgressTitle => 'Nie udało się zapisać zmiany'; @override String get editAlreadyInProgressMessage => - 'An edit is already in progress. Please wait for it to complete.'; + 'Operacja zmiany w toku. Zaczekaj na jej zakończenie.'; @override - String get savingMessageEditLabel => 'SAVING EDIT…'; + String get savingMessageEditLabel => 'ZAPIS ZMIANY…'; @override - String get savingMessageEditFailedLabel => 'EDIT NOT SAVED'; + String get savingMessageEditFailedLabel => 'NIE ZAPISANO ZMIANY'; @override String get discardDraftConfirmationDialogTitle => - 'Discard the message you’re writing?'; + 'Czy chcesz przerwać szykowanie wpisu?'; @override String get discardDraftConfirmationDialogMessage => - 'When you edit a message, the content that was previously in the compose box is discarded.'; + 'Miej na uwadze, że przechodząc do zmiany wiadomości wyczyścisz okno nowej wiadomości.'; @override - String get discardDraftConfirmationDialogConfirmButton => 'Discard'; + String get discardDraftConfirmationDialogConfirmButton => 'Odrzuć'; @override String get composeBoxAttachFilesTooltip => 'Dołącz pliki'; @@ -357,7 +357,7 @@ class ZulipLocalizationsPl extends ZulipLocalizations { } @override - String get preparingEditMessageContentInput => 'Preparing…'; + String get preparingEditMessageContentInput => 'Przygotowywanie…'; @override String get composeBoxSendTooltip => 'Wyślij'; @@ -511,7 +511,7 @@ class ZulipLocalizationsPl extends ZulipLocalizations { } @override - String get errorInvalidResponse => 'Nieprawidłowa odpowiedź serwera'; + String get errorInvalidResponse => 'Nieprawidłowa odpowiedź serwera.'; @override String get errorNetworkRequestFailed => 'Dostęp do sieci bez powodzenia'; @@ -532,7 +532,7 @@ class ZulipLocalizationsPl extends ZulipLocalizations { } @override - String get errorVideoPlayerFailed => 'Nie da rady odtworzyć wideo'; + String get errorVideoPlayerFailed => 'Nie da rady odtworzyć wideo.'; @override String get serverUrlValidationErrorEmpty => 'Proszę podaj URL.'; From 1a157a9fc2fcaef9dba8120c51b4dbed7276104a Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Thu, 22 May 2025 11:40:37 -0700 Subject: [PATCH 2/8] android [nfc]: Add a TODO(#855) for removing a lint suppression Added in 3013e0725, a file_picker package upgrade; #855 is for dropping the file_picker dependency. --- android/app/lint-baseline.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/lint-baseline.xml b/android/app/lint-baseline.xml index 006b010637..a3d1aeac5c 100644 --- a/android/app/lint-baseline.xml +++ b/android/app/lint-baseline.xml @@ -1,6 +1,7 @@ + From 15f3f5929daf15f614f72559c44a0574d0168003 Mon Sep 17 00:00:00 2001 From: Chris Bobbe Date: Thu, 22 May 2025 13:41:31 -0700 Subject: [PATCH 3/8] android build: Disable lint AndroidGradlePluginVersion We're successfully managing our Gradle and AGP upgrades without this lint rule, and it's annoying that it fails our CI builds. Discussion: https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/CI.20fail.3A.20lint.20on.20Gradle.20version.3F/near/2179323 --- android/app/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/build.gradle b/android/app/build.gradle index 640d7a1fdb..84ad671523 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -78,6 +78,7 @@ android { checkAllWarnings = true warningsAsErrors = true baseline = file("lint-baseline.xml") + disable += ['AndroidGradlePluginVersion'] } } From e5f703a52ee62dcda41e49e47b3678c187a586b2 Mon Sep 17 00:00:00 2001 From: Zixuan James Li Date: Thu, 30 Jan 2025 14:39:21 -0500 Subject: [PATCH 4/8] api: Add savedSnippets to initial snapshot --- lib/api/model/initial_snapshot.dart | 3 +++ lib/api/model/initial_snapshot.g.dart | 5 +++++ lib/api/model/model.dart | 24 ++++++++++++++++++++++++ lib/api/model/model.g.dart | 15 +++++++++++++++ test/example_data.dart | 2 ++ 5 files changed, 49 insertions(+) diff --git a/lib/api/model/initial_snapshot.dart b/lib/api/model/initial_snapshot.dart index 054230a256..f4cc2fe5fc 100644 --- a/lib/api/model/initial_snapshot.dart +++ b/lib/api/model/initial_snapshot.dart @@ -48,6 +48,8 @@ class InitialSnapshot { final List recentPrivateConversations; + final List? savedSnippets; // TODO(server-10) + final List subscriptions; final UnreadMessagesSnapshot unreadMsgs; @@ -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, diff --git a/lib/api/model/initial_snapshot.g.dart b/lib/api/model/initial_snapshot.g.dart index 570d7c2bba..36afb0a39f 100644 --- a/lib/api/model/initial_snapshot.g.dart +++ b/lib/api/model/initial_snapshot.g.dart @@ -45,6 +45,10 @@ InitialSnapshot _$InitialSnapshotFromJson( (json['recent_private_conversations'] as List) .map((e) => RecentDmConversation.fromJson(e as Map)) .toList(), + savedSnippets: + (json['saved_snippets'] as List?) + ?.map((e) => SavedSnippet.fromJson(e as Map)) + .toList(), subscriptions: (json['subscriptions'] as List) .map((e) => Subscription.fromJson(e as Map)) @@ -128,6 +132,7 @@ Map _$InitialSnapshotToJson(InitialSnapshot instance) => instance.serverTypingStartedWaitPeriodMilliseconds, 'realm_emoji': instance.realmEmoji, 'recent_private_conversations': instance.recentPrivateConversations, + 'saved_snippets': instance.savedSnippets, 'subscriptions': instance.subscriptions, 'unread_msgs': instance.unreadMsgs, 'streams': instance.streams, diff --git a/lib/api/model/model.dart b/lib/api/model/model.dart index a2874c4c44..131a51991b 100644 --- a/lib/api/model/model.dart +++ b/lib/api/model/model.dart @@ -311,6 +311,30 @@ enum UserRole{ } } +/// An item in `saved_snippets` from the initial snapshot. +/// +/// For docs, search for "saved_snippets:" +/// in . +@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 json) => + _$SavedSnippetFromJson(json); + + Map toJson() => _$SavedSnippetToJson(this); +} + /// As in `streams` in the initial snapshot. /// /// Not called `Stream` because dart:async uses that name. diff --git a/lib/api/model/model.g.dart b/lib/api/model/model.g.dart index cddf78beb0..67fc606031 100644 --- a/lib/api/model/model.g.dart +++ b/lib/api/model/model.g.dart @@ -162,6 +162,21 @@ Map _$ProfileFieldUserDataToJson( 'rendered_value': instance.renderedValue, }; +SavedSnippet _$SavedSnippetFromJson(Map json) => SavedSnippet( + id: (json['id'] as num).toInt(), + title: json['title'] as String, + content: json['content'] as String, + dateCreated: (json['date_created'] as num).toInt(), +); + +Map _$SavedSnippetToJson(SavedSnippet instance) => + { + 'id': instance.id, + 'title': instance.title, + 'content': instance.content, + 'date_created': instance.dateCreated, + }; + ZulipStream _$ZulipStreamFromJson(Map json) => ZulipStream( streamId: (json['stream_id'] as num).toInt(), name: json['name'] as String, diff --git a/test/example_data.dart b/test/example_data.dart index c80fcf4528..c437a0a10a 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -959,6 +959,7 @@ InitialSnapshot initialSnapshot({ int? serverTypingStartedWaitPeriodMilliseconds, Map? realmEmoji, List? recentPrivateConversations, + List? savedSnippets, List? subscriptions, UnreadMessagesSnapshot? unreadMsgs, List? streams, @@ -994,6 +995,7 @@ InitialSnapshot initialSnapshot({ serverTypingStartedWaitPeriodMilliseconds ?? 10000, realmEmoji: realmEmoji ?? {}, recentPrivateConversations: recentPrivateConversations ?? [], + savedSnippets: savedSnippets ?? [], subscriptions: subscriptions ?? [], // TODO add subscriptions to default unreadMsgs: unreadMsgs ?? _unreadMsgs(), streams: streams ?? [], // TODO add streams to default From 477a7775979112633dd579de5081045c8524730d Mon Sep 17 00:00:00 2001 From: Zixuan James Li Date: Tue, 11 Mar 2025 14:31:34 -0400 Subject: [PATCH 5/8] api: Add saved_snippets events --- lib/api/model/events.dart | 69 +++++++++++++++++++++++++++++++++++++ lib/api/model/events.g.dart | 49 ++++++++++++++++++++++++++ lib/model/store.dart | 4 +++ 3 files changed, 122 insertions(+) diff --git a/lib/api/model/events.dart b/lib/api/model/events.dart index 0479b0428f..62789333e1 100644 --- a/lib/api/model/events.dart +++ b/lib/api/model/events.dart @@ -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); @@ -336,6 +343,68 @@ class RealmUserUpdateEvent extends RealmUserEvent { Map 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 json) => + _$SavedSnippetsAddEventFromJson(json); + + @override + Map 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 json) => + _$SavedSnippetsUpdateEventFromJson(json); + + @override + Map 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 json) => + _$SavedSnippetsRemoveEventFromJson(json); + + @override + Map toJson() => _$SavedSnippetsRemoveEventToJson(this); +} + /// A Zulip event of type `stream`. /// /// The corresponding API docs are in several places for diff --git a/lib/api/model/events.g.dart b/lib/api/model/events.g.dart index 35206d77b9..94fe288150 100644 --- a/lib/api/model/events.g.dart +++ b/lib/api/model/events.g.dart @@ -203,6 +203,55 @@ Json? _$JsonConverterToJson( Json? Function(Value value) toJson, ) => value == null ? null : toJson(value); +SavedSnippetsAddEvent _$SavedSnippetsAddEventFromJson( + Map json, +) => SavedSnippetsAddEvent( + id: (json['id'] as num).toInt(), + savedSnippet: SavedSnippet.fromJson( + json['saved_snippet'] as Map, + ), +); + +Map _$SavedSnippetsAddEventToJson( + SavedSnippetsAddEvent instance, +) => { + 'id': instance.id, + 'type': instance.type, + 'saved_snippet': instance.savedSnippet, +}; + +SavedSnippetsUpdateEvent _$SavedSnippetsUpdateEventFromJson( + Map json, +) => SavedSnippetsUpdateEvent( + id: (json['id'] as num).toInt(), + savedSnippet: SavedSnippet.fromJson( + json['saved_snippet'] as Map, + ), +); + +Map _$SavedSnippetsUpdateEventToJson( + SavedSnippetsUpdateEvent instance, +) => { + 'id': instance.id, + 'type': instance.type, + 'saved_snippet': instance.savedSnippet, +}; + +SavedSnippetsRemoveEvent _$SavedSnippetsRemoveEventFromJson( + Map json, +) => SavedSnippetsRemoveEvent( + id: (json['id'] as num).toInt(), + savedSnippetId: (json['saved_snippet_id'] as num).toInt(), +); + +Map _$SavedSnippetsRemoveEventToJson( + SavedSnippetsRemoveEvent instance, +) => { + 'id': instance.id, + 'type': instance.type, + 'saved_snippet_id': instance.savedSnippetId, +}; + ChannelCreateEvent _$ChannelCreateEventFromJson(Map json) => ChannelCreateEvent( id: (json['id'] as num).toInt(), diff --git a/lib/model/store.dart b/lib/model/store.dart index 297e4300bc..de8c28e79a 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -871,6 +871,10 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor autocompleteViewManager.handleRealmUserUpdateEvent(event); notifyListeners(); + case SavedSnippetsEvent(): + // TODO handle + break; + case ChannelEvent(): assert(debugLog("server event: stream/${event.op}")); _channels.handleChannelEvent(event); From ab2bbbf39587aa375462369d5d419fec7e1f9be8 Mon Sep 17 00:00:00 2001 From: Zixuan James Li Date: Tue, 20 May 2025 16:38:41 -0400 Subject: [PATCH 6/8] model: Add PerAccountStore.savedSnippets, updating with events --- lib/model/saved_snippet.dart | 38 ++++++++++++++++++++++++++ lib/model/store.dart | 16 +++++++++-- test/api/model/model_checks.dart | 7 +++++ test/example_data.dart | 22 +++++++++++++++ test/model/saved_snippet_test.dart | 44 ++++++++++++++++++++++++++++++ test/model/store_checks.dart | 1 + 6 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 lib/model/saved_snippet.dart create mode 100644 test/model/saved_snippet_test.dart diff --git a/lib/model/saved_snippet.dart b/lib/model/saved_snippet.dart new file mode 100644 index 0000000000..59c8347591 --- /dev/null +++ b/lib/model/saved_snippet.dart @@ -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 get savedSnippets; +} + +class SavedSnippetStoreImpl extends PerAccountStoreBase with SavedSnippetStore { + SavedSnippetStoreImpl({ + required super.core, + required Iterable savedSnippets, + }) : _savedSnippets = { + for (final savedSnippet in savedSnippets) + savedSnippet.id: savedSnippet, + }; + + @override + late Map savedSnippets = UnmodifiableMapView(_savedSnippets); + final Map _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); + } + } +} diff --git a/lib/model/store.dart b/lib/model/store.dart index de8c28e79a..240e3ab4e4 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -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'; @@ -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 @@ -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( @@ -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, @@ -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; @@ -624,6 +629,10 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor final UserSettings? userSettings; // TODO(server-5) + @override + Map get savedSnippets => _savedSnippets.savedSnippets; + final SavedSnippetStoreImpl _savedSnippets; + final TypingNotifier typingNotifier; //////////////////////////////// @@ -872,8 +881,9 @@ class PerAccountStore extends PerAccountStoreBase with ChangeNotifier, EmojiStor notifyListeners(); case SavedSnippetsEvent(): - // TODO handle - break; + assert(debugLog('server event: saved_snippets/${event.op}')); + _savedSnippets.handleSavedSnippetsEvent(event); + notifyListeners(); case ChannelEvent(): assert(debugLog("server event: stream/${event.op}")); diff --git a/test/api/model/model_checks.dart b/test/api/model/model_checks.dart index 1a17f70f60..b90238ae35 100644 --- a/test/api/model/model_checks.dart +++ b/test/api/model/model_checks.dart @@ -21,6 +21,13 @@ extension UserChecks on Subject { Subject get isSystemBot => has((x) => x.isSystemBot, 'isSystemBot'); } +extension SavedSnippetChecks on Subject { + Subject get id => has((x) => x.id, 'id'); + Subject get title => has((x) => x.title, 'title'); + Subject get content => has((x) => x.content, 'content'); + Subject get dateCreated => has((x) => x.dateCreated, 'dateCreated'); +} + extension ZulipStreamChecks on Subject { } diff --git a/test/example_data.dart b/test/example_data.dart index c437a0a10a..d803196269 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -286,6 +286,28 @@ final User thirdUser = user(fullName: 'Third User'); final User fourthUser = user(fullName: 'Fourth User'); +//////////////////////////////////////////////////////////////// +// Data attached to the self-account on the realm +// + +int _nextSavedSnippetId() => _lastSavedSnippetId++; +int _lastSavedSnippetId = 1; + +SavedSnippet savedSnippet({ + int? id, + String? title, + String? content, + int? dateCreated, +}) { + _checkPositive(id, 'saved snippet ID'); + return SavedSnippet( + id: id ?? _nextSavedSnippetId(), + title: title ?? 'A saved snippet', + content: content ?? 'foo bar baz', + dateCreated: dateCreated ?? 1234567890, // TODO generate timestamp + ); +} + //////////////////////////////////////////////////////////////// // Streams and subscriptions. // diff --git a/test/model/saved_snippet_test.dart b/test/model/saved_snippet_test.dart new file mode 100644 index 0000000000..3c6756f977 --- /dev/null +++ b/test/model/saved_snippet_test.dart @@ -0,0 +1,44 @@ +import 'package:checks/checks.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zulip/api/model/events.dart'; +import 'package:zulip/api/model/model.dart'; + +import '../api/model/model_checks.dart'; +import '../example_data.dart' as eg; +import 'store_checks.dart'; + +void main() { + test('handleSavedSnippetsEvent', () async { + final store = eg.store(initialSnapshot: eg.initialSnapshot( + savedSnippets: [eg.savedSnippet(id: 101)])); + check(store).savedSnippets.values.single.id.equals(101); + + await store.handleEvent(SavedSnippetsAddEvent(id: 1, + savedSnippet: eg.savedSnippet( + id: 102, + title: 'foo title', + content: 'foo content', + ))); + check(store).savedSnippets.values.deepEquals(>[ + (it) => it.isA().id.equals(101), + (it) => it.isA()..id.equals(102) + ..title.equals('foo title') + ..content.equals('foo content') + ]); + + await store.handleEvent(SavedSnippetsRemoveEvent(id: 1, savedSnippetId: 101)); + check(store).savedSnippets.values.single.id.equals(102); + + await store.handleEvent(SavedSnippetsUpdateEvent(id: 1, + savedSnippet: eg.savedSnippet( + id: 102, + title: 'bar title', + content: 'bar content', + dateCreated: store.savedSnippets.values.single.dateCreated, + ))); + check(store).savedSnippets.values.single + ..id.equals(102) + ..title.equals('bar title') + ..content.equals('bar content'); + }); +} diff --git a/test/model/store_checks.dart b/test/model/store_checks.dart index 32379a6f06..93e24dffdd 100644 --- a/test/model/store_checks.dart +++ b/test/model/store_checks.dart @@ -56,6 +56,7 @@ extension PerAccountStoreChecks on Subject { Subject get account => has((x) => x.account, 'account'); Subject get selfUserId => has((x) => x.selfUserId, 'selfUserId'); Subject get userSettings => has((x) => x.userSettings, 'userSettings'); + Subject> get savedSnippets => has((x) => x.savedSnippets, 'savedSnippets'); Subject> get streams => has((x) => x.streams, 'streams'); Subject> get streamsByName => has((x) => x.streamsByName, 'streamsByName'); Subject> get subscriptions => has((x) => x.subscriptions, 'subscriptions'); From 2a6c8afb0256763c489c918ffd6851a59dc4f36e Mon Sep 17 00:00:00 2001 From: Zixuan James Li Date: Thu, 6 Mar 2025 14:23:52 -0500 Subject: [PATCH 7/8] api: Add createSavedSnippet route --- lib/api/route/saved_snippets.dart | 31 +++++++++++++++++++++++++ lib/api/route/saved_snippets.g.dart | 19 +++++++++++++++ test/api/route/route_checks.dart | 4 ++++ test/api/route/saved_snippets_test.dart | 27 +++++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 lib/api/route/saved_snippets.dart create mode 100644 lib/api/route/saved_snippets.g.dart create mode 100644 test/api/route/saved_snippets_test.dart diff --git a/lib/api/route/saved_snippets.dart b/lib/api/route/saved_snippets.dart new file mode 100644 index 0000000000..047a35051e --- /dev/null +++ b/lib/api/route/saved_snippets.dart @@ -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 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 json) => + _$CreateSavedSnippetResultFromJson(json); + + Map toJson() => _$CreateSavedSnippetResultToJson(this); +} diff --git a/lib/api/route/saved_snippets.g.dart b/lib/api/route/saved_snippets.g.dart new file mode 100644 index 0000000000..aeb3c2a6c5 --- /dev/null +++ b/lib/api/route/saved_snippets.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: constant_identifier_names, unnecessary_cast + +part of 'saved_snippets.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CreateSavedSnippetResult _$CreateSavedSnippetResultFromJson( + Map json, +) => CreateSavedSnippetResult( + savedSnippetId: (json['saved_snippet_id'] as num).toInt(), +); + +Map _$CreateSavedSnippetResultToJson( + CreateSavedSnippetResult instance, +) => {'saved_snippet_id': instance.savedSnippetId}; diff --git a/test/api/route/route_checks.dart b/test/api/route/route_checks.dart index 6d310ab200..1ecd90e9c8 100644 --- a/test/api/route/route_checks.dart +++ b/test/api/route/route_checks.dart @@ -1,8 +1,12 @@ import 'package:checks/checks.dart'; import 'package:zulip/api/route/messages.dart'; +import 'package:zulip/api/route/saved_snippets.dart'; extension SendMessageResultChecks on Subject { Subject get id => has((e) => e.id, 'id'); } +extension CreateSavedSnippetResultChecks on Subject { + Subject get savedSnippetId => has((e) => e.savedSnippetId, 'savedSnippetId'); +} // TODO add similar extensions for other classes in api/route/*.dart diff --git a/test/api/route/saved_snippets_test.dart b/test/api/route/saved_snippets_test.dart new file mode 100644 index 0000000000..3eeccbde8b --- /dev/null +++ b/test/api/route/saved_snippets_test.dart @@ -0,0 +1,27 @@ +import 'package:checks/checks.dart'; +import 'package:http/http.dart' as http; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zulip/api/route/saved_snippets.dart'; + +import '../../stdlib_checks.dart'; +import '../fake_api.dart'; +import 'route_checks.dart'; + +void main() { + test('smoke', () async { + return FakeApiConnection.with_((connection) async { + connection.prepare( + json: CreateSavedSnippetResult(savedSnippetId: 123).toJson()); + final result = await createSavedSnippet(connection, + title: 'test saved snippet', content: 'content'); + check(connection.takeRequests()).single.isA() + ..method.equals('POST') + ..url.path.equals('/api/v1/saved_snippets') + ..bodyFields.deepEquals({ + 'title': 'test saved snippet', + 'content': 'content', + }); + check(result).savedSnippetId.equals(123); + }); + }); +} From 290b7c0205500ce5ea0baab4e30737f6b63781e8 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 22 May 2025 08:01:46 +0200 Subject: [PATCH 8/8] l10n: Update translations from Weblate. --- assets/l10n/app_ru.arb | 72 ++++++++++++++++++- .../l10n/zulip_localizations_ru.dart | 36 +++++----- 2 files changed, 87 insertions(+), 21 deletions(-) diff --git a/assets/l10n/app_ru.arb b/assets/l10n/app_ru.arb index 57cf48e3e0..bf36f0c900 100644 --- a/assets/l10n/app_ru.arb +++ b/assets/l10n/app_ru.arb @@ -113,7 +113,7 @@ } } }, - "errorCouldNotFetchMessageSource": "Не удалось извлечь источник сообщения", + "errorCouldNotFetchMessageSource": "Не удалось извлечь источник сообщения.", "@errorCouldNotFetchMessageSource": { "description": "Error message when the source of a message could not be fetched." }, @@ -393,7 +393,7 @@ "@serverUrlValidationErrorNoUseEmail": { "description": "Error message when URL looks like an email" }, - "errorVideoPlayerFailed": "Не удается воспроизвести видео", + "errorVideoPlayerFailed": "Не удается воспроизвести видео.", "@errorVideoPlayerFailed": { "description": "Error message when a video fails to play." }, @@ -525,7 +525,7 @@ "@successMessageTextCopied": { "description": "Message when content of a message was copied to the user's system clipboard." }, - "errorInvalidResponse": "Получен недопустимый ответ сервера", + "errorInvalidResponse": "Сервер отправил недопустимый ответ.", "@errorInvalidResponse": { "description": "Error message when an API call returned an invalid response." }, @@ -1006,5 +1006,71 @@ "experimentalFeatureSettingsWarning": "Эти параметры включают функции, которые все еще находятся в стадии разработки и не готовы. Они могут не работать и вызывать проблемы в других местах приложения.\n\nЦель этих настроек — экспериментирование людьми, работающими над разработкой Zulip.", "@experimentalFeatureSettingsWarning": { "description": "Warning text on settings page for experimental, in-development features" + }, + "errorCouldNotEditMessageTitle": "Сбой редактирования", + "@errorCouldNotEditMessageTitle": { + "description": "Error title when an exception prevented us from opening the compose box for editing a message." + }, + "composeBoxBannerButtonSave": "Сохранить", + "@composeBoxBannerButtonSave": { + "description": "Label text for the 'Save' button in the compose-box banner when you are editing a message." + }, + "editAlreadyInProgressTitle": "Редактирование недоступно", + "@editAlreadyInProgressTitle": { + "description": "Error title when a message edit cannot be saved because there is another edit already in progress." + }, + "savingMessageEditLabel": "ЗАПИСЬ ПРАВОК…", + "@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": "ПРАВКИ НЕ СОХРАНЕНЫ", + "@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": "Отказаться от написанного сообщения?", + "@discardDraftConfirmationDialogTitle": { + "description": "Title for a confirmation dialog for discarding message text that was typed into the compose box." + }, + "discardDraftConfirmationDialogConfirmButton": "Сбросить", + "@discardDraftConfirmationDialogConfirmButton": { + "description": "Label for the 'Discard' button on a confirmation dialog for discarding message text that was typed into the compose box." + }, + "composeBoxBannerButtonCancel": "Отмена", + "@composeBoxBannerButtonCancel": { + "description": "Label text for the 'Cancel' button in the compose-box banner when you are editing a message." + }, + "actionSheetOptionEditMessage": "Редактировать сообщение", + "@actionSheetOptionEditMessage": { + "description": "Label for the 'Edit message' button in the message action sheet." + }, + "errorMessageEditNotSaved": "Сообщение не сохранено", + "@errorMessageEditNotSaved": { + "description": "Error message for compose box when a message edit could not be saved." + }, + "preparingEditMessageContentInput": "Подготовка…", + "@preparingEditMessageContentInput": { + "description": "Hint text for content input when the compose box is preparing to edit a message." + }, + "composeBoxEnterTopicOrSkipHintText": "Укажите тему (или оставьте “{defaultTopicName}”)", + "@composeBoxEnterTopicOrSkipHintText": { + "description": "Hint text for topic input widget in compose box when topics are optional.", + "placeholders": { + "defaultTopicName": { + "type": "String", + "example": "general chat" + } + } + }, + "composeBoxBannerLabelEditMessage": "Редактирование сообщения", + "@composeBoxBannerLabelEditMessage": { + "description": "Label text for the compose-box banner when you are editing a message." + }, + "editAlreadyInProgressMessage": "Редактирование уже выполняется. Дождитесь завершения.", + "@editAlreadyInProgressMessage": { + "description": "Error message when a message edit cannot be saved because there is another edit already in progress." + }, + "discardDraftConfirmationDialogMessage": "При изменении сообщения текст из поля для редактирования удаляется.", + "@discardDraftConfirmationDialogMessage": { + "description": "Message for a confirmation dialog for discarding message text that was typed into the compose box." } } diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart index 5d8899290d..a83fb32e9e 100644 --- a/lib/generated/l10n/zulip_localizations_ru.dart +++ b/lib/generated/l10n/zulip_localizations_ru.dart @@ -128,7 +128,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get actionSheetOptionUnstarMessage => 'Снять отметку с сообщения'; @override - String get actionSheetOptionEditMessage => 'Edit message'; + String get actionSheetOptionEditMessage => 'Редактировать сообщение'; @override String get actionSheetOptionMarkTopicAsRead => @@ -150,7 +150,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String get errorCouldNotFetchMessageSource => - 'Не удалось извлечь источник сообщения'; + 'Не удалось извлечь источник сообщения.'; @override String get errorCopyingFailed => 'Сбой копирования'; @@ -201,7 +201,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations { String get errorMessageNotSent => 'Сообщение не отправлено'; @override - String get errorMessageEditNotSaved => 'Message not saved'; + String get errorMessageEditNotSaved => 'Сообщение не сохранено'; @override String errorLoginCouldNotConnect(String url) { @@ -277,7 +277,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations { 'Не удалось снять отметку с сообщения'; @override - String get errorCouldNotEditMessageTitle => 'Could not edit message'; + String get errorCouldNotEditMessageTitle => 'Сбой редактирования'; @override String get successLinkCopied => 'Ссылка скопирована'; @@ -297,37 +297,37 @@ class ZulipLocalizationsRu extends ZulipLocalizations { 'У вас нет права писать в этом канале.'; @override - String get composeBoxBannerLabelEditMessage => 'Edit message'; + String get composeBoxBannerLabelEditMessage => 'Редактирование сообщения'; @override - String get composeBoxBannerButtonCancel => 'Cancel'; + String get composeBoxBannerButtonCancel => 'Отмена'; @override - String get composeBoxBannerButtonSave => 'Save'; + String get composeBoxBannerButtonSave => 'Сохранить'; @override - String get editAlreadyInProgressTitle => 'Cannot edit message'; + String get editAlreadyInProgressTitle => 'Редактирование недоступно'; @override String get editAlreadyInProgressMessage => - 'An edit is already in progress. Please wait for it to complete.'; + 'Редактирование уже выполняется. Дождитесь завершения.'; @override - String get savingMessageEditLabel => 'SAVING EDIT…'; + String get savingMessageEditLabel => 'ЗАПИСЬ ПРАВОК…'; @override - String get savingMessageEditFailedLabel => 'EDIT NOT SAVED'; + String get savingMessageEditFailedLabel => 'ПРАВКИ НЕ СОХРАНЕНЫ'; @override String get discardDraftConfirmationDialogTitle => - 'Discard the message you’re writing?'; + 'Отказаться от написанного сообщения?'; @override String get discardDraftConfirmationDialogMessage => - 'When you edit a message, the content that was previously in the compose box is discarded.'; + 'При изменении сообщения текст из поля для редактирования удаляется.'; @override - String get discardDraftConfirmationDialogConfirmButton => 'Discard'; + String get discardDraftConfirmationDialogConfirmButton => 'Сбросить'; @override String get composeBoxAttachFilesTooltip => 'Прикрепить файлы'; @@ -358,7 +358,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations { } @override - String get preparingEditMessageContentInput => 'Preparing…'; + String get preparingEditMessageContentInput => 'Подготовка…'; @override String get composeBoxSendTooltip => 'Отправить'; @@ -371,7 +371,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations { @override String composeBoxEnterTopicOrSkipHintText(String defaultTopicName) { - return 'Enter a topic (skip for “$defaultTopicName”)'; + return 'Укажите тему (или оставьте “$defaultTopicName”)'; } @override @@ -514,7 +514,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations { } @override - String get errorInvalidResponse => 'Получен недопустимый ответ сервера'; + String get errorInvalidResponse => 'Сервер отправил недопустимый ответ.'; @override String get errorNetworkRequestFailed => 'Сбой сетевого запроса'; @@ -535,7 +535,7 @@ class ZulipLocalizationsRu extends ZulipLocalizations { } @override - String get errorVideoPlayerFailed => 'Не удается воспроизвести видео'; + String get errorVideoPlayerFailed => 'Не удается воспроизвести видео.'; @override String get serverUrlValidationErrorEmpty => 'Пожалуйста, введите URL-адрес.';