mirror of https://github.com/zulip/zulip.git
drafts: Send events to clients when drafts change.
With this, the core of the new drafts system is complete.
This commit is contained in:
parent
c00089ac28
commit
6fee946a43
|
@ -24,6 +24,7 @@ from zerver.lib.validator import (
|
||||||
check_union,
|
check_union,
|
||||||
)
|
)
|
||||||
from zerver.models import Draft, UserProfile
|
from zerver.models import Draft, UserProfile
|
||||||
|
from zerver.tornado.django_api import send_event
|
||||||
|
|
||||||
VALID_DRAFT_TYPES: Set[str] = {"", "private", "stream"}
|
VALID_DRAFT_TYPES: Set[str] = {"", "private", "stream"}
|
||||||
|
|
||||||
|
@ -117,6 +118,14 @@ def do_create_drafts(draft_dicts: List[Dict[str, Any]], user_profile: UserProfil
|
||||||
)
|
)
|
||||||
|
|
||||||
created_draft_objects = Draft.objects.bulk_create(draft_objects)
|
created_draft_objects = Draft.objects.bulk_create(draft_objects)
|
||||||
|
|
||||||
|
event = {
|
||||||
|
"type": "drafts",
|
||||||
|
"op": "add",
|
||||||
|
"drafts": [draft.to_dict() for draft in created_draft_objects],
|
||||||
|
}
|
||||||
|
send_event(user_profile.realm, event, [user_profile.id])
|
||||||
|
|
||||||
return created_draft_objects
|
return created_draft_objects
|
||||||
|
|
||||||
|
|
||||||
|
@ -135,6 +144,9 @@ def do_edit_draft(draft_id: int, draft_dict: Dict[str, Any], user_profile: UserP
|
||||||
draft_object.last_edit_time = valid_draft_dict["last_edit_time"]
|
draft_object.last_edit_time = valid_draft_dict["last_edit_time"]
|
||||||
draft_object.save()
|
draft_object.save()
|
||||||
|
|
||||||
|
event = {"type": "drafts", "op": "update", "draft": draft_object.to_dict()}
|
||||||
|
send_event(user_profile.realm, event, [user_profile.id])
|
||||||
|
|
||||||
|
|
||||||
def do_delete_draft(draft_id: int, user_profile: UserProfile) -> None:
|
def do_delete_draft(draft_id: int, user_profile: UserProfile) -> None:
|
||||||
"""Delete a draft belonging to a particular user."""
|
"""Delete a draft belonging to a particular user."""
|
||||||
|
@ -142,4 +154,9 @@ def do_delete_draft(draft_id: int, user_profile: UserProfile) -> None:
|
||||||
draft_object = Draft.objects.get(id=draft_id, user_profile=user_profile)
|
draft_object = Draft.objects.get(id=draft_id, user_profile=user_profile)
|
||||||
except Draft.DoesNotExist:
|
except Draft.DoesNotExist:
|
||||||
raise ResourceNotFoundError(_("Draft does not exist"))
|
raise ResourceNotFoundError(_("Draft does not exist"))
|
||||||
|
|
||||||
|
draft_id = draft_object.id
|
||||||
draft_object.delete()
|
draft_object.delete()
|
||||||
|
|
||||||
|
event = {"type": "drafts", "op": "remove", "draft_id": draft_id}
|
||||||
|
send_event(user_profile.realm, event, [user_profile.id])
|
||||||
|
|
|
@ -54,6 +54,7 @@ from zerver.models import (
|
||||||
MAX_TOPIC_NAME_LENGTH,
|
MAX_TOPIC_NAME_LENGTH,
|
||||||
Client,
|
Client,
|
||||||
CustomProfileField,
|
CustomProfileField,
|
||||||
|
Draft,
|
||||||
Message,
|
Message,
|
||||||
Realm,
|
Realm,
|
||||||
Stream,
|
Stream,
|
||||||
|
@ -169,6 +170,17 @@ def fetch_initial_state_data(
|
||||||
else:
|
else:
|
||||||
state["max_message_id"] = -1
|
state["max_message_id"] = -1
|
||||||
|
|
||||||
|
if want("drafts"):
|
||||||
|
# Note: if a user ever disables synching drafts then all of
|
||||||
|
# their old drafts stored on the server will be deleted and
|
||||||
|
# simply retained in local storage. In which case user_drafts
|
||||||
|
# would just be an empty queryset.
|
||||||
|
user_draft_objects = Draft.objects.filter(user_profile=user_profile).order_by(
|
||||||
|
"-last_edit_time"
|
||||||
|
)[: settings.MAX_DRAFTS_IN_REGISTER_RESPONSE]
|
||||||
|
user_draft_dicts = [draft.to_dict() for draft in user_draft_objects]
|
||||||
|
state["drafts"] = user_draft_dicts
|
||||||
|
|
||||||
if want("muted_topics"):
|
if want("muted_topics"):
|
||||||
state["muted_topics"] = [] if user_profile is None else get_topic_mutes(user_profile)
|
state["muted_topics"] = [] if user_profile is None else get_topic_mutes(user_profile)
|
||||||
|
|
||||||
|
@ -618,6 +630,34 @@ def apply_event(
|
||||||
# It may be impossible for a heartbeat event to actually reach
|
# It may be impossible for a heartbeat event to actually reach
|
||||||
# this code path. But in any case, they're noops.
|
# this code path. But in any case, they're noops.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
elif event["type"] == "drafts":
|
||||||
|
if event["op"] == "add":
|
||||||
|
state["drafts"].extend(event["drafts"])
|
||||||
|
else:
|
||||||
|
if event["op"] == "update":
|
||||||
|
event_draft_idx = event["draft"]["id"]
|
||||||
|
|
||||||
|
def _draft_update_action(i: int) -> None:
|
||||||
|
state["drafts"][i] = event["draft"]
|
||||||
|
|
||||||
|
elif event["op"] == "remove":
|
||||||
|
event_draft_idx = event["draft_id"]
|
||||||
|
|
||||||
|
def _draft_update_action(i: int) -> None:
|
||||||
|
del state["drafts"][i]
|
||||||
|
|
||||||
|
# We have to perform a linear search for the draft that
|
||||||
|
# was either edited or removed since we have a list
|
||||||
|
# ordered by the last edited timestamp and not id.
|
||||||
|
state_draft_idx = None
|
||||||
|
for idx, draft in enumerate(state["drafts"]):
|
||||||
|
if draft["id"] == event_draft_idx:
|
||||||
|
state_draft_idx = idx
|
||||||
|
break
|
||||||
|
assert state_draft_idx is not None
|
||||||
|
_draft_update_action(state_draft_idx)
|
||||||
|
|
||||||
elif event["type"] == "hotspots":
|
elif event["type"] == "hotspots":
|
||||||
state["hotspots"] = event["hotspots"]
|
state["hotspots"] = event["hotspots"]
|
||||||
elif event["type"] == "custom_profile_fields":
|
elif event["type"] == "custom_profile_fields":
|
||||||
|
|
|
@ -3818,6 +3818,102 @@ paths:
|
||||||
},
|
},
|
||||||
"id": 0,
|
"id": 0,
|
||||||
}
|
}
|
||||||
|
- type: object
|
||||||
|
additionalProperties: false
|
||||||
|
description: |
|
||||||
|
Event containing details of newly created drafts.
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/EventIdSchema"
|
||||||
|
type:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/EventTypeSchema"
|
||||||
|
- enum:
|
||||||
|
- "drafts"
|
||||||
|
op:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- "add"
|
||||||
|
drafts:
|
||||||
|
type: array
|
||||||
|
description: |
|
||||||
|
An array containing objects for the newly created drafts.
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Draft"
|
||||||
|
example:
|
||||||
|
{
|
||||||
|
"type": "drafts",
|
||||||
|
"op": "add",
|
||||||
|
"drafts":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"type": "private",
|
||||||
|
"to": [6],
|
||||||
|
"topic": "",
|
||||||
|
"content": "Hello there!",
|
||||||
|
"timestamp": 15954790200,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
- type: object
|
||||||
|
additionalProperties: false
|
||||||
|
description: |
|
||||||
|
Event containing details for an edited draft.
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/EventIdSchema"
|
||||||
|
type:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/EventTypeSchema"
|
||||||
|
- enum:
|
||||||
|
- "drafts"
|
||||||
|
op:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- "update"
|
||||||
|
draft:
|
||||||
|
$ref: "#/components/schemas/Draft"
|
||||||
|
example:
|
||||||
|
{
|
||||||
|
"type": "drafts",
|
||||||
|
"op": "update",
|
||||||
|
"draft":
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"type": "private",
|
||||||
|
"to": [6, 7, 8, 9, 10],
|
||||||
|
"topic": "",
|
||||||
|
"content": "Hello everyone!",
|
||||||
|
"timestamp": 15954790200,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
- type: object
|
||||||
|
additionalProperties: false
|
||||||
|
description: |
|
||||||
|
Event containing the id of a deleted draft.
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/EventIdSchema"
|
||||||
|
type:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/EventTypeSchema"
|
||||||
|
- enum:
|
||||||
|
- "drafts"
|
||||||
|
op:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- "remove"
|
||||||
|
draft_id:
|
||||||
|
type: integer
|
||||||
|
description: |
|
||||||
|
The ID of the draft that was just deleted.
|
||||||
|
example:
|
||||||
|
{
|
||||||
|
"type": "drafts",
|
||||||
|
"op": "update",
|
||||||
|
"draft_id": 17,
|
||||||
|
}
|
||||||
queue_id:
|
queue_id:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
|
@ -7980,6 +8076,14 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
The name of the custom profile field type.
|
The name of the custom profile field type.
|
||||||
|
drafts:
|
||||||
|
type: array
|
||||||
|
description: |
|
||||||
|
An array containing draft objects for the user. These drafts are being
|
||||||
|
stored on the backend for the purpose of syncing across devices. This
|
||||||
|
array will be empty if `enable_drafts_synchronization` is set to `false`.
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Draft"
|
||||||
hotspots:
|
hotspots:
|
||||||
type: array
|
type: array
|
||||||
description: |
|
description: |
|
||||||
|
@ -8835,7 +8939,8 @@ paths:
|
||||||
description: |
|
description: |
|
||||||
Present if `update_display_settings` is present in `fetch_event_types`.
|
Present if `update_display_settings` is present in `fetch_event_types`.
|
||||||
|
|
||||||
Whether drafts synchronization is enabled for the user.
|
Whether drafts synchronization is enabled for the user. If disabled,
|
||||||
|
clients will receive an error when trying to use the `drafts` endpoints.
|
||||||
|
|
||||||
See [PATCH /settings](/api/update-settings) for details on
|
See [PATCH /settings](/api/update-settings) for details on
|
||||||
the meaning of this setting.
|
the meaning of this setting.
|
||||||
|
@ -12800,6 +12905,59 @@ components:
|
||||||
description: |
|
description: |
|
||||||
Whether the client is capable of showing mobile/push notifications
|
Whether the client is capable of showing mobile/push notifications
|
||||||
to the user.
|
to the user.
|
||||||
|
Draft:
|
||||||
|
type: object
|
||||||
|
description: |
|
||||||
|
A dictionary for representing a message draft.
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
description: |
|
||||||
|
The unique ID of the draft. It will only used whenever the drafts are
|
||||||
|
fetched. This field should not be specified when the draft is being
|
||||||
|
created or edited.
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
The type of the draft. Either unaddressed (empty string), "stream",
|
||||||
|
or "private" (for PMs and private group messages).
|
||||||
|
enum:
|
||||||
|
- ""
|
||||||
|
- "stream"
|
||||||
|
- "private"
|
||||||
|
to:
|
||||||
|
type: array
|
||||||
|
description: |
|
||||||
|
An array of the tentative target audience IDs. For "stream"
|
||||||
|
messages, this should contain exactly 1 ID, the ID of the
|
||||||
|
target stream. For private messages, this should be an array
|
||||||
|
of target user IDs. For unaddressed drafts this is ignored
|
||||||
|
so it's best to leave it as an empty array.
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
topic:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
For stream message drafts, the tentative topic name. For private
|
||||||
|
or unaddressed messages this will be ignored and should ideally
|
||||||
|
be an empty string. Should not contain null bytes.
|
||||||
|
content:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
The body of the draft. Should not contain null bytes.
|
||||||
|
timestamp:
|
||||||
|
type: number
|
||||||
|
description: |
|
||||||
|
A Unix timestamp (seconds only) representing when the draft was
|
||||||
|
last edited. When creating a draft, this key need not be present
|
||||||
|
and it will be filled in automatically by the server.
|
||||||
|
example: 1595479019
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
- to
|
||||||
|
- topic
|
||||||
|
- content
|
||||||
User:
|
User:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: "#/components/schemas/UserBase"
|
- $ref: "#/components/schemas/UserBase"
|
||||||
|
|
|
@ -980,13 +980,14 @@ class FetchQueriesTest(ZulipTestCase):
|
||||||
with mock.patch("zerver.lib.events.always_want") as want_mock:
|
with mock.patch("zerver.lib.events.always_want") as want_mock:
|
||||||
fetch_initial_state_data(user)
|
fetch_initial_state_data(user)
|
||||||
|
|
||||||
self.assert_length(queries, 33)
|
self.assert_length(queries, 34)
|
||||||
|
|
||||||
expected_counts = dict(
|
expected_counts = dict(
|
||||||
alert_words=1,
|
alert_words=1,
|
||||||
custom_profile_fields=1,
|
custom_profile_fields=1,
|
||||||
default_streams=1,
|
default_streams=1,
|
||||||
default_stream_groups=1,
|
default_stream_groups=1,
|
||||||
|
drafts=1,
|
||||||
hotspots=0,
|
hotspots=0,
|
||||||
message=1,
|
message=1,
|
||||||
muted_topics=1,
|
muted_topics=1,
|
||||||
|
|
|
@ -101,6 +101,7 @@ from zerver.lib.actions import (
|
||||||
try_add_realm_custom_profile_field,
|
try_add_realm_custom_profile_field,
|
||||||
try_update_realm_custom_profile_field,
|
try_update_realm_custom_profile_field,
|
||||||
)
|
)
|
||||||
|
from zerver.lib.drafts import do_create_drafts, do_delete_draft, do_edit_draft
|
||||||
from zerver.lib.event_schema import (
|
from zerver.lib.event_schema import (
|
||||||
check_alert_words,
|
check_alert_words,
|
||||||
check_attachment_add,
|
check_attachment_add,
|
||||||
|
@ -2308,6 +2309,45 @@ class DraftActionTest(BaseAction):
|
||||||
def do_disable_drafts_synchronization(self, user_profile: UserProfile) -> None:
|
def do_disable_drafts_synchronization(self, user_profile: UserProfile) -> None:
|
||||||
do_set_user_display_setting(user_profile, "enable_drafts_synchronization", False)
|
do_set_user_display_setting(user_profile, "enable_drafts_synchronization", False)
|
||||||
|
|
||||||
|
def test_draft_create_event(self) -> None:
|
||||||
|
self.do_enable_drafts_synchronization(self.user_profile)
|
||||||
|
dummy_draft = {
|
||||||
|
"type": "draft",
|
||||||
|
"to": "",
|
||||||
|
"topic": "",
|
||||||
|
"content": "Sample draft content",
|
||||||
|
"timestamp": 1596820995,
|
||||||
|
}
|
||||||
|
action = lambda: do_create_drafts([dummy_draft], self.user_profile)
|
||||||
|
self.verify_action(action)
|
||||||
|
|
||||||
|
def test_draft_edit_event(self) -> None:
|
||||||
|
self.do_enable_drafts_synchronization(self.user_profile)
|
||||||
|
dummy_draft = {
|
||||||
|
"type": "draft",
|
||||||
|
"to": "",
|
||||||
|
"topic": "",
|
||||||
|
"content": "Sample draft content",
|
||||||
|
"timestamp": 1596820995,
|
||||||
|
}
|
||||||
|
draft_id = do_create_drafts([dummy_draft], self.user_profile)[0].id
|
||||||
|
dummy_draft["content"] = "Some more sample draft content"
|
||||||
|
action = lambda: do_edit_draft(draft_id, dummy_draft, self.user_profile)
|
||||||
|
self.verify_action(action)
|
||||||
|
|
||||||
|
def test_draft_delete_event(self) -> None:
|
||||||
|
self.do_enable_drafts_synchronization(self.user_profile)
|
||||||
|
dummy_draft = {
|
||||||
|
"type": "draft",
|
||||||
|
"to": "",
|
||||||
|
"topic": "",
|
||||||
|
"content": "Sample draft content",
|
||||||
|
"timestamp": 1596820995,
|
||||||
|
}
|
||||||
|
draft_id = do_create_drafts([dummy_draft], self.user_profile)[0].id
|
||||||
|
action = lambda: do_delete_draft(draft_id, self.user_profile)
|
||||||
|
self.verify_action(action)
|
||||||
|
|
||||||
def test_enable_syncing_drafts(self) -> None:
|
def test_enable_syncing_drafts(self) -> None:
|
||||||
self.do_disable_drafts_synchronization(self.user_profile)
|
self.do_disable_drafts_synchronization(self.user_profile)
|
||||||
action = lambda: self.do_enable_drafts_synchronization(self.user_profile)
|
action = lambda: self.do_enable_drafts_synchronization(self.user_profile)
|
||||||
|
|
|
@ -24,6 +24,7 @@ from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.test_helpers import get_user_messages, queries_captured
|
from zerver.lib.test_helpers import get_user_messages, queries_captured
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
DefaultStream,
|
DefaultStream,
|
||||||
|
Draft,
|
||||||
Realm,
|
Realm,
|
||||||
UserActivity,
|
UserActivity,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
|
@ -64,6 +65,7 @@ class HomeTest(ZulipTestCase):
|
||||||
"dense_mode",
|
"dense_mode",
|
||||||
"desktop_icon_count_display",
|
"desktop_icon_count_display",
|
||||||
"development_environment",
|
"development_environment",
|
||||||
|
"drafts",
|
||||||
"email",
|
"email",
|
||||||
"email_notifications_batching_period_seconds",
|
"email_notifications_batching_period_seconds",
|
||||||
"emojiset",
|
"emojiset",
|
||||||
|
@ -271,7 +273,7 @@ class HomeTest(ZulipTestCase):
|
||||||
set(result["Cache-Control"].split(", ")), {"must-revalidate", "no-store", "no-cache"}
|
set(result["Cache-Control"].split(", ")), {"must-revalidate", "no-store", "no-cache"}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assert_length(queries, 42)
|
self.assert_length(queries, 43)
|
||||||
self.assert_length(cache_mock.call_args_list, 5)
|
self.assert_length(cache_mock.call_args_list, 5)
|
||||||
|
|
||||||
html = result.content.decode("utf-8")
|
html = result.content.decode("utf-8")
|
||||||
|
@ -351,7 +353,7 @@ class HomeTest(ZulipTestCase):
|
||||||
result = self._get_home_page()
|
result = self._get_home_page()
|
||||||
self.check_rendered_logged_in_app(result)
|
self.check_rendered_logged_in_app(result)
|
||||||
self.assert_length(cache_mock.call_args_list, 6)
|
self.assert_length(cache_mock.call_args_list, 6)
|
||||||
self.assert_length(queries, 39)
|
self.assert_length(queries, 40)
|
||||||
|
|
||||||
def test_num_queries_with_streams(self) -> None:
|
def test_num_queries_with_streams(self) -> None:
|
||||||
main_user = self.example_user("hamlet")
|
main_user = self.example_user("hamlet")
|
||||||
|
@ -382,7 +384,7 @@ class HomeTest(ZulipTestCase):
|
||||||
with queries_captured() as queries2:
|
with queries_captured() as queries2:
|
||||||
result = self._get_home_page()
|
result = self._get_home_page()
|
||||||
|
|
||||||
self.assert_length(queries2, 37)
|
self.assert_length(queries2, 38)
|
||||||
|
|
||||||
# Do a sanity check that our new streams were in the payload.
|
# Do a sanity check that our new streams were in the payload.
|
||||||
html = result.content.decode("utf-8")
|
html = result.content.decode("utf-8")
|
||||||
|
@ -1041,3 +1043,35 @@ class HomeTest(ZulipTestCase):
|
||||||
|
|
||||||
page_params = self._get_page_params(result)
|
page_params = self._get_page_params(result)
|
||||||
self.assertEqual(page_params["default_language"], "es")
|
self.assertEqual(page_params["default_language"], "es")
|
||||||
|
|
||||||
|
@override_settings(MAX_DRAFTS_IN_REGISTER_RESPONSE=5)
|
||||||
|
def test_limit_drafts(self) -> None:
|
||||||
|
draft_objects = []
|
||||||
|
hamlet = self.example_user("hamlet")
|
||||||
|
base_time = timezone_now()
|
||||||
|
|
||||||
|
step_value = timedelta(seconds=1)
|
||||||
|
# Create 11 drafts.
|
||||||
|
for i in range(0, settings.MAX_DRAFTS_IN_REGISTER_RESPONSE + 1):
|
||||||
|
draft_objects.append(
|
||||||
|
Draft(
|
||||||
|
user_profile=hamlet,
|
||||||
|
recipient=None,
|
||||||
|
topic="",
|
||||||
|
content="sample draft",
|
||||||
|
last_edit_time=base_time + i * step_value,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Draft.objects.bulk_create(draft_objects)
|
||||||
|
|
||||||
|
# Now fetch the drafts part of the initial state and make sure
|
||||||
|
# that we only got back settings.MAX_DRAFTS_IN_REGISTER_RESPONSE.
|
||||||
|
# No more. Also make sure that the drafts returned are the most
|
||||||
|
# recently edited ones.
|
||||||
|
self.login("hamlet")
|
||||||
|
page_params = self._get_page_params(self._get_home_page())
|
||||||
|
self.assertEqual(page_params["enable_drafts_synchronization"], True)
|
||||||
|
self.assert_length(page_params["drafts"], settings.MAX_DRAFTS_IN_REGISTER_RESPONSE)
|
||||||
|
self.assertEqual(Draft.objects.count(), settings.MAX_DRAFTS_IN_REGISTER_RESPONSE + 1)
|
||||||
|
for draft in page_params["drafts"]:
|
||||||
|
self.assertNotEqual(draft["timestamp"], base_time)
|
||||||
|
|
|
@ -455,3 +455,8 @@ OUTGOING_WEBHOOK_TIMEOUT_SECONDS = 10
|
||||||
# Any message content exceeding this limit will be truncated.
|
# Any message content exceeding this limit will be truncated.
|
||||||
# See: `_internal_prep_message` function in zerver/lib/actions.py.
|
# See: `_internal_prep_message` function in zerver/lib/actions.py.
|
||||||
MAX_MESSAGE_LENGTH = 10000
|
MAX_MESSAGE_LENGTH = 10000
|
||||||
|
|
||||||
|
# The maximum number of drafts to send in the response to /register.
|
||||||
|
# More drafts, should they exist for some crazy reason, could be
|
||||||
|
# fetched in a separate request.
|
||||||
|
MAX_DRAFTS_IN_REGISTER_RESPONSE = 1000
|
||||||
|
|
Loading…
Reference in New Issue