Rework sidebar selection

Summary:
- Make sure newly connecting apps are automatically selected
- Improved the sidebar UI by using more consistent, spacious styling, and giving some more attention to error states

Reviewed By: passy

Differential Revision: D18505636

fbshipit-source-id: 18b2c8e78be13aabb3a54c60553f6b0d1e613b27
This commit is contained in:
Michel Weststrate
2019-11-18 02:18:04 -08:00
committed by Facebook Github Bot
parent 5bd0cd3d63
commit 9f7be13e39
12 changed files with 284 additions and 214 deletions

View File

@@ -314,6 +314,9 @@ async function startFlipper(userArguments: UserArguments) {
const ports = store.getState().application.serverPorts; const ports = store.getState().application.serverPorts;
matchedDevice.reverse([ports.secure, ports.insecure]); matchedDevice.reverse([ports.secure, ports.insecure]);
} }
matchedDevice.loadDevicePlugins(
store.getState().plugins.devicePlugins,
);
store.dispatch({ store.dispatch({
type: 'REGISTER_DEVICE', type: 'REGISTER_DEVICE',
payload: matchedDevice, payload: matchedDevice,

View File

@@ -224,7 +224,7 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
deepLinkPayload, deepLinkPayload,
pluginKey, pluginKey,
isArchivedDevice, isArchivedDevice,
selectedApp, selectedApp: selectedApp || null,
}; };
return s; return s;
}, },

View File

@@ -11,7 +11,7 @@ import config from '../fb-stubs/config';
import BaseDevice from '../devices/BaseDevice'; import BaseDevice from '../devices/BaseDevice';
import Client from '../Client'; import Client from '../Client';
import {UninitializedClient} from '../UninitializedClient'; import {UninitializedClient} from '../UninitializedClient';
import {FlipperBasePlugin} from '../plugin'; import {FlipperBasePlugin, sortPluginsByName} from '../plugin';
import {PluginNotification} from '../reducers/notifications'; import {PluginNotification} from '../reducers/notifications';
import {ActiveSheet, ACTIVE_SHEET_PLUGINS} from '../reducers/application'; import {ActiveSheet, ACTIVE_SHEET_PLUGINS} from '../reducers/application';
import {State as Store} from '../reducers'; import {State as Store} from '../reducers';
@@ -31,6 +31,8 @@ import {
Button, Button,
StarButton, StarButton,
ArchivedDevice, ArchivedDevice,
Heading,
Spacer,
} from 'flipper'; } from 'flipper';
import React, {Component, PureComponent, Fragment} from 'react'; import React, {Component, PureComponent, Fragment} from 'react';
import NotificationsHub from '../NotificationsHub'; import NotificationsHub from '../NotificationsHub';
@@ -40,6 +42,8 @@ import {
StaticView, StaticView,
setStaticView, setStaticView,
selectClient, selectClient,
getAvailableClients,
getClientById,
} from '../reducers/connections'; } from '../reducers/connections';
import {setActiveSheet} from '../reducers/application'; import {setActiveSheet} from '../reducers/application';
import UserAccount from './UserAccount'; import UserAccount from './UserAccount';
@@ -56,7 +60,7 @@ const ListItem = styled('div')(({active}: {active?: boolean}) => ({
paddingLeft: 10, paddingLeft: 10,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
marginBottom: 2, marginBottom: 6,
flexShrink: 0, flexShrink: 0,
backgroundColor: active ? colors.macOSTitleBarIconSelected : 'none', backgroundColor: active ? colors.macOSTitleBarIconSelected : 'none',
color: active ? colors.white : colors.macOSSidebarSectionItem, color: active ? colors.white : colors.macOSSidebarSectionItem,
@@ -83,7 +87,7 @@ const SidebarButton = styled(Button)(({small}: {small?: boolean}) => ({
const PluginShape = styled(FlexBox)( const PluginShape = styled(FlexBox)(
({backgroundColor}: {backgroundColor?: BackgroundColorProperty}) => ({ ({backgroundColor}: {backgroundColor?: BackgroundColorProperty}) => ({
marginRight: 5, marginRight: 8,
backgroundColor, backgroundColor,
borderRadius: 3, borderRadius: 3,
flexShrink: 0, flexShrink: 0,
@@ -91,6 +95,7 @@ const PluginShape = styled(FlexBox)(
height: 18, height: 18,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
top: '-1px',
}), }),
); );
@@ -225,7 +230,6 @@ type StateFromProps = {
selectedApp: string | null | undefined; selectedApp: string | null | undefined;
userStarredPlugins: Store['connections']['userStarredPlugins']; userStarredPlugins: Store['connections']['userStarredPlugins'];
clients: Array<Client>; clients: Array<Client>;
selectedClient: string;
uninitializedClients: Array<{ uninitializedClients: Array<{
client: UninitializedClient; client: UninitializedClient;
deviceId?: string; deviceId?: string;
@@ -273,30 +277,18 @@ class MainSidebar extends PureComponent<Props, State> {
render() { render() {
const { const {
selectedDevice, selectedDevice,
selectedClient,
selectClient, selectClient,
selectedPlugin, selectedPlugin,
selectedApp,
staticView, staticView,
selectPlugin, selectPlugin,
setStaticView, setStaticView,
windowIsFocused, windowIsFocused,
numNotifications, numNotifications,
uninitializedClients,
} = this.props; } = this.props;
let {clients, uninitializedClients} = this.props; const clients = getAvailableClients(selectedDevice, this.props.clients);
clients = clients const client: Client | undefined = getClientById(clients, selectedApp);
.filter(
(client: Client) =>
(selectedDevice &&
selectedDevice.supportsOS(client.query.os) &&
client.query.device_id === selectedDevice.serial) ||
// Old android sdk versions don't know their device_id
// Display their plugins under all selected devices until they die out
client.query.device_id === 'unknown',
)
.sort((a, b) => (a.query.app || '').localeCompare(b.query.app));
const client: Client | undefined = clients.find(
client => client.query.app === selectedClient,
);
return ( return (
<Sidebar <Sidebar
@@ -306,71 +298,87 @@ class MainSidebar extends PureComponent<Props, State> {
process.platform === 'darwin' && windowIsFocused ? 'transparent' : '' process.platform === 'darwin' && windowIsFocused ? 'transparent' : ''
}> }>
<Plugins> <Plugins>
{selectedDevice && ( {selectedDevice ? (
<> <>
<ListItem> <ListItem>
<SidebarButton>{selectedDevice.title}</SidebarButton> <SidebarButton>{selectedDevice.title}</SidebarButton>
</ListItem> </ListItem>
{this.showArchivedDeviceDetails(selectedDevice)} {this.showArchivedDeviceDetails(selectedDevice)}
</> {selectedDevice.devicePlugins.map(pluginName => {
)} const plugin = this.props.devicePlugins.get(pluginName)!;
{selectedDevice && return (
Array.from(this.props.devicePlugins.values()) <PluginSidebarListItem
.filter(plugin => plugin.supportsDevice(selectedDevice)) key={plugin.id}
.sort(sortPluginsByName) isActive={plugin.id === selectedPlugin}
.map((plugin: typeof FlipperDevicePlugin) => ( onClick={() =>
<PluginSidebarListItem selectPlugin({
key={plugin.id} selectedPlugin: plugin.id,
isActive={plugin.id === selectedPlugin} selectedApp: null,
onClick={() => deepLinkPayload: null,
selectPlugin({ })
selectedPlugin: plugin.id, }
selectedApp: null, plugin={plugin}
deepLinkPayload: null, />
}) );
} })}
plugin={plugin} <ListItem>
/> <SidebarButton
))} title="Select an app to see available plugins"
<ListItem> compact={true}
<SidebarButton dropdown={clients.map(c => ({
title="Select an app to see available plugins" checked: client === c,
compact={true} label: c.query.app,
dropdown={clients.map(c => ({ type: 'checkbox',
checked: client === c, click: () => selectClient(c.id),
label: c.query.app, }))}>
type: 'checkbox', {clients.length === 0 ? (
click: () => selectClient(c.query.app), <>
}))}> <Glyph
{clients.length === 0 ? ( name="mobile-engagement"
'(No clients connected)' size={16}
) : !client ? ( color={colors.red}
'(Select client)' style={{marginRight: 10}}
) : ( />
<> No clients connected
{client.query.app} </>
{clients.length > 1 && ( ) : !client ? (
<Glyph 'Select client'
size={12} ) : (
name="chevron-down" <>
style={{marginLeft: 8}} {client.query.app}
/> <Glyph
size={12}
name="chevron-down"
style={{marginLeft: 8}}
/>
</>
)} )}
</> </SidebarButton>
)} </ListItem>
</SidebarButton> {this.renderClientPlugins(client)}
</ListItem> {uninitializedClients.map(entry => (
{this.renderClientPlugins(client)} <ListItem key={JSON.stringify(entry.client)}>
{uninitializedClients.map(entry => ( {entry.client.appName}
<ListItem key={JSON.stringify(entry.client)}> {entry.errorMessage ? (
{entry.client.appName} <ErrorIndicator name={'mobile-cross'} size={16} />
{entry.errorMessage ? ( ) : (
<ErrorIndicator name={'mobile-cross'} size={16} /> <Spinner size={16} />
) : ( )}
<Spinner size={16} /> </ListItem>
)} ))}
</>
) : (
<ListItem
style={{
textAlign: 'center',
marginTop: 50,
flexDirection: 'column',
}}>
<Glyph name="mobile" size={32} color={colors.red}></Glyph>
<Spacer style={{height: 20}} />
<Heading>Select a device to get started</Heading>
</ListItem> </ListItem>
))} )}
</Plugins> </Plugins>
{!GK.get('flipper_disable_notifications') && ( {!GK.get('flipper_disable_notifications') && (
<ListItem <ListItem
@@ -535,13 +543,26 @@ class MainSidebar extends PureComponent<Props, State> {
this.props.userStarredPlugins, this.props.userStarredPlugins,
true, true,
); );
const showAllPlugins =
this.state.showAllPlugins ||
favoritePlugins.length === 0 ||
// If the plugin is part of the hidden section, make sure sidebar is expanded
(client.plugins.includes(this.props.selectedPlugin!) &&
!favoritePlugins.find(
plugin => plugin.id === this.props.selectedPlugin,
));
return ( return (
<> <>
{favoritePlugins.length === 0 ? ( {favoritePlugins.length === 0 ? (
<ListItem> <ListItem>
<div style={{textAlign: 'center', width: '100%'}}> <div
Star some plugins! style={{
<hr style={{width: '100%'}} /> textAlign: 'center',
width: '100%',
color: colors.light30,
fontStyle: 'italic',
}}>
star your favorite plugins!
</div> </div>
</ListItem> </ListItem>
) : ( ) : (
@@ -562,12 +583,10 @@ class MainSidebar extends PureComponent<Props, State> {
showAllPlugins: !state.showAllPlugins, showAllPlugins: !state.showAllPlugins,
})) }))
}> }>
{this.state.showAllPlugins ? 'Show less' : 'Show more'} {showAllPlugins ? 'Show less' : 'Show more'}
<Glyph <Glyph
size={8} size={8}
name={ name={showAllPlugins ? 'chevron-up' : 'chevron-down'}
this.state.showAllPlugins ? 'chevron-up' : 'chevron-down'
}
style={{ style={{
marginLeft: 4, marginLeft: 4,
}} }}
@@ -582,7 +601,7 @@ class MainSidebar extends PureComponent<Props, State> {
overflow: 'auto', overflow: 'auto',
height: 'auto', height: 'auto',
}}> }}>
{this.state.showAllPlugins || favoritePlugins.length === 0 {showAllPlugins
? this.renderPluginsByCategory( ? this.renderPluginsByCategory(
client, client,
getFavoritePlugins( getFavoritePlugins(
@@ -633,13 +652,6 @@ function groupPluginsByCategory(plugins: FlipperPlugins): PluginsByCategory {
return res; return res;
} }
function sortPluginsByName(
a: typeof FlipperBasePlugin,
b: typeof FlipperBasePlugin,
): number {
return (a.title || a.id) > (b.title || b.id) ? 1 : -1;
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>( export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({ ({
application: {windowIsFocused}, application: {windowIsFocused},
@@ -647,7 +659,6 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
selectedDevice, selectedDevice,
selectedPlugin, selectedPlugin,
selectedApp, selectedApp,
selectedClient,
userStarredPlugins, userStarredPlugins,
clients, clients,
uninitializedClients, uninitializedClients,
@@ -664,7 +675,6 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
})(), })(),
windowIsFocused, windowIsFocused,
selectedDevice, selectedDevice,
selectedClient,
staticView, staticView,
selectedPlugin, selectedPlugin,
selectedApp, selectedApp,

View File

@@ -8,6 +8,8 @@
*/ */
import stream from 'stream'; import stream from 'stream';
import {FlipperDevicePlugin} from 'flipper';
import {sortPluginsByName} from '../plugin';
export type LogLevel = export type LogLevel =
| 'unknown' | 'unknown'
@@ -81,6 +83,9 @@ export default class BaseDevice {
// if imported, stores the original source location // if imported, stores the original source location
source = ''; source = '';
// sorted list of supported device plugins
devicePlugins!: string[];
supportsOS(os: OS) { supportsOS(os: OS) {
return os.toLowerCase() === this.os.toLowerCase(); return os.toLowerCase() === this.os.toLowerCase();
} }
@@ -164,4 +169,11 @@ export default class BaseDevice {
async stopScreenCapture(): Promise<string | null> { async stopScreenCapture(): Promise<string | null> {
return null; return null;
} }
loadDevicePlugins(devicePlugins?: Map<string, typeof FlipperDevicePlugin>) {
this.devicePlugins = Array.from(devicePlugins ? devicePlugins.values() : [])
.filter(plugin => plugin.supportsDevice(this))
.sort(sortPluginsByName)
.map(plugin => plugin.id);
}
} }

View File

@@ -186,6 +186,7 @@ export default (store: Store, logger: Logger) => {
payload: new Set(reconnectedDevices), payload: new Set(reconnectedDevices),
}); });
androidDevice.loadDevicePlugins(store.getState().plugins.devicePlugins);
store.dispatch({ store.dispatch({
type: 'REGISTER_DEVICE', type: 'REGISTER_DEVICE',
payload: androidDevice, payload: androidDevice,
@@ -223,12 +224,13 @@ export default (store: Store, logger: Logger) => {
payload: new Set(deviceIds), payload: new Set(deviceIds),
}); });
archivedDevices.forEach((payload: BaseDevice) => archivedDevices.forEach((device: BaseDevice) => {
device.loadDevicePlugins(store.getState().plugins.devicePlugins);
store.dispatch({ store.dispatch({
type: 'REGISTER_DEVICE', type: 'REGISTER_DEVICE',
payload, payload: device,
}), });
); });
} }
watchAndroidDevices(); watchAndroidDevices();

View File

@@ -22,6 +22,7 @@ export default (store: Store, logger: Logger) => {
} else { } else {
return; return;
} }
device.loadDevicePlugins(store.getState().plugins.devicePlugins);
store.dispatch({ store.dispatch({
type: 'REGISTER_DEVICE', type: 'REGISTER_DEVICE',
payload: device, payload: device,

View File

@@ -91,6 +91,7 @@ async function queryDevices(store: Store, logger: Logger): Promise<void> {
serial: udid, serial: udid,
}); });
const iOSDevice = new IOSDevice(udid, type, name); const iOSDevice = new IOSDevice(udid, type, name);
iOSDevice.loadDevicePlugins(store.getState().plugins.devicePlugins);
store.dispatch({ store.dispatch({
type: 'REGISTER_DEVICE', type: 'REGISTER_DEVICE',
payload: iOSDevice, payload: iOSDevice,

View File

@@ -265,3 +265,10 @@ export class FlipperPlugin<
this.init(); this.init();
} }
} }
export function sortPluginsByName(
a: typeof FlipperBasePlugin,
b: typeof FlipperBasePlugin,
): number {
return (a.title || a.id) > (b.title || b.id) ? 1 : -1;
}

View File

@@ -40,16 +40,13 @@ export type State = {
androidEmulators: Array<string>; androidEmulators: Array<string>;
selectedDevice: null | BaseDevice; selectedDevice: null | BaseDevice;
selectedPlugin: null | string; selectedPlugin: null | string;
selectedApp: null | string; selectedApp: null | string | undefined;
userPreferredDevice: null | string; userPreferredDevice: null | string;
userPreferredPlugin: null | string; userPreferredPlugin: null | string;
userPreferredApp: null | string; userPreferredApp: null | string;
userStarredPlugins: {[key: string]: Array<string>}; userStarredPlugins: {[key: string]: Array<string>};
errors: FlipperError[]; errors: FlipperError[];
clients: Array<Client>; clients: Array<Client>;
// refers to the client that is selected in the main side bar, not to be confused with
// selectedApp, which represents the app of the currently active plugin!
selectedClient: string;
uninitializedClients: Array<{ uninitializedClients: Array<{
client: UninitializedClient; client: UninitializedClient;
deviceId?: string; deviceId?: string;
@@ -154,7 +151,6 @@ const INITAL_STATE: State = {
userStarredPlugins: {}, userStarredPlugins: {},
errors: [], errors: [],
clients: [], clients: [],
selectedClient: '',
uninitializedClients: [], uninitializedClients: [],
deepLinkPayload: null, deepLinkPayload: null,
staticView: WelcomeScreen, staticView: WelcomeScreen,
@@ -173,14 +169,13 @@ const reducer = (state: State = INITAL_STATE, action: Actions): State => {
} }
case 'SELECT_DEVICE': { case 'SELECT_DEVICE': {
const {payload} = action; const {payload} = action;
return { return updateSelection({
...state, ...state,
selectedApp: null,
staticView: null,
selectedPlugin: DEFAULT_PLUGIN,
selectedDevice: payload, selectedDevice: payload,
userPreferredDevice: payload.title, userPreferredDevice: payload
}; ? payload.title
: state.userPreferredDevice,
});
} }
case 'REGISTER_ANDROID_EMULATORS': { case 'REGISTER_ANDROID_EMULATORS': {
const {payload} = action; const {payload} = action;
@@ -191,76 +186,27 @@ const reducer = (state: State = INITAL_STATE, action: Actions): State => {
} }
case 'REGISTER_DEVICE': { case 'REGISTER_DEVICE': {
const {payload} = action; const {payload} = action;
const devices = state.devices.concat(payload);
let {selectedDevice, selectedPlugin} = state;
let staticView: StaticView = state.staticView;
// select the default plugin
let selection: Partial<State> = {
selectedApp: null,
selectedPlugin: DEFAULT_PLUGIN,
};
const canBeDefaultDevice = !DEFAULT_DEVICE_BLACKLIST.some( return updateSelection({
blacklistedDevice => payload instanceof blacklistedDevice,
);
if (!selectedDevice && canBeDefaultDevice) {
selectedDevice = payload;
staticView = null;
if (selectedPlugin) {
// We already had a plugin selected, but no device. This is happening
// when the Client connected before the Device.
selection = {};
}
} else if (payload.title === state.userPreferredDevice) {
selectedDevice = payload;
staticView = null;
} else {
// We didn't select the newly connected device, so we don't want to
// change the plugin.
selection = {};
}
return {
...state, ...state,
devices, devices: state.devices.concat(payload),
// select device if none was selected before });
selectedDevice,
...selection,
staticView,
};
} }
case 'UNREGISTER_DEVICES': { case 'UNREGISTER_DEVICES': {
const {payload} = action; const {payload} = action;
const {selectedDevice} = state;
let selectedDeviceWasRemoved = false;
const devices = state.devices.filter((device: BaseDevice) => { const devices = state.devices.filter((device: BaseDevice) => {
if (payload.has(device.serial)) { if (payload.has(device.serial)) {
if (selectedDevice === device) {
// removed device is the selected
selectedDeviceWasRemoved = true;
}
return false; return false;
} else { } else {
return true; return true;
} }
}); });
let selection = {}; return updateSelection({
if (selectedDeviceWasRemoved) {
selection = {
selectedDevice: devices[devices.length - 1] || null,
staticView: selectedDevice != null ? null : WelcomeScreen,
selectedApp: null,
selectedPlugin: DEFAULT_PLUGIN,
};
}
return {
...state, ...state,
devices, devices,
...selection, });
};
} }
case 'SELECT_PLUGIN': { case 'SELECT_PLUGIN': {
const {payload} = action; const {payload} = action;
@@ -269,15 +215,14 @@ const reducer = (state: State = INITAL_STATE, action: Actions): State => {
performance.mark(`activePlugin-${selectedPlugin}`); performance.mark(`activePlugin-${selectedPlugin}`);
} }
const userPreferredApp = selectedApp || state.userPreferredApp; return updateSelection({
return {
...state, ...state,
...payload, selectedApp,
staticView: null, selectedPlugin,
userPreferredApp: userPreferredApp, userPreferredPlugin: selectedPlugin || state.userPreferredPlugin,
userPreferredPlugin: selectedPlugin, });
};
} }
case 'STAR_PLUGIN': { case 'STAR_PLUGIN': {
const {selectedPlugin, selectedApp} = action.payload; const {selectedPlugin, selectedApp} = action.payload;
const starredPluginsForApp = [ const starredPluginsForApp = [
@@ -302,22 +247,11 @@ const reducer = (state: State = INITAL_STATE, action: Actions): State => {
const {payload} = action; const {payload} = action;
return {...state, userPreferredPlugin: payload}; return {...state, userPreferredPlugin: payload};
} }
case 'NEW_CLIENT': { case 'NEW_CLIENT': {
const {payload} = action; const {payload} = action;
const {userPreferredApp, userPreferredPlugin} = state;
let {selectedApp, selectedPlugin} = state;
if ( return updateSelection({
userPreferredApp &&
userPreferredPlugin &&
payload.id === userPreferredApp &&
payload.plugins.includes(userPreferredPlugin)
) {
// user preferred client did reconnect, so let's select it
selectedApp = userPreferredApp;
selectedPlugin = userPreferredPlugin;
}
return {
...state, ...state,
clients: state.clients.concat(payload), clients: state.clients.concat(payload),
uninitializedClients: state.uninitializedClients.filter(c => { uninitializedClients: state.uninitializedClients.filter(c => {
@@ -326,10 +260,18 @@ const reducer = (state: State = INITAL_STATE, action: Actions): State => {
c.client.appName !== payload.query.app c.client.appName !== payload.query.app
); );
}), }),
selectedApp, });
selectedPlugin,
};
} }
case 'SELECT_CLIENT': {
const {payload} = action;
return updateSelection({
...state,
selectedApp: payload,
userPreferredApp: payload || state.userPreferredApp,
});
}
case 'NEW_CLIENT_SANITY_CHECK': { case 'NEW_CLIENT_SANITY_CHECK': {
const {payload} = action; const {payload} = action;
// Check for clients initialised when there is no matching device // Check for clients initialised when there is no matching device
@@ -346,26 +288,19 @@ const reducer = (state: State = INITAL_STATE, action: Actions): State => {
); );
} }
} }
return state; return state;
} }
case 'CLIENT_REMOVED': { case 'CLIENT_REMOVED': {
const {payload} = action; const {payload} = action;
return updateSelection({
const selected: Partial<State> = {};
if (state.selectedApp === payload) {
selected.selectedApp = null;
selected.selectedPlugin = DEFAULT_PLUGIN;
}
return {
...state, ...state,
...selected,
clients: state.clients.filter( clients: state.clients.filter(
(client: Client) => client.id !== payload, (client: Client) => client.id !== payload,
), ),
}; });
} }
case 'PREFER_DEVICE': { case 'PREFER_DEVICE': {
const {payload: userPreferredDevice} = action; const {payload: userPreferredDevice} = action;
return {...state, userPreferredDevice}; return {...state, userPreferredDevice};
@@ -433,12 +368,6 @@ const reducer = (state: State = INITAL_STATE, action: Actions): State => {
errors, errors,
}; };
} }
case 'SELECT_CLIENT': {
return {
...state,
selectedClient: action.payload,
};
}
default: default:
return state; return state;
} }
@@ -523,11 +452,6 @@ export const starPlugin = (payload: {
payload, payload,
}); });
export const userPreferredPlugin = (payload: string): Action => ({
type: 'SELECT_USER_PREFERRED_PLUGIN',
payload,
});
export const dismissError = (index: number): Action => ({ export const dismissError = (index: number): Action => ({
type: 'DISMISS_ERROR', type: 'DISMISS_ERROR',
payload: index, payload: index,
@@ -537,3 +461,112 @@ export const selectClient = (clientId: string): Action => ({
type: 'SELECT_CLIENT', type: 'SELECT_CLIENT',
payload: clientId, payload: clientId,
}); });
export function getAvailableClients(
device: null | undefined | BaseDevice,
clients: Client[],
): Client[] {
if (!device) {
return [];
}
return clients
.filter(
(client: Client) =>
(device &&
device.supportsOS(client.query.os) &&
client.query.device_id === device.serial) ||
// Old android sdk versions don't know their device_id
// Display their plugins under all selected devices until they die out
client.query.device_id === 'unknown',
)
.sort((a, b) => (a.query.app || '').localeCompare(b.query.app));
}
function getBestAvailableClient(
device: BaseDevice | null | undefined,
clients: Client[],
preferredClient: string | null,
): Client | undefined {
const availableClients = getAvailableClients(device, clients);
if (availableClients.length === 0) {
return undefined;
}
return (
getClientById(availableClients, preferredClient) ||
availableClients[0] ||
null
);
}
export function getClientById(
clients: Client[],
clientId: string | null | undefined,
): Client | undefined {
return clients.find(client => client.id === clientId);
}
function canBeDefaultDevice(device: BaseDevice) {
return !DEFAULT_DEVICE_BLACKLIST.some(
blacklistedDevice => device instanceof blacklistedDevice,
);
}
/**
* This function, given the current state, tries to build to build the best
* selection possible, preselection device if there is non, plugins based on preferences, etc
* @param state
*/
function updateSelection(state: Readonly<State>): State {
const updates: Partial<State> = {
staticView: null,
};
// Find the selected device if it still exists
let device: BaseDevice | null =
state.selectedDevice && state.devices.includes(state.selectedDevice)
? state.selectedDevice
: null;
if (!device) {
device =
state.devices.find(
device => device.title === state.userPreferredDevice,
) ||
state.devices.find(device => canBeDefaultDevice(device)) ||
null;
}
updates.selectedDevice = device;
if (!device) {
updates.staticView = WelcomeScreen;
}
// Select client based on device
const client = getBestAvailableClient(
device,
state.clients,
state.selectedApp || state.userPreferredApp,
);
updates.selectedApp = client ? client.id : null;
const availablePlugins: string[] = [
...(device?.devicePlugins || []),
...(client?.plugins || []),
];
if (
// Try the preferred plugin first
state.userPreferredPlugin &&
availablePlugins.includes(state.userPreferredPlugin)
) {
updates.selectedPlugin = state.userPreferredPlugin;
} else if (
!state.selectedPlugin ||
!availablePlugins.includes(state.selectedPlugin)
) {
// currently selected plugin is not available in this state,
// fall back to the default
updates.selectedPlugin = DEFAULT_PLUGIN;
}
const res = {...state, ...updates};
console.log(res.selectedDevice, res.selectedApp, res.selectedPlugin);
return res;
}

View File

@@ -97,7 +97,6 @@ export default combineReducers<State, Actions>({
'userPreferredPlugin', 'userPreferredPlugin',
'userPreferredApp', 'userPreferredApp',
'userStarredPlugins', 'userStarredPlugins',
'selectedClient',
], ],
}, },
connections, connections,

View File

@@ -468,7 +468,7 @@ export async function getStoreExport(
const state = store.getState(); const state = store.getState();
const {clients} = state.connections; const {clients} = state.connections;
const client = clients.find( const client = clients.find(
client => client.query.app === state.connections.selectedClient, client => client.query.app === state.connections.selectedApp,
); );
const {pluginStates} = state; const {pluginStates} = state;
const {plugins} = state; const {plugins} = state;
@@ -616,6 +616,7 @@ export function importDataToStore(source: string, data: string, store: Store) {
}); });
return; return;
} }
archivedDevice.loadDevicePlugins(store.getState().plugins.devicePlugins);
store.dispatch({ store.dispatch({
type: 'REGISTER_DEVICE', type: 'REGISTER_DEVICE',
payload: archivedDevice, payload: archivedDevice,

View File

@@ -32,6 +32,7 @@ const ICONS = {
'magic-wand': [20], 'magic-wand': [20],
'magnifying-glass': [16, 20], 'magnifying-glass': [16, 20],
'minus-circle': [12], 'minus-circle': [12],
'mobile-engagement': [16],
'question-circle-outline': [16], 'question-circle-outline': [16],
'star-outline': [12, 16, 24], 'star-outline': [12, 16, 24],
'triangle-down': [12], 'triangle-down': [12],
@@ -47,7 +48,7 @@ const ICONS = {
desktop: [12], desktop: [12],
directions: [12], directions: [12],
internet: [12], internet: [12],
mobile: [12], mobile: [12, 32],
posts: [20], posts: [20],
profile: [12], profile: [12],
rocket: [20], rocket: [20],