mirror of
https://github.com/zulip/zulip.git
synced 2025-10-27 10:03:56 +00:00
message_reminders: Add support for notes.
This commit adds the ability for users to include notes with their message reminders. Fixes #35070. Co-Authored-By: Aman Agrawal <amanagr@zulip.com>
This commit is contained in:
@@ -20,6 +20,14 @@ format used by the Zulip server that they are interacting with.
|
|||||||
|
|
||||||
## Changes in Zulip 11.0
|
## Changes in Zulip 11.0
|
||||||
|
|
||||||
|
**Feature level 415**
|
||||||
|
|
||||||
|
* [`POST /reminders`](/api/create-message-reminder): Added parameter
|
||||||
|
`note` to allow users to add notes to their reminders.
|
||||||
|
* [`POST /register`](/api/register-queue): Added `max_reminder_note_length`
|
||||||
|
for clients to restrict the reminder note length before sending it to
|
||||||
|
the server.
|
||||||
|
|
||||||
**Feature level 414**
|
**Feature level 414**
|
||||||
|
|
||||||
* [`POST /channel_folders/create`](/api/create-channel-folder),
|
* [`POST /channel_folders/create`](/api/create-channel-folder),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
|||||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
# entries in the endpoint's documentation in `zulip.yaml`.
|
||||||
|
|
||||||
API_FEATURE_LEVEL = 414
|
API_FEATURE_LEVEL = 415
|
||||||
|
|
||||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
# 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
|
# only when going from an old version of the code to a newer version. Bump
|
||||||
|
|||||||
@@ -35,13 +35,6 @@ export function open_schedule_message_menu(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let interval;
|
let interval;
|
||||||
const message_schedule_callback = (time) => {
|
|
||||||
if (remind_message_id !== undefined) {
|
|
||||||
do_schedule_reminder(time, remind_message_id);
|
|
||||||
} else {
|
|
||||||
do_schedule_message(time);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
popover_menus.toggle_popover_menu(target, {
|
popover_menus.toggle_popover_menu(target, {
|
||||||
theme: "popover-menu",
|
theme: "popover-menu",
|
||||||
@@ -77,7 +70,23 @@ export function open_schedule_message_menu(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
onMount(instance) {
|
onMount(instance) {
|
||||||
|
if (remind_message_id !== undefined) {
|
||||||
|
popover_menus.focus_first_popover_item(
|
||||||
|
popover_menus.get_popover_items_for_instance(instance),
|
||||||
|
);
|
||||||
|
}
|
||||||
const $popper = $(instance.popper);
|
const $popper = $(instance.popper);
|
||||||
|
const message_schedule_callback = (time) => {
|
||||||
|
if (remind_message_id !== undefined) {
|
||||||
|
do_schedule_reminder(
|
||||||
|
time,
|
||||||
|
remind_message_id,
|
||||||
|
$popper.find(".schedule-reminder-note").val(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
do_schedule_message(time);
|
||||||
|
}
|
||||||
|
};
|
||||||
$popper.on("click", ".send_later_custom", (e) => {
|
$popper.on("click", ".send_later_custom", (e) => {
|
||||||
const $send_later_options_content = $popper.find(".popover-menu-list");
|
const $send_later_options_content = $popper.find(".popover-menu-list");
|
||||||
const current_time = new Date();
|
const current_time = new Date();
|
||||||
@@ -149,9 +158,9 @@ export function do_schedule_message(send_at_time) {
|
|||||||
compose.finish(true);
|
compose.finish(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function do_schedule_reminder(send_at_time, remind_message_id) {
|
export function do_schedule_reminder(send_at_time, remind_message_id, note_text) {
|
||||||
send_at_time = parse_sent_at_time(send_at_time);
|
send_at_time = parse_sent_at_time(send_at_time);
|
||||||
message_reminder.set_message_reminder(send_at_time, remind_message_id);
|
message_reminder.set_message_reminder(send_at_time, remind_message_id, note_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_send_later_menu_items() {
|
function get_send_later_menu_items() {
|
||||||
|
|||||||
@@ -523,6 +523,17 @@ function handle_popover_events(event_name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (popover_menu_visible_instance) {
|
if (popover_menu_visible_instance) {
|
||||||
|
if (
|
||||||
|
// Allow all hotkeys except for `down_arrow` and `up_arrow`
|
||||||
|
// to be handled by the browser.
|
||||||
|
// Added to handle `schedule-reminder-note` textarea.
|
||||||
|
processing_text() &&
|
||||||
|
event_name !== "down_arrow" &&
|
||||||
|
event_name !== "up_arrow"
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
popover_menus.sidebar_menu_instance_handle_keyboard(
|
popover_menus.sidebar_menu_instance_handle_keyboard(
|
||||||
popover_menu_visible_instance,
|
popover_menu_visible_instance,
|
||||||
event_name,
|
event_name,
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ export type Reminder = z.infer<typeof reminder_schema>;
|
|||||||
|
|
||||||
export const reminders_by_id = new Map<number, Reminder>();
|
export const reminders_by_id = new Map<number, Reminder>();
|
||||||
|
|
||||||
export function set_message_reminder(send_at_time: number, message_id: number): void {
|
export function set_message_reminder(send_at_time: number, message_id: number, note: string): void {
|
||||||
channel.post({
|
channel.post({
|
||||||
url: "/json/reminders",
|
url: "/json/reminders",
|
||||||
data: {
|
data: {
|
||||||
message_id,
|
message_id,
|
||||||
scheduled_delivery_timestamp: send_at_time,
|
scheduled_delivery_timestamp: send_at_time,
|
||||||
|
note,
|
||||||
},
|
},
|
||||||
success(): void {
|
success(): void {
|
||||||
const populate: (element: JQuery) => void = ($container) => {
|
const populate: (element: JQuery) => void = ($container) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type * as z from "zod/mini";
|
|||||||
import * as channel from "./channel.ts";
|
import * as channel from "./channel.ts";
|
||||||
import {$t} from "./i18n.ts";
|
import {$t} from "./i18n.ts";
|
||||||
import type {StateData, scheduled_message_schema} from "./state_data.ts";
|
import type {StateData, scheduled_message_schema} from "./state_data.ts";
|
||||||
|
import {realm} from "./state_data.ts";
|
||||||
import * as timerender from "./timerender.ts";
|
import * as timerender from "./timerender.ts";
|
||||||
|
|
||||||
export type ScheduledMessage = z.infer<typeof scheduled_message_schema>;
|
export type ScheduledMessage = z.infer<typeof scheduled_message_schema>;
|
||||||
@@ -91,6 +92,7 @@ export function get_filtered_send_opts(date: Date): {
|
|||||||
send_later_tomorrow: SendOption;
|
send_later_tomorrow: SendOption;
|
||||||
possible_send_later_monday: SendOption | false;
|
possible_send_later_monday: SendOption | false;
|
||||||
send_later_custom: {text: string};
|
send_later_custom: {text: string};
|
||||||
|
max_reminder_note_length: number;
|
||||||
} {
|
} {
|
||||||
const send_times = compute_send_times(date);
|
const send_times = compute_send_times(date);
|
||||||
|
|
||||||
@@ -195,6 +197,7 @@ export function get_filtered_send_opts(date: Date): {
|
|||||||
send_later_tomorrow,
|
send_later_tomorrow,
|
||||||
possible_send_later_monday,
|
possible_send_later_monday,
|
||||||
send_later_custom,
|
send_later_custom,
|
||||||
|
max_reminder_note_length: realm.max_reminder_note_length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ export const realm_schema = z.object({
|
|||||||
max_icon_file_size_mib: z.number(),
|
max_icon_file_size_mib: z.number(),
|
||||||
max_logo_file_size_mib: z.number(),
|
max_logo_file_size_mib: z.number(),
|
||||||
max_message_length: z.number(),
|
max_message_length: z.number(),
|
||||||
|
max_reminder_note_length: z.number(),
|
||||||
max_stream_description_length: z.number(),
|
max_stream_description_length: z.number(),
|
||||||
max_stream_name_length: z.number(),
|
max_stream_name_length: z.number(),
|
||||||
max_topic_length: z.number(),
|
max_topic_length: z.number(),
|
||||||
|
|||||||
@@ -19,3 +19,11 @@
|
|||||||
#reminders-overlay-container .message_content {
|
#reminders-overlay-container .message_content {
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.schedule-reminder-note {
|
||||||
|
/* Horizontal resizing in the popover doesn't look good at all. */
|
||||||
|
resize: vertical;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 10em;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="popover-menu" id="send-later-options" data-simplebar data-simplebar-tab-index="-1">
|
<div class="popover-menu {{#if is_reminder}}message-reminder-popover{{/if}}" id="send-later-options" data-simplebar data-simplebar-tab-index="-1">
|
||||||
<ul role="menu" class="popover-menu-list">
|
<ul role="menu" class="popover-menu-list">
|
||||||
<li role="none" class="text-item popover-menu-list-item">
|
<li role="none" class="text-item popover-menu-list-item">
|
||||||
<span class="popover-header-name">
|
<span class="popover-header-name">
|
||||||
@@ -9,6 +9,14 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
{{#if is_reminder}}
|
||||||
|
<li role="separator" class="popover-menu-separator"></li>
|
||||||
|
<li role="none" class="text-item popover-menu-list-item schedule-reminder-note-item">
|
||||||
|
<textarea class="schedule-reminder-note" placeholder="{{t 'Note'}}" tabindex="0"
|
||||||
|
maxlength="{{max_reminder_note_length}}"
|
||||||
|
></textarea>
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
{{#if possible_send_later_today}}
|
{{#if possible_send_later_today}}
|
||||||
<li role="separator" class="popover-menu-separator"></li>
|
<li role="separator" class="popover-menu-separator"></li>
|
||||||
{{#each possible_send_later_today}}
|
{{#each possible_send_later_today}}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ const {run_test} = require("./lib/test.cjs");
|
|||||||
const scheduled_messages = zrequire("scheduled_messages");
|
const scheduled_messages = zrequire("scheduled_messages");
|
||||||
const compose_send_menu_popover = zrequire("compose_send_menu_popover");
|
const compose_send_menu_popover = zrequire("compose_send_menu_popover");
|
||||||
const {initialize_user_settings} = zrequire("user_settings");
|
const {initialize_user_settings} = zrequire("user_settings");
|
||||||
|
const {set_realm} = zrequire("state_data");
|
||||||
|
set_realm({
|
||||||
|
max_reminder_note_length: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
initialize_user_settings({user_settings: {}});
|
initialize_user_settings({user_settings: {}});
|
||||||
|
|
||||||
@@ -73,6 +77,7 @@ function get_expected_send_opts(day, expecteds) {
|
|||||||
},
|
},
|
||||||
possible_send_later_today: false,
|
possible_send_later_today: false,
|
||||||
possible_send_later_monday: false,
|
possible_send_later_monday: false,
|
||||||
|
max_reminder_note_length: 1000,
|
||||||
};
|
};
|
||||||
const optional_modal_opts = {
|
const optional_modal_opts = {
|
||||||
send_later_today: {
|
send_later_today: {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ def schedule_reminder_for_message(
|
|||||||
client: Client,
|
client: Client,
|
||||||
message_id: int,
|
message_id: int,
|
||||||
deliver_at: datetime.datetime,
|
deliver_at: datetime.datetime,
|
||||||
|
note: str,
|
||||||
) -> int:
|
) -> int:
|
||||||
message = access_message(current_user, message_id, is_modifying_message=False)
|
message = access_message(current_user, message_id, is_modifying_message=False)
|
||||||
# Even though reminder will be sent from NOTIFICATION_BOT, we still
|
# Even though reminder will be sent from NOTIFICATION_BOT, we still
|
||||||
@@ -26,12 +27,13 @@ def schedule_reminder_for_message(
|
|||||||
current_user,
|
current_user,
|
||||||
client,
|
client,
|
||||||
addressee,
|
addressee,
|
||||||
get_reminder_formatted_content(message, current_user),
|
get_reminder_formatted_content(message, current_user, note),
|
||||||
current_user.realm,
|
current_user.realm,
|
||||||
forwarder_user_profile=current_user,
|
forwarder_user_profile=current_user,
|
||||||
)
|
)
|
||||||
send_request.deliver_at = deliver_at
|
send_request.deliver_at = deliver_at
|
||||||
send_request.reminder_target_message_id = message_id
|
send_request.reminder_target_message_id = message_id
|
||||||
|
send_request.reminder_note = note
|
||||||
|
|
||||||
return do_schedule_messages(
|
return do_schedule_messages(
|
||||||
[send_request],
|
[send_request],
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ def do_schedule_messages(
|
|||||||
|
|
||||||
if delivery_type == ScheduledMessage.REMIND:
|
if delivery_type == ScheduledMessage.REMIND:
|
||||||
scheduled_message.reminder_target_message_id = send_request.reminder_target_message_id
|
scheduled_message.reminder_target_message_id = send_request.reminder_target_message_id
|
||||||
|
scheduled_message.reminder_note = send_request.reminder_note
|
||||||
|
|
||||||
scheduled_messages.append((scheduled_message, send_request))
|
scheduled_messages.append((scheduled_message, send_request))
|
||||||
|
|
||||||
@@ -307,7 +308,9 @@ def send_reminder(scheduled_message: ScheduledMessage) -> None:
|
|||||||
current_user = scheduled_message.sender
|
current_user = scheduled_message.sender
|
||||||
try:
|
try:
|
||||||
message = access_message(current_user, message_id, is_modifying_message=False)
|
message = access_message(current_user, message_id, is_modifying_message=False)
|
||||||
content = get_reminder_formatted_content(message, current_user)
|
content = get_reminder_formatted_content(
|
||||||
|
message, current_user, scheduled_message.reminder_note
|
||||||
|
)
|
||||||
except JsonableError:
|
except JsonableError:
|
||||||
# If we no longer have access to the message, we send the reminder with the
|
# If we no longer have access to the message, we send the reminder with the
|
||||||
# last known message position and content.
|
# last known message position and content.
|
||||||
|
|||||||
@@ -563,6 +563,7 @@ def fetch_initial_state_data(
|
|||||||
state["max_message_length"] = settings.MAX_MESSAGE_LENGTH
|
state["max_message_length"] = settings.MAX_MESSAGE_LENGTH
|
||||||
state["max_channel_folder_name_length"] = ChannelFolder.MAX_NAME_LENGTH
|
state["max_channel_folder_name_length"] = ChannelFolder.MAX_NAME_LENGTH
|
||||||
state["max_channel_folder_description_length"] = ChannelFolder.MAX_DESCRIPTION_LENGTH
|
state["max_channel_folder_description_length"] = ChannelFolder.MAX_DESCRIPTION_LENGTH
|
||||||
|
state["max_reminder_note_length"] = settings.MAX_REMINDER_NOTE_LENGTH
|
||||||
if realm.demo_organization_scheduled_deletion_date is not None:
|
if realm.demo_organization_scheduled_deletion_date is not None:
|
||||||
state["demo_organization_scheduled_deletion_date"] = datetime_to_timestamp(
|
state["demo_organization_scheduled_deletion_date"] = datetime_to_timestamp(
|
||||||
realm.demo_organization_scheduled_deletion_date
|
realm.demo_organization_scheduled_deletion_date
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ class SendMessageRequest:
|
|||||||
automatic_new_visibility_policy: int | None = None
|
automatic_new_visibility_policy: int | None = None
|
||||||
recipients_for_user_creation_events: dict[UserProfile, set[int]] | None = None
|
recipients_for_user_creation_events: dict[UserProfile, set[int]] | None = None
|
||||||
reminder_target_message_id: int | None = None
|
reminder_target_message_id: int | None = None
|
||||||
|
reminder_note: str | None = None
|
||||||
|
|
||||||
|
|
||||||
# We won't try to fetch more unread message IDs from the database than
|
# We won't try to fetch more unread message IDs from the database than
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from zerver.lib.exceptions import ResourceNotFoundError
|
from zerver.lib.exceptions import JsonableError, ResourceNotFoundError
|
||||||
from zerver.lib.markdown.fenced_code import get_unused_fence
|
from zerver.lib.markdown.fenced_code import get_unused_fence
|
||||||
from zerver.lib.mention import silent_mention_syntax_for_user
|
from zerver.lib.mention import silent_mention_syntax_for_user
|
||||||
from zerver.lib.message import truncate_content
|
from zerver.lib.message import truncate_content
|
||||||
@@ -12,7 +12,26 @@ from zerver.models import Message, Stream, UserProfile
|
|||||||
from zerver.models.scheduled_jobs import ScheduledMessage
|
from zerver.models.scheduled_jobs import ScheduledMessage
|
||||||
|
|
||||||
|
|
||||||
def get_reminder_formatted_content(message: Message, current_user: UserProfile) -> str:
|
def normalize_note_text(body: str) -> str:
|
||||||
|
# Similar to zerver.lib.message.normalize_body
|
||||||
|
body = body.rstrip().lstrip("\n")
|
||||||
|
|
||||||
|
if len(body) > settings.MAX_REMINDER_NOTE_LENGTH:
|
||||||
|
raise JsonableError(
|
||||||
|
_("Maximum reminder note length: {max_length} characters").format(
|
||||||
|
max_length=settings.MAX_REMINDER_NOTE_LENGTH
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def get_reminder_formatted_content(
|
||||||
|
message: Message, current_user: UserProfile, note: str | None = None
|
||||||
|
) -> str:
|
||||||
|
if note:
|
||||||
|
note = normalize_note_text(note)
|
||||||
|
|
||||||
if message.is_stream_message():
|
if message.is_stream_message():
|
||||||
# We don't need to check access here since we already have the message
|
# We don't need to check access here since we already have the message
|
||||||
# whose access has already been checked by the caller.
|
# whose access has already been checked by the caller.
|
||||||
@@ -20,13 +39,29 @@ def get_reminder_formatted_content(message: Message, current_user: UserProfile)
|
|||||||
id=message.recipient.type_id,
|
id=message.recipient.type_id,
|
||||||
realm=current_user.realm,
|
realm=current_user.realm,
|
||||||
)
|
)
|
||||||
content = _("You requested a reminder for {message_pretty_link}.").format(
|
|
||||||
message_pretty_link = get_message_link_syntax(
|
message_pretty_link = get_message_link_syntax(
|
||||||
stream_id=stream.id,
|
stream_id=stream.id,
|
||||||
stream_name=stream.name,
|
stream_name=stream.name,
|
||||||
topic_name=message.topic_name(),
|
topic_name=message.topic_name(),
|
||||||
message_id=message.id,
|
message_id=message.id,
|
||||||
)
|
)
|
||||||
|
if note:
|
||||||
|
content = _(
|
||||||
|
"You requested a reminder for {message_pretty_link}. Note:\n > {note}"
|
||||||
|
).format(
|
||||||
|
message_pretty_link=message_pretty_link,
|
||||||
|
note=note,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
content = _("You requested a reminder for {message_pretty_link}.").format(
|
||||||
|
message_pretty_link=message_pretty_link,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if note:
|
||||||
|
content = _(
|
||||||
|
"You requested a reminder for the following direct message. Note:\n > {note}"
|
||||||
|
).format(
|
||||||
|
note=note,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
content = _("You requested a reminder for the following direct message.")
|
content = _("You requested a reminder for the following direct message.")
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.4 on 2025-07-31 03:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("zerver", "0748_channelfolder_add_order"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="scheduledmessage",
|
||||||
|
name="reminder_note",
|
||||||
|
field=models.TextField(null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -166,6 +166,7 @@ class ScheduledMessage(models.Model):
|
|||||||
request_timestamp = models.DateTimeField(default=timezone_now)
|
request_timestamp = models.DateTimeField(default=timezone_now)
|
||||||
# Only used for REMIND delivery_type messages.
|
# Only used for REMIND delivery_type messages.
|
||||||
reminder_target_message_id = models.IntegerField(null=True)
|
reminder_target_message_id = models.IntegerField(null=True)
|
||||||
|
reminder_note = models.TextField(null=True)
|
||||||
|
|
||||||
# Metadata for messages that failed to send when their scheduled
|
# Metadata for messages that failed to send when their scheduled
|
||||||
# moment arrived.
|
# moment arrived.
|
||||||
|
|||||||
@@ -7276,6 +7276,13 @@ paths:
|
|||||||
The UNIX timestamp for when the reminder will be sent,
|
The UNIX timestamp for when the reminder will be sent,
|
||||||
in UTC seconds.
|
in UTC seconds.
|
||||||
example: 5681662420
|
example: 5681662420
|
||||||
|
note:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
A note associated with the reminder shown in the Notification Bot message.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 11.0 (feature level 415).
|
||||||
|
example: "This is a reminder note."
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Success.
|
description: Success.
|
||||||
@@ -16290,6 +16297,12 @@ paths:
|
|||||||
**Deprecated**: This field may be removed in future versions as it no
|
**Deprecated**: This field may be removed in future versions as it no
|
||||||
longer has a clear purpose. Clients wishing to fetch the latest messages
|
longer has a clear purpose. Clients wishing to fetch the latest messages
|
||||||
should pass `"anchor": "latest"` to `GET /messages`.
|
should pass `"anchor": "latest"` to `GET /messages`.
|
||||||
|
max_reminder_note_length:
|
||||||
|
type: integer
|
||||||
|
description: |
|
||||||
|
The maximum allowed length for a reminder note.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 11.0 (feature level 415).
|
||||||
max_stream_name_length:
|
max_stream_name_length:
|
||||||
type: integer
|
type: integer
|
||||||
description: |
|
description: |
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class HomeTest(ZulipTestCase):
|
|||||||
"max_logo_file_size_mib",
|
"max_logo_file_size_mib",
|
||||||
"max_message_id",
|
"max_message_id",
|
||||||
"max_message_length",
|
"max_message_length",
|
||||||
|
"max_reminder_note_length",
|
||||||
"max_stream_description_length",
|
"max_stream_description_length",
|
||||||
"max_stream_name_length",
|
"max_stream_name_length",
|
||||||
"max_bulk_new_subscription_messages",
|
"max_bulk_new_subscription_messages",
|
||||||
|
|||||||
@@ -18,13 +18,16 @@ class RemindersTest(ZulipTestCase):
|
|||||||
self,
|
self,
|
||||||
message_id: int,
|
message_id: int,
|
||||||
scheduled_delivery_timestamp: int,
|
scheduled_delivery_timestamp: int,
|
||||||
|
note: str | None = None,
|
||||||
) -> "TestHttpResponse":
|
) -> "TestHttpResponse":
|
||||||
self.login("hamlet")
|
self.login("hamlet")
|
||||||
|
|
||||||
payload = {
|
payload: dict[str, int | str] = {
|
||||||
"message_id": message_id,
|
"message_id": message_id,
|
||||||
"scheduled_delivery_timestamp": scheduled_delivery_timestamp,
|
"scheduled_delivery_timestamp": scheduled_delivery_timestamp,
|
||||||
}
|
}
|
||||||
|
if note is not None:
|
||||||
|
payload["note"] = note
|
||||||
|
|
||||||
result = self.client_post("/json/reminders", payload)
|
result = self.client_post("/json/reminders", payload)
|
||||||
return result
|
return result
|
||||||
@@ -414,3 +417,49 @@ class RemindersTest(ZulipTestCase):
|
|||||||
f"@_**King Hamlet|10** [sent](http://zulip.testserver/#narrow/dm/10,12/near/{reminder.reminder_target_message_id}) a todo list.",
|
f"@_**King Hamlet|10** [sent](http://zulip.testserver/#narrow/dm/10,12/near/{reminder.reminder_target_message_id}) a todo list.",
|
||||||
)
|
)
|
||||||
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
|
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
|
||||||
|
|
||||||
|
def test_notes_in_reminder(self) -> None:
|
||||||
|
content = "Test message with notes"
|
||||||
|
note = "This is a note for the reminder."
|
||||||
|
scheduled_delivery_timestamp = int(time.time() + 86400)
|
||||||
|
|
||||||
|
message_id = self.send_channel_message_for_hamlet(content)
|
||||||
|
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp, note)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
scheduled_message = self.last_scheduled_reminder()
|
||||||
|
self.assertEqual(
|
||||||
|
scheduled_message.content,
|
||||||
|
f"You requested a reminder for #**Verona>test@{message_id}**. Note:\n > {note}\n\n"
|
||||||
|
f"@_**King Hamlet|10** [said](http://zulip.testserver/#narrow/channel/3-Verona/topic/test/near/{message_id}):\n```quote\n{content}\n```",
|
||||||
|
)
|
||||||
|
|
||||||
|
message_id = self.send_dm_from_hamlet_to_othello(content)
|
||||||
|
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp, note)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
scheduled_message = self.last_scheduled_reminder()
|
||||||
|
self.assertEqual(
|
||||||
|
scheduled_message.content,
|
||||||
|
f"You requested a reminder for the following direct message. Note:\n > {note}\n\n"
|
||||||
|
f"@_**King Hamlet|10** [said](http://zulip.testserver/#narrow/dm/10,12/near/{message_id}):\n```quote\n{content}\n```",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with no note
|
||||||
|
message_id = self.send_dm_from_hamlet_to_othello(content)
|
||||||
|
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
scheduled_message = self.last_scheduled_reminder()
|
||||||
|
self.assertEqual(
|
||||||
|
scheduled_message.content,
|
||||||
|
f"You requested a reminder for the following direct message.\n\n"
|
||||||
|
f"@_**King Hamlet|10** [said](http://zulip.testserver/#narrow/dm/10,12/near/{message_id}):\n```quote\n{content}\n```",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with note exceeding maximum length
|
||||||
|
note = "long note"
|
||||||
|
with self.settings(MAX_REMINDER_NOTE_LENGTH=len(note) - 1):
|
||||||
|
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp, note)
|
||||||
|
self.assert_json_error(
|
||||||
|
result,
|
||||||
|
f"Maximum reminder note length: {len(note) - 1} characters",
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ def create_reminders_message_backend(
|
|||||||
*,
|
*,
|
||||||
message_id: Json[int],
|
message_id: Json[int],
|
||||||
scheduled_delivery_timestamp: Json[int],
|
scheduled_delivery_timestamp: Json[int],
|
||||||
|
note: str | None = None,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
deliver_at = timestamp_to_datetime(scheduled_delivery_timestamp)
|
deliver_at = timestamp_to_datetime(scheduled_delivery_timestamp)
|
||||||
if deliver_at <= timezone_now():
|
if deliver_at <= timezone_now():
|
||||||
@@ -32,6 +33,7 @@ def create_reminders_message_backend(
|
|||||||
client,
|
client,
|
||||||
message_id,
|
message_id,
|
||||||
deliver_at,
|
deliver_at,
|
||||||
|
note=note or "",
|
||||||
)
|
)
|
||||||
return json_success(request, data={"reminder_id": reminder_id})
|
return json_success(request, data={"reminder_id": reminder_id})
|
||||||
|
|
||||||
|
|||||||
@@ -653,6 +653,11 @@ OUTGOING_WEBHOOK_TIMEOUT_SECONDS = 10
|
|||||||
# See: `_internal_prep_message` function in zerver/actions/message_send.py.
|
# See: `_internal_prep_message` function in zerver/actions/message_send.py.
|
||||||
MAX_MESSAGE_LENGTH = 10000
|
MAX_MESSAGE_LENGTH = 10000
|
||||||
|
|
||||||
|
# Maximum length of note text for a reminder.
|
||||||
|
# NOTE: Keep it significantly smaller than MAX_MESSAGE_LENGTH
|
||||||
|
# to avoid message being completely truncated when reminder is sent.
|
||||||
|
MAX_REMINDER_NOTE_LENGTH = 1000
|
||||||
|
|
||||||
# The maximum number of drafts to send in the response to /register.
|
# The maximum number of drafts to send in the response to /register.
|
||||||
# More drafts, should they exist for some crazy reason, could be
|
# More drafts, should they exist for some crazy reason, could be
|
||||||
# fetched in a separate request.
|
# fetched in a separate request.
|
||||||
|
|||||||
Reference in New Issue
Block a user