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

View File

@@ -1,17 +1,33 @@
import $ from "jquery";
import type * as z from "zod/mini";
import render_message_reminders from "../templates/message_reminders.hbs";
import * as channel from "./channel.ts";
import * as feedback_widget from "./feedback_widget.ts";
import {$t} from "./i18n.ts";
import * as message_lists from "./message_lists.ts";
import type {StateData, reminder_schema} from "./state_data.ts";
import * as timerender from "./timerender.ts";
import * as ui_report from "./ui_report.ts";
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_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 {
channel.post({
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 {
const message_ids_to_rerender = new Set<number>();
for (const reminder of reminders) {
message_ids_to_rerender.add(reminder.reminder_target_message_id);
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 {
if (reminders_by_id.has(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 {
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 type {RawLocalMessage} from "./echo.ts";
import type {NewMessage, ProcessedMessage} from "./message_helper.ts";
import type {TimeFormattedReminder} from "./message_reminder.ts";
import * as people from "./people.ts";
import {topic_link_schema} from "./types.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
// been sent for this message.
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";

View File

@@ -100,7 +100,7 @@
ensures the timestamp will always have enough space
in the column. */
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
8px minmax(var(--message-box-timestamp-column-width), max-content);
/* Named grid areas provide flexibility for positioning grid items
@@ -109,7 +109,16 @@
"edited message controls . time"
". message . . . "
". more . . . "
". reactions . . . ";
". reactions . . . "
". reminders . . . ";
&:has(.message-reminders) {
.message_reactions {
margin-bottom: calc(
var(--message-box-markdown-aligned-vertical-space) / 2
);
}
}
&.content_edit_mode {
cursor: default;
@@ -203,6 +212,16 @@
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 {
grid-area: message;
/* Align self to start, rather than baseline, so the baseline
@@ -274,7 +293,8 @@
"avatar sender controls . time"
"avatar message . . . "
". more . . . "
". reactions . . . ";
". reactions . . . "
". reminders . . . ";
.message_edit {
/* No top margin when there's a sender row */
@@ -287,12 +307,13 @@
"avatar sender controls . time"
"avatar sender . . . "
". more . . . "
". reactions . . . ";
". reactions . . . "
". reminders . . . ";
grid-template-rows:
var(--message-box-vertical-margin) var(
--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,
and unset the align-content property. */
align-items: baseline;

View File

@@ -74,3 +74,7 @@
{{#if (and (not is_hidden) msg.message_reactions)}}
{{> message_reactions . }}
{{/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>