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:
@ -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: [
|
||||||
|
@ -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 > Dismiss Web Banner setting
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@ -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
|
||||||
|
@ -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: '',
|
||||||
|
@ -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`
|
||||||
|
@ -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'
|
||||||
|
}
|
||||||
|
@ -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,26 +486,28 @@ function GeneratedSetting({
|
|||||||
)
|
)
|
||||||
: []
|
: []
|
||||||
}, [setting, settingsLevel, context])
|
}, [setting, settingsLevel, context])
|
||||||
|
const inputType = getSettingInputType(setting)
|
||||||
|
|
||||||
if (setting.Component)
|
switch (inputType) {
|
||||||
return (
|
case 'component':
|
||||||
<setting.Component
|
return (
|
||||||
value={setting[settingsLevel] || setting.getFallback(settingsLevel)}
|
setting.Component && (
|
||||||
onChange={(e) => {
|
<setting.Component
|
||||||
if ('value' in e.target) {
|
value={setting[settingsLevel] || setting.getFallback(settingsLevel)}
|
||||||
send({
|
onChange={(e) => {
|
||||||
type: `set.${category}.${settingName}`,
|
if ('value' in e.target) {
|
||||||
data: {
|
send({
|
||||||
level: settingsLevel,
|
type: `set.${category}.${settingName}`,
|
||||||
value: e.target.value,
|
data: {
|
||||||
},
|
level: settingsLevel,
|
||||||
} as unknown as Event<WildcardSetEvent>)
|
value: e.target.value,
|
||||||
}
|
},
|
||||||
}}
|
} as unknown as Event<WildcardSetEvent>)
|
||||||
/>
|
}
|
||||||
)
|
}}
|
||||||
|
/>
|
||||||
switch (setting.commandConfig?.inputType) {
|
)
|
||||||
|
)
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return (
|
return (
|
||||||
<Toggle
|
<Toggle
|
||||||
|
@ -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,
|
||||||
|
Reference in New Issue
Block a user