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:
@@ -19,6 +19,8 @@ import { usePluginContext } from "../../plugins/context";
|
||||
import { isPreviewable } from "../thread/previewKind";
|
||||
|
||||
interface InviteSignal { guests: string[]; n: number }
|
||||
/** A day (YYYY-MM-DD) and optionally one event occurrence to show in the calendar. */
|
||||
export interface CalendarFocus { date: string; event?: string; start?: string; n: number }
|
||||
|
||||
// Lazy: the preview bundles pdf.js — loaded only when a preview is opened.
|
||||
const AttachmentPreview = lazy(() =>
|
||||
@@ -66,6 +68,7 @@ export function RightPanel({
|
||||
onClose,
|
||||
thread,
|
||||
single = false,
|
||||
calendarFocus = null,
|
||||
}: {
|
||||
tab: PanelTab;
|
||||
onTab: (t: PanelTab) => void;
|
||||
@@ -74,6 +77,8 @@ export function RightPanel({
|
||||
/** Opened as Calendar or Contacts on its own (the phone's menu): a title
|
||||
* instead of the tabs, and Contacts starts on all contacts. */
|
||||
single?: boolean;
|
||||
/** Calendar: open on this day with this event's details (a reminder). */
|
||||
calendarFocus?: CalendarFocus | null;
|
||||
}) {
|
||||
const t = useT();
|
||||
// "Invite all to new event" (People tab) opens the SHARED calendar event form in
|
||||
@@ -110,7 +115,7 @@ export function RightPanel({
|
||||
</div>
|
||||
{/* Calendar stays mounted (keeps its state) while hidden, so switching tabs is instant. */}
|
||||
<div className={`min-h-0 flex-1 ${tab === "calendar" ? "" : "hidden"}`}>
|
||||
<PanelCalendar invite={invite} />
|
||||
<PanelCalendar invite={invite} focus={calendarFocus} />
|
||||
</div>
|
||||
{tab !== "calendar" && (
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
@@ -134,13 +139,15 @@ export function RightPanel({
|
||||
// This is the shared surface: the agenda, mini-month, event popover, event form
|
||||
// and search results are the calendar app's own code, so future changes propagate.
|
||||
const CAL_BUNDLE = "/api/plugins/calendar/ui/page.js";
|
||||
function PanelCalendar({ invite }: { invite: InviteSignal | null }) {
|
||||
function PanelCalendar({ invite, focus }: { invite: InviteSignal | null; focus: CalendarFocus | null }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const ctx = usePluginContext("calendar");
|
||||
// A phone has no use for "Open calendar in new tab".
|
||||
const mobile = useIsMobile();
|
||||
// Remount when a new "Invite all" fires so the form opens with those guests.
|
||||
// Remount when a new "Invite all" fires so the form opens with those guests,
|
||||
// and when asked to show a day / event (a reminder notification).
|
||||
const inviteN = invite?.n ?? 0;
|
||||
const focusN = focus?.n ?? 0;
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
let cleanup: (() => void) | void;
|
||||
@@ -151,11 +158,12 @@ function PanelCalendar({ invite }: { invite: InviteSignal | null }) {
|
||||
embed: "panel",
|
||||
newEventGuests: invite?.guests,
|
||||
newTabLink: !mobile,
|
||||
focus: focus ? { date: focus.date, event: focus.event, start: focus.start } : null,
|
||||
});
|
||||
});
|
||||
return () => { alive = false; if (typeof cleanup === "function") cleanup(); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ctx, inviteN, mobile]);
|
||||
}, [ctx, inviteN, focusN, mobile]);
|
||||
return <div ref={ref} className="h-full" />;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useEffect, useMemo, useRef } from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
|
||||
import { useT } from "../i18n";
|
||||
import { useIsMobile } from "../lib/useIsMobile";
|
||||
import { loadBundle } from "./bundleLoader";
|
||||
import { usePluginContext } from "./context";
|
||||
import { usePluginManifest, usePluginPages } from "./manifest";
|
||||
@@ -48,11 +49,20 @@ export function PluginPage() {
|
||||
const location = useLocation();
|
||||
const { data, isLoading } = usePluginManifest();
|
||||
const pages = usePluginPages();
|
||||
const mobile = useIsMobile();
|
||||
|
||||
// Sub-path under "/p" (e.g. "/calendar/2026-07"); match the longest page route.
|
||||
const subpath = location.pathname.replace(/^\/p/, "") || "/";
|
||||
const match = pages.find((p) => subpath === p.route || subpath.startsWith(`${p.route}/`));
|
||||
|
||||
// The calendar page is made for a big screen: a phone gets Mail with the
|
||||
// Calendar panel slid in instead (a reminder's link included, with its day and
|
||||
// event).
|
||||
if (mobile && subpath.startsWith("/calendar")) {
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.set("panel", "calendar");
|
||||
return <Navigate to={`/mail?${params.toString()}`} replace />;
|
||||
}
|
||||
if (isLoading || !data) {
|
||||
return <div className="p-8 text-app-muted">{t("common.loading")}</div>;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import { BackfillBanner } from "../components/BackfillBanner";
|
||||
import { Clock, NotSavedOffline, OfflineIndicator } from "../components/Offline";
|
||||
import { ThreadView } from "../components/thread/ThreadView";
|
||||
import { MailboxDisplay } from "../components/mail/MailboxDisplay";
|
||||
import { RightPanel, type PanelTab } from "../components/mail/RightPanel";
|
||||
import { RightPanel, type CalendarFocus, type PanelTab } from "../components/mail/RightPanel";
|
||||
import { ScheduledSendPane } from "../components/mail/ScheduledSendPane";
|
||||
import { useT } from "../i18n";
|
||||
import type { MessageKey } from "../i18n/en";
|
||||
@@ -873,6 +873,27 @@ export function MailLayout() {
|
||||
// Deep link (e.g. Contacts "Compose"): /mail?compose=1&to=<email> opens a new,
|
||||
// maximized composer prefilled with the recipient, then strips the params.
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// ?panel=calendar[&date=&event=&start=]: the phone's way to the calendar (the
|
||||
// calendar page sends it here; a reminder's link carries its day and event).
|
||||
const [calendarFocus, setCalendarFocus] = useState<CalendarFocus | null>(null);
|
||||
useEffect(() => {
|
||||
if (searchParams.get("panel") !== "calendar") return;
|
||||
const date = searchParams.get("date");
|
||||
setCalendarFocus(
|
||||
date
|
||||
? {
|
||||
date,
|
||||
event: searchParams.get("event") ?? undefined,
|
||||
start: searchParams.get("start") ?? undefined,
|
||||
n: Date.now(),
|
||||
}
|
||||
: null,
|
||||
);
|
||||
if (mobile) openFromMenu("calendar");
|
||||
else openPanel("calendar");
|
||||
setSearchParams({}, { replace: true });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams]);
|
||||
useEffect(() => {
|
||||
if (searchParams.get("compose") !== "1") return;
|
||||
const first = accountsQuery.data?.[0];
|
||||
@@ -1713,6 +1734,7 @@ export function MailLayout() {
|
||||
onClose={() => setSheetOpen(false)}
|
||||
thread={detailQuery.data}
|
||||
single={sheetSingle}
|
||||
calendarFocus={calendarFocus}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -1721,7 +1743,13 @@ export function MailLayout() {
|
||||
<>
|
||||
<Resizer onDelta={(dx) => setRightW((w) => clampRight(w - dx))} />
|
||||
<div style={{ width: rightW }} className="shrink-0 border-l border-app-border">
|
||||
<RightPanel tab={rightTab} onTab={setRightTab} onClose={() => setRightOpen(false)} thread={detailQuery.data} />
|
||||
<RightPanel
|
||||
tab={rightTab}
|
||||
onTab={setRightTab}
|
||||
onClose={() => setRightOpen(false)}
|
||||
thread={detailQuery.data}
|
||||
calendarFocus={calendarFocus}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user