chore: remove unused exports and the dead plugin error boundary

This commit is contained in:
Linus Rath
2026-10-13 13:36:51 +02:00
parent 8ad072e458
commit 4d733aa598
31 changed files with 6 additions and 510 deletions
-51
View File
@@ -179,54 +179,3 @@ export function MobileHeader({
</header>
);
}
/**
* Viewer header for mobile - shows when viewing an email
*/
interface MobileViewerHeaderProps {
subject?: string;
onBack: () => void;
onDelete?: () => void;
onArchive?: () => void;
className?: string;
}
export function MobileViewerHeader({
subject,
onBack,
onDelete: _onDelete,
onArchive: _onArchive,
className,
}: MobileViewerHeaderProps) {
const t = useTranslations('sidebar');
const isPaneDesktop = useIsDesktop();
if (isPaneDesktop) return null;
return (
<header
className={cn(
"flex items-center justify-between px-2 h-14 border-b border-border bg-background shrink-0",
className
)}
>
<Button
variant="ghost"
size="icon"
onClick={onBack}
className="h-11 w-11"
aria-label={t('mobile.go_back')}
>
<ArrowLeft className="h-5 w-5" />
</Button>
<h1 className="flex-1 font-medium text-sm truncate px-2 text-center">
{subject || "(No Subject)"}
</h1>
<div className="flex items-center">
{/* Placeholder for additional actions - kept minimal */}
<div className="w-10" />
</div>
</header>
);
}
@@ -1,36 +0,0 @@
'use client';
import React from 'react';
export interface PluginErrorBoundaryProps {
pluginId: string;
children?: React.ReactNode;
fallback?: React.ReactNode;
}
interface PluginErrorBoundaryState {
hasError: boolean;
error?: Error;
}
export class PluginErrorBoundary extends React.Component<PluginErrorBoundaryProps, PluginErrorBoundaryState> {
constructor(props: PluginErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): PluginErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
console.error(`[plugin:${this.props.pluginId}] Render error:`, error, errorInfo);
}
render(): React.ReactNode {
if (this.state.hasError) {
return this.props.fallback ?? null;
}
return this.props.children;
}
}
-6
View File
@@ -108,12 +108,6 @@ async function fetchServerConfig(): Promise<ConfigData> {
throw lastError;
}
/** Test hook: forget the cached config so the next fetch hits the network again. */
export function resetConfigCache(): void {
configCache = null;
configPromise = null;
}
export async function fetchConfig(): Promise<ConfigData> {
// Return cached config if available
if (configCache) {
-16
View File
@@ -96,24 +96,8 @@ export function useIsMobile() {
return paneWidth !== null ? paneWidth < BREAKPOINTS.md : viewport;
}
export function useIsTablet() {
const paneWidth = usePaneSize();
const viewport = useMediaQuery(
`(min-width: ${BREAKPOINTS.md}px) and (max-width: ${BREAKPOINTS.lg - 1}px)`
);
return paneWidth !== null
? paneWidth >= BREAKPOINTS.md && paneWidth < BREAKPOINTS.lg
: viewport;
}
export function useIsDesktop() {
const paneWidth = usePaneSize();
const viewport = useMediaQuery(`(min-width: ${BREAKPOINTS.lg}px)`);
return paneWidth !== null ? paneWidth >= BREAKPOINTS.lg : viewport;
}
export function useBreakpoint(breakpoint: keyof typeof BREAKPOINTS) {
const paneWidth = usePaneSize();
const viewport = useMediaQuery(`(min-width: ${BREAKPOINTS[breakpoint]}px)`);
return paneWidth !== null ? paneWidth >= BREAKPOINTS[breakpoint] : viewport;
}
-11
View File
@@ -26,17 +26,6 @@ export function cachedRemoteIdentities(): Identity[] {
return [...remoteIdentityCache.values()].flat();
}
/** Cross-account identity IDs are namespaced to avoid collisions between JMAP
* servers that happen to issue the same opaque ID. EVERY aggregated account is
* namespaced (including the active one) so an id doesn't change form when the
* active account changes; consumers resolve the raw id via
* stripCrossAccountIdentityPrefix. Single-account mode (hook disabled) uses the
* identity store's raw ids directly.
*/
export function isCrossAccountIdentityId(id: string): boolean {
return id.includes(CROSS_ACCOUNT_IDENTITY_DELIMITER);
}
export function stripCrossAccountIdentityPrefix(id: string): { localAccountId: string | null; rawId: string } {
const idx = id.indexOf(CROSS_ACCOUNT_IDENTITY_DELIMITER);
if (idx < 0) return { localAccountId: null, rawId: id };
-43
View File
@@ -21,7 +21,6 @@ vi.mock('@/components/plugins/plugin-iframe-slot', () => ({
// Import after mocks
import { PluginSlot } from '@/components/plugins/plugin-slot';
import { PluginErrorBoundary } from '@/components/plugins/plugin-error-boundary';
beforeEach(() => {
Object.keys(mockOffers).forEach(k => delete mockOffers[k]);
@@ -60,45 +59,3 @@ describe('PluginSlot', () => {
expect(container.querySelector('[data-plugin-slot="sidebar-widget"]')).toBeTruthy();
});
});
describe('PluginErrorBoundary', () => {
it('renders children when no error', () => {
const { getByText } = render(
React.createElement(
PluginErrorBoundary,
{ pluginId: 'test' },
React.createElement('span', null, 'Child')
)
);
expect(getByText('Child')).toBeTruthy();
});
it('renders fallback on error', () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const ThrowingComponent = () => { throw new Error('boom'); };
const { getByText } = render(
React.createElement(
PluginErrorBoundary,
{ pluginId: 'err', fallback: React.createElement('span', null, 'Error caught') },
React.createElement(ThrowingComponent)
)
);
expect(getByText('Error caught')).toBeTruthy();
consoleSpy.mockRestore();
});
it('renders null on error when no fallback provided', () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const ThrowingComponent = () => { throw new Error('boom'); };
const { container } = render(
React.createElement(
PluginErrorBoundary,
{ pluginId: 'err2' },
React.createElement(ThrowingComponent)
)
);
// ErrorBoundary renders null fallback
expect(container.innerHTML).toBe('');
consoleSpy.mockRestore();
});
});
-5
View File
@@ -92,11 +92,6 @@ export function getInitials(name: string, email?: string): string {
return '?';
}
/** Build an account-scoped localStorage key */
export function getAccountScopedKey(baseKey: string, accountId: string): string {
return `${baseKey}::${accountId}`;
}
/**
* Hard upper bound on cookie slots. Each slot can hold up to ~3 cookies
* (session, refresh token, server id, auth context), so 50 slots ≈ 125
-1
View File
@@ -58,7 +58,6 @@ export function sanitizeFrameOrigins(input: unknown): string[] {
* intent is explicit at every call site (frame embedding vs. HTTP fetch).
*/
export const sanitizeHttpOrigins = sanitizeFrameOrigins;
export const isValidHttpOrigin = isValidFrameOrigin;
// ─── apiPostPaths (manifest field) ────────────────────────────
-18
View File
@@ -2,8 +2,6 @@
* Multi-server JMAP support: schema, parsing, lookup, and redaction helpers.
*/
import { toAsciiDomain } from '@/lib/idn';
export interface JmapServerOAuthConfig {
clientId?: string;
issuerUrl?: string;
@@ -152,22 +150,6 @@ export function findServerByUrl(servers: JmapServerEntry[], url: string | null |
return servers.find((s) => normalizeUrl(s.url) === target);
}
function domainKey(domain: string): string {
return toAsciiDomain(domain) ?? domain.trim().toLowerCase();
}
/**
* Find the server whose `domains` array matches the given email's domain
* (case-insensitive, IDN domains compared in their ASCII form).
*/
export function findServerByEmailDomain(servers: JmapServerEntry[], email: string | null | undefined): JmapServerEntry | undefined {
if (!email || !email.includes('@')) return undefined;
const domain = email.split('@')[1]?.trim();
if (!domain) return undefined;
const key = domainKey(domain);
return servers.find((s) => (s.domains ?? []).some((d) => domainKey(d) === key));
}
/**
* Resolve a client-supplied JMAP URL to a trusted upstream URL by checking it
* against the configured server list and the global `jmapServerUrl`. Returns
+1 -32
View File
@@ -1,4 +1,4 @@
import { readFile, writeFile, rename, stat, unlink } from 'node:fs/promises';
import { readFile, writeFile, rename, unlink } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { logger } from '@/lib/logger';
import {
@@ -223,34 +223,3 @@ async function migrateAuditLog(): Promise<boolean> {
}
return moved;
}
/**
* Returns approximate size of legacy data still mixed in the config dir
* (for diagnostics / admin UI). Always returns 0 once migration has run.
*/
export async function getLegacyDataInfo(): Promise<{ adminJsonHasTimestamps: boolean; auditLogInConfigDir: boolean }> {
let adminJsonHasTimestamps = false;
const adminJsonPath = getConfigPath('admin.json');
if (existsSync(adminJsonPath)) {
try {
const raw = await readFile(adminJsonPath, 'utf-8');
const parsed = JSON.parse(raw);
adminJsonHasTimestamps =
'createdAt' in parsed ||
'lastLogin' in parsed ||
'passwordChangedAt' in parsed;
} catch {
/* ignore */
}
}
let auditLogInConfigDir = false;
try {
await stat(getConfigPath('audit.log'));
auditLogInConfigDir = true;
} catch {
/* not present - good */
}
return { adminJsonHasTimestamps, auditLogInConfigDir };
}
+1 -22
View File
@@ -1,7 +1,6 @@
import { existsSync } from 'node:fs';
import { mkdir, writeFile, unlink } from 'node:fs/promises';
import { mkdir } from 'node:fs/promises';
import path from 'node:path';
import { logger } from '@/lib/logger';
/**
* Admin data directories.
@@ -91,26 +90,6 @@ export function isConfigReadOnly(): boolean {
return cachedReadOnly;
}
/**
* Probe the config dir by writing a temp file. Used to auto-detect RO mounts
* when ADMIN_CONFIG_READONLY is not set explicitly. Run once at startup;
* cheap on local FS, can be slow on networked FS, hence opt-in.
*/
export async function probeConfigReadOnly(): Promise<boolean> {
if (process.env.ADMIN_CONFIG_READONLY) return isConfigReadOnly();
try {
const probe = path.join(getConfigDir(), '.rw-probe');
await writeFile(probe, '');
await unlink(probe);
cachedReadOnly = false;
return false;
} catch {
cachedReadOnly = true;
logger.info('Config dir is read-only (auto-detected)');
return true;
}
}
export class ConfigReadOnlyError extends Error {
constructor(operation: string) {
super(
-6
View File
@@ -167,9 +167,3 @@ export async function revokeApproval(pluginId: string, bundleHash: string): Prom
file.entries.splice(idx, 1);
await flushToDisk();
}
/** Force a re-read on next access. Used in tests / after a manual file edit. */
export function invalidateApprovalsCache(): void {
cached = null;
loadPromise = null;
}
-8
View File
@@ -71,11 +71,3 @@ export async function deletePluginConfigKey(pluginId: string, key: string): Prom
await writeFile(tmpPath, JSON.stringify(config, null, 2), 'utf-8');
await rename(tmpPath, filePath);
}
/**
* Delete all config for a plugin (used when uninstalling).
*/
export async function deleteAllPluginConfig(pluginId: string): Promise<void> {
assertWritable('delete plugin config');
try { await unlink(configPath(pluginId)); } catch { /* ok if missing */ }
}
-6
View File
@@ -20,12 +20,6 @@ export interface AdminStateData {
passwordChangedAt: string;
}
/**
* Combined view used by getAdminMeta() and tests. Constructed by merging
* admin.json + admin-state.json at read time.
*/
export interface AdminData extends AdminConfigData, AdminStateData {}
export interface AdminSessionPayload {
role: 'admin';
iat: number;
-5
View File
@@ -110,11 +110,6 @@ async function isOwnEndpoint(endpoint: string, serverUrl: string): Promise<boole
const DISCOVERY_CACHE_MAX = 32;
const discoveryCache = new Map<string, LiteOAuthDiscovery>();
/** Test hook. */
export function resetLiteOAuthDiscoveryCache(): void {
discoveryCache.clear();
}
/**
* Asks Stalwart which provider signs `username` in. `null` when the server
* has no such endpoint (not Stalwart, or older than 0.16), the answer is
-7
View File
@@ -20,13 +20,6 @@ export interface EmailFilenameOptions extends FilenameTransformOptions {
template?: string;
}
export const DEFAULT_TRANSFORM: Required<FilenameTransformOptions> = {
spaceReplacement: "keep",
lowercase: false,
stripDiacritics: false,
collapseSeparators: true,
};
export const DEFAULT_EMAIL_TEMPLATE = "{date} ({from}-{to}) {subject}";
export const DEFAULT_ATTACHMENT_TEMPLATE = "{filename}";
export const DEFAULT_BUNDLE_TEMPLATE = "emails-{count}";
-4
View File
@@ -40,10 +40,6 @@ export function hasRemoteQuery(parsed: ParsedQuery, kind: SearchKind): boolean {
|| mail.dateAfter || mail.dateBefore || parsed.mailboxRole);
}
export function isAbortError(error: unknown): boolean {
return error instanceof Error && error.name === 'AbortError';
}
function snapshot(outcome: SearchOutcome): SearchOutcome {
return {
hits: { ...outcome.hits },
-4
View File
@@ -15,10 +15,6 @@ export type SearchKind = 'mail' | 'contacts' | 'calendar' | 'files';
export const SEARCH_KINDS: readonly SearchKind[] = ['mail', 'contacts', 'calendar', 'files'];
export function isSearchKind(value: unknown): value is SearchKind {
return typeof value === 'string' && (SEARCH_KINDS as readonly string[]).includes(value);
}
/** Where a hit came from. Remote hits win over local ones when both exist (they carry snippets). */
export type HitSource = 'local' | 'remote';
-11
View File
@@ -63,11 +63,6 @@ export function jalaliMonthLength(jy: number, jm: number): number {
return jalaali.jalaaliMonthLength(jy, jm);
}
/** Is the given Jalali year a leap year? */
export function isJalaliLeapYear(jy: number): boolean {
return jalaali.isLeapJalaaliYear(jy);
}
// ---------------------------------------------------------------------------
// Calendar grid helpers (analogous to date-fns startOfWeek / eachDayOfInterval)
// ---------------------------------------------------------------------------
@@ -161,9 +156,3 @@ export function getDayHeaderKeys(
export function shouldUseJalaliCalendar(locale: string): boolean {
return locale === 'fa';
}
/** Default `firstDayOfWeek` for a given locale. */
export function defaultFirstDayOfWeek(locale: string): number {
if (locale === 'fa') return 6; // Saturday
return 1; // Monday (ISO convention)
}
+1 -4
View File
@@ -1,7 +1,4 @@
export interface EmailHeader {
name: string;
value: string;
}
/**
* A session-visible account (the user's own primary account plus any
-42
View File
@@ -3,48 +3,6 @@ import type { SettingsPolicy } from '@/lib/admin/types';
import { DEFAULT_POLICY } from '@/lib/admin/types';
import { httpUrlOrEmpty } from '@/lib/config-validation';
/**
* Runtime configuration for the static Lite build.
*
* `/api/config` used to merge env vars, the admin dashboard and per-host
* branding on the server. Lite reads `config.json` instead: a deployer edits a
* handful of keys (server URL, app name, logos) and everything else falls
* back to the same defaults `useConfig` already assumes. Whatever the file
* says, the flags that only work with a Next.js server behind them are forced
* off so no surface ever calls an endpoint that does not exist.
*/
/** Keys a deployer may set in config.json. Everything else is ignored. */
export const LITE_CONFIG_KEYS = [
'appName',
'jmapServerUrl',
'allowCustomJmapEndpoint',
'jmapServers',
'jmapServerAutoPickByDomain',
'demoMode',
'rememberMeEnabled',
'faviconUrl',
'appLogoLightUrl',
'appLogoDarkUrl',
'loginLogoLightUrl',
'loginLogoDarkUrl',
'loginCompanyName',
'loginImprintUrl',
'loginPrivacyPolicyUrl',
'loginWebsiteUrl',
'loginLogoMaxHeight',
'loginLogoMaxWidth',
'loginShowHeading',
'loginShowSubtitle',
'loginShowTotp',
'loginShowTokenLogin',
'loginShowRememberMe',
'loginShowVersion',
'embeddedMode',
'parentOrigin',
'sourceCodeUrl',
] as const;
/**
* Flags that need the Next.js server (cookies, filesystem, OAuth relay). They
* are pinned regardless of what config.json contains. `oauthEnabled` is the
-55
View File
@@ -11,10 +11,6 @@
import type { SlotName, PluginTier } from '../plugin-types';
import type { ThemeSnapshot } from './host-theme';
// ─── Sandbox mode ────────────────────────────────────────────
export type SandboxMode = 'background' | 'slot';
/** Initialisation payload for a background-instance iframe (one per plugin). */
export interface BackgroundInit {
mode: 'background';
@@ -200,29 +196,6 @@ export type HostToSandbox =
| PropsUpdateMsg
| SlotShouldShowMsg;
/** Marker used in extraProps for function values that the host owns. */
export interface PluginCallbackMarker {
__pluginCallback: string;
}
export function isCallbackMarker(value: unknown): value is PluginCallbackMarker {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as { __pluginCallback?: unknown }).__pluginCallback === 'string'
);
}
// ─── Type guards ─────────────────────────────────────────────
export function isSandboxMessage(value: unknown): value is SandboxToHost {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as { type?: unknown }).type === 'string'
);
}
// ─── Constants ───────────────────────────────────────────────
/** Path used for the untrusted (null-origin) sandbox iframe `src`. Matched in
@@ -236,31 +209,3 @@ export const SANDBOX_PATH = '/plugin-sandbox';
* `proxy.ts`. Renders the identical `SandboxRuntime`.
*/
export const SANDBOX_PRIVILEGED_PATH = '/plugin-sandbox-privileged';
/** Methods callable by a plugin via api-request. Host enforces permissions. */
export const API_METHODS = [
'storage.get', 'storage.set', 'storage.remove', 'storage.keys',
'http.post', 'http.fetch',
'crypto.createWebAuthn', 'crypto.getWebAuthn', 'crypto.getPublicKeys', 'crypto.createPublicKey', 'crypto.removePublicKey', 'crypto.getEncryptionAtRest', 'crypto.setEncryptionAtRest', 'crypto.getPublicKeyFromWKD',
'jmap.fetchBlob', 'jmap.uploadBlob', 'jmap.sendRaw',
// Narrow keyword helpers. Unlike the raw-blob methods, these are available
// to untrusted plugins with email:read/email:write as appropriate.
'jmap.getKeywords', 'jmap.setKeywords', 'jmap.setKeyword', 'jmap.removeKeyword',
'upfiles.get', 'upfiles.save',
'contact.get', 'contact.update', 'contact.create', 'contact.search',
'admin.getConfig', 'admin.getAllConfig', 'admin.setConfig', 'admin.deleteConfig',
'toast.success', 'toast.error', 'toast.info', 'toast.warning',
'ui.confirm', 'ui.alert', 'ui.prompt', 'ui.rerenderEmail', 'ui.rerenderFetchedEmails', 'ui.openExternalUrl', 'ui.downloadFile',
// Email keyword mutations (JMAP Email/set keyword patches).
'email.setKeyword', 'email.removeKeyword',
// Native tag definitions, discovery, and sidebar counts.
'keywords.list', 'keywords.add', 'keywords.reorder', 'keywords.discover', 'keywords.getCounts', 'keywords.refreshCounts',
// Message-list category tabs (Gmail-style inbox tabs).
'tabs.set', 'tabs.clear', 'tabs.getState', 'tabs.categorize', 'tabs.refreshCounts',
// Run a JMAP Email filter as the message-list search.
'search.run',
// Sieve integration for delivery-time classification plugins.
'sieve.isSupported', 'sieve.getActiveScript', 'sieve.validateScript', 'sieve.regenerate',
] as const;
export type ApiMethod = (typeof API_METHODS)[number];
-11
View File
@@ -112,14 +112,3 @@ export function registerShortcuts(
for (const k of keys) bindings.delete(k);
};
}
/** Snapshot of currently active shortcuts. Used by the help modal. */
export function listShortcuts(): Array<{ pluginId: string; id: string; keys: string; label: string; category?: string }> {
return [...bindings.values()].map(b => ({
pluginId: b.pluginId,
id: b.shortcutId,
keys: b.keys,
label: b.label,
category: b.category,
}));
}
-4
View File
@@ -15,10 +15,6 @@ export function setServerAuthIssue(reason: string | null): void {
issue = reason;
}
export function getServerAuthIssue(): string | null {
return issue;
}
/**
* Error text for a failed response from a cookie-gated route: the recorded
* reason when the route answered 401, otherwise the route's own message.
+1 -20
View File
@@ -1,6 +1,5 @@
import { existsSync } from 'node:fs';
import { configManager } from '@/lib/admin/config-manager';
import { getConfigPath, isConfigReadOnly } from '@/lib/admin/paths';
import { isConfigReadOnly } from '@/lib/admin/paths';
/**
* The three lifecycle states for the running container.
@@ -33,21 +32,3 @@ export function detectSetupState(): SetupState {
if (isConfigReadOnly()) return 'bootstrap';
return 'bootstrap';
}
/**
* Whether the wizard's UI and APIs should be reachable.
*/
export function isSetupActive(): boolean {
return detectSetupState() === 'bootstrap';
}
/**
* The persisted `.config-locked` marker the wizard drops when the operator
* checks "lock configuration after setup" on the review screen. Purely
* advisory - the actual locking is the operator's `:ro` mount or the
* ADMIN_CONFIG_READONLY env var. This file is what the admin UI uses to
* remind the operator that they intended to lock.
*/
export function lockMarkerExists(): boolean {
return existsSync(getConfigPath('.config-locked'));
}
+1 -18
View File
@@ -1,5 +1,5 @@
import { randomBytes, timingSafeEqual } from 'node:crypto';
import { readFile, writeFile, unlink, stat } from 'node:fs/promises';
import { readFile, writeFile, unlink } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { logger } from '@/lib/logger';
import { ensureStateDir, getStatePath } from '@/lib/admin/paths';
@@ -92,20 +92,3 @@ export async function clearSetupToken(): Promise<void> {
});
}
}
/**
* For diagnostics / startup logging.
*/
export async function getTokenInfo(): Promise<{ exists: boolean; expiresInSeconds: number | null }> {
const path = getStatePath(TOKEN_FILE);
if (!existsSync(path)) return { exists: false, expiresInSeconds: null };
try {
await stat(path);
const payload = await readToken();
if (!payload) return { exists: false, expiresInSeconds: null };
const elapsed = Date.now() / 1000 - payload.issuedAt;
return { exists: true, expiresInSeconds: Math.max(0, Math.floor(payload.ttlSeconds - elapsed)) };
} catch {
return { exists: false, expiresInSeconds: null };
}
}
-10
View File
@@ -63,11 +63,6 @@ export function readStalwartAuthContextFromStore(
return accountName ? { serverUrl, username, authHeader, accountName } : { serverUrl, username, authHeader };
}
export async function readStalwartAuthContext(slot: number): Promise<StalwartAuthContext | null> {
const cookieStore = await cookies();
return readStalwartAuthContextFromStore(cookieStore, slot);
}
export function setStalwartAuthContextInStore(
cookieStore: CookieStore,
slot: number,
@@ -88,8 +83,3 @@ export async function setStalwartAuthContext(slot: number, context: StalwartAuth
export function clearStalwartAuthContextInStore(cookieStore: CookieStore, slot: number): void {
cookieStore.delete(stalwartAuthContextCookieName(slot));
}
export async function clearStalwartAuthContext(slot: number): Promise<void> {
const cookieStore = await cookies();
clearStalwartAuthContextInStore(cookieStore, slot);
}
-7
View File
@@ -16,11 +16,6 @@ export interface EmailTemplate {
updatedAt: string;
}
export interface PlaceholderVariable {
name: string;
value: string;
}
export const BUILT_IN_PLACEHOLDERS = [
'recipient_name',
'company',
@@ -28,5 +23,3 @@ export const BUILT_IN_PLACEHOLDERS = [
'day_of_week',
'sender_name',
] as const;
export type BuiltInPlaceholder = (typeof BUILT_IN_PLACEHOLDERS)[number];
-5
View File
@@ -79,11 +79,6 @@ export function getEffectiveTimeZone(): string {
return resolveTimeZone(useSettingsStore.getState().timeZone);
}
/** True when the user's override is active and differs from the browser zone. */
export function isTimeZoneOverridden(): boolean {
return getEffectiveTimeZone() !== getBrowserTimeZone();
}
export interface WallClock {
year: number;
/** 1-12 */
+1 -40
View File
@@ -1,7 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { Mailbox, UNIFIED_MAILBOX_IDS } from "./jmap/types";
import type { UnifiedMailboxRole } from "./jmap/types";
import { Mailbox } from "./jmap/types";
import { debug } from "./debug";
import { getEffectiveLocale } from '@/i18n/detect-locale';
import { useSettingsStore } from "@/stores/settings-store";
@@ -326,11 +325,6 @@ export function cleanPreview(text: string | null | undefined): string {
return stripInvisibleLeading(stripLeadingCss(stripInvisibleLeading(text ?? '')));
}
export function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength).trim() + "...";
}
/**
* A size people can read at a glance: whole numbers from 10 up ("67 KB"),
* one decimal below that ("2.1 MB"), never a trailing ".0".
@@ -645,39 +639,6 @@ export function buildMailboxTree(mailboxes: Mailbox[]): MailboxNode[] {
return rootMailboxes;
}
/**
* Builds virtual MailboxNode entries for unified mailbox roles with aggregated counts.
*/
export function buildUnifiedMailboxNodes(
counts: Array<{ role: UnifiedMailboxRole; unreadEmails: number; totalEmails: number }>,
): MailboxNode[] {
return counts.map((count) => ({
id: UNIFIED_MAILBOX_IDS[count.role],
name: count.role, // Display name is handled by i18n in the component
role: count.role,
parentId: undefined,
sortOrder: 0,
totalEmails: count.totalEmails,
unreadEmails: count.unreadEmails,
totalThreads: 0,
unreadThreads: 0,
myRights: {
mayReadItems: true,
mayAddItems: false,
mayRemoveItems: false,
maySetSeen: true,
maySetKeywords: true,
mayCreateChild: false,
mayRename: false,
mayDelete: false,
maySubmit: false,
},
isSubscribed: true,
children: [],
depth: 0,
}));
}
// Flatten a mailbox tree for rendering with proper depth info
export function flattenMailboxTree(nodes: MailboxNode[]): MailboxNode[] {
const result: MailboxNode[] = [];
-2
View File
@@ -165,8 +165,6 @@ export type Density = 'extra-compact' | 'compact' | 'regular' | 'comfortable';
* docked compose window) that keeps the theme's icons, type and palette.
*/
export type InterfaceLayout = 'default' | 'mountain-view';
/** @deprecated Use Density instead */
export type ListDensity = Density;
export type DeleteAction = 'trash' | 'trash-and-read' | 'permanent';
export type ReplyMode = 'reply' | 'replyAll';
export type SignaturePosition = 'above_quote' | 'below_quote';