diff --git a/templates/zerver/api/changelog.md b/templates/zerver/api/changelog.md index b4da1600e7..c199f7af97 100644 --- a/templates/zerver/api/changelog.md +++ b/templates/zerver/api/changelog.md @@ -11,6 +11,15 @@ below features are supported. ## Changes in Zulip 5.0 +**Feature level 87** + +* [`PATCH /settings`](/api/update-settings): Added a new + `enable_drafts_synchronization` setting, which controls whether the + syncing drafts between different clients is enabled. +* [`GET /events`](/api/get-events), [`POST /register`](/api/register-queue): + Added new `enable_drafts_synchronization` setting under + `update_display_settings`. + **Feature level 86** * [`GET /events`](/api/get-events): Added `emoji_name`, diff --git a/version.py b/version.py index 991226cb94..71018460e9 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3" # Changes should be accompanied by documentation explaining what the # new level means in templates/zerver/api/changelog.md, as well as # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 86 +API_FEATURE_LEVEL = 87 # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 78a978eabd..244679732f 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -197,6 +197,7 @@ from zerver.models import ( CustomProfileFieldValue, DefaultStream, DefaultStreamGroup, + Draft, EmailChangeStatus, Message, MultiuseInvite, @@ -5101,6 +5102,13 @@ def do_set_user_display_setting( active_user_ids(user_profile.realm_id), ) + if setting_name == "enable_drafts_synchronization" and setting_value is False: + # Delete all of the drafts from the backend but don't send delete events + # for them since all that's happened is that we stopped syncing changes, + # not deleted every previously synced draft - to do that use the DELETE + # endpoint. + Draft.objects.filter(user_profile=user_profile).delete() + def lookup_default_stream_groups( default_stream_group_names: List[str], realm: Realm diff --git a/zerver/models.py b/zerver/models.py index 9d5c680806..a078d5cd51 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -1354,6 +1354,7 @@ class UserBaseSettings(models.Model): demote_inactive_streams=int, dense_mode=bool, emojiset=str, + enable_drafts_synchronization=bool, enter_sends=bool, fluid_layout_width=bool, high_contrast_mode=bool, diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 46feaf744d..93f3d25c66 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -8830,6 +8830,17 @@ paths: See [PATCH /settings](/api/update-settings) for details on the meaning of this setting. + enable_drafts_synchronization: + type: boolean + description: | + Present if `update_display_settings` is present in `fetch_event_types`. + + Whether drafts synchronization is enabled for the user. + + See [PATCH /settings](/api/update-settings) for details on + the meaning of this setting. + + **Changes**: New in Zulip 5.0 (feature level 87). fluid_layout_width: type: boolean description: | @@ -10294,6 +10305,20 @@ paths: - 2 - 3 example: 1 + - name: enable_drafts_synchronization + in: query + description: | + A boolean parameter to control whether synchronizing drafts is enabled for + the user. When synchronization is disabled, all drafts stored in the server + will be automatically deleted from the server. + + This does not do anything (like sending events) to delete local copies of + drafts stored in clients. + + **Changes**: New in Zulip 5.0 (feature level 87). + schema: + type: boolean + example: true - name: translate_emoticons in: query description: | diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index bc6248c05d..8dbab9d0f4 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -2299,3 +2299,21 @@ class SubscribeActionTest(BaseAction): events[0]["streams"][0]["message_retention_days"], 10, ) + + +class DraftActionTest(BaseAction): + def do_enable_drafts_synchronization(self, user_profile: UserProfile) -> None: + do_set_user_display_setting(user_profile, "enable_drafts_synchronization", True) + + def do_disable_drafts_synchronization(self, user_profile: UserProfile) -> None: + do_set_user_display_setting(user_profile, "enable_drafts_synchronization", False) + + def test_enable_syncing_drafts(self) -> None: + self.do_disable_drafts_synchronization(self.user_profile) + action = lambda: self.do_enable_drafts_synchronization(self.user_profile) + self.verify_action(action) + + def test_disable_syncing_drafts(self) -> None: + self.do_enable_drafts_synchronization(self.user_profile) + action = lambda: self.do_disable_drafts_synchronization(self.user_profile) + self.verify_action(action) diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index dd7669abf4..1550e41a7d 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -70,6 +70,7 @@ class HomeTest(ZulipTestCase): "emojiset_choices", "enable_desktop_notifications", "enable_digest_emails", + "enable_drafts_synchronization", "enable_login_emails", "enable_marketing_emails", "enable_offline_email_notifications", diff --git a/zerver/tests/test_settings.py b/zerver/tests/test_settings.py index d9b4bee1d7..2d4b3f0e8d 100644 --- a/zerver/tests/test_settings.py +++ b/zerver/tests/test_settings.py @@ -11,7 +11,7 @@ from zerver.lib.rate_limiter import add_ratelimit_rule, remove_ratelimit_rule from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import get_test_image_file from zerver.lib.users import get_all_api_keys -from zerver.models import UserProfile, get_user_profile_by_api_key +from zerver.models import Draft, UserProfile, get_user_profile_by_api_key class ChangeSettingsTest(ZulipTestCase): @@ -473,3 +473,52 @@ class UserChangesTest(ZulipTestCase): for api_key in current_api_keys: self.assertEqual(get_user_profile_by_api_key(api_key).email, email) + + +class UserDraftSettingsTests(ZulipTestCase): + def test_enable_drafts_syncing(self) -> None: + hamlet = self.example_user("hamlet") + hamlet.enable_drafts_synchronization = False + hamlet.save() + payload = {"enable_drafts_synchronization": orjson.dumps(True).decode()} + resp = self.api_patch(hamlet, "/api/v1/settings", payload) + self.assert_json_success(resp) + hamlet = self.example_user("hamlet") + self.assertTrue(hamlet.enable_drafts_synchronization) + + def test_disable_drafts_syncing(self) -> None: + aaron = self.example_user("aaron") + self.assertTrue(aaron.enable_drafts_synchronization) + + initial_count = Draft.objects.count() + + # Create some drafts. These should be deleted once aaron disables + # syncing drafts. + visible_stream_id = self.get_stream_id(self.get_streams(aaron)[0]) + draft_dicts = [ + { + "type": "stream", + "to": [visible_stream_id], + "topic": "thinking out loud", + "content": "What if pigs really could fly?", + "timestamp": 15954790199, + }, + { + "type": "private", + "to": [], + "topic": "", + "content": "What if made it possible to sync drafts in Zulip?", + "timestamp": 1595479020, + }, + ] + payload = {"drafts": orjson.dumps(draft_dicts).decode()} + resp = self.api_post(aaron, "/api/v1/drafts", payload) + self.assert_json_success(resp) + self.assertEqual(Draft.objects.count() - initial_count, 2) + + payload = {"enable_drafts_synchronization": orjson.dumps(False).decode()} + resp = self.api_patch(aaron, "/api/v1/settings", payload) + self.assert_json_success(resp) + aaron = self.example_user("aaron") + self.assertFalse(aaron.enable_drafts_synchronization) + self.assertEqual(Draft.objects.count() - initial_count, 0) diff --git a/zerver/views/user_settings.py b/zerver/views/user_settings.py index 048ca20369..466b711fff 100644 --- a/zerver/views/user_settings.py +++ b/zerver/views/user_settings.py @@ -123,6 +123,7 @@ def json_change_settings( email_notifications_batching_period_seconds: Optional[int] = REQ( json_validator=check_int, default=None ), + enable_drafts_synchronization: Optional[bool] = REQ(json_validator=check_bool, default=None), enable_stream_desktop_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None ),