From 64d83f888c6bfee727497a32cb4a3efec6c3bab4 Mon Sep 17 00:00:00 2001 From: Rachel Moore Date: Thu, 20 Aug 2026 10:11:43 -0400 Subject: [PATCH] Blockquote the Weekly Summary event list; drop dead links on removed meetings Wraps the whole entry list in a single Slack blockquote instead of a bare list, and strips calendarLink from a removed meeting's struck-through line since the calendar event it pointed to no longer exists. --- src/domain/weeklySummary.ts | 25 +++++++++++++++++++--- test/weeklySummary.test.ts | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/domain/weeklySummary.ts b/src/domain/weeklySummary.ts index 0480fdc..ae0956b 100644 --- a/src/domain/weeklySummary.ts +++ b/src/domain/weeklySummary.ts @@ -190,9 +190,13 @@ export function renderEditedLine(args: { ].join("\n"); } -/** A removed Event: struck through and labeled, staying visible rather than deleted from the listing. */ +/** + * A removed Event: struck through and labeled, staying visible rather than + * deleted from the listing. Never linked — the calendar event itself is + * gone, so its `calendarLink` (if any) would just 404. + */ export function renderRemovedLine(snapshot: WeeklySummaryEventInfo): string { - return `~${formatWeeklySummaryLine(snapshot)}~ _(removed)_`; + return `~${formatWeeklySummaryLine({ ...snapshot, calendarLink: null })}~ _(removed)_`; } /** An Event discovered after the summary's initial post — tagged, not struck through (no "original" to contrast). */ @@ -228,6 +232,18 @@ export type WeeklySummaryLineEntry = { text: string; }; +/** + * Slack mrkdwn blockquotes are per-line (`> `), so a multi-line entry (an + * edited line's struck-through/current pair) needs the marker on each of + * its lines to stay inside the same quote. + */ +function blockquoteLines(text: string): string { + return text + .split("\n") + .map((line) => `> ${line}`) + .join("\n"); +} + /** * The standard Google Calendar "add this calendar" URL for a calendar id, or * `undefined` when there's no id (a calendar setting left unconfigured) — @@ -278,5 +294,8 @@ export function assembleWeeklySummaryMessage(args: { const sorted = [...args.entries].sort( (a, b) => a.sortKey.getTime() - b.sortKey.getTime() ); - return [header, ...sorted.map((e) => e.text), ...footer].join("\n\n"); + // Joined with a blank *quoted* line (`>`), not `\n\n`, so entries stay one + // continuous blockquote instead of splitting into a separate quote box per entry. + const list = sorted.map((e) => blockquoteLines(e.text)).join("\n>\n"); + return [header, list, ...footer].join("\n\n"); } diff --git a/test/weeklySummary.test.ts b/test/weeklySummary.test.ts index c56f03d..17291f3 100644 --- a/test/weeklySummary.test.ts +++ b/test/weeklySummary.test.ts @@ -212,6 +212,16 @@ test("a removed line strikes through the whole line and labels it removed", () = assert.match(line, /\(removed\)/); }); +test("a removed line never links the title, even if the snapshot had a calendar link", () => { + const linked: WeeklySummaryEventInfo = { + ...hourlyInfo, + calendarLink: "https://calendar.google.com/event?eid=abc123", + }; + const line = renderRemovedLine(linked); + assert.doesNotMatch(line, / { const line = renderNewLine(hourlyInfo); assert.match(line, /^🆕 _New_/); @@ -320,6 +330,37 @@ test("a subscribe link appends a footer line to the assembled message", () => { ); }); +test("the entry list is rendered as a single blockquote, not a bare list", () => { + const message = assembleWeeklySummaryMessage({ + weekStart: new Date("2026-01-05T00:00:00Z"), + weekEnd: new Date("2026-01-12T00:00:00Z"), + entries: [ + { sortKey: new Date("2026-01-06T00:00:00Z"), text: "First event" }, + { sortKey: new Date("2026-01-10T00:00:00Z"), text: "Second event" }, + ], + }); + assert.match(message, /> First event\n>\n> Second event/); + // The header stays outside the quote. + assert.match(message, /^\*This Week\*/); +}); + +test("a multi-line entry (an edited line) gets the quote marker on every line", () => { + const edited = { ...hourlyInfo, location: "Room 310" }; + const line = renderEditedLine({ + snapshot: hourlyInfo, + current: edited, + changedFields: ["location"], + }); + const message = assembleWeeklySummaryMessage({ + weekStart: new Date("2026-01-05T00:00:00Z"), + weekEnd: new Date("2026-01-12T00:00:00Z"), + entries: [{ sortKey: hourlyInfo.startsAt, text: line }], + }); + for (const l of line.split("\n")) { + assert.ok(message.includes(`> ${l}`)); + } +}); + test("no subscribe link means no footer line", () => { const message = assembleWeeklySummaryMessage({ weekStart: new Date("2026-01-05T00:00:00Z"),