Make it possible to permanently dismiss the web banner from the settings (#2021)

* Make it possible to include a setting only on the Settings dialog, not also in the command bar.

* Add web-only setting to permanently dismiss banner

* Honor the dismiss web banner setting

* Remove unused state from useStore

* Make the banner only appear in production builds again
This commit is contained in:
Frank Noirot
2024-04-05 00:30:11 -04:00
committed by GitHub
parent 8ac0bf4953
commit 233f81a879
8 changed files with 115 additions and 46 deletions

View File

@ -66,10 +66,10 @@ const router = createBrowserRouter([
<Outlet /> <Outlet />
<App /> <App />
<CommandBar /> <CommandBar />
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</ModelingMachineProvider> </ModelingMachineProvider>
<WasmErrBanner /> <WasmErrBanner />
</FileMachineProvider> </FileMachineProvider>
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
</Auth> </Auth>
), ),
children: [ children: [

View File

@ -1,12 +1,13 @@
import { Dialog } from '@headlessui/react' import { Dialog } from '@headlessui/react'
import { useStore } from '../useStore'
import { ActionButton } from './ActionButton' import { ActionButton } from './ActionButton'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useState } from 'react'
const DownloadAppBanner = () => { const DownloadAppBanner = () => {
const { isBannerDismissed, setBannerDismissed } = useStore((s) => ({ const { settings } = useSettingsAuthContext()
isBannerDismissed: s.isBannerDismissed, const [isBannerDismissed, setIsBannerDismissed] = useState(
setBannerDismissed: s.setBannerDismissed, settings.context.app.dismissWebBanner.current
})) )
return ( return (
<Dialog <Dialog
@ -23,7 +24,7 @@ const DownloadAppBanner = () => {
</h2> </h2>
<ActionButton <ActionButton
Element="button" Element="button"
onClick={() => setBannerDismissed(true)} onClick={() => setIsBannerDismissed(true)}
icon={{ icon={{
icon: 'close', icon: 'close',
className: 'p-1', className: 'p-1',
@ -51,6 +52,24 @@ const DownloadAppBanner = () => {
</a>{' '} </a>{' '}
to download the app for the best experience. to download the app for the best experience.
</p> </p>
<p className="mt-6">
If you're on Linux and the browser is your only way to use the app,
you can permanently dismiss this banner by{' '}
<a
onClick={() => {
setIsBannerDismissed(true)
settings.send({
type: 'set.app.dismissWebBanner',
data: { level: 'user', value: true },
})
}}
href="/"
className="!text-warn-80 dark:!text-warn-80 dark:hover:!text-warn-70 underline"
>
toggling the App &gt; Dismiss Web Banner setting
</a>
.
</p>
</div> </div>
</Dialog.Panel> </Dialog.Panel>
</Dialog> </Dialog>

View File

@ -150,7 +150,16 @@ function ProjectMenuPopover({
closePanel={close} closePanel={close}
/> />
) : ( ) : (
<div className="flex-1 overflow-hidden" /> <div className="flex-1 p-4 text-sm overflow-hidden">
<p>
In the browser version of Modeling App you can only have one
part, and the code is stored in your browser's storage.
</p>
<p className="my-6">
Please save any code you want to keep more permanently, as
your browser's storage is not guaranteed to be permanent.
</p>
</div>
)} )}
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90"> <div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
<ActionButton <ActionButton

View File

@ -135,6 +135,15 @@ export function createSettings() {
onboardingStatus: new Setting<string>({ onboardingStatus: new Setting<string>({
defaultValue: '', defaultValue: '',
validate: (v) => typeof v === 'string', validate: (v) => typeof v === 'string',
hideOnPlatform: 'both',
}),
/** Permanently dismiss the banner warning to download the desktop app. */
dismissWebBanner: new Setting<boolean>({
defaultValue: false,
description:
'Permanently dismiss the banner warning to download the desktop app.',
validate: (v) => typeof v === 'boolean',
hideOnPlatform: 'desktop',
}), }),
projectDirectory: new Setting<string>({ projectDirectory: new Setting<string>({
defaultValue: '', defaultValue: '',

View File

@ -86,7 +86,7 @@ export interface SettingProps<T = unknown> {
* Whether to hide the setting on a certain platform. * Whether to hide the setting on a certain platform.
* This will be applied in both the settings panel and the command bar. * This will be applied in both the settings panel and the command bar.
*/ */
hideOnPlatform?: 'web' | 'desktop' hideOnPlatform?: 'web' | 'desktop' | 'both'
/** /**
* A React component to use for the setting in the settings panel. * A React component to use for the setting in the settings panel.
* If this is not provided but a commandConfig is, the `inputType` * If this is not provided but a commandConfig is, the `inputType`

View File

@ -139,16 +139,14 @@ export function setSettingsAtLevel(
Object.entries(newSettings).forEach(([category, settingsCategory]) => { Object.entries(newSettings).forEach(([category, settingsCategory]) => {
const categoryKey = category as keyof typeof settings const categoryKey = category as keyof typeof settings
if (!allSettings[categoryKey]) return // ignore unrecognized categories if (!allSettings[categoryKey]) return // ignore unrecognized categories
Object.entries(settingsCategory).forEach( Object.entries(settingsCategory).forEach(([settingKey, settingValue]) => {
([settingKey, settingValue]: [string, Setting]) => {
// TODO: How do you get a valid type for allSettings[categoryKey][settingKey]? // TODO: How do you get a valid type for allSettings[categoryKey][settingKey]?
// it seems to always collapses to `never`, which is not correct // it seems to always collapses to `never`, which is not correct
// @ts-ignore // @ts-ignore
if (!allSettings[categoryKey][settingKey]) return // ignore unrecognized settings if (!allSettings[categoryKey][settingKey]) return // ignore unrecognized settings
// @ts-ignore // @ts-ignore
allSettings[categoryKey][settingKey][level] = settingValue as unknown allSettings[categoryKey][settingKey][level] = settingValue as unknown
} })
)
}) })
return allSettings return allSettings
@ -165,8 +163,42 @@ export function shouldHideSetting(
) { ) {
return ( return (
setting.hideOnLevel === settingsLevel || setting.hideOnLevel === settingsLevel ||
setting.hideOnPlatform === 'both' ||
(setting.hideOnPlatform && isTauri() (setting.hideOnPlatform && isTauri()
? setting.hideOnPlatform === 'desktop' ? setting.hideOnPlatform === 'desktop'
: setting.hideOnPlatform === 'web') : setting.hideOnPlatform === 'web')
) )
} }
/**
* Returns true if the setting meets the requirements
* to appear in the settings modal in this context
* based on its config, the current settings level,
* and the current platform
*/
export function shouldShowSettingInput(
setting: Setting<unknown>,
settingsLevel: SettingsLevel
) {
return (
!shouldHideSetting(setting, settingsLevel) &&
(setting.Component ||
['string', 'boolean'].some((t) => typeof setting.default === t) ||
(setting.commandConfig?.inputType &&
['string', 'options', 'boolean'].some(
(t) => setting.commandConfig?.inputType === t
)))
)
}
/**
* Get the appropriate input type to show given a
* command's config. Highly dependent on the filtering logic from
* shouldShowSettingInput being applied
*/
export function getSettingInputType(setting: Setting) {
if (setting.Component) return 'component'
if (setting.commandConfig)
return setting.commandConfig.inputType as 'string' | 'options' | 'boolean'
return typeof setting.default as 'string' | 'boolean'
}

View File

@ -31,7 +31,11 @@ import { Event } from 'xstate'
import { Dialog, RadioGroup, Transition } from '@headlessui/react' import { Dialog, RadioGroup, Transition } from '@headlessui/react'
import { CustomIcon, CustomIconName } from 'components/CustomIcon' import { CustomIcon, CustomIconName } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { shouldHideSetting } from 'lib/settings/settingsUtils' import {
getSettingInputType,
shouldHideSetting,
shouldShowSettingInput,
} from 'lib/settings/settingsUtils'
export const Settings = () => { export const Settings = () => {
const APP_VERSION = import.meta.env.PACKAGE_VERSION || 'unknown' const APP_VERSION = import.meta.env.PACKAGE_VERSION || 'unknown'
@ -235,9 +239,7 @@ export const Settings = () => {
// Filter out settings that don't have a Component or inputType // Filter out settings that don't have a Component or inputType
// or are hidden on the current level or the current platform // or are hidden on the current level or the current platform
(item: [string, Setting<unknown>]) => (item: [string, Setting<unknown>]) =>
!shouldHideSetting(item[1], settingsLevel) && shouldShowSettingInput(item[1], settingsLevel)
(item[1].Component ||
item[1].commandConfig?.inputType)
) )
.map(([settingName, s]) => { .map(([settingName, s]) => {
const setting = s as Setting const setting = s as Setting
@ -484,9 +486,12 @@ function GeneratedSetting({
) )
: [] : []
}, [setting, settingsLevel, context]) }, [setting, settingsLevel, context])
const inputType = getSettingInputType(setting)
if (setting.Component) switch (inputType) {
case 'component':
return ( return (
setting.Component && (
<setting.Component <setting.Component
value={setting[settingsLevel] || setting.getFallback(settingsLevel)} value={setting[settingsLevel] || setting.getFallback(settingsLevel)}
onChange={(e) => { onChange={(e) => {
@ -502,8 +507,7 @@ function GeneratedSetting({
}} }}
/> />
) )
)
switch (setting.commandConfig?.inputType) {
case 'boolean': case 'boolean':
return ( return (
<Toggle <Toggle

View File

@ -84,8 +84,6 @@ export interface StoreState {
showHomeMenu: boolean showHomeMenu: boolean
setHomeShowMenu: (showMenu: boolean) => void setHomeShowMenu: (showMenu: boolean) => void
isBannerDismissed: boolean
setBannerDismissed: (isBannerDismissed: boolean) => void
openPanes: PaneType[] openPanes: PaneType[]
setOpenPanes: (panes: PaneType[]) => void setOpenPanes: (panes: PaneType[]) => void
homeMenuItems: { homeMenuItems: {
@ -150,8 +148,6 @@ export const useStore = create<StoreState>()(
defaultDir: { defaultDir: {
dir: '', dir: '',
}, },
isBannerDismissed: false,
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
openPanes: ['code'], openPanes: ['code'],
setOpenPanes: (openPanes) => set({ openPanes }), setOpenPanes: (openPanes) => set({ openPanes }),
showHomeMenu: true, showHomeMenu: true,