reminder: Show scheduled reminder info on render message.

This commit is contained in:
Aman Agrawal
2025-09-19 13:42:16 +05:30
committed by Tim Abbott
parent c6d720f532
commit afa38e8db9
6 changed files with 141 additions and 5 deletions

View File

@@ -22,6 +22,7 @@ import * as message_edit from "./message_edit.ts";
import type {MessageList} from "./message_list.ts"; import type {MessageList} from "./message_list.ts";
import * as message_list_tooltips from "./message_list_tooltips.ts"; import * as message_list_tooltips from "./message_list_tooltips.ts";
import * as message_lists from "./message_lists.ts"; import * as message_lists from "./message_lists.ts";
import * as message_reminder from "./message_reminder.ts";
import * as message_store from "./message_store.ts"; import * as message_store from "./message_store.ts";
import type {Message} from "./message_store.ts"; import type {Message} from "./message_store.ts";
import * as message_viewport from "./message_viewport.ts"; import * as message_viewport from "./message_viewport.ts";
@@ -777,6 +778,7 @@ export class MessageListView {
for (const message of messages) { for (const message of messages) {
const message_reactions = reactions.get_message_reactions(message); const message_reactions = reactions.get_message_reactions(message);
message.message_reactions = message_reactions; message.message_reactions = message_reactions;
message.reminders = message_reminder.get_reminders(message.id);
// These will be used to build the message container // These will be used to build the message container
let include_recipient = false; let include_recipient = false;
@@ -1077,6 +1079,7 @@ export class MessageListView {
_get_message_template(message_container: MessageContainer): string { _get_message_template(message_container: MessageContainer): string {
const msg_reactions = reactions.get_message_reactions(message_container.msg); const msg_reactions = reactions.get_message_reactions(message_container.msg);
message_container.msg.message_reactions = msg_reactions; message_container.msg.message_reactions = msg_reactions;
message_container.msg.reminders = message_reminder.get_reminders(message_container.msg.id);
const msg_to_render = { const msg_to_render = {
...message_container, ...message_container,
message_list_id: this.list.id, message_list_id: this.list.id,

View File

@@ -1,17 +1,33 @@
import $ from "jquery"; import $ from "jquery";
import type * as z from "zod/mini"; import type * as z from "zod/mini";
import render_message_reminders from "../templates/message_reminders.hbs";
import * as channel from "./channel.ts"; import * as channel from "./channel.ts";
import * as feedback_widget from "./feedback_widget.ts"; import * as feedback_widget from "./feedback_widget.ts";
import {$t} from "./i18n.ts"; import {$t} from "./i18n.ts";
import * as message_lists from "./message_lists.ts";
import type {StateData, reminder_schema} from "./state_data.ts"; import type {StateData, reminder_schema} from "./state_data.ts";
import * as timerender from "./timerender.ts"; import * as timerender from "./timerender.ts";
import * as ui_report from "./ui_report.ts"; import * as ui_report from "./ui_report.ts";
export type Reminder = z.infer<typeof reminder_schema>; export type Reminder = z.infer<typeof reminder_schema>;
// Used to render reminders in message list.
export type TimeFormattedReminder = {
reminder_id: number;
formatted_delivery_time: string;
scheduled_delivery_timestamp: number;
};
export const reminders_by_id = new Map<number, Reminder>(); export const reminders_by_id = new Map<number, Reminder>();
export const reminders_by_message_id = new Map<number, TimeFormattedReminder[]>();
export function get_reminders(message_id: number): TimeFormattedReminder[] | undefined {
return reminders_by_message_id.get(message_id);
}
export function set_message_reminder(send_at_time: number, message_id: number, note: string): 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",
@@ -48,8 +64,38 @@ export function set_message_reminder(send_at_time: number, message_id: number, n
} }
export function add_reminders(reminders: Reminder[]): void { export function add_reminders(reminders: Reminder[]): void {
const message_ids_to_rerender = new Set<number>();
for (const reminder of reminders) { for (const reminder of reminders) {
message_ids_to_rerender.add(reminder.reminder_target_message_id);
reminders_by_id.set(reminder.reminder_id, reminder); reminders_by_id.set(reminder.reminder_id, reminder);
// Do all the formatting and sorting needed to display
// reminders for a message to avoid doing at the time of render.
const formatted_delivery_time = timerender.get_full_datetime(
new Date(reminder.scheduled_delivery_timestamp * 1000),
"time",
);
const time_formatted_reminder: TimeFormattedReminder = {
reminder_id: reminder.reminder_id,
formatted_delivery_time,
scheduled_delivery_timestamp: reminder.scheduled_delivery_timestamp,
};
if (!reminders_by_message_id.has(reminder.reminder_target_message_id)) {
reminders_by_message_id.set(reminder.reminder_target_message_id, [
time_formatted_reminder,
]);
continue;
}
const message_reminders = get_reminders(reminder.reminder_target_message_id)!;
message_reminders.push(time_formatted_reminder);
// Sort reminders to show the earliest one first.
message_reminders.sort(
(a, b) => a.scheduled_delivery_timestamp - b.scheduled_delivery_timestamp,
);
}
for (const message_id of message_ids_to_rerender) {
rerender_reminders_for_message(message_id);
} }
} }
@@ -60,6 +106,18 @@ export function initialize(reminders_params: StateData["reminders"]): void {
export function remove_reminder(reminder_id: number): void { export function remove_reminder(reminder_id: number): void {
if (reminders_by_id.has(reminder_id)) { if (reminders_by_id.has(reminder_id)) {
reminders_by_id.delete(reminder_id); reminders_by_id.delete(reminder_id);
for (const [message_id, message_reminders] of reminders_by_message_id) {
const index = message_reminders.findIndex((r) => r.reminder_id === reminder_id);
if (index !== -1) {
message_reminders.splice(index, 1);
if (message_reminders.length === 0) {
reminders_by_message_id.delete(message_id);
}
rerender_reminders_for_message(message_id);
break;
}
}
} }
} }
@@ -73,3 +131,34 @@ export function delete_reminder(reminder_id: number, success?: () => void): void
export function get_count(): number { export function get_count(): number {
return reminders_by_id.size; return reminders_by_id.size;
} }
export function rerender_reminders_for_message(message_id: number): void {
const $rows = message_lists.all_rendered_row_for_message_id(message_id);
if ($rows.length === 0) {
return;
}
const message_reminders = get_reminders(message_id) ?? [];
if (message_reminders.length === 0) {
$rows.find(".message-reminders").remove();
return;
}
const rendered_message_reminders_html = render_message_reminders({
msg: {
reminders: message_reminders,
},
});
$rows.each(function () {
const $row = $(this);
const $existing = $row.find(".message-reminders");
if ($existing.length > 0) {
$existing.replaceWith($(rendered_message_reminders_html));
} else {
// Insert after reactions if they exist, otherwise after "more" section.
const $content = $row.find(".messagebox-content");
$content.append($(rendered_message_reminders_html));
}
});
}

View File

@@ -4,6 +4,7 @@ import * as z from "zod/mini";
import * as blueslip from "./blueslip.ts"; import * as blueslip from "./blueslip.ts";
import type {RawLocalMessage} from "./echo.ts"; import type {RawLocalMessage} from "./echo.ts";
import type {NewMessage, ProcessedMessage} from "./message_helper.ts"; import type {NewMessage, ProcessedMessage} from "./message_helper.ts";
import type {TimeFormattedReminder} from "./message_reminder.ts";
import * as people from "./people.ts"; import * as people from "./people.ts";
import {topic_link_schema} from "./types.ts"; import {topic_link_schema} from "./types.ts";
import type {UserStatusEmojiInfo} from "./user_status.ts"; import type {UserStatusEmojiInfo} from "./user_status.ts";
@@ -197,6 +198,10 @@ export type Message = (
// Used in message_notifications to track if a notification has already // Used in message_notifications to track if a notification has already
// been sent for this message. // been sent for this message.
notification_sent?: boolean; notification_sent?: boolean;
// Added during message rendering in message_list_view.ts. Should
// never be accessed outside rendering, as the value may be stale.
reminders?: TimeFormattedReminder[] | undefined;
} & ( } & (
| { | {
type: "private"; type: "private";

View File

@@ -100,7 +100,7 @@
ensures the timestamp will always have enough space ensures the timestamp will always have enough space
in the column. */ in the column. */
grid-template: grid-template:
minmax(var(--message-box-sender-line-height), auto) repeat(3, auto) / minmax(var(--message-box-sender-line-height), auto) repeat(4, auto) /
var(--message-box-avatar-column-width) minmax(0, 1fr) max-content var(--message-box-avatar-column-width) minmax(0, 1fr) max-content
8px minmax(var(--message-box-timestamp-column-width), max-content); 8px minmax(var(--message-box-timestamp-column-width), max-content);
/* Named grid areas provide flexibility for positioning grid items /* Named grid areas provide flexibility for positioning grid items
@@ -109,7 +109,16 @@
"edited message controls . time" "edited message controls . time"
". message . . . " ". message . . . "
". more . . . " ". more . . . "
". reactions . . . "; ". reactions . . . "
". reminders . . . ";
&:has(.message-reminders) {
.message_reactions {
margin-bottom: calc(
var(--message-box-markdown-aligned-vertical-space) / 2
);
}
}
&.content_edit_mode { &.content_edit_mode {
cursor: default; cursor: default;
@@ -203,6 +212,16 @@
gap: 4px; gap: 4px;
} }
.message-reminders {
grid-area: reminders;
font-style: italic;
color: hsl(0deg 0% 53%);
.message-reminder {
margin-bottom: 0.1875em; /* 3px at 16px/1em */
}
}
.message_edit { .message_edit {
grid-area: message; grid-area: message;
/* Align self to start, rather than baseline, so the baseline /* Align self to start, rather than baseline, so the baseline
@@ -274,7 +293,8 @@
"avatar sender controls . time" "avatar sender controls . time"
"avatar message . . . " "avatar message . . . "
". more . . . " ". more . . . "
". reactions . . . "; ". reactions . . . "
". reminders . . . ";
.message_edit { .message_edit {
/* No top margin when there's a sender row */ /* No top margin when there's a sender row */
@@ -287,12 +307,13 @@
"avatar sender controls . time" "avatar sender controls . time"
"avatar sender . . . " "avatar sender . . . "
". more . . . " ". more . . . "
". reactions . . . "; ". reactions . . . "
". reminders . . . ";
grid-template-rows: grid-template-rows:
var(--message-box-vertical-margin) var( var(--message-box-vertical-margin) var(
--message-box-avatar-height --message-box-avatar-height
) )
minmax(0, auto) repeat(3, auto); minmax(0, auto) repeat(4, auto);
/* We align items to the baseline on me messages, /* We align items to the baseline on me messages,
and unset the align-content property. */ and unset the align-content property. */
align-items: baseline; align-items: baseline;

View File

@@ -74,3 +74,7 @@
{{#if (and (not is_hidden) msg.message_reactions)}} {{#if (and (not is_hidden) msg.message_reactions)}}
{{> message_reactions . }} {{> message_reactions . }}
{{/if}} {{/if}}
{{#if (and (not is_hidden) msg.reminders)}}
{{> message_reminders . }}
{{/if}}

View File

@@ -0,0 +1,14 @@
<div class="message-reminders">
{{#each this/msg/reminders}}
<p class="message-reminder">
{{#tr}}
<z-link>Reminder</z-link> scheduled for {formatted_delivery_time}.
{{#*inline "z-link"~}}
<a href="#reminders" class="message-reminder-overlay-link" data-reminder-id="{{ this/reminder_id }}">
{{> @partial-block}}
</a>
{{~/inline}}
{{/tr}}
</p>
{{/each}}
</div>