Files
webmail/components/calendar/event-context-menu.tsx
T
lucletoffeandClaude Opus 5.5 12410124ad fix(calendar): surface the meeting link and open the location in maps
Invitations - Teams above all - leave virtualLocations empty: LOCATION reads
"Microsoft Teams Meeting" and the join URL sits in the description between
help and dial-in links, so the popover showed no meeting link and "Copy
meeting link" was not offered. Find the join URL of known conferencing
services (Teams, Zoom, Meet, Webex, Jitsi, Whereby, GoTo) in the location
or description, unwrapping Outlook Safe Links. Nothing is written to the
event.

The location was plain text unless it was a URL. It is now always a link:
a URL opens as is, a location that only names the service joins the
meeting, anything else opens a Google Maps search (the Maps app on a
phone). A copy button sits next to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 09:52:56 +02:00

112 lines
2.8 KiB
TypeScript

"use client";
import { useTranslations } from "next-intl";
import {
ContextMenu,
ContextMenuItem,
ContextMenuSeparator,
} from "@/components/ui/context-menu";
import {
Pencil,
Copy,
Download,
ClipboardCopy,
Link as LinkIcon,
CalendarArrowUp as CalendarLinkIcon,
Trash2,
} from "@/components/icons";
import type { CalendarEvent } from "@/lib/jmap/types";
import { buildCalendarPath } from "@/lib/deep-links";
import { useCopyLink } from "@/hooks/use-copy-link";
import { findMeetingLink } from "@/lib/event-links";
interface Position {
x: number;
y: number;
}
interface EventContextMenuProps {
event: CalendarEvent;
position: Position;
isOpen: boolean;
onClose: () => void;
menuRef: React.RefObject<HTMLDivElement | null>;
onEdit: () => void;
onDuplicate: () => void;
onExportICS: () => void;
onCopyTitle: () => void;
onCopyMeetingLink?: () => void;
onDelete: () => void;
}
export function EventContextMenu({
event,
position,
isOpen,
onClose,
menuRef,
onEdit,
onDuplicate,
onExportICS,
onCopyTitle,
onCopyMeetingLink,
onDelete,
}: EventContextMenuProps) {
const t = useTranslations("calendar");
const tDeepLink = useTranslations("deep_link");
const copyLink = useCopyLink();
const handle = (fn: () => void) => () => {
fn();
onClose();
};
const hasMeetingLink = !!findMeetingLink(event);
return (
<ContextMenu ref={menuRef} isOpen={isOpen} position={position} onClose={onClose}>
<ContextMenuItem icon={Pencil} label={t("events.edit")} onClick={handle(onEdit)} />
<ContextMenuItem icon={Copy} label={t("events.duplicate")} onClick={handle(onDuplicate)} />
<ContextMenuSeparator />
<ContextMenuItem
icon={Download}
label={t("events.export_ics")}
onClick={handle(onExportICS)}
/>
<ContextMenuItem
icon={ClipboardCopy}
label={t("events.copy_title")}
onClick={handle(onCopyTitle)}
/>
{hasMeetingLink && onCopyMeetingLink && (
<ContextMenuItem
icon={LinkIcon}
label={t("events.copy_link")}
onClick={handle(onCopyMeetingLink)}
/>
)}
{/* Permalink into Bulwark (#733) - distinct from the meeting URL above,
which points at whatever conferencing tool the organiser used. */}
<ContextMenuItem
icon={CalendarLinkIcon}
label={tDeepLink("copy_event")}
onClick={handle(() => {
void copyLink(buildCalendarPath({
view: "month",
date: null,
eventId: event.id,
accountId: event.accountId,
}));
})}
/>
<ContextMenuSeparator />
<ContextMenuItem
icon={Trash2}
label={t("events.delete")}
onClick={handle(onDelete)}
destructive
/>
</ContextMenu>
);
}