diff --git a/src/PluginContainer.tsx b/src/PluginContainer.tsx index 15965b8de..d813d2b9b 100644 --- a/src/PluginContainer.tsx +++ b/src/PluginContainer.tsx @@ -28,7 +28,12 @@ import { VBox, View, } from 'flipper'; -import {StaticView, setStaticView} from './reducers/connections'; +import { + StaticView, + setStaticView, + pluginIsStarred, + starPlugin, +} from './reducers/connections'; import React, {PureComponent} from 'react'; import {connect, ReactReduxContext} from 'react-redux'; import {setPluginState} from './reducers/pluginStates'; @@ -38,6 +43,7 @@ import {activateMenuItems} from './MenuBar'; import {Message} from './reducers/pluginMessageQueue'; import {Idler} from './utils/Idler'; import {processMessageQueue} from './utils/messageQueue'; +import {ToggleButton, SmallText} from './ui'; const Container = styled(FlexColumn)({ width: 0, @@ -95,6 +101,7 @@ type StateFromProps = { selectedApp: string | null; isArchivedDevice: boolean; pendingMessages: Message[] | undefined; + pluginIsEnabled: boolean; }; type DispatchFromProps = { @@ -105,6 +112,7 @@ type DispatchFromProps = { }) => any; setPluginState: (payload: {pluginKey: string; state: any}) => void; setStaticView: (payload: StaticView) => void; + starPlugin: typeof starPlugin; }; type Props = StateFromProps & DispatchFromProps & OwnProps; @@ -167,12 +175,18 @@ class PluginContainer extends PureComponent { } processMessageQueue() { - const {pluginKey, pendingMessages, activePlugin} = this.props; + const { + pluginKey, + pendingMessages, + activePlugin, + pluginIsEnabled, + } = this.props; if (pluginKey !== this.pluginBeingProcessed) { this.pluginBeingProcessed = pluginKey; this.cancelCurrentQueue(); this.setState({progress: {current: 0, total: 0}}); if ( + pluginIsEnabled && activePlugin && activePlugin.persistedStateReducer && pluginKey && @@ -200,16 +214,62 @@ class PluginContainer extends PureComponent { } render() { - const {activePlugin, pluginKey, target, pendingMessages} = this.props; + const { + activePlugin, + pluginKey, + target, + pendingMessages, + pluginIsEnabled, + } = this.props; if (!activePlugin || !target || !pluginKey) { console.warn(`No selected plugin. Rendering empty!`); return null; } + + if (!pluginIsEnabled) { + return this.renderPluginEnabler(); + } if (!pendingMessages || pendingMessages.length === 0) { return this.renderPlugin(); - } else { - return this.renderPluginLoader(); } + return this.renderPluginLoader(); + } + + renderPluginEnabler() { + const activePlugin = this.props.activePlugin!; + return ( + + + + + + + + + { + this.props.starPlugin({ + selectedPlugin: activePlugin.id, + selectedApp: (this.props.target as Client).query.app, + }); + }} + large + /> + + + Click to enable this plugin + + + + ); } renderPluginLoader() { @@ -319,6 +379,7 @@ export default connect( selectedApp, clients, deepLinkPayload, + userStarredPlugins, }, pluginStates, plugins: {devicePlugins, clientPlugins}, @@ -330,24 +391,33 @@ export default connect( | typeof FlipperDevicePlugin | typeof FlipperPlugin | null = null; + let pluginIsEnabled = false; if (selectedPlugin) { activePlugin = devicePlugins.get(selectedPlugin) || null; target = selectedDevice; if (selectedDevice && activePlugin) { pluginKey = getPluginKey(selectedDevice.serial, activePlugin.id); + pluginIsEnabled = true; } else { target = clients.find((client: Client) => client.id === selectedApp) || null; activePlugin = clientPlugins.get(selectedPlugin) || null; if (activePlugin && target) { pluginKey = getPluginKey(target.id, activePlugin.id); + pluginIsEnabled = pluginIsStarred( + {selectedApp, userStarredPlugins}, + activePlugin.id, + ); } } } const isArchivedDevice = !selectedDevice ? false : selectedDevice instanceof ArchivedDevice; + if (isArchivedDevice) { + pluginIsEnabled = true; + } const pendingMessages = pluginKey ? pluginMessageQueue[pluginKey] @@ -362,6 +432,7 @@ export default connect( isArchivedDevice, selectedApp: selectedApp || null, pendingMessages, + pluginIsEnabled, }; return s; }, @@ -369,5 +440,6 @@ export default connect( setPluginState, selectPlugin, setStaticView, + starPlugin, }, )(PluginContainer); diff --git a/src/chrome/StatusBar.tsx b/src/chrome/StatusBar.tsx index 3e3163013..d0a56c6a3 100644 --- a/src/chrome/StatusBar.tsx +++ b/src/chrome/StatusBar.tsx @@ -8,12 +8,11 @@ */ import {colors} from '../ui/components/colors'; -import {styled, Glyph} from '../ui'; +import {styled} 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, @@ -48,39 +47,8 @@ export default connect((state: State) => { } = state; if (statusMessages.length > 0) { return {statusMessage: statusMessages[statusMessages.length - 1]}; - } else if (isPreviewingBackgroundPlugin(state)) { - return { - 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. - - ), - }; } 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/chrome/mainsidebar/MainSidebar.tsx b/src/chrome/mainsidebar/MainSidebar.tsx index 1c05028ca..1aa3e6530 100644 --- a/src/chrome/mainsidebar/MainSidebar.tsx +++ b/src/chrome/mainsidebar/MainSidebar.tsx @@ -55,6 +55,7 @@ import { CategoryName, PluginSidebarListItem, NoDevices, + getFavoritePlugins, } from './sidebarUtils'; const SidebarButton = styled(Button)<{small?: boolean}>(({small}) => ({ @@ -187,7 +188,7 @@ class MainSidebar extends PureComponent { )} - {this.renderClientPlugins(client)} + {this.renderClientPlugins(selectedDevice, client)} {uninitializedClients.map(entry => ( {entry.client.appName} @@ -287,7 +288,7 @@ class MainSidebar extends PureComponent { )); } - renderClientPlugins(client?: Client) { + renderClientPlugins(device: BaseDevice, client?: Client) { if (!client) { return null; } @@ -301,6 +302,8 @@ class MainSidebar extends PureComponent { (p: typeof FlipperPlugin) => client.plugins.indexOf(p.id) > -1, ); const favoritePlugins: FlipperPlugins = getFavoritePlugins( + device, + client, allPlugins, this.props.userStarredPlugins[client.query.app], true, @@ -317,7 +320,7 @@ class MainSidebar extends PureComponent { <> {favoritePlugins.length === 0 ? ( - Star your favorite plugins! + No plugins enabled ) : ( <> @@ -361,6 +364,8 @@ class MainSidebar extends PureComponent { ? this.renderPluginsByCategory( client, getFavoritePlugins( + device, + client, allPlugins, this.props.userStarredPlugins[client.query.app], false, @@ -375,20 +380,6 @@ class MainSidebar extends PureComponent { } } -function getFavoritePlugins( - allPlugins: FlipperPlugins, - starredPlugins: undefined | string[], - favorite: boolean, -): FlipperPlugins { - if (!starredPlugins || !starredPlugins.length) { - return favorite ? [] : allPlugins; - } - return allPlugins.filter(plugin => { - const idx = starredPlugins.indexOf(plugin.id); - return idx === -1 ? !favorite : favorite; - }); -} - function groupPluginsByCategory(plugins: FlipperPlugins): PluginsByCategory { const sortedPlugins = plugins.slice().sort(sortPluginsByName); const byCategory: {[cat: string]: FlipperPlugins} = {}; diff --git a/src/chrome/mainsidebar/MainSidebar2.tsx b/src/chrome/mainsidebar/MainSidebar2.tsx index 54a20ac8d..b7fc31fd2 100644 --- a/src/chrome/mainsidebar/MainSidebar2.tsx +++ b/src/chrome/mainsidebar/MainSidebar2.tsx @@ -60,6 +60,7 @@ import { NoClients, NoDevices, getColorByApp, + getFavoritePlugins, } from './sidebarUtils'; type FlipperPlugins = typeof FlipperPlugin[]; @@ -127,6 +128,13 @@ const SidebarSection: React.FC<{ const [collapsed, setCollapsed] = useState(!!defaultCollapsed); color = color || colors.macOSTitleBarIconActive; + useEffect(() => { + // if default collapsed changed to false, propagate that + if (!defaultCollapsed && collapsed) { + setCollapsed(!collapsed); + } + }, [defaultCollapsed]); + return ( <> { - const idx = starredPlugins.indexOf(plugin.id); - return idx === -1 ? !favorite : favorite; - }); -} - function groupPluginsByCategory(plugins: FlipperPlugins): PluginsByCategory { const sortedPlugins = plugins.slice().sort(sortPluginsByName); const byCategory: {[cat: string]: FlipperPlugins} = {}; @@ -511,6 +505,8 @@ const PluginList = memo(function PluginList({ (p: typeof FlipperPlugin) => client.plugins.indexOf(p.id) > -1, ); const favoritePlugins: FlipperPlugins = getFavoritePlugins( + device, + client, allPlugins, userStarredPlugins[client.query.app], true, @@ -529,7 +525,7 @@ const PluginList = memo(function PluginList({ color={getColorByApp(client.query.app)}> {favoritePlugins.length === 0 ? ( - Star your favorite plugins! + No plugins enabled ) : ( onFavorite(plugin.id)} - starred={starred} + starred={device.isArchived ? undefined : starred} /> ))} diff --git a/src/chrome/mainsidebar/sidebarUtils.tsx b/src/chrome/mainsidebar/sidebarUtils.tsx index cfa8d0421..180e920c2 100644 --- a/src/chrome/mainsidebar/sidebarUtils.tsx +++ b/src/chrome/mainsidebar/sidebarUtils.tsx @@ -7,6 +7,7 @@ * @format */ +import React from 'react'; import { FlexBox, colors, @@ -17,32 +18,39 @@ import { FlexColumn, LoadingIndicator, FlipperBasePlugin, - StarButton, + ToggleButton, brandColors, Spacer, Heading, + Client, + BaseDevice, } from 'flipper'; -import React, {useState, useCallback} from 'react'; import {StaticView} from '../../reducers/connections'; import {BackgroundColorProperty} from 'csstype'; export type FlipperPlugins = typeof FlipperPlugin[]; export type PluginsByCategory = [string, FlipperPlugins][]; -export const ListItem = styled.div<{active?: boolean}>(({active}) => ({ - paddingLeft: 10, - display: 'flex', - alignItems: 'center', - marginBottom: 6, - flexShrink: 0, - backgroundColor: active ? colors.macOSTitleBarIconSelected : 'none', - color: active ? colors.white : colors.macOSSidebarSectionItem, - lineHeight: '25px', - padding: '0 10px', - '&[disabled]': { - color: 'rgba(0, 0, 0, 0.5)', - }, -})); +export const ListItem = styled.div<{active?: boolean; disabled?: boolean}>( + ({active, disabled}) => ({ + paddingLeft: 10, + display: 'flex', + alignItems: 'center', + marginBottom: 6, + flexShrink: 0, + backgroundColor: active ? colors.macOSTitleBarIconSelected : 'none', + color: disabled + ? 'rgba(0, 0, 0, 0.5)' + : active + ? colors.white + : colors.macOSSidebarSectionItem, + lineHeight: '25px', + padding: '0 10px', + '&[disabled]': { + color: 'rgba(0, 0, 0, 0.5)', + }, + }), +); export function PluginIcon({ isActive, @@ -143,30 +151,29 @@ export const PluginSidebarListItem: React.FC<{ helpRef?: any; provided?: any; onFavorite?: () => void; - starred?: boolean; + starred?: boolean; // undefined means: not starrable }> = function(props) { const {isActive, plugin, onFavorite, starred} = props; const iconColor = getColorByApp(props.app); - const [hovered, setHovered] = useState(false); - - const onEnter = useCallback(() => setHovered(true), []); - const onLeave = useCallback(() => setHovered(false), []); return ( + disabled={starred === false}> {plugin.title || plugin.id} - {starred !== undefined && (hovered || isActive) && ( - + {starred !== undefined && (!starred || isActive) && ( + )} ); @@ -222,3 +229,28 @@ export const NoClients = () => ( No clients connected ); + +export function getFavoritePlugins( + device: BaseDevice, + client: Client, + allPlugins: FlipperPlugins, + starredPlugins: undefined | string[], + returnFavoredPlugins: boolean, // if false, unfavoried plugins are returned +): FlipperPlugins { + if (device.isArchived) { + if (!returnFavoredPlugins) { + return []; + } + // for archived plugins, all stored plugins are enabled + return allPlugins.filter( + plugin => client.plugins.indexOf(plugin.id) !== -1, + ); + } + if (!starredPlugins || !starredPlugins.length) { + return returnFavoredPlugins ? [] : allPlugins; + } + return allPlugins.filter(plugin => { + const idx = starredPlugins.indexOf(plugin.id); + return idx === -1 ? !returnFavoredPlugins : returnFavoredPlugins; + }); +} diff --git a/src/reducers/connections.tsx b/src/reducers/connections.tsx index 3cc446dc1..b113a4907 100644 --- a/src/reducers/connections.tsx +++ b/src/reducers/connections.tsx @@ -607,7 +607,13 @@ export function getSelectedPluginKey(state: State): string | undefined { : undefined; } -export function pluginIsStarred(state: State, pluginId: string): boolean { +export function pluginIsStarred( + state: { + selectedApp: string | null; + userStarredPlugins: State['userStarredPlugins']; + }, + pluginId: string, +): boolean { const {selectedApp} = state; if (!selectedApp) { return false; diff --git a/src/ui/components/ToggleSwitch.tsx b/src/ui/components/ToggleSwitch.tsx index 24bdf8fca..cfae194b0 100644 --- a/src/ui/components/ToggleSwitch.tsx +++ b/src/ui/components/ToggleSwitch.tsx @@ -7,33 +7,35 @@ * @format */ -import React from 'react'; +import React, {useState, useRef, useEffect} from 'react'; import styled from '@emotion/styled'; import {colors} from './colors'; import Text from './Text'; import FlexRow from './FlexRow'; -export const StyledButton = styled.div<{toggled: boolean}>(props => ({ - width: '30px', - height: '16px', - background: props.toggled ? colors.green : colors.grey, - display: 'block', - borderRadius: '100px', - position: 'relative', - marginLeft: '15px', - flexShrink: 0, - '&::after': { - content: '""', - position: 'absolute', - top: '3px', - left: props.toggled ? '18px' : '3px', - width: '10px', - height: '10px', - background: 'white', +export const StyledButton = styled.div<{toggled: boolean; large: boolean}>( + ({large, toggled}) => ({ + width: large ? 60 : 30, + height: large ? 32 : 16, + background: toggled ? colors.green : colors.grey, + display: 'block', borderRadius: '100px', - transition: 'all cubic-bezier(0.3, 1.5, 0.7, 1) 0.3s', - }, -})); + position: 'relative', + marginLeft: large ? 0 : 15, // margins in components should die :-/ + flexShrink: 0, + '&::after': { + content: '""', + position: 'absolute', + top: large ? 6 : 3, + left: large ? (toggled ? 34 : 6) : toggled ? 18 : 3, + width: large ? 20 : 10, + height: large ? 20 : 10, + background: 'white', + borderRadius: '100px', + transition: 'all cubic-bezier(0.3, 1.5, 0.7, 1) 0.3s', + }, + }), +); StyledButton.displayName = 'ToggleSwitch:StyledButton'; const Container = styled(FlexRow)({ @@ -60,6 +62,7 @@ type Props = { className?: string; label?: string; tooltip?: string; + large?: boolean; }; /** @@ -72,16 +75,34 @@ type Props = { * * ``` */ -export default class ToggleButton extends React.Component { - render() { - return ( - - - {this.props.label && } - - ); - } +export default function ToggleButton(props: Props) { + const unmounted = useRef(false); + const [switching, setSwitching] = useState(false); + useEffect( + () => () => { + // suppress switching after unmount + unmounted.current = true; + }, + [], + ); + return ( + { + setSwitching(true); + setTimeout(() => { + props?.onClick?.(e); + if (unmounted.current === false) { + setSwitching(false); + } + }, 300); + }} + title={props.tooltip}> + + {props.label && } + + ); } diff --git a/static/icons.json b/static/icons.json index da406b524..2609664fb 100644 --- a/static/icons.json +++ b/static/icons.json @@ -332,5 +332,19 @@ ], "checkmark-circle-outline": [ 24 + ], + "target-outline": [ + 16, + 24 + ], + "internet-outline": [ + 24, + 32 + ], + "profile-outline": [ + 32 + ], + "app-react-outline": [ + 16 ] -} \ No newline at end of file +}