"use client"; import { useState, useRef, useEffect } from 'react'; import { useTranslations } from 'next-intl'; import { useSettingsStore } from '@/stores/settings-store'; import { useConfig } from '@/hooks/use-config'; import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section'; import { Button } from '@/components/ui/button'; import { usePolicyStore } from '@/stores/policy-store'; import { useUpdateStore } from '@/stores/update-store'; import { ExternalLink } from '@/components/icons'; import { cn } from '@/lib/utils'; import { getPathPrefix, withBasePath } from '@/lib/browser-navigation'; import { clearCachedData } from '@/lib/clear-cached-data'; import { SpamSiegeGame } from './spam-siege-game'; import { DebugSettings } from './debug-settings'; const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0"; const GIT_COMMIT = process.env.NEXT_PUBLIC_GIT_COMMIT || "unknown"; const UPSTREAM_SOURCE_URL = "https://github.com/bulwarkmail/webmail"; function VersionUpdateTag() { const status = useUpdateStore((s) => s.status); const startPolling = useUpdateStore((s) => s.startPolling); useEffect(() => { startPolling(); }, [startPolling]); if (!status?.updateAvailable) return null; if (status.severity === 'unknown' || status.severity === 'none') return null; const important = status.severity === 'security' || status.severity === 'deprecated'; const label = status.severity === 'security' ? 'security' : status.severity === 'deprecated' ? 'deprecated' : status.latest ?? 'update'; return ( {important ? label : `update: ${label}`} ); } /** * The About card, in the settings sidebar's footer: logo, version with the * update tag, and the source link. A modified build must offer its own * source (AGPL-3.0 ยง13), so a configured URL replaces the upstream link. * Three clicks on the logo open Spam Siege, as before. */ export function AboutFooter() { const t = useTranslations('settings.advanced'); const { sourceCodeUrl, appName, appLogoLightUrl, appLogoDarkUrl } = useConfig(); // A branded deployment (APP_NAME / APP_LOGO_*) shows its own name and logo // here; without them the card keeps the Bulwark mark and title. (#1160) const branded = appName && appName !== 'Webmail'; const logoLight = withBasePath(appLogoLightUrl || appLogoDarkUrl) || `${getPathPrefix()}/branding/Bulwark_Logo_Color.svg`; const logoDark = withBasePath(appLogoDarkUrl || appLogoLightUrl) || `${getPathPrefix()}/branding/Bulwark_Logo_White.svg`; const [showGame, setShowGame] = useState(false); const logoClickCount = useRef(0); const logoClickTimer = useRef | null>(null); const handleLogoClick = () => { logoClickCount.current++; if (logoClickTimer.current) clearTimeout(logoClickTimer.current); if (logoClickCount.current >= 3) { logoClickCount.current = 0; setShowGame(true); } else { logoClickTimer.current = setTimeout(() => { logoClickCount.current = 0; }, 2000); } }; return (
{showGame && setShowGame(false)} />}
{sourceCodeUrl ? t('about.source_code') : 'GitHub'}
); } /** * The Backup & reset page: keep settings (sync, export, import), fix a stale * view, start over, and (Advanced, where the admin allows it) debug logging. * The About card moved to the settings sidebar's footer. */ export function AboutDataSettings() { const t = useTranslations('settings.advanced'); const tCommon = useTranslations('common'); const tSettings = useTranslations('settings'); const { settingsSyncDisabled, updateSetting, resetToDefaults, exportSettings, importSettings } = useSettingsStore(); const { settingsSyncEnabled } = useConfig(); const [showResetConfirm, setShowResetConfirm] = useState(false); const [showRefreshConfirm, setShowRefreshConfirm] = useState(false); const fileInputRef = useRef(null); const { isFeatureEnabled } = usePolicyStore(); const handleExport = () => { const settingsJson = exportSettings(); const blob = new Blob([settingsJson], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `webmail-settings-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const handleImport = () => { fileInputRef.current?.click(); }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const json = event.target?.result as string; const success = importSettings(json); if (success) { alert(tSettings('import_success')); } else { alert(tSettings('import_error')); } }; reader.readAsText(file); }; const handleRefreshCache = () => { if (showRefreshConfirm) { clearCachedData(); // reloads the page } else { setShowRefreshConfirm(true); setTimeout(() => setShowRefreshConfirm(false), 5000); } }; const handleReset = () => { if (showResetConfirm) { resetToDefaults(); setShowResetConfirm(false); alert(tSettings('save_success')); } else { setShowResetConfirm(true); setTimeout(() => setShowResetConfirm(false), 5000); } }; return (
{settingsSyncEnabled && ( updateSetting('settingsSyncDisabled', !checked)} /> )} {isFeatureEnabled('settingsExportEnabled') && ( )} {isFeatureEnabled('settingsExportEnabled') && ( <> )}
); }