diff --git a/docs/extending/js-plugin-api.md b/docs/extending/js-plugin-api.md index b05374471..b991fc615 100644 --- a/docs/extending/js-plugin-api.md +++ b/docs/extending/js-plugin-api.md @@ -80,7 +80,13 @@ static persistedStateReducer( The job of the `persistedStateReducer` is to merge incoming data into the state, so that next time the plugin is activated, the persisted state will be ready. If a plugin has a `persistedStateReducer`, and the plugin is not open in flipper, incoming messages are queued until the plugin is opened. -The amount of events that is cached for a plugin is controlled by the optional static field `maxQueueSize`, which by default is set to `10000` events. +The number of events that are cached for a plugin is controlled by the optional static field `maxQueueSize`, which defaults to `10000` events. + +
+ +Note that if a plugin is not starred by the user, it will not receive any messages when it is not selected by the user. Even when it has a `persistedStateReducer`. This prevents plugins that are not actively used by the user from wasting a lot of CPU / memory. + +
The data that is produced from `persistedStateReducer` should be immutable, but also structurally sharing unchanged parts of the state with the previous state to avoid performance hiccups. To simplify this process we recommend using the [Immer](https://immerjs.github.io/immer/docs/introduction) package. Immer makes it possible to keep the reducer concise by directly supporting "writing" to the current state, and keeping track of that in the background. diff --git a/src/PluginContainer.tsx b/src/PluginContainer.tsx index 2bd7e5920..7d2ace44a 100644 --- a/src/PluginContainer.tsx +++ b/src/PluginContainer.tsx @@ -33,7 +33,7 @@ import React, {PureComponent} from 'react'; import {connect, ReactReduxContext} from 'react-redux'; import {setPluginState} from './reducers/pluginStates'; import {selectPlugin} from './reducers/connections'; -import {State as Store} from './reducers/index'; +import {State as Store, MiddlewareAPI} from './reducers/index'; import {activateMenuItems} from './MenuBar'; import {Message} from './reducers/pluginMessageQueue'; import {Idler} from './utils/Idler'; @@ -146,6 +146,10 @@ class PluginContainer extends PureComponent { state = {progress: {current: 0, total: 0}}; + get store(): MiddlewareAPI { + return this.context.store; + } + componentWillUnmount() { if (this.plugin) { this.plugin._teardown(); @@ -179,7 +183,7 @@ class PluginContainer extends PureComponent { processMessageQueue( activePlugin, pluginKey, - this.context.store, + this.store, progress => { this.setState({progress}); }, diff --git a/src/chrome/StatusBar.tsx b/src/chrome/StatusBar.tsx index 43fd2405b..3e3163013 100644 --- a/src/chrome/StatusBar.tsx +++ b/src/chrome/StatusBar.tsx @@ -8,11 +8,12 @@ */ import {colors} from '../ui/components/colors'; -import {styled} from '../ui'; +import {styled, Glyph} from '../ui'; import {connect} from 'react-redux'; import {State} from '../reducers'; import React, {ReactElement} from 'react'; import Text from '../ui/components/Text'; +import {pluginIsStarred} from '../reducers/connections'; const StatusBarContainer = styled(Text)({ backgroundColor: colors.macOSTitleBarBackgroundBlur, @@ -21,17 +22,18 @@ const StatusBarContainer = styled(Text)({ padding: '0 10px', textOverflow: 'ellipsis', overflow: 'hidden', + textAlign: 'center', }); type Props = { - statusMessage: string | null; + statusMessage: React.ReactNode | string | null; }; export function statusBarView(props: Props): ReactElement | null { const {statusMessage} = props; if (statusMessage) { return ( - + {statusMessage} ); @@ -40,13 +42,45 @@ export function statusBarView(props: Props): ReactElement | null { } } -export default connect( - ({application: {statusMessages}}) => { - if (statusMessages.length > 0) { - return {statusMessage: statusMessages[statusMessages.length - 1]}; - } +export default connect((state: State) => { + const { + application: {statusMessages}, + } = state; + if (statusMessages.length > 0) { + return {statusMessage: statusMessages[statusMessages.length - 1]}; + } else if (isPreviewingBackgroundPlugin(state)) { return { - statusMessage: null, + statusMessage: ( + <> + + The current plugin would like to send messages while it is in the + background. However, since this plugin is not starred, these messages + will be dropped. Star this plugin to unlock its full capabilities. + + ), }; - }, -)(statusBarView); + } + return { + statusMessage: null, + }; +})(statusBarView); + +function isPreviewingBackgroundPlugin(state: State): boolean { + const { + connections: {selectedApp, selectedPlugin}, + } = state; + if (!selectedPlugin || !selectedApp) { + return false; + } + const activePlugin = state.plugins.clientPlugins.get(selectedPlugin); + if (!activePlugin || !activePlugin.persistedStateReducer) { + return false; + } + return !pluginIsStarred(state.connections, selectedPlugin); +} diff --git a/src/reducers/connections.tsx b/src/reducers/connections.tsx index c88427379..bf3220166 100644 --- a/src/reducers/connections.tsx +++ b/src/reducers/connections.tsx @@ -7,6 +7,8 @@ * @format */ +import {produce} from 'immer'; + import BaseDevice from '../devices/BaseDevice'; import MacDevice from '../devices/MacDevice'; import Client from '../Client'; @@ -23,7 +25,8 @@ import NotificationScreen from '../chrome/NotificationScreen'; import SupportRequestForm from '../fb-stubs/SupportRequestFormManager'; import SupportRequestFormV2 from '../fb-stubs/SupportRequestFormV2'; import SupportRequestDetails from '../fb-stubs/SupportRequestDetails'; -import {produce} from 'immer'; +import {getPluginKey} from '../utils/pluginUtils'; +import {deconstructClientId} from '../utils/clientUtils'; export type StaticView = | null @@ -581,3 +584,23 @@ function updateSelection(state: Readonly): State { return {...state, ...updates}; } + +export function getSelectedPluginKey(state: State): string | undefined { + return state.selectedPlugin + ? getPluginKey( + state.selectedApp, + state.selectedDevice, + state.selectedPlugin, + ) + : undefined; +} + +export function pluginIsStarred(state: State, pluginId: string): boolean { + const {selectedApp} = state; + if (!selectedApp) { + return false; + } + const appInfo = deconstructClientId(selectedApp); + const starred = state.userStarredPlugins[appInfo.app]; + return starred && starred.indexOf(pluginId) > -1; +} diff --git a/src/utils/messageQueue.tsx b/src/utils/messageQueue.tsx index d57b3516b..2b64a0a46 100644 --- a/src/utils/messageQueue.tsx +++ b/src/utils/messageQueue.tsx @@ -17,8 +17,7 @@ import { Message, } from '../reducers/pluginMessageQueue'; import {Idler, BaseIdler} from './Idler'; -import {getPluginKey} from './pluginUtils'; -import {deconstructClientId} from './clientUtils'; +import {pluginIsStarred, getSelectedPluginKey} from '../reducers/connections'; const MAX_BACKGROUND_TASK_TIME = 25; @@ -138,14 +137,15 @@ export function processMessageLater( }, message: {method: string; params?: any}, ) { - const isSelected = pluginKey === getSelectedPluginKey(store.getState()); + const isSelected = + pluginKey === getSelectedPluginKey(store.getState().connections); switch (true) { case isSelected && getPendingMessages(store, pluginKey).length === 0: processMessageImmediately(store, pluginKey, plugin, message); break; case isSelected: case plugin instanceof FlipperDevicePlugin: - case pluginIsStarred(store.getState(), plugin.id): + case pluginIsStarred(store.getState().connections, plugin.id): store.dispatch( queueMessage( pluginKey, @@ -158,26 +158,6 @@ export function processMessageLater( } } -function getSelectedPluginKey(state: State): string | undefined { - return state.connections.selectedPlugin - ? getPluginKey( - state.connections.selectedApp, - state.connections.selectedDevice, - state.connections.selectedPlugin, - ) - : undefined; -} - -function pluginIsStarred(state: State, pluginId: string): boolean { - const {selectedApp} = state.connections; - if (!selectedApp) { - return false; - } - const appInfo = deconstructClientId(selectedApp); - const starred = state.connections.userStarredPlugins[appInfo.app]; - return starred && starred.indexOf(pluginId) > -1; -} - export async function processMessageQueue( plugin: { defaultPersistedState: any;