Calendar reminders open the event; phones get the Calendar panel
A reminder notification links to its day, event and occurrence (/p/calendar?date=…&event=…&start=…). The calendar opens on that day with the event's details showing — the full calendar on a computer; on a phone any calendar link leads to Mail with the Calendar panel slid in (the big-screen page never opens there), scrolled to the event, details open. The panel's agenda kept no scroll position across re-renders: opening an event's details lower down jumped the list back to the top. It stays put now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import uuid
|
||||
from collections import defaultdict
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from sqlalchemy import text
|
||||
@@ -94,7 +95,10 @@ async def _notify_user(
|
||||
user_id,
|
||||
title=title,
|
||||
body=body,
|
||||
url=f"/p/calendar?date={_day(start, inst['all_day'], tz)}",
|
||||
url=(
|
||||
f"/p/calendar?date={_day(start, inst['all_day'], tz)}"
|
||||
f"&event={inst['id']}&start={quote(inst['start'])}"
|
||||
),
|
||||
# One notification per occurrence: a later reminder replaces an earlier one.
|
||||
tag=f"event-{inst['id']}-{inst['start']}",
|
||||
sticky=True,
|
||||
|
||||
@@ -456,6 +456,9 @@ export default function mount(el, ctx, props) {
|
||||
// any change here propagates to both places.
|
||||
const embed = !!(props && props.embed === "panel");
|
||||
const newEventGuests = (props && props.newEventGuests) || null;
|
||||
// Open on a day, with one event's details showing (a reminder notification):
|
||||
// { date: "YYYY-MM-DD", event: id, start: ISO of the occurrence }.
|
||||
const focus = (props && props.focus) || null;
|
||||
// Embed: the host can drop the "Open calendar in new tab" footer (Mail on a
|
||||
// phone, where a new tab is no use).
|
||||
const newTabLink = !(props && props.newTabLink === false);
|
||||
@@ -1095,6 +1098,10 @@ export default function mount(el, ctx, props) {
|
||||
// Compact single-column layout for Mail's right panel. Reuses miniMonth/agenda/
|
||||
// searchPanelHtml + the form/popover/confirm overlays exactly as the full app.
|
||||
function renderEmbed() {
|
||||
// Re-rendering replaces the agenda: keep where it was scrolled to (else
|
||||
// opening an event's details lower down jumped the list back to the top).
|
||||
const prevBody = el.querySelector(".cal-embed-body");
|
||||
const prevScroll = prevBody ? prevBody.scrollTop : 0;
|
||||
el.innerHTML = `<div class="cal-app cal-embed">
|
||||
${panelBar()}
|
||||
${state.search ? "" : `<div class="cal-embed-mini">${miniMonth()}</div>`}
|
||||
@@ -1107,6 +1114,8 @@ export default function mount(el, ctx, props) {
|
||||
${state.confirm ? confirmHtml() : ""}
|
||||
</div>`;
|
||||
wire();
|
||||
const body0 = el.querySelector(".cal-embed-body");
|
||||
if (body0 && prevScroll && !state.scrollToAnchor) body0.scrollTop = prevScroll;
|
||||
if (state.scrollToAnchor) {
|
||||
state.scrollToAnchor = false;
|
||||
const a = state.anchor;
|
||||
@@ -1850,20 +1859,46 @@ export default function mount(el, ctx, props) {
|
||||
(async () => {
|
||||
await loadChrome();
|
||||
await loadCalendars();
|
||||
// Deep link (a reminder notification): /p/calendar?date=YYYY-MM-DD opens that
|
||||
// day, then strips the param so refresh is clean.
|
||||
// Deep link (a reminder notification): /p/calendar?date=YYYY-MM-DD[&event=id&start=iso]
|
||||
// opens that day with the event's details; in the panel, the same comes as
|
||||
// props.focus. The params are stripped so a refresh is clean.
|
||||
let target = focus;
|
||||
try {
|
||||
const day = new URLSearchParams(window.location.search).get("date");
|
||||
if (!state.embed && day && /^\d{4}-\d{2}-\d{2}$/.test(day)) {
|
||||
const [y, m, d] = day.split("-").map(Number);
|
||||
state.anchor = startOfDay(new Date(y, m - 1, d));
|
||||
state.mini = startOfMonth(state.anchor);
|
||||
state.view = "day";
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (!state.embed && params.get("date")) {
|
||||
target = { date: params.get("date"), event: params.get("event"), start: params.get("start") };
|
||||
window.history.replaceState({}, "", window.location.pathname);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
if (target && /^\d{4}-\d{2}-\d{2}$/.test(target.date || "")) {
|
||||
const [y, m, d] = target.date.split("-").map(Number);
|
||||
state.anchor = startOfDay(new Date(y, m - 1, d));
|
||||
state.mini = startOfMonth(state.anchor);
|
||||
if (state.embed) state.scrollToAnchor = true;
|
||||
else state.view = "day";
|
||||
}
|
||||
await loadEvents();
|
||||
render();
|
||||
if (target && target.event) {
|
||||
const ev = state.events.find((x) => x.id === target.event && (!target.start || x.start === target.start))
|
||||
|| state.events.find((x) => x.id === target.event);
|
||||
if (ev) {
|
||||
// In the panel, wait for it to finish sliding in (a phone's sheet), then
|
||||
// bring the event into view so the details sit beside it.
|
||||
await new Promise((r) => setTimeout(r, state.embed ? 400 : 50));
|
||||
const block = el.querySelector(`[data-ev="${ev.id}"][data-start="${ev.start}"]`);
|
||||
if (block) {
|
||||
block.scrollIntoView({ block: "center" });
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
openEvent(ev, block);
|
||||
} else {
|
||||
const view = el.querySelector(".cal-view") || el;
|
||||
const r = view.getBoundingClientRect();
|
||||
state.eventPopover = { ev, rect: { top: r.top + 24, left: r.left + 16, right: r.left + 60, bottom: r.top + 44 } };
|
||||
render();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Embed mode ("Invite all to new event" from Mail's People tab): open a new-event
|
||||
// form with the given guests, using the SAME form as the calendar app.
|
||||
if (state.embed && newEventGuests && newEventGuests.length) {
|
||||
|
||||
Reference in New Issue
Block a user