diff --git a/client/config.tsx b/client/config.tsx index 0b4ae72d..ff2b1fb4 100644 --- a/client/config.tsx +++ b/client/config.tsx @@ -5,4 +5,6 @@ export const lean4webConfig: LeanWebConfig = { serverCountry: "Germany", contactDetails: null, impressum: null, + comparator: "https://comparator.live.lean-lang.org/", + comparatorSafeList: ["lean-lang.org", /^.*\.lean-lang\.org$/], }; diff --git a/client/src/api/config-types.ts b/client/src/api/config-types.ts index 7f05a165..0757c00c 100644 --- a/client/src/api/config-types.ts +++ b/client/src/api/config-types.ts @@ -13,4 +13,13 @@ export type LeanWebConfig = { * (example: `<>

vat number: 000

<>`) */ impressum: JSX.Element | null + /** Base href for a comparator live deployment that encourages the verification of proofs from untrusted sources. + * If `null`, the "Can I Trust This Proof?" button will be hidden. + * (example: `"https://comparator.live.lean-lang.org/"`) + */ + comparator: string | null + /** URLs to safelist so that refer links do not prompt a popup highlighting the "Can I Trust This Proof?" button. + * Ignored if `comparator` option is `null`. + */ + comparatorSafeList: (string | RegExp)[] | null } diff --git a/client/src/css/Navigation.css b/client/src/css/Navigation.css index dc155427..56b3d98f 100644 --- a/client/src/css/Navigation.css +++ b/client/src/css/Navigation.css @@ -78,6 +78,10 @@ nav select { padding-bottom: 0.6rem; } +nav .nav-link.disabled { + cursor: not-allowed; +} + /* Zulip logo (instead of font-awesome icon) */ nav .nav-link svg { width: 1.4rem; @@ -148,3 +152,77 @@ nav select:hover { font-style: italic; color: var(--vscode-textLink-foreground); } + +.comparator-warning { + max-width: 290px; + color: var(--vscode-editorHoverWidget-foreground); + background-color: var(--comparator-warning-bg); + font-size: 90%; + padding: 0.9em 1em; + box-shadow: 0 0.2rem 0.6rem 0 var(--vscode-widget-shadow); + border: 1px solid var(--vscode-editorHoverWidget-border); + border-radius: 0.4em; + position: relative; +} + +.comparator-warning.dark { + --comparator-warning-bg: oklch(0.2264 0.1066 0); +} +.comparator-warning.light { + --comparator-warning-bg: oklch(0.9581 0.0293 0); +} +.comparator-warning.cobalt { + --comparator-warning-bg: oklch(0.2981 0.1382 0); +} + +.comparator-warning button.comparator-warning-close { + float: right; + font-size: 1.6em; + cursor: pointer; + padding-top: 0.1em; + border: none; + background: none; + color: inherit; + margin: 0; +} +.comparator-warning button.comparator-warning-close:hover { + color: var(--vscode-inputOption-activeForeground); +} + +.comparator-warning p { + margin: 0 0 0.6em; +} + +.comparator-warning-arrow { + position: absolute; + width: 1em; + height: 1em; + margin-top: -0.5em; + top: 0; +} + +.comparator-warning-arrow::before { + content: ''; + display: block; + width: 100%; + height: 100%; + background-color: var(--comparator-warning-bg); + border: 1px solid var(--vscode-editorHoverWidget-border); + transform: rotate(45deg); + border-right: none; + border-bottom: none; +} + +.comparator-warning-perma-dismiss { + padding: 0; + background: none; + border: none; + cursor: pointer; + font-size: inherit; + color: var(--vscode-textLink-foreground); + font-weight: bold; +} + +.comparator-warning-perma-dismiss:hover { + text-decoration: underline; +} diff --git a/client/src/navigation/NavButton.tsx b/client/src/navigation/NavButton.tsx index a7a1920e..cc1c6321 100644 --- a/client/src/navigation/NavButton.tsx +++ b/client/src/navigation/NavButton.tsx @@ -1,6 +1,6 @@ import { IconDefinition } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { JSX, MouseEventHandler } from 'react' +import { JSX, MouseEventHandler, Ref } from 'react' /** A button to appear in the hamburger menu or to navigation bar. */ export function NavButton({ @@ -10,6 +10,8 @@ export function NavButton({ title, onClick = () => {}, href = undefined, + disabled = false, + ref, }: { icon?: IconDefinition iconElement?: JSX.Element @@ -17,11 +19,21 @@ export function NavButton({ title?: string onClick?: MouseEventHandler href?: string + disabled?: boolean + ref?: Ref }) { // note: it seems that we can just leave the `target="_blank"` and it has no // effect on links without a `href`. If not, add `if (href)` statement here... return ( - + e.preventDefault() : onClick} + href={href!} + target="_blank" + > {iconElement ?? } {text} ) diff --git a/client/src/navigation/Navigation.tsx b/client/src/navigation/Navigation.tsx index f6100f0a..5a192594 100644 --- a/client/src/navigation/Navigation.tsx +++ b/client/src/navigation/Navigation.tsx @@ -1,7 +1,13 @@ import '../css/Modal.css' import '../css/Navigation.css' -import { faArrowRotateRight, faCode, faInfoCircle, faEye } from '@fortawesome/free-solid-svg-icons' +import { + faArrowRotateRight, + faCode, + faEye, + faHandshake, + faInfoCircle, +} from '@fortawesome/free-solid-svg-icons' import { faArrowUpRightFromSquare, faBars, @@ -15,26 +21,44 @@ import { faXmark, } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { useAtom } from 'jotai' +import ClickAwayListener from '@mui/material/ClickAwayListener' +import Popper from '@mui/material/Popper' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react' import { lean4webConfig } from '../../config' import ZulipIcon from '../assets/zulip.svg' +import { useNavBar } from '../context/NavBarContext' import { codeAtom } from '../editor/code-atoms' import ImpressumPopup from '../Popups/Impressum' import LoadUrlPopup from '../Popups/LoadUrl' import LoadZulipPopup from '../Popups/LoadZulip' import PrivacyPopup from '../Popups/PrivacyPolicy' import ToolsPopup from '../Popups/Tools' -import { mobileAtom } from '../settings/settings-atoms' +import { localOnlySettingsAtom, mobileAtom, settingsAtom } from '../settings/settings-atoms' +import { lightThemes } from '../settings/settings-types' import { SettingsPopup } from '../settings/SettingsPopup' import { setImportUrlAndProjectAtom } from '../store/import-atoms' import { currentProjectAtom, projectsAtom, visibleProjectsAtom } from '../store/project-atoms' +import { urlArgsStableAtom } from '../store/url-atoms' +import { parseArgs } from '../store/url-converters' import { save } from '../utils/SaveToFile' import { Dropdown } from './Dropdown' import { NavButton } from './NavButton' -import { useNavBar } from '../context/NavBarContext' +const referrerNeedsComparator = (() => { + // Never highlight comparator option if there's no code + const args = parseArgs(window.location.hash) + if (!args.code?.trim() && !args.codez?.trim()) return false // No warning for empty code or example-url-driven code + + // Highlight comparator if there's no referrer or if the referrer isn't safelisted + if (!document.referrer) return true + const referrer = new URL(document.referrer) + if (!lean4webConfig.comparatorSafeList) return true + return !lean4webConfig.comparatorSafeList.some((item) => + item instanceof RegExp ? referrer.hostname.match(item) : referrer.hostname === item, + ) +})() /** The menu items either appearing inside the dropdown or outside */ function FlexibleMenu({ @@ -58,8 +82,30 @@ function FlexibleMenu({ setLoadUrlOpen: Dispatch> setLoadZulipOpen: Dispatch> }) { - const [, setImportUrlAndProject] = useAtom(setImportUrlAndProjectAtom) - const [{ data: projects }] = useAtom(projectsAtom) + const setImportUrlAndProject = useSetAtom(setImportUrlAndProjectAtom) + const { data: projects } = useAtomValue(projectsAtom) + const urlArgs = useAtomValue(urlArgsStableAtom) + const code = useAtomValue(codeAtom) + const isUsingUrlCode = !!urlArgs?.url + + const settings = useAtomValue(settingsAtom) + const themeVariant = lightThemes.includes(settings.theme) + ? 'light' + : settings.theme === 'Cobalt' + ? 'cobalt' + : 'dark' + const [localOnlySettings, setLocalOnlySettings] = useAtom(localOnlySettingsAtom) + const [comparatorWarningDismissed, setComparatorWarningDismissed] = useState(false) + const comparatorWarningEligible = + !isInDropdown && + !isUsingUrlCode && + referrerNeedsComparator && + !localOnlySettings.ignoreComparatorWarning + const [trustButtonEl, setTrustButtonEl] = useState(null) + const [trustArrowEl, setTrustArrowEl] = useState(null) + const comparatorWarningOpen = + !!trustButtonEl && comparatorWarningEligible && !comparatorWarningDismissed + const loadFileFromDisk = (event: ChangeEvent) => { console.debug('Loading file from disk') const fileToLoad = event.target.files![0] @@ -141,6 +187,66 @@ function FlexibleMenu({ }} /> + {lean4webConfig.comparator && ( + { + window.location.assign(lean4webConfig.comparator + window.location.hash) + }} + /> + )} + + setComparatorWarningDismissed(true)}> +
+ + +
+
+
) } @@ -251,7 +357,13 @@ export function Menu({ }} /> setToolsOpen(true)} /> - { navbar.requiresNavBar != 0 && navbar.setHideNavBar(!navbar.hideNavBar)} />} + {navbar.requiresNavBar != 0 && ( + navbar.setHideNavBar(!navbar.hideNavBar)} + /> + )} (settings) + const [localOnlySettings, setLocalOnlySettings] = useAtom(localOnlySettingsAtom) function updateSetting(key: K, value: Settings[K]) { setNewSettings((prev) => ({ ...prev, [key]: value })) @@ -198,6 +199,23 @@ export function SettingsPopup({ onClick={() => applySettings(newSettings)} />

+

Local Settings

+

+ These settings are always preserved in the browser's local storage. +

+

+ { + setLocalOnlySettings({ + ...localOnlySettings, + ignoreComparatorWarning: !localOnlySettings.ignoreComparatorWarning, + }) + }} + checked={!!localOnlySettings.ignoreComparatorWarning} + /> + +

) diff --git a/client/src/settings/settings-atoms.ts b/client/src/settings/settings-atoms.ts index a777f43a..4d1e6a6c 100644 --- a/client/src/settings/settings-atoms.ts +++ b/client/src/settings/settings-atoms.ts @@ -13,6 +13,20 @@ const settingsStoreAtom = atomWithStorage('lean4web:setting getOnInit: true, }) +interface PartialLocalOnlyUserSettings { + ignoreComparatorWarning?: boolean +} + +/** Settings always stored on localstorage, never in the URL */ +export const localOnlySettingsAtom = atomWithStorage( + 'lean4web:local-settings', + {}, + undefined, + { + getOnInit: true, + }, +) + /** The settings which are set in the searchParams of the opened URL */ const settingsUrlAtom = atom( (get) => {