diff --git a/api_docs/changelog.md b/api_docs/changelog.md index b2c70f9495..5a0bca59f4 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 361** + +* [`POST /messages/{message_id}/typing`](/api/set-typing-status-for-message-edit): + Renamed `POST /messages/{message_id}/typing` to + `POST /message_edit_typing`, passing the one `message_id` parameter + in the URL path, for consistency with the rest of the API. + **Feature level 360** * [`GET /messages/{message_id}`](/api/get-message), [`GET diff --git a/version.py b/version.py index 7085f85bfd..92bf817ff5 100644 --- a/version.py +++ b/version.py @@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 360 # Last bumped for allowing access to archived channel messages on read. +API_FEATURE_LEVEL = 361 # 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/web/src/typing.ts b/web/src/typing.ts index 521dfb4dd5..b35a201d95 100644 --- a/web/src/typing.ts +++ b/web/src/typing.ts @@ -51,11 +51,10 @@ function send_message_edit_typing_notification_ajax( operation: "start" | "stop", ): void { const data = { - message_id: JSON.stringify(message_id), op: operation, }; void channel.post({ - url: "/json/message_edit_typing", + url: `/json/messages/${message_id}/typing`, data, error(xhr) { if (xhr.readyState !== 0) { diff --git a/zerver/openapi/python_examples.py b/zerver/openapi/python_examples.py index 67569625b8..9b020e7f25 100644 --- a/zerver/openapi/python_examples.py +++ b/zerver/openapi/python_examples.py @@ -1608,43 +1608,35 @@ def set_typing_status(client: Client) -> None: validate_against_openapi_schema(result, "/typing", "post", "200") -@openapi_test_function("/message_edit_typing:post") -def set_message_edit_typing_status(client: Client) -> None: - message = {"type": "stream", "to": "Verona", "topic": "test_topic", "content": "test content"} - response = client.send_message(message) - message_id = response["id"] +@openapi_test_function("/messages/{message_id}/typing:post") +def set_message_edit_typing_status(client: Client, message_id: int) -> None: # {code_example|start} - # The user has started typing while editing a message + # The user has started typing while editing a message. request = { "op": "start", - "message_id": message_id, } result = client.call_endpoint( - "message_edit_typing", + f"/messages/{message_id}/typing", method="POST", request=request, ) # {code_example|end} assert_success_response(result) - validate_against_openapi_schema(result, "/message_edit_typing", "post", "200") + validate_against_openapi_schema(result, f"/messages/{message_id}/typing", "post", "200") - message = {"type": "stream", "to": "Verona", "topic": "test_topic", "content": "test content"} - response = client.send_message(message) - message_id = response["id"] # {code_example|start} # The user has stopped typing while editing a message. request = { "op": "stop", - "message_id": message_id, } result = client.call_endpoint( - "message_edit_typing", + f"/messages/{message_id}/typing", method="POST", request=request, ) # {code_example|end} assert_success_response(result) - validate_against_openapi_schema(result, "/message_edit_typing", "post", "200") + validate_against_openapi_schema(result, "/messages/{message_id}/typing", "post", "200") @openapi_test_function("/realm/emoji/{emoji_name}:post") @@ -1794,7 +1786,7 @@ def test_invalid_stream_error(client: Client) -> None: def test_messages(client: Client, nonadmin_client: Client) -> None: render_message(client) message_id = send_message(client) - set_message_edit_typing_status(client) + set_message_edit_typing_status(client, message_id) add_reaction(client, message_id) remove_reaction(client, message_id) update_message(client, message_id) diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 618ad6557a..16b73b88ff 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -21320,7 +21320,7 @@ paths: description: | An example JSON error response when the user composes a channel message and `stream_id` is not specified: - /message_edit_typing: + /messages/{message_id}/typing: post: operationId: set-typing-status-for-message-edit summary: Set "typing" status for message editing @@ -21329,10 +21329,25 @@ paths: Notify other users whether the current user is editing a message. Typing notifications for editing messages follow the same protocol as - [set-typing-status](/api/set-typing-status), see that endpoint for details. + [set-typing-status](/api/set-typing-status), see that endpoint for + details. - **Changes**: New in Zulip 10.0 (feature level 351). Previously, - typing notifications were not available when editing messages. + **Changes**: Before Zulip 10.0 (feature level 361), the endpoint was + named `/message_edit_typing` with `message_id` a required parameter in + the request body. Clients are recommended to start using sending these + typing notifications starting from this feature level. + + New in Zulip 10.0 (feature level 351). Previously, typing notifications were + not available when editing messages. + parameters: + - name: message_id + in: path + description: | + The target message's ID. + schema: + type: integer + example: 47 + required: true requestBody: required: true content: @@ -21348,14 +21363,8 @@ paths: - start - stop example: start - message_id: - description: | - Describes the message id of the message being edited. - type: integer - example: 47 required: - op - - message_id responses: "200": $ref: "#/components/responses/SimpleSuccess" diff --git a/zerver/tests/test_typing.py b/zerver/tests/test_typing.py index 9635620530..bfebc76e99 100644 --- a/zerver/tests/test_typing.py +++ b/zerver/tests/test_typing.py @@ -41,12 +41,13 @@ class TypingValidateOperatorTest(ZulipTestCase): def test_invalid_parameter_message_edit(self) -> None: sender = self.example_user("hamlet") - + msg_id = self.send_stream_message( + sender, "Denmark", topic_name="editing", content="before edit" + ) params = dict( op="foo", - message_id=7, ) - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_error(result, "Invalid op") @@ -112,10 +113,9 @@ class TypingValidateToArgumentsTest(ZulipTestCase): ) result = self.api_post( sender, - "/api/v1/message_edit_typing", + f"/api/v1/messages/{msg_id}/typing", { "op": "start", - "message_id": str(msg_id), }, ) self.assert_json_error(result, "You don't have permission to edit this message") @@ -569,7 +569,6 @@ class TypingHappyPathTestStreams(ZulipTestCase): params = dict( op="start", - message_id=msg_id, ) with self.settings(MAX_STREAM_SIZE_FOR_TYPING_NOTIFICATIONS=5): @@ -577,7 +576,7 @@ class TypingHappyPathTestStreams(ZulipTestCase): self.assert_database_query_count(5), self.capture_send_event_calls(expected_num_events=0) as events, ): - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_success(result) self.assert_length(events, 0) @@ -747,13 +746,13 @@ class TestSendTypingNotificationsSettings(ZulipTestCase): ) expected_user_ids = self.not_long_term_idle_subscriber_ids(channel_name, sender.realm) - params = dict(op="start", message_id=msg_id) + params = dict(op="start") # Test typing events sent when `send_stream_typing_notifications` set to `True`. self.assertTrue(sender.send_stream_typing_notifications) with self.capture_send_event_calls(expected_num_events=1) as events: - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_success(result) self.assert_length(events, 1) self.assertEqual(orjson.loads(result.content)["msg"], "") @@ -763,7 +762,7 @@ class TestSendTypingNotificationsSettings(ZulipTestCase): do_deactivate_stream(channel, acting_user=sender) # No events should be sent if stream is deactivated. with self.capture_send_event_calls(expected_num_events=0) as events: - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_error(result, "Invalid message(s)") self.assertEqual(events, []) do_unarchive_stream(channel, channel_name, acting_user=sender) @@ -773,7 +772,7 @@ class TestSendTypingNotificationsSettings(ZulipTestCase): # No events should be sent now with self.capture_send_event_calls(expected_num_events=0) as events: - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_error( result, "User has disabled typing notifications for channel messages" ) @@ -786,7 +785,7 @@ class TestSendTypingNotificationsSettings(ZulipTestCase): aaron.save() with self.capture_send_event_calls(expected_num_events=1) as events: - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_success(result) self.assert_length(events, 1) @@ -805,14 +804,13 @@ class TestSendTypingNotificationsSettings(ZulipTestCase): params = dict( op="start", - message_id=msg_id, ) # Test typing events sent when `send_private_typing_notifications` set to `True`. self.assertTrue(sender.send_private_typing_notifications) with self.capture_send_event_calls(expected_num_events=1) as events: - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_success(result) self.assert_length(events, 1) @@ -825,7 +823,7 @@ class TestSendTypingNotificationsSettings(ZulipTestCase): # No events should be sent now with self.capture_send_event_calls(expected_num_events=0) as events: - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_error(result, "User has disabled typing notifications for direct messages") self.assertEqual(events, []) @@ -837,7 +835,7 @@ class TestSendTypingNotificationsSettings(ZulipTestCase): recipient_user.save() with self.capture_send_event_calls(expected_num_events=1) as events: - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_success(result) self.assert_length(events, 1) @@ -858,12 +856,11 @@ class TestSendTypingNotificationsSettings(ZulipTestCase): params = dict( op="start", - message_id=str(msg_id), ) # Test typing events sent when `send_private_typing_notifications` set to `True`. self.assertTrue(sender.send_private_typing_notifications) with self.capture_send_event_calls(expected_num_events=1) as events: - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_success(result) self.assert_length(events, 1) @@ -876,7 +873,7 @@ class TestSendTypingNotificationsSettings(ZulipTestCase): # No events should be sent now with self.capture_send_event_calls(expected_num_events=0) as events: - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_error(result, "User has disabled typing notifications for direct messages") self.assertEqual(events, []) @@ -891,7 +888,7 @@ class TestSendTypingNotificationsSettings(ZulipTestCase): expected_recipient_ids = {hamlet.id, cordelia.id} with self.capture_send_event_calls(expected_num_events=1) as events: - result = self.api_post(sender, "/api/v1/message_edit_typing", params) + result = self.api_post(sender, f"/api/v1/messages/{msg_id}/typing", params) self.assert_json_success(result) self.assert_length(events, 1) event_user_ids = set(events[0]["users"]) diff --git a/zerver/views/typing.py b/zerver/views/typing.py index 6f5dadf131..0af11455e8 100644 --- a/zerver/views/typing.py +++ b/zerver/views/typing.py @@ -2,7 +2,7 @@ from typing import Annotated, Literal from django.http import HttpRequest, HttpResponse from django.utils.translation import gettext as _ -from pydantic import Json +from pydantic import Json, NonNegativeInt from zerver.actions.message_edit import validate_user_can_edit_message from zerver.actions.typing import ( @@ -16,7 +16,7 @@ from zerver.lib.message import access_message from zerver.lib.response import json_success from zerver.lib.streams import access_stream_by_id_for_message, access_stream_for_send_message from zerver.lib.topic import maybe_rename_general_chat_to_empty_topic -from zerver.lib.typed_endpoint import ApiParamConfig, OptionalTopic, typed_endpoint +from zerver.lib.typed_endpoint import ApiParamConfig, OptionalTopic, PathOnly, typed_endpoint from zerver.models import Recipient, UserProfile from zerver.models.recipients import get_direct_message_group_user_ids @@ -77,8 +77,8 @@ def send_message_edit_notification_backend( request: HttpRequest, user_profile: UserProfile, *, + message_id: PathOnly[NonNegativeInt], operator: Annotated[Literal["start", "stop"], ApiParamConfig("op")], - message_id: Json[int], ) -> HttpResponse: # Technically, this endpoint doesn't modify the message, but we're # attempting to send a typing notification that we're editing the diff --git a/zproject/urls.py b/zproject/urls.py index 0031f1eb0a..768927fded 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -399,7 +399,7 @@ v1_api_and_json_patterns = [ # POST sends a typing notification event to recipients rest_path("typing", POST=send_notification_backend), # POST sends a message edit typing notification - rest_path("message_edit_typing", POST=send_message_edit_notification_backend), + rest_path("messages//typing", POST=send_message_edit_notification_backend), # user_uploads -> zerver.views.upload rest_path("user_uploads", POST=upload_file_backend), rest_path(