diff --git a/app/(main)/[locale]/page.tsx b/app/(main)/[locale]/page.tsx index 5f03ea1d7..ad703c93e 100644 --- a/app/(main)/[locale]/page.tsx +++ b/app/(main)/[locale]/page.tsx @@ -73,7 +73,7 @@ import { usePluginStore } from "@/stores/plugin-store"; import { AppTopBannerSlot } from "@/components/plugins/app-top-banner-slot"; import { useThemeStore } from "@/stores/theme-store"; import { consumePendingMailto, subscribeToPendingMailto } from "@/lib/protocol-handlers/session"; -import type { ParsedMailto } from "@/lib/protocol-handlers/mailto"; +import { INTERNAL_MAILTO_EVENT, type ParsedMailto } from "@/lib/protocol-handlers/mailto"; import { plainTextToComposerBody, getQuoteBodies } from "@/lib/email-composer-utils"; import { appLifecycleHooks, uiHooks, routerHooks, toastHooks, emailHooks } from "@/lib/plugin-hooks"; import { emailToReadView } from "@/lib/plugin-projection"; @@ -958,6 +958,25 @@ export default function Home() { return subscribeToPendingMailto(openPendingMailto); }, [isAuthenticated, client, handleMailtoProtocolRequest]); + // Address links in the app's own chrome (contact cards, sidebars, contact + // details) compose in place. Unlike a request arriving from the OS handler, + // an in-app click carries its own context - the account being read - so this + // goes straight to the composer instead of through the account picker. + // Cancelling the event tells the link its click was taken. + useEffect(() => { + if (!isAuthenticated || !client) return; + + const onInternalMailto = (event: Event) => { + const pending = (event as CustomEvent).detail; + if (!pending) return; + event.preventDefault(); + openMailtoDraft(pending); + }; + + window.addEventListener(INTERNAL_MAILTO_EVENT, onInternalMailto); + return () => window.removeEventListener(INTERNAL_MAILTO_EVENT, onInternalMailto); + }, [isAuthenticated, client, openMailtoDraft]); + // Fallback fetch for paths that didn't go through login()'s prefetch // (notably checkAuth on page refresh). Settings pages can also prefill // mailboxes without emails, so bootstrap emails once per client when the diff --git a/components/contacts/contact-detail.tsx b/components/contacts/contact-detail.tsx index 1b07d86e2..b520c23c3 100644 --- a/components/contacts/contact-detail.tsx +++ b/components/contacts/contact-detail.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react"; import { useTranslations } from "next-intl"; import { Mail, Phone, Building, MapPin, StickyNote, Pencil, Trash2, BookUser, Copy, Send, Globe, Cake, KeyRound, Users, Briefcase, Heart, Languages, Calendar, UserCircle, Download, MoreHorizontal, Printer } from "lucide-react"; import { Avatar } from "@/components/ui/avatar"; +import { MailtoLink } from "@/components/ui/mailto-link"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import type { ContactCard, AnniversaryDate, PartialDate } from "@/lib/jmap/types"; @@ -232,18 +233,18 @@ export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDupli {emails.map((e, i) => (
- + {e.address} - + - - +
diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index bbdc0a76b..4b3c3d1c7 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -97,6 +97,7 @@ import { useTour } from "@/components/tour/tour-provider"; import { useIsEmbedded } from "@/hooks/use-is-embedded"; import { findCalendarAttachment, isCalendarMimeType } from "@/lib/calendar-invitation"; import { RecipientPopover } from "./recipient-popover"; +import { MailtoLink } from "@/components/ui/mailto-link"; import { isFilePreviewable, isMimeTypeSafeForInlinePreview } from "@/lib/file-preview"; import { parseTnef, isTnefAttachment } from "@/lib/tnef"; import { debug } from "@/lib/debug"; @@ -419,14 +420,14 @@ export function ContactSidebarPanel({ {/* Quick actions */}
- {t('contact_sidebar.action_email')} - + - Email - + {onViewContact && (