diff --git a/web/src/message_list_view.ts b/web/src/message_list_view.ts index 4dd4634197..3ef0fb0a4b 100644 --- a/web/src/message_list_view.ts +++ b/web/src/message_list_view.ts @@ -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, diff --git a/web/src/message_reminder.ts b/web/src/message_reminder.ts index 1fef6e4968..7afdc84bfa 100644 --- a/web/src/message_reminder.ts +++ b/web/src/message_reminder.ts @@ -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; +// 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(); +export const reminders_by_message_id = new Map(); + +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(); 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)); + } + }); +} diff --git a/web/src/message_store.ts b/web/src/message_store.ts index bc06d4c9a5..54a7086bec 100644 --- a/web/src/message_store.ts +++ b/web/src/message_store.ts @@ -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"; diff --git a/web/styles/message_row.css b/web/styles/message_row.css index f761387bfd..ab0e223033 100644 --- a/web/styles/message_row.css +++ b/web/styles/message_row.css @@ -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; diff --git a/web/templates/message_body.hbs b/web/templates/message_body.hbs index 336f1b0c00..7f49cace1d 100644 --- a/web/templates/message_body.hbs +++ b/web/templates/message_body.hbs @@ -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}} diff --git a/web/templates/message_reminders.hbs b/web/templates/message_reminders.hbs new file mode 100644 index 0000000000..0bdc978169 --- /dev/null +++ b/web/templates/message_reminders.hbs @@ -0,0 +1,14 @@ +
+ {{#each this/msg/reminders}} +

+ {{#tr}} + Reminder scheduled for {formatted_delivery_time}. + {{#*inline "z-link"~}} + + {{> @partial-block}} + + {{~/inline}} + {{/tr}} +

+ {{/each}} +