diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 4b979935b2..a061b6e8d7 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -41,6 +41,7 @@ format used by the Zulip server that they are interacting with. * `subject` field in the `message` event type * `topic` field in the `delete_message` event type * `orig_subject` and `subject` fields in the `update_message` event type + * `topic_name` field in the `user_topic` event type * [`GET /messages`](/api/get-messages), [`GET /messages/{message_id}`](/api/get-message): Added `allow_empty_topic_name` diff --git a/zerver/actions/user_topics.py b/zerver/actions/user_topics.py index 0e485f5f15..9046878908 100644 --- a/zerver/actions/user_topics.py +++ b/zerver/actions/user_topics.py @@ -5,6 +5,7 @@ from django.db import transaction from django.utils.timezone import now as timezone_now from zerver.lib.timestamp import datetime_to_timestamp +from zerver.lib.topic import maybe_rename_general_chat_to_empty_topic from zerver.lib.user_topics import ( bulk_set_user_topic_visibility_policy_in_database, get_topic_mutes, @@ -26,6 +27,8 @@ def bulk_do_set_user_topic_visibility_policy( if last_updated is None: last_updated = timezone_now() + topic_name = maybe_rename_general_chat_to_empty_topic(topic_name) + user_profiles_with_changed_user_topic_rows = bulk_set_user_topic_visibility_policy_in_database( user_profiles, stream.id, diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index c58098ce6c..725079a2aa 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -2207,6 +2207,17 @@ paths: type: string description: | The name of the topic. + + For clients that don't support the `empty_topic_name` [client capability][client-capabilities], + if the actual topic name is empty string, this field's value will instead + be the value of `realm_empty_topic_display_name` found in the + [`POST /register`](/api/register-queue) response. + + **Changes**: Before 10.0 (feature level 334), `empty_topic_name` + client capability didn't exist and empty string as the topic name for + channel messages wasn't allowed. + + [client-capabilities]: /api/register-queue#parameter-client_capabilities last_updated: type: integer description: | @@ -10948,6 +10959,13 @@ paths: Clients should use the `max_topic_length` returned by the [`POST /register`](/api/register-queue) endpoint to determine the maximum topic length. + + Note: When the value of `realm_empty_topic_display_name` found in + the [POST /register](/api/register-queue) response is used for this + parameter, it is interpreted as an empty string. + + **Changes**: Before Zulip 10.0 (feature level 334), empty string + was not a valid topic name for channel messages. type: string example: dinner visibility_policy: diff --git a/zerver/tests/test_message_topics.py b/zerver/tests/test_message_topics.py index 3a284a15ed..1e05b1fe2b 100644 --- a/zerver/tests/test_message_topics.py +++ b/zerver/tests/test_message_topics.py @@ -5,6 +5,7 @@ import orjson from django.utils.timezone import now as timezone_now from zerver.actions.streams import do_change_stream_permission +from zerver.actions.user_topics import do_set_user_topic_visibility_policy from zerver.lib.test_classes import ZulipTestCase from zerver.lib.user_topics import set_topic_visibility_policy, topic_has_visibility_policy from zerver.models import Message, UserMessage, UserTopic @@ -376,7 +377,7 @@ class EmptyTopicNameTest(ZulipTestCase): apply_markdown=True, client_type_name="website", empty_topic_name=True, - event_types=["message", "update_message", "delete_message"], + event_types=["message", "update_message", "delete_message", "user_topic"], last_connection_time=time.time(), queue_timeout=600, realm_id=hamlet.realm.id, @@ -422,6 +423,37 @@ class EmptyTopicNameTest(ZulipTestCase): self.assertEqual(events[4]["topic"], "") self.assertEqual(events[5]["topic"], "") + # reset + self.send_stream_message( + iago, "Denmark", topic_name="", skip_capture_on_commit_callbacks=True + ) + self.send_stream_message( + iago, + "Verona", + topic_name=Message.EMPTY_TOPIC_FALLBACK_NAME, + skip_capture_on_commit_callbacks=True, + ) + + self.login_user(hamlet) + denmark = get_stream("Denmark", hamlet.realm) + verona = get_stream("Verona", hamlet.realm) + with self.captureOnCommitCallbacks(execute=True): + do_set_user_topic_visibility_policy( + hamlet, + denmark, + "", + visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED, + ) + do_set_user_topic_visibility_policy( + hamlet, + verona, + Message.EMPTY_TOPIC_FALLBACK_NAME, + visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED, + ) + events = client.event_queue.contents() + self.assertEqual(events[6]["topic_name"], "") + self.assertEqual(events[7]["topic_name"], "") + def test_client_not_supports_empty_topic_name(self) -> None: iago = self.example_user("iago") hamlet = self.example_user("hamlet") @@ -430,7 +462,7 @@ class EmptyTopicNameTest(ZulipTestCase): apply_markdown=True, client_type_name="zulip-mobile", empty_topic_name=False, - event_types=["message", "update_message", "delete_message"], + event_types=["message", "update_message", "delete_message", "user_topic"], last_connection_time=time.time(), queue_timeout=600, realm_id=hamlet.realm.id, @@ -476,6 +508,37 @@ class EmptyTopicNameTest(ZulipTestCase): self.assertEqual(events[4]["topic"], Message.EMPTY_TOPIC_FALLBACK_NAME) self.assertEqual(events[5]["topic"], Message.EMPTY_TOPIC_FALLBACK_NAME) + # reset + self.send_stream_message( + iago, "Denmark", topic_name="", skip_capture_on_commit_callbacks=True + ) + self.send_stream_message( + iago, + "Verona", + topic_name=Message.EMPTY_TOPIC_FALLBACK_NAME, + skip_capture_on_commit_callbacks=True, + ) + + self.login_user(hamlet) + denmark = get_stream("Denmark", hamlet.realm) + verona = get_stream("Verona", hamlet.realm) + with self.captureOnCommitCallbacks(execute=True): + do_set_user_topic_visibility_policy( + hamlet, + denmark, + "", + visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED, + ) + do_set_user_topic_visibility_policy( + hamlet, + verona, + Message.EMPTY_TOPIC_FALLBACK_NAME, + visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED, + ) + events = client.event_queue.contents() + self.assertEqual(events[6]["topic_name"], Message.EMPTY_TOPIC_FALLBACK_NAME) + self.assertEqual(events[7]["topic_name"], Message.EMPTY_TOPIC_FALLBACK_NAME) + def test_fetch_messages(self) -> None: hamlet = self.example_user("hamlet") self.login_user(hamlet) diff --git a/zerver/tornado/event_queue.py b/zerver/tornado/event_queue.py index 4925179f99..de1f1451f2 100644 --- a/zerver/tornado/event_queue.py +++ b/zerver/tornado/event_queue.py @@ -1623,6 +1623,25 @@ def process_user_group_name_update_event(event: Mapping[str, Any], users: Iterab client.add_event(user_group_event) +def process_user_topic_event(event: Mapping[str, Any], users: Iterable[int]) -> None: + empty_topic_name_fallback_event: Mapping[str, Any] | dict[str, Any] + if event.get("topic_name") == "": + empty_topic_name_fallback_event = dict(event) + empty_topic_name_fallback_event["topic_name"] = Message.EMPTY_TOPIC_FALLBACK_NAME + else: + empty_topic_name_fallback_event = event + + for user_profile_id in users: + for client in get_client_descriptors_for_user(user_profile_id): + if not client.accepts_event(event): + continue + + if client.empty_topic_name: + client.add_event(event) + else: + client.add_event(empty_topic_name_fallback_event) + + def process_notification(notice: Mapping[str, Any]) -> None: event: Mapping[str, Any] = notice["event"] users: list[int] | list[Mapping[str, Any]] = notice["users"] @@ -1651,6 +1670,8 @@ def process_notification(notice: Mapping[str, Any]) -> None: # event sent for updating name separately for clients with different # capabilities. process_user_group_name_update_event(event, cast(list[int], users)) + elif event["type"] == "user_topic": + process_user_topic_event(event, cast(list[int], users)) elif event["type"] == "cleanup_queue": # cleanup_event_queue may generate this event to forward cleanup # requests to the right shard.