feat: expose PWA, app identity, and extension directory keys in JSON config #312
This commit is contained in:
@@ -541,7 +541,7 @@ export function ContactsSidebar({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Shared accounts with address books — only when not already split
|
||||
{/* Shared accounts with address books - only when not already split
|
||||
into per-account groups above (multi-account Pro mode). */}
|
||||
{!multiAccountMode && sharedBookGroups.map((group) => (
|
||||
<div key={group.accountId} className="mt-2">
|
||||
|
||||
@@ -389,7 +389,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
setActionError(null);
|
||||
try {
|
||||
// JMAP strips parameters from Content-Type (RFC 8621), so method=REQUEST
|
||||
// is lost. Fetch raw ICS to extract METHOD as a reliable fallback — in
|
||||
// is lost. Fetch raw ICS to extract METHOD as a reliable fallback - in
|
||||
// parallel with parsing to save a roundtrip.
|
||||
const [events, rawText] = await Promise.all([
|
||||
client.parseCalendarEvents(client.getCalendarsAccountId(), attachment.blobId),
|
||||
@@ -420,7 +420,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
|
||||
|
||||
setState('parsed');
|
||||
|
||||
// Hydrate the calendar store with the matching event in the background —
|
||||
// Hydrate the calendar store with the matching event in the background -
|
||||
// only needed for the "already in calendar" pill, must not block the banner.
|
||||
// Filter by UID server-side; the previous unfiltered query fetched up to
|
||||
// 1000 events plus multiple /get batches just to find one match.
|
||||
|
||||
@@ -982,7 +982,7 @@ export function EmailComposer({
|
||||
try {
|
||||
const previousDraftId = draftIdRef.current;
|
||||
// Use the JMAP client and raw identity id for the *owning* account
|
||||
// — falls back to active client for single-account / same-account
|
||||
// - falls back to active client for single-account / same-account
|
||||
// identities. See `composerClient` derivation above.
|
||||
const savedDraftId = await composerClient.createDraft(
|
||||
toAddresses,
|
||||
@@ -1287,7 +1287,7 @@ export function EmailComposer({
|
||||
|
||||
// S/MIME send pipeline: build raw MIME → sign → encrypt → sendRawEmail
|
||||
if ((smimeSign_ || smimeEncrypt_) && client && currentIdentity?.id) {
|
||||
// S/MIME keys are scoped to one JMAP account's identity — sending
|
||||
// S/MIME keys are scoped to one JMAP account's identity - sending
|
||||
// from a cross-account identity via S/MIME would mix accounts'
|
||||
// certs/clients. Refuse upfront and tell the user to switch.
|
||||
const crossAccount = stripCrossAccountIdentityPrefix(currentIdentity.id);
|
||||
@@ -1440,7 +1440,7 @@ export function EmailComposer({
|
||||
const outgoing = await emailHooks.onTransformOutgoingEmail.transform(transformInput);
|
||||
|
||||
// Strip the cross-account namespace from the identity id before
|
||||
// handing it to the parent — the JMAP server only knows the raw
|
||||
// handing it to the parent - the JMAP server only knows the raw
|
||||
// id. The owning local account travels alongside so the parent
|
||||
// can route the send through the right client.
|
||||
const rawIdentityId = outgoing.identityId || currentIdentity?.id;
|
||||
|
||||
@@ -1142,7 +1142,7 @@ export function EmailViewer({
|
||||
|
||||
const handleViewContactSidebar = (contact: ContactCard | null, recipientEmail: string) => {
|
||||
if (isMobileDevice) {
|
||||
// No room for a sidebar on mobile — send the user to the contacts page
|
||||
// No room for a sidebar on mobile - send the user to the contacts page
|
||||
// with params describing what to show. The `from=email` flag turns the
|
||||
// page's mobile back button into a router.back() that returns here.
|
||||
const allRecipients = [
|
||||
@@ -2896,7 +2896,7 @@ export function EmailViewer({
|
||||
// window between selectedEmail changing and isLoading flipping true, so the
|
||||
// quick reply / body don't flicker through a partial render.
|
||||
// An empty bodyValues with no referenced parts means the email has no body
|
||||
// (e.g. calendar-only invites) — not "still loading".
|
||||
// (e.g. calendar-only invites) - not "still loading".
|
||||
const hasBodyParts = (email?.textBody?.length ?? 0) > 0 || (email?.htmlBody?.length ?? 0) > 0;
|
||||
const isBodyLoading = isLoading || (hasBodyParts && (!email?.bodyValues || Object.keys(email.bodyValues).length === 0));
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ interface FileBrowserProps {
|
||||
/** Pro shell only: all connected accounts surfaced as top-level folders at the root. */
|
||||
accountFolders?: AccountFolderEntry[];
|
||||
onSelectAccount?: (accountId: string) => void;
|
||||
/** Pro shell only: when true, the root is a pure account picker — hide the file toolbar and don't render a regular listing. */
|
||||
/** Pro shell only: when true, the root is a pure account picker - hide the file toolbar and don't render a regular listing. */
|
||||
accountPickerMode?: boolean;
|
||||
/** Pro shell only: label of the currently-attached account, shown as a breadcrumb segment after Home. */
|
||||
accountLabel?: string | null;
|
||||
@@ -471,7 +471,7 @@ export function FileBrowser({
|
||||
}, [resources, searchQuery, sortKey, sortDir, folderLayout]);
|
||||
|
||||
// Build breadcrumb segments. In Pro mode an account is mounted "between"
|
||||
// Home and the account's filesystem — surfaced as a non-clickable label
|
||||
// Home and the account's filesystem - surfaced as a non-clickable label
|
||||
// (clicking the actual account again would be a no-op; Home detaches it).
|
||||
const breadcrumbs: { name: string; path: string; isAccount?: boolean }[] = currentPath === '/'
|
||||
? [{ name: t("breadcrumb_root"), path: '/' }]
|
||||
|
||||
@@ -47,7 +47,7 @@ interface NavigationRailProps {
|
||||
activeAppId?: string | null;
|
||||
/**
|
||||
* If provided, intercepts the rail's built-in route navigation. Return
|
||||
* `true` to prevent the underlying `<Link>` from navigating — used by the
|
||||
* `true` to prevent the underlying `<Link>` from navigating - used by the
|
||||
* Pro interface to open the route as a tab instead. The visual rail is
|
||||
* unchanged.
|
||||
*/
|
||||
|
||||
@@ -78,7 +78,7 @@ interface SidebarProps {
|
||||
/**
|
||||
* Multi-account (Pro) mode props. When `multiAccountMode` is true, the
|
||||
* sidebar renders a per-connected-account group instead of a single
|
||||
* folders section — Thunderbird-style. `accountMailboxes` provides the
|
||||
* folders section - Thunderbird-style. `accountMailboxes` provides the
|
||||
* mailbox list for non-active accounts (the active account still flows
|
||||
* through the `mailboxes` prop). `viewingAccountId` highlights which
|
||||
* account's folder is currently selected (null = active account).
|
||||
@@ -715,7 +715,7 @@ export function Sidebar({
|
||||
} catch { return new Set(); }
|
||||
});
|
||||
// Per-connected-account collapse state for Pro / Thunderbird-style mode.
|
||||
// Stored as the set of accountIds the user has explicitly collapsed —
|
||||
// Stored as the set of accountIds the user has explicitly collapsed -
|
||||
// anything not in the set is treated as expanded. Inverting the storage
|
||||
// model lets new accounts default to expanded automatically.
|
||||
const [collapsedAccountGroups, setCollapsedAccountGroups] = useState<Set<string>>(() => {
|
||||
@@ -738,7 +738,7 @@ export function Sidebar({
|
||||
const connectedAccounts = accounts.filter(a => a.isConnected);
|
||||
// Pro shell treats the unified mailbox as a core part of the multi-account
|
||||
// UI, so it ignores the user-facing `enableUnifiedMailbox` toggle. The
|
||||
// 2+ account requirement still applies — with a single account the
|
||||
// 2+ account requirement still applies - with a single account the
|
||||
// unified counts would just duplicate that account's inbox.
|
||||
const showUnified = (multiAccountMode || enableUnifiedMailbox) && connectedAccounts.length > 1;
|
||||
const { unifiedCounts } = useEmailStore();
|
||||
@@ -930,7 +930,7 @@ export function Sidebar({
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Header — hidden in the Pro shell, which owns its own chrome and
|
||||
{/* Header - hidden in the Pro shell, which owns its own chrome and
|
||||
would otherwise render an empty strip (no collapse, no switcher). */}
|
||||
{!isEmbedded && (
|
||||
<div className={cn("flex items-center border-b border-border", isCollapsed ? "justify-center px-2 py-2" : "gap-1 px-2 py-2")}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
// Sandboxed slot mount. One iframe per (plugin, slot) — created lazily after
|
||||
// Sandboxed slot mount. One iframe per (plugin, slot) - created lazily after
|
||||
// the background instance confirms `shouldShow(context)` (if defined). The
|
||||
// iframe renders the plugin's slot component using the plugin's bundle in a
|
||||
// null-origin context; its height is pushed back via postMessage and applied
|
||||
@@ -59,7 +59,7 @@ export function PluginIframeSlot({ pluginId, slot, extraProps }: Props) {
|
||||
try { inst.destroy(); } catch { /* ignore */ }
|
||||
instanceRef.current = null;
|
||||
};
|
||||
// We intentionally don't depend on extraProps here — propagating prop
|
||||
// We intentionally don't depend on extraProps here - propagating prop
|
||||
// changes happens via postMessage below to avoid iframe churn.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [show, pluginId, slot]);
|
||||
|
||||
@@ -18,7 +18,7 @@ interface ProComposeTabBodyProps {
|
||||
/**
|
||||
* Renders a standalone `<EmailComposer />` inside its own Pro tab. Sending,
|
||||
* draft autosave, and discard all flow through the shared `email-store`, so
|
||||
* the result is identical to composing inline in the mail page — the
|
||||
* the result is identical to composing inline in the mail page - the
|
||||
* composer is just hosted in its own tab instead of in the right pane.
|
||||
*/
|
||||
export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) {
|
||||
|
||||
@@ -39,7 +39,7 @@ function buildReplyContext(email: Email): ProReplyContext {
|
||||
|
||||
/**
|
||||
* Renders a single email in its own Pro tab. Fetches the email content on
|
||||
* mount via `email-store.fetchEmailContent` so the tab is self-sufficient —
|
||||
* mount via `email-store.fetchEmailContent` so the tab is self-sufficient -
|
||||
* it doesn't depend on what the Mail tab has selected.
|
||||
*/
|
||||
export function ProEmailTabBody({ tabId, data }: ProEmailTabBodyProps) {
|
||||
@@ -160,7 +160,7 @@ export function ProEmailTabBody({ tabId, data }: ProEmailTabBodyProps) {
|
||||
if (!client || !email) return;
|
||||
try {
|
||||
await toggleStar(client, email.id);
|
||||
// Reflect locally — the viewer re-reads from email-store's selectedEmail
|
||||
// Reflect locally - the viewer re-reads from email-store's selectedEmail
|
||||
// shape only for the mail tab; here we update our local copy too.
|
||||
setEmail((prev) => prev ? {
|
||||
...prev,
|
||||
|
||||
@@ -16,7 +16,7 @@ const STANDARD_PATH_TO_TAB: Record<string, Exclude<ProTabKind, 'compose' | 'emai
|
||||
|
||||
/**
|
||||
* When the Pro interface is enabled, the standard mail/calendar/contacts/
|
||||
* files/settings routes are taken over by the Pro shell — the user shouldn't
|
||||
* files/settings routes are taken over by the Pro shell - the user shouldn't
|
||||
* have to click "Open" in settings to land there. Mobile/tablet keeps the
|
||||
* standard layout because Pro is desktop-only (see pro/page.tsx).
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@ export function EmbeddedBridgeProvider({ children }: { children: React.ReactNode
|
||||
|
||||
useEffect(() => {
|
||||
if (!embeddedMode || !isEmbedded()) return;
|
||||
// Refuse to attach the listener without a pinned parent origin —
|
||||
// Refuse to attach the listener without a pinned parent origin -
|
||||
// otherwise any cross-origin frame could forge sso:trigger-logout.
|
||||
if (!parentOrigin) {
|
||||
console.error(
|
||||
|
||||
Reference in New Issue
Block a user