diff --git a/desktop/app/src/PluginContainer.tsx b/desktop/app/src/PluginContainer.tsx index 8d5baf8de..3fd1b9669 100644 --- a/desktop/app/src/PluginContainer.tsx +++ b/desktop/app/src/PluginContainer.tsx @@ -46,10 +46,17 @@ import {Message} from './reducers/pluginMessageQueue'; import {Idler} from './utils/Idler'; import {processMessageQueue} from './utils/messageQueue'; import {ToggleButton, SmallText, Layout} from './ui'; -import {TrackingScope, _SandyPluginRenderer} from 'flipper-plugin'; +import {theme, TrackingScope, _SandyPluginRenderer} from 'flipper-plugin'; import {isDevicePluginDefinition} from './utils/pluginUtils'; import ArchivedDevice from './devices/ArchivedDevice'; import {ContentContainer} from './sandy-chrome/ContentContainer'; +import {Alert, Typography} from 'antd'; +import {InstalledPluginDetails} from 'plugin-lib'; +import semver from 'semver'; +import {activatePlugin} from './reducers/pluginManager'; +import {produce} from 'immer'; + +const {Text, Link} = Typography; const Container = styled(FlexColumn)({ width: 0, @@ -109,6 +116,7 @@ type StateFromProps = { pendingMessages: Message[] | undefined; pluginIsEnabled: boolean; settingsState: Settings; + latestInstalledVersion: InstalledPluginDetails | undefined; }; type DispatchFromProps = { @@ -120,17 +128,24 @@ type DispatchFromProps = { setPluginState: (payload: {pluginKey: string; state: any}) => void; setStaticView: (payload: StaticView) => void; starPlugin: typeof starPlugin; + activatePlugin: typeof activatePlugin; }; type Props = StateFromProps & DispatchFromProps & OwnProps; type State = { progress: {current: number; total: number}; + autoUpdateAlertSuppressed: Set; }; class PluginContainer extends PureComponent { static contextType = ReactReduxContext; + constructor(props: Props) { + super(props); + this.reloadPlugin = this.reloadPlugin.bind(this); + } + plugin: | FlipperPlugin | FlipperDevicePlugin @@ -160,7 +175,10 @@ class PluginContainer extends PureComponent { idler?: Idler; pluginBeingProcessed: string | null = null; - state = {progress: {current: 0, total: 0}}; + state = { + progress: {current: 0, total: 0}, + autoUpdateAlertSuppressed: new Set(), + }; get store(): MiddlewareAPI { return this.context.store; @@ -200,7 +218,11 @@ class PluginContainer extends PureComponent { if (pluginKey !== this.pluginBeingProcessed) { this.pluginBeingProcessed = pluginKey; this.cancelCurrentQueue(); - this.setState({progress: {current: 0, total: 0}}); + this.setState((state) => + produce(state, (draft) => { + draft.progress = {current: 0, total: 0}; + }), + ); // device plugins don't have connections so no message queues if (!activePlugin || isDevicePluginDefinition(activePlugin)) { return; @@ -222,7 +244,11 @@ class PluginContainer extends PureComponent { pluginKey, this.store, (progress) => { - this.setState({progress}); + this.setState((state) => + produce(state, (draft) => { + draft.progress = progress; + }), + ); }, this.idler, ).then((completed) => { @@ -353,6 +379,17 @@ class PluginContainer extends PureComponent { ); } + reloadPlugin() { + const {activatePlugin, latestInstalledVersion} = this.props; + if (latestInstalledVersion) { + activatePlugin({ + plugin: latestInstalledVersion, + enable: false, + notifyIfFailed: true, + }); + } + } + renderPlugin() { const { pluginState, @@ -364,12 +401,20 @@ class PluginContainer extends PureComponent { selectedApp, settingsState, isSandy, + latestInstalledVersion, } = this.props; if (!activePlugin || !target || !pluginKey) { console.warn(`No selected plugin. Rendering empty!`); return this.renderNoPluginActive(); } let pluginElement: null | React.ReactElement; + const showUpdateAlert = + latestInstalledVersion && + activePlugin && + !this.state.autoUpdateAlertSuppressed.has( + `${latestInstalledVersion.name}@${latestInstalledVersion.version}`, + ) && + semver.gt(latestInstalledVersion.version, activePlugin.version); if (isSandyPlugin(activePlugin)) { // Make sure we throw away the container for different pluginKey! const instance = target.sandyPluginStates.get(activePlugin.id); @@ -438,15 +483,44 @@ class PluginContainer extends PureComponent { ); } return isSandy ? ( - - - {pluginElement} - - - + +
+ {showUpdateAlert && ( + + Plugin "{activePlugin.title}" v + {latestInstalledVersion?.version} downloaded and ready to + install. Reload to + start using new version. + + } + type="info" + onClose={() => + this.setState((state) => + produce(state, (draft) => { + draft.autoUpdateAlertSuppressed.add( + `${latestInstalledVersion?.name}@${latestInstalledVersion?.version}`, + ); + }), + ) + } + style={{marginBottom: theme.space.large}} + showIcon + closable + /> + )} +
+ + + {pluginElement} + + + +
) : ( @@ -475,6 +549,7 @@ export default connect( }, pluginStates, plugins: {devicePlugins, clientPlugins}, + pluginManager: {installedPlugins}, pluginMessageQueue, settingsState, }) => { @@ -525,6 +600,9 @@ export default connect( pendingMessages, pluginIsEnabled, settingsState, + latestInstalledVersion: installedPlugins.get( + activePlugin?.packageName ?? '', + ), }; return s; }, @@ -533,5 +611,6 @@ export default connect( selectPlugin, setStaticView, starPlugin, + activatePlugin, }, )(PluginContainer); diff --git a/desktop/app/src/chrome/SettingsSheet.tsx b/desktop/app/src/chrome/SettingsSheet.tsx index fba40db33..af083cb0b 100644 --- a/desktop/app/src/chrome/SettingsSheet.tsx +++ b/desktop/app/src/chrome/SettingsSheet.tsx @@ -30,6 +30,7 @@ import {reportUsage} from '../utils/metrics'; import {Modal, message} from 'antd'; import {Layout, withTrackingScope, _NuxManagerContext} from 'flipper-plugin'; import GK from '../fb-stubs/GK'; +import ReleaseChannel from '../ReleaseChannel'; const Container = styled(FlexColumn)({ padding: 20, @@ -137,6 +138,9 @@ class SettingsSheet extends Component { disableSandy, darkMode, } = this.state.updatedSettings; + + const {releaseChannel} = this.state.updatedLauncherSettings; + const {useSandy} = this.props; const settingsPristine = @@ -255,20 +259,22 @@ class SettingsSheet extends Component { }); }} /> - {GK.get('flipper_sandy') && !disableSandy && ( - { - this.setState((prevState) => ({ - updatedSettings: { - ...prevState.updatedSettings, - darkMode: enabled, - }, - })); - }} - /> - )} + {(GK.get('flipper_sandy') || + releaseChannel == ReleaseChannel.INSIDERS) && + !disableSandy && ( + { + this.setState((prevState) => ({ + updatedSettings: { + ...prevState.updatedSettings, + darkMode: enabled, + }, + })); + }} + /> + )} ; }; type DispatchFromProps = { @@ -289,7 +289,7 @@ function InstallButton(props: { function useNPMSearch( query: string, onInstall: () => void, - installedPlugins: PluginDetails[], + installedPlugins: Map, ): TableRows_immutable { useEffect(() => { reportUsage(`${TAG}:open`); diff --git a/desktop/app/src/dispatcher/fb-stubs/pluginAutoUpdate.tsx b/desktop/app/src/dispatcher/fb-stubs/pluginAutoUpdate.tsx deleted file mode 100644 index 129d408f6..000000000 --- a/desktop/app/src/dispatcher/fb-stubs/pluginAutoUpdate.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - */ - -export default () => { - // Auto-updates of plugins not implemented in public version of Flipper -}; diff --git a/desktop/app/src/dispatcher/index.tsx b/desktop/app/src/dispatcher/index.tsx index b66a95f2f..e0d4f77aa 100644 --- a/desktop/app/src/dispatcher/index.tsx +++ b/desktop/app/src/dispatcher/index.tsx @@ -20,7 +20,6 @@ import plugins from './plugins'; import user from './user'; import pluginManager from './pluginManager'; import reactNative from './reactNative'; -import pluginAutoUpdate from './fb-stubs/pluginAutoUpdate'; import pluginMarketplace from './fb-stubs/pluginMarketplace'; import pluginDownloads from './pluginDownloads'; @@ -48,7 +47,6 @@ export default function (store: Store, logger: Logger): () => Promise { user, pluginManager, reactNative, - pluginAutoUpdate, pluginMarketplace, pluginDownloads, ].filter(notNull); diff --git a/desktop/app/src/dispatcher/pluginDownloads.tsx b/desktop/app/src/dispatcher/pluginDownloads.tsx index 16f52533a..b6977cc0a 100644 --- a/desktop/app/src/dispatcher/pluginDownloads.tsx +++ b/desktop/app/src/dispatcher/pluginDownloads.tsx @@ -11,9 +11,10 @@ import { DownloadablePluginDetails, getInstalledPluginDetails, getPluginVersionInstallationDir, + InstalledPluginDetails, installPluginFromFile, } from 'flipper-plugin-lib'; -import {Store} from '../reducers/index'; +import {Actions, State, Store} from '../reducers/index'; import { PluginDownloadStatus, pluginDownloadStarted, @@ -26,12 +27,16 @@ import path from 'path'; import tmp from 'tmp'; import {promisify} from 'util'; import {requirePlugin} from './plugins'; -import {registerPluginUpdate, setStaticView} from '../reducers/connections'; -import {notification, Typography} from 'antd'; +import {registerPluginUpdate, selectPlugin} from '../reducers/connections'; +import {Button} from 'antd'; import React from 'react'; -import {ConsoleLogs} from '../chrome/ConsoleLogs'; - -const {Text, Link} = Typography; +import {reportUsage} from '../utils/metrics'; +import {addNotification, removeNotification} from '../reducers/notifications'; +import reloadFlipper from '../utils/reloadFlipper'; +import {activatePlugin, pluginInstalled} from '../reducers/pluginManager'; +import {Dispatch} from 'redux'; +import {showErrorNotification} from '../utils/notifications'; +import isSandyEnabled from '../utils/isSandyEnabled'; // Adapter which forces node.js implementation for axios instead of browser implementation // used by default in Electron. Node.js implementation is better, because it @@ -71,6 +76,7 @@ async function handlePluginDownload( ); const tmpDir = await getTempDirName(); const tmpFile = path.join(tmpDir, `${name}-${version}.tgz`); + let installedPlugin: InstalledPluginDetails | undefined; try { const cancellationSource = axios.CancelToken.source(); dispatch( @@ -80,6 +86,7 @@ async function handlePluginDownload( console.log( `Using existing files instead of downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}"`, ); + installedPlugin = await getInstalledPluginDetails(installationDir); } else { await fs.ensureDir(tmpDir); let percentCompleted = 0; @@ -111,17 +118,19 @@ async function handlePluginDownload( await new Promise((resolve, reject) => writeStream.once('finish', resolve).once('error', reject), ); - await installPluginFromFile(tmpFile); + installedPlugin = await installPluginFromFile(tmpFile); + dispatch(pluginInstalled(installedPlugin)); } - const installedPlugin = await getInstalledPluginDetails(installationDir); - if (!store.getState().plugins.clientPlugins.has(plugin.id)) { - const pluginDefinition = requirePlugin(installedPlugin); + if (pluginIsDisabledForAllConnectedClients(store.getState(), plugin)) { dispatch( - registerPluginUpdate({ - plugin: pluginDefinition, - enablePlugin: startedByUser, + activatePlugin({ + plugin: installedPlugin, + enable: startedByUser, + notifyIfFailed: startedByUser, }), ); + } else if (!isSandyEnabled()) { + notifyAboutUpdatedPluginNonSandy(installedPlugin, store.dispatch); } console.log( `Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`, @@ -132,22 +141,80 @@ async function handlePluginDownload( error, ); if (startedByUser) { - notification.error({ - message: `Failed to install plugin "${title}".`, - description: ( - - See{' '} - dispatch(setStaticView(ConsoleLogs))}> - logs - {' '} - for details. - - ), - placement: 'bottomLeft', - }); + showErrorNotification( + `Failed to download plugin "${title}" v${version}.`, + ); } } finally { dispatch(pluginDownloadFinished({plugin})); await fs.remove(tmpDir); } } + +function pluginIsDisabledForAllConnectedClients( + state: State, + plugin: DownloadablePluginDetails, +) { + return ( + !state.plugins.clientPlugins.has(plugin.id) || + !state.connections.clients.some((c) => + state.connections.userStarredPlugins[c.query.app]?.includes(plugin.id), + ) + ); +} + +function notifyAboutUpdatedPluginNonSandy( + plugin: InstalledPluginDetails, + dispatch: Dispatch, +) { + const {name, version, title, id} = plugin; + const reloadPluginAndRemoveNotification = () => { + reportUsage('plugin-auto-update:notification:reloadClicked', undefined, id); + dispatch( + registerPluginUpdate({ + plugin: requirePlugin(plugin), + enablePlugin: false, + }), + ); + dispatch( + removeNotification({ + pluginId: 'plugin-auto-update', + client: null, + notificationId: `auto-update.${name}.${version}`, + }), + ); + dispatch( + selectPlugin({ + selectedPlugin: id, + deepLinkPayload: null, + }), + ); + }; + const reloadAll = () => { + reportUsage('plugin-auto-update:notification:reloadAllClicked'); + reloadFlipper(); + }; + dispatch( + addNotification({ + pluginId: 'plugin-auto-update', + client: null, + notification: { + id: `auto-update.${name}.${version}`, + title: `${title} ${version} is ready to install`, + message: ( +
+ {title} {version} has been downloaded. Reload is required to apply + the update.{' '} + + +
+ ), + severity: 'warning', + timestamp: Date.now(), + category: `Plugin Auto Update`, + }, + }), + ); +} diff --git a/desktop/app/src/dispatcher/pluginManager.tsx b/desktop/app/src/dispatcher/pluginManager.tsx index 48a53931a..06d4e9257 100644 --- a/desktop/app/src/dispatcher/pluginManager.tsx +++ b/desktop/app/src/dispatcher/pluginManager.tsx @@ -9,12 +9,19 @@ import {Store} from '../reducers/index'; import {Logger} from '../fb-interfaces/Logger'; -import {registerInstalledPlugins} from '../reducers/pluginManager'; +import { + pluginActivationHandled, + registerInstalledPlugins, +} from '../reducers/pluginManager'; import { getInstalledPlugins, cleanupOldInstalledPluginVersions, removePlugins, } from 'flipper-plugin-lib'; +import {sideEffect} from '../utils/sideEffect'; +import {requirePlugin} from './plugins'; +import {registerPluginUpdate} from '../reducers/connections'; +import {showErrorNotification} from '../utils/notifications'; const maxInstalledPluginVersionsToKeep = 2; @@ -32,4 +39,35 @@ export default (store: Store, _logger: Logger) => { window.requestIdleCallback(() => { refreshInstalledPlugins(store); }); + + sideEffect( + store, + {name: 'handlePluginActivation', throttleMs: 1000, fireImmediately: true}, + (state) => state.pluginManager.pluginActivationQueue, + (queue, store) => { + for (const request of queue) { + try { + const plugin = requirePlugin(request.plugin); + const enablePlugin = request.enable; + store.dispatch( + registerPluginUpdate({ + plugin, + enablePlugin, + }), + ); + } catch (err) { + console.error( + `Failed to activate plugin ${request.plugin.title} v${request.plugin.version}`, + err, + ); + if (request.notifyIfFailed) { + showErrorNotification( + `Failed to load plugin "${request.plugin.title}" v${request.plugin.version}`, + ); + } + } + } + store.dispatch(pluginActivationHandled(queue.length)); + }, + ); }; diff --git a/desktop/app/src/init.tsx b/desktop/app/src/init.tsx index 30e9ff4d5..ce480b759 100644 --- a/desktop/app/src/init.tsx +++ b/desktop/app/src/init.tsx @@ -20,7 +20,6 @@ import {Store} from './reducers/index'; import dispatcher from './dispatcher/index'; import TooltipProvider from './ui/components/TooltipProvider'; import config from './utils/processConfig'; -import appConfig from '../src/fb-stubs/config'; import {initLauncherHooks} from './utils/launcher'; import {setPersistor} from './utils/persistor'; import React from 'react'; @@ -43,7 +42,7 @@ import { _LoggerContext, } from 'flipper-plugin'; import isProduction from './utils/isProduction'; -import ReleaseChannel from './ReleaseChannel'; +import isSandyEnabled from './utils/isSandyEnabled'; if (process.env.NODE_ENV === 'development' && os.platform() === 'darwin') { // By default Node.JS has its internal certificate storage and doesn't use @@ -125,10 +124,7 @@ function init() { store, {name: 'loadTheme', fireImmediately: true, throttleMs: 500}, (state) => ({ - sandy: - (GK.get('flipper_sandy') || - appConfig.getReleaseChannel() === ReleaseChannel.INSIDERS) && - !state.settingsState.disableSandy, + sandy: isSandyEnabled(), dark: state.settingsState.darkMode, }), (theme) => { diff --git a/desktop/app/src/reducers/__tests__/pluginManager.node.tsx b/desktop/app/src/reducers/__tests__/pluginManager.node.tsx index bd2778650..4d7f5dd08 100644 --- a/desktop/app/src/reducers/__tests__/pluginManager.node.tsx +++ b/desktop/app/src/reducers/__tests__/pluginManager.node.tsx @@ -12,7 +12,7 @@ import {InstalledPluginDetails} from 'flipper-plugin-lib'; test('reduce empty registerInstalledPlugins', () => { const result = reducer(undefined, registerInstalledPlugins([])); - expect(result.installedPlugins).toEqual([]); + expect(result.installedPlugins).toEqual(new Map()); }); const EXAMPLE_PLUGIN = { @@ -32,7 +32,9 @@ const EXAMPLE_PLUGIN = { test('reduce registerInstalledPlugins, clear again', () => { const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN])); - expect(result.installedPlugins).toEqual([EXAMPLE_PLUGIN]); + expect(result.installedPlugins).toEqual( + new Map([[EXAMPLE_PLUGIN.name, EXAMPLE_PLUGIN]]), + ); const result2 = reducer(result, registerInstalledPlugins([])); - expect(result2.installedPlugins).toEqual([]); + expect(result2.installedPlugins).toEqual(new Map()); }); diff --git a/desktop/app/src/reducers/pluginManager.tsx b/desktop/app/src/reducers/pluginManager.tsx index 7d3664b88..b1130c118 100644 --- a/desktop/app/src/reducers/pluginManager.tsx +++ b/desktop/app/src/reducers/pluginManager.tsx @@ -8,13 +8,24 @@ */ import {Actions} from './'; -import {InstalledPluginDetails} from 'flipper-plugin-lib'; +import { + ActivatablePluginDetails, + InstalledPluginDetails, +} from 'flipper-plugin-lib'; import {PluginDefinition} from '../plugin'; import {produce} from 'immer'; +import semver from 'semver'; export type State = { - installedPlugins: InstalledPluginDetails[]; + installedPlugins: Map; uninstalledPlugins: Set; + pluginActivationQueue: PluginActivationRequest[]; +}; + +export type PluginActivationRequest = { + plugin: ActivatablePluginDetails; + enable: boolean; + notifyIfFailed: boolean; }; export type Action = @@ -26,11 +37,24 @@ export type Action = // Implemented by rootReducer in `store.tsx` type: 'UNINSTALL_PLUGIN'; payload: PluginDefinition; + } + | { + type: 'PLUGIN_INSTALLED'; + payload: InstalledPluginDetails; + } + | { + type: 'ACTIVATE_PLUGINS'; + payload: PluginActivationRequest[]; + } + | { + type: 'PLUGIN_ACTIVATION_HANDLED'; + payload: number; }; const INITIAL_STATE: State = { - installedPlugins: [], + installedPlugins: new Map(), uninstalledPlugins: new Set(), + pluginActivationQueue: [], }; export default function reducer( @@ -39,10 +63,28 @@ export default function reducer( ): State { if (action.type === 'REGISTER_INSTALLED_PLUGINS') { return produce(state, (draft) => { - draft.installedPlugins = action.payload.filter( - (p) => !state.uninstalledPlugins?.has(p.name), + draft.installedPlugins = new Map( + action.payload + .filter((p) => !state.uninstalledPlugins?.has(p.name)) + .map((p) => [p.name, p]), ); }); + } else if (action.type === 'PLUGIN_INSTALLED') { + const plugin = action.payload; + return produce(state, (draft) => { + const existing = draft.installedPlugins.get(plugin.name); + if (!existing || semver.gt(plugin.version, existing.version)) { + draft.installedPlugins.set(plugin.name, plugin); + } + }); + } else if (action.type === 'ACTIVATE_PLUGINS') { + return produce(state, (draft) => { + draft.pluginActivationQueue.push(...action.payload); + }); + } else if (action.type === 'PLUGIN_ACTIVATION_HANDLED') { + return produce(state, (draft) => { + draft.pluginActivationQueue.splice(0, action.payload); + }); } else { return {...state}; } @@ -59,3 +101,18 @@ export const uninstallPlugin = (payload: PluginDefinition): Action => ({ type: 'UNINSTALL_PLUGIN', payload, }); + +export const pluginInstalled = (payload: InstalledPluginDetails): Action => ({ + type: 'PLUGIN_INSTALLED', + payload, +}); + +export const activatePlugin = (payload: PluginActivationRequest): Action => ({ + type: 'ACTIVATE_PLUGINS', + payload: [payload], +}); + +export const pluginActivationHandled = (payload: number): Action => ({ + type: 'PLUGIN_ACTIVATION_HANDLED', + payload, +}); diff --git a/desktop/app/src/utils/isSandyEnabled.tsx b/desktop/app/src/utils/isSandyEnabled.tsx new file mode 100644 index 000000000..5249c9ba4 --- /dev/null +++ b/desktop/app/src/utils/isSandyEnabled.tsx @@ -0,0 +1,21 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import config from '../fb-stubs/config'; +import GK from '../fb-stubs/GK'; +import ReleaseChannel from '../ReleaseChannel'; +import {store} from '../store'; + +export default function isSandyEnabled() { + return ( + (GK.get('flipper_sandy') || + config.getReleaseChannel() === ReleaseChannel.INSIDERS) && + !store.getState().settingsState.disableSandy + ); +} diff --git a/desktop/app/src/utils/notifications.tsx b/desktop/app/src/utils/notifications.tsx new file mode 100644 index 000000000..4e1317133 --- /dev/null +++ b/desktop/app/src/utils/notifications.tsx @@ -0,0 +1,32 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import {notification, Typography} from 'antd'; +import React from 'react'; +import {ConsoleLogs} from '../chrome/ConsoleLogs'; +import {setStaticView} from '../reducers/connections'; +import {store} from '../store'; + +const {Text, Link} = Typography; + +export function showErrorNotification(message: string) { + notification.error({ + message, + description: ( + + See{' '} + store.dispatch(setStaticView(ConsoleLogs))}> + logs + {' '} + for details. + + ), + placement: 'bottomLeft', + }); +} diff --git a/desktop/app/src/utils/useIsDarkMode.tsx b/desktop/app/src/utils/useIsDarkMode.tsx index 1dba64626..20abaf33e 100644 --- a/desktop/app/src/utils/useIsDarkMode.tsx +++ b/desktop/app/src/utils/useIsDarkMode.tsx @@ -8,20 +8,12 @@ */ import {useStore} from '../../../app/src/utils/useStore'; -import config from '../fb-stubs/config'; -import GK from '../fb-stubs/GK'; -import ReleaseChannel from '../ReleaseChannel'; +import isSandyEnabled from './isSandyEnabled'; /** * This hook returns whether dark mode is currently being used. * Generally should be avoided in favor of using the above theme object, * which will provide colors that reflect the theme */ export function useIsDarkMode(): boolean { - return useStore( - (state) => - (GK.get('flipper_sandy') || - config.getReleaseChannel() === ReleaseChannel.INSIDERS) && - !state.settingsState.disableSandy && - state.settingsState.darkMode, - ); + return useStore((state) => isSandyEnabled() && state.settingsState.darkMode); } diff --git a/desktop/plugin-lib/src/pluginInstaller.ts b/desktop/plugin-lib/src/pluginInstaller.ts index 49daa678e..11ce2ec95 100644 --- a/desktop/plugin-lib/src/pluginInstaller.ts +++ b/desktop/plugin-lib/src/pluginInstaller.ts @@ -230,6 +230,7 @@ async function getInstalledPluginVersionDirs(): Promise< pmap(dirs, (dir) => fs .readdir(dir) + .then((versionDirs) => versionDirs.filter((d) => semver.valid(d))) .then((versionDirs) => versionDirs.sort((v1, v2) => semver.compare(v2, v1, true)), ) diff --git a/desktop/scripts/start-dev-server.ts b/desktop/scripts/start-dev-server.ts index eeca20985..6de165085 100644 --- a/desktop/scripts/start-dev-server.ts +++ b/desktop/scripts/start-dev-server.ts @@ -50,6 +50,11 @@ const argv = yargs '[FB-internal only] Enable plugin auto-updates. The flag is disabled by default in dev mode. Env var FLIPPER_NO_PLUGIN_AUTO_UPDATE is equivalent to the command-line option "--no-plugin-auto-update"', type: 'boolean', }, + 'plugin-auto-update-interval': { + describe: + '[FB-internal only] Set custom interval in milliseconds for plugin auto-update checks. Env var FLIPPER_PLUGIN_AUTO_UPDATE_POLLING_INTERVAL is equivalent to this command-line option.', + type: 'number', + }, 'enabled-plugins': { describe: 'Load only specified plugins and skip loading rest. This is useful when you are developing only one or few plugins. Plugins to load can be specified as a comma-separated list with either plugin id or name used as identifier, e.g. "--enabled-plugins network,inspector". The flag is not provided by default which means that all plugins loaded.', @@ -115,6 +120,10 @@ if ( process.env.FLIPPER_DISABLE_PLUGIN_AUTO_UPDATE = 'true'; } +if (argv['plugin-auto-update-interval']) { + process.env.FLIPPER_PLUGIN_AUTO_UPDATE_POLLING_INTERVAL = `${argv['plugin-auto-update-interval']}`; +} + // Force participating in all GKs. Mostly intersting for Flipper team members. if (argv['enable-all-gks'] === true) { process.env.FLIPPER_ENABLE_ALL_GKS = 'true';