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 />
|
||||
<App />
|
||||
<CommandBar />
|
||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||
</ModelingMachineProvider>
|
||||
<WasmErrBanner />
|
||||
</FileMachineProvider>
|
||||
{!isTauri() && import.meta.env.PROD && <DownloadAppBanner />}
|
||||
</Auth>
|
||||
),
|
||||
children: [
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { useStore } from '../useStore'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useState } from 'react'
|
||||
|
||||
const DownloadAppBanner = () => {
|
||||
const { isBannerDismissed, setBannerDismissed } = useStore((s) => ({
|
||||
isBannerDismissed: s.isBannerDismissed,
|
||||
setBannerDismissed: s.setBannerDismissed,
|
||||
}))
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const [isBannerDismissed, setIsBannerDismissed] = useState(
|
||||
settings.context.app.dismissWebBanner.current
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@ -23,7 +24,7 @@ const DownloadAppBanner = () => {
|
||||
</h2>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => setBannerDismissed(true)}
|
||||
onClick={() => setIsBannerDismissed(true)}
|
||||
icon={{
|
||||
icon: 'close',
|
||||
className: 'p-1',
|
||||
@ -51,6 +52,24 @@ const DownloadAppBanner = () => {
|
||||
</a>{' '}
|
||||
to download the app for the best experience.
|
||||
</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>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
|
@ -150,7 +150,16 @@ function ProjectMenuPopover({
|
||||
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">
|
||||
<ActionButton
|
||||
|
@ -135,6 +135,15 @@ export function createSettings() {
|
||||
onboardingStatus: new Setting<string>({
|
||||
defaultValue: '',
|
||||
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>({
|
||||
defaultValue: '',
|
||||
|
@ -86,7 +86,7 @@ export interface SettingProps<T = unknown> {
|
||||
* Whether to hide the setting on a certain platform.
|
||||
* 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.
|
||||
* If this is not provided but a commandConfig is, the `inputType`
|
||||
|
@ -139,16 +139,14 @@ export function setSettingsAtLevel(
|
||||
Object.entries(newSettings).forEach(([category, settingsCategory]) => {
|
||||
const categoryKey = category as keyof typeof settings
|
||||
if (!allSettings[categoryKey]) return // ignore unrecognized categories
|
||||
Object.entries(settingsCategory).forEach(
|
||||
([settingKey, settingValue]: [string, Setting]) => {
|
||||
Object.entries(settingsCategory).forEach(([settingKey, settingValue]) => {
|
||||
// TODO: How do you get a valid type for allSettings[categoryKey][settingKey]?
|
||||
// it seems to always collapses to `never`, which is not correct
|
||||
// @ts-ignore
|
||||
if (!allSettings[categoryKey][settingKey]) return // ignore unrecognized settings
|
||||
// @ts-ignore
|
||||
allSettings[categoryKey][settingKey][level] = settingValue as unknown
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
return allSettings
|
||||
@ -165,8 +163,42 @@ export function shouldHideSetting(
|
||||
) {
|
||||
return (
|
||||
setting.hideOnLevel === settingsLevel ||
|
||||
setting.hideOnPlatform === 'both' ||
|
||||
(setting.hideOnPlatform && isTauri()
|
||||
? setting.hideOnPlatform === 'desktop'
|
||||
: 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 { CustomIcon, CustomIconName } from 'components/CustomIcon'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { shouldHideSetting } from 'lib/settings/settingsUtils'
|
||||
import {
|
||||
getSettingInputType,
|
||||
shouldHideSetting,
|
||||
shouldShowSettingInput,
|
||||
} from 'lib/settings/settingsUtils'
|
||||
|
||||
export const Settings = () => {
|
||||
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
|
||||
// or are hidden on the current level or the current platform
|
||||
(item: [string, Setting<unknown>]) =>
|
||||
!shouldHideSetting(item[1], settingsLevel) &&
|
||||
(item[1].Component ||
|
||||
item[1].commandConfig?.inputType)
|
||||
shouldShowSettingInput(item[1], settingsLevel)
|
||||
)
|
||||
.map(([settingName, s]) => {
|
||||
const setting = s as Setting
|
||||
@ -484,9 +486,12 @@ function GeneratedSetting({
|
||||
)
|
||||
: []
|
||||
}, [setting, settingsLevel, context])
|
||||
const inputType = getSettingInputType(setting)
|
||||
|
||||
if (setting.Component)
|
||||
switch (inputType) {
|
||||
case 'component':
|
||||
return (
|
||||
setting.Component && (
|
||||
<setting.Component
|
||||
value={setting[settingsLevel] || setting.getFallback(settingsLevel)}
|
||||
onChange={(e) => {
|
||||
@ -502,8 +507,7 @@ function GeneratedSetting({
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
switch (setting.commandConfig?.inputType) {
|
||||
)
|
||||
case 'boolean':
|
||||
return (
|
||||
<Toggle
|
||||
|
@ -84,8 +84,6 @@ export interface StoreState {
|
||||
|
||||
showHomeMenu: boolean
|
||||
setHomeShowMenu: (showMenu: boolean) => void
|
||||
isBannerDismissed: boolean
|
||||
setBannerDismissed: (isBannerDismissed: boolean) => void
|
||||
openPanes: PaneType[]
|
||||
setOpenPanes: (panes: PaneType[]) => void
|
||||
homeMenuItems: {
|
||||
@ -150,8 +148,6 @@ export const useStore = create<StoreState>()(
|
||||
defaultDir: {
|
||||
dir: '',
|
||||
},
|
||||
isBannerDismissed: false,
|
||||
setBannerDismissed: (isBannerDismissed) => set({ isBannerDismissed }),
|
||||
openPanes: ['code'],
|
||||
setOpenPanes: (openPanes) => set({ openPanes }),
|
||||
showHomeMenu: true,
|
||||
|
Reference in New Issue
Block a user