diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 893210c589..114323d88a 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -43,6 +43,7 @@ format used by the Zulip server that they are interacting with. * `orig_subject` and `subject` fields in the `update_message` event type * `topic_name` field in the `user_topic` event type * `topic` field in the `typing` event type + * `topic` field in the `update_message_flags` event type when removing `read` flag * [`GET /messages`](/api/get-messages), [`GET /messages/{message_id}`](/api/get-message): Added `allow_empty_topic_name` diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index c55602510a..dac8bf6f55 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -3129,6 +3129,17 @@ paths: Present only if `type` is `"stream"`. Name of the topic where the message was sent. + + 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 unmuted_stream_msg: type: boolean deprecated: true diff --git a/zerver/tests/test_message_topics.py b/zerver/tests/test_message_topics.py index 577c49d668..44fe62beb8 100644 --- a/zerver/tests/test_message_topics.py +++ b/zerver/tests/test_message_topics.py @@ -377,7 +377,14 @@ class EmptyTopicNameTest(ZulipTestCase): apply_markdown=True, client_type_name="website", empty_topic_name=True, - event_types=["message", "update_message", "delete_message", "user_topic", "typing"], + event_types=[ + "message", + "update_message", + "delete_message", + "user_topic", + "typing", + "update_message_flags", + ], last_connection_time=time.time(), queue_timeout=600, realm_id=hamlet.realm.id, @@ -425,10 +432,10 @@ class EmptyTopicNameTest(ZulipTestCase): self.assertEqual(events[5]["topic"], "") # reset - self.send_stream_message( + message_id = self.send_stream_message( iago, "Denmark", topic_name="", skip_capture_on_commit_callbacks=True ) - self.send_stream_message( + message_id_2 = self.send_stream_message( iago, "Verona", topic_name=Message.EMPTY_TOPIC_FALLBACK_NAME, @@ -473,6 +480,25 @@ class EmptyTopicNameTest(ZulipTestCase): self.assertEqual(events[8]["topic"], "") self.assertEqual(events[9]["topic"], "") + # Prep to mark it as read before marking it as unread. + params = { + "messages": orjson.dumps([message_id, message_id_2]).decode(), + "op": "add", + "flag": "read", + } + self.client_post("/json/messages/flags", params) + + with self.captureOnCommitCallbacks(execute=True): + params = { + "messages": orjson.dumps([message_id, message_id_2]).decode(), + "op": "remove", + "flag": "read", + } + self.client_post("/json/messages/flags", params) + events = client.event_queue.contents() + self.assertEqual(events[10]["message_details"][str(message_id)]["topic"], "") + self.assertEqual(events[10]["message_details"][str(message_id_2)]["topic"], "") + def test_client_not_supports_empty_topic_name(self) -> None: iago = self.example_user("iago") hamlet = self.example_user("hamlet") @@ -481,7 +507,14 @@ class EmptyTopicNameTest(ZulipTestCase): apply_markdown=True, client_type_name="zulip-mobile", empty_topic_name=False, - event_types=["message", "update_message", "delete_message", "user_topic", "typing"], + event_types=[ + "message", + "update_message", + "delete_message", + "user_topic", + "typing", + "update_message_flags", + ], last_connection_time=time.time(), queue_timeout=600, realm_id=hamlet.realm.id, @@ -529,10 +562,10 @@ class EmptyTopicNameTest(ZulipTestCase): self.assertEqual(events[5]["topic"], Message.EMPTY_TOPIC_FALLBACK_NAME) # reset - self.send_stream_message( + message_id = self.send_stream_message( iago, "Denmark", topic_name="", skip_capture_on_commit_callbacks=True ) - self.send_stream_message( + message_id_2 = self.send_stream_message( iago, "Verona", topic_name=Message.EMPTY_TOPIC_FALLBACK_NAME, @@ -577,6 +610,31 @@ class EmptyTopicNameTest(ZulipTestCase): self.assertEqual(events[8]["topic"], Message.EMPTY_TOPIC_FALLBACK_NAME) self.assertEqual(events[9]["topic"], Message.EMPTY_TOPIC_FALLBACK_NAME) + # Prep to mark it as read before marking it as unread. + params = { + "messages": orjson.dumps([message_id, message_id_2]).decode(), + "op": "add", + "flag": "read", + } + self.client_post("/json/messages/flags", params) + + with self.captureOnCommitCallbacks(execute=True): + params = { + "messages": orjson.dumps([message_id, message_id_2]).decode(), + "op": "remove", + "flag": "read", + } + self.client_post("/json/messages/flags", params) + events = client.event_queue.contents() + self.assertEqual( + events[10]["message_details"][str(message_id)]["topic"], + Message.EMPTY_TOPIC_FALLBACK_NAME, + ) + self.assertEqual( + events[10]["message_details"][str(message_id_2)]["topic"], + 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 a5e4525d10..11c01a61a6 100644 --- a/zerver/tornado/event_queue.py +++ b/zerver/tornado/event_queue.py @@ -1663,6 +1663,25 @@ def process_stream_typing_notification_event( client.add_event(empty_topic_name_fallback_event) +def process_mark_message_unread_event(event: Mapping[str, Any], users: Iterable[int]) -> None: + empty_topic_name_fallback_event = copy.deepcopy(dict(event)) + for message_id, message_detail in empty_topic_name_fallback_event["message_details"].items(): + if message_detail["type"] == "stream" and message_detail.get("topic") == "": + empty_topic_name_fallback_event["message_details"][message_id]["topic"] = ( + Message.EMPTY_TOPIC_FALLBACK_NAME + ) + + 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"] @@ -1695,6 +1714,12 @@ def process_notification(notice: Mapping[str, Any]) -> None: process_user_topic_event(event, cast(list[int], users)) elif event["type"] == "typing" and event["message_type"] == "stream": process_stream_typing_notification_event(event, cast(list[int], users)) + elif ( + event["type"] == "update_message_flags" + and event["op"] == "remove" + and event["flag"] == "read" + ): + process_mark_message_unread_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.