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:
committed by
Facebook Github Bot
parent
5bd0cd3d63
commit
9f7be13e39
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ export default combineReducers<State, Actions>({
|
|||||||
'userPreferredPlugin',
|
'userPreferredPlugin',
|
||||||
'userPreferredApp',
|
'userPreferredApp',
|
||||||
'userStarredPlugins',
|
'userStarredPlugins',
|
||||||
'selectedClient',
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
connections,
|
connections,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
Reference in New Issue
Block a user