Convert Navigation plugin to Sandy

Summary:
Converted the Navigation plugin to Sandy, and updated Locations bookmark accordingly.
This is a prerequisite step of supporting the bookmarkswidgetin the new AppInspect tab.

Updated LocationsButton accordingly, and overal simplified implementation a bit; locationsbutton now reuses the logic of the NavigationPlugin, rather than reimplemting it. This reduces code duplication and also makes sure the state between plugin and location button stays in sync.

Made sure that search providers are derived and cached rather than stored, again simplifying logic

That being said, the navigation plugin is buggy, but all these things failed before this diff as well:
* No events happening when using iOS, despite the plugin being enabled. But these seems to be a long time know issue, looks like it was never implemented
* Not sure if the parameterized bookmarks is working correctly
* screenshots not always happening at the right time (but fixed a race condition where the wrong bookmark might get updated)
* Locations button doesn't show up if the navigation plugin is supported but not enabled (will try to fix in next diff)

Would be great if bnelo12 could do some exploratory testing to verify what ought to be working, but currently isn't.

Reviewed By: cekkaewnumchai

Differential Revision: D24860757

fbshipit-source-id: e4b56072de8c42af2ada0f5bb022cb9f8c04bb47
This commit is contained in:
Michel Weststrate
2020-11-12 04:13:16 -08:00
committed by Facebook GitHub Bot
parent ba541e76dc
commit 661bea1d5b
8 changed files with 325 additions and 365 deletions

View File

@@ -8,7 +8,7 @@
* @flow strict-local
*/
import {FlipperPlugin, FlexColumn, bufferToBlob} from 'flipper';
import {bufferToBlob} from 'flipper';
import {
BookmarksSidebar,
SaveBookmarkDialog,
@@ -17,217 +17,227 @@ import {
RequiredParametersDialog,
} from './components';
import {
removeBookmark,
removeBookmarkFromDB,
readBookmarksFromDB,
writeBookmarkToDB,
} from './util/indexedDB';
import {
appMatchPatternsToAutoCompleteProvider,
bookmarksToAutoCompleteProvider,
DefaultProvider,
} from './util/autoCompleteProvider';
import {getAppMatchPatterns} from './util/appMatchPatterns';
import {getRequiredParameters, filterOptionalParameters} from './util/uri';
import {
State,
PersistedState,
Bookmark,
NavigationEvent,
AppMatchPattern,
URI,
RawNavigationEvent,
} from './types';
import React from 'react';
import React, {useMemo} from 'react';
import {
PluginClient,
createState,
useValue,
usePlugin,
Layout,
} from 'flipper-plugin';
export default class extends FlipperPlugin<State, any, PersistedState> {
static defaultPersistedState = {
navigationEvents: [],
bookmarks: new Map<string, Bookmark>(),
currentURI: '',
bookmarksProvider: DefaultProvider(),
appMatchPatterns: [],
appMatchPatternsProvider: DefaultProvider(),
};
export type State = {
shouldShowSaveBookmarkDialog: boolean;
shouldShowURIErrorDialog: boolean;
saveBookmarkURI: URI | null;
requiredParameters: Array<string>;
};
state = {
shouldShowSaveBookmarkDialog: false,
saveBookmarkURI: null as string | null,
shouldShowURIErrorDialog: false,
requiredParameters: [],
};
type Events = {
nav_event: RawNavigationEvent;
};
static persistedStateReducer = (
persistedState: PersistedState,
method: string,
payload: any,
) => {
switch (method) {
case 'nav_event':
const navigationEvent: NavigationEvent = {
uri:
payload.uri === undefined ? null : decodeURIComponent(payload.uri),
date: new Date(payload.date) || new Date(),
className: payload.class === undefined ? null : payload.class,
screenshot: null,
};
type Methods = {
navigate_to(params: {url: string}): Promise<void>;
};
return {
...persistedState,
currentURI:
navigationEvent.uri == null
? persistedState.currentURI
: decodeURIComponent(navigationEvent.uri),
navigationEvents: [
navigationEvent,
...persistedState.navigationEvents,
],
};
default:
return persistedState;
}
};
export type NavigationPlugin = ReturnType<typeof plugin>;
subscribeToNavigationEvents = () => {
this.client.subscribe('nav_event', () =>
// Wait for view to render and then take a screenshot
setTimeout(async () => {
const device = await this.getDevice();
const screenshot = await device.screenshot();
const blobURL = URL.createObjectURL(bufferToBlob(screenshot));
this.props.persistedState.navigationEvents[0].screenshot = blobURL;
this.props.setPersistedState({...this.props.persistedState});
}, 1000),
);
};
export function plugin(client: PluginClient<Events, Methods>) {
const bookmarks = createState(new Map<URI, Bookmark>(), {
persist: 'bookmarks',
});
const navigationEvents = createState<NavigationEvent[]>([], {
persist: 'navigationEvents',
});
const appMatchPatterns = createState<AppMatchPattern[]>([], {
persist: 'appMatchPatterns',
});
const currentURI = createState('');
const shouldShowURIErrorDialog = createState(false);
const requiredParameters = createState<string[]>([]);
const shouldShowSaveBookmarkDialog = createState(false);
const saveBookmarkURI = createState<null | string>(null);
componentDidMount() {
const {selectedApp} = this.props;
this.subscribeToNavigationEvents();
this.getDevice()
.then((device) => getAppMatchPatterns(selectedApp, device))
.then((patterns: Array<AppMatchPattern>) => {
this.props.setPersistedState({
appMatchPatterns: patterns,
appMatchPatternsProvider: appMatchPatternsToAutoCompleteProvider(
patterns,
),
});
})
.catch(() => {
/* Silently fail here. */
});
readBookmarksFromDB().then((bookmarks) => {
this.props.setPersistedState({
bookmarks: bookmarks,
bookmarksProvider: bookmarksToAutoCompleteProvider(bookmarks),
});
client.onMessage('nav_event', async (payload) => {
const navigationEvent: NavigationEvent = {
uri: payload.uri === undefined ? null : decodeURIComponent(payload.uri),
date: payload.date ? new Date(payload.date) : new Date(),
className: payload.class === undefined ? null : payload.class,
screenshot: null,
};
if (navigationEvent.uri) currentURI.set(navigationEvent.uri);
navigationEvents.update((draft) => {
draft.unshift(navigationEvent);
});
}
navigateTo = async (query: string) => {
const screenshot: Buffer = await client.device.realDevice.screenshot();
const blobURL = URL.createObjectURL(bufferToBlob(screenshot));
// this process is async, make sure we update the correct one..
const navigationEventIndex = navigationEvents
.get()
.indexOf(navigationEvent);
if (navigationEventIndex !== -1) {
navigationEvents.update((draft) => {
draft[navigationEventIndex].screenshot = blobURL;
});
}
});
getAppMatchPatterns(client.appId, client.device.realDevice)
.then((patterns) => {
appMatchPatterns.set(patterns);
})
.catch((e) => {
console.error('[Navigation] Failed to find appMatchPatterns', e);
});
readBookmarksFromDB().then((bookmarksData) => {
bookmarks.set(bookmarksData);
});
function navigateTo(query: string) {
const filteredQuery = filterOptionalParameters(query);
this.props.setPersistedState({currentURI: filteredQuery});
const requiredParameters = getRequiredParameters(filteredQuery);
if (requiredParameters.length === 0) {
const device = await this.getDevice();
if (this.realClient.query.app === 'Facebook' && device.os === 'iOS') {
currentURI.set(filteredQuery);
const params = getRequiredParameters(filteredQuery);
if (params.length === 0) {
if (client.appName === 'Facebook' && client.device.os === 'iOS') {
// use custom navigate_to event for Wilde
this.client.send('navigate_to', {
client.send('navigate_to', {
url: filterOptionalParameters(filteredQuery),
});
} else {
device.navigateToLocation(filterOptionalParameters(filteredQuery));
client.device.realDevice.navigateToLocation(
filterOptionalParameters(filteredQuery),
);
}
} else {
this.setState({
requiredParameters,
shouldShowURIErrorDialog: true,
});
requiredParameters.set(params);
shouldShowURIErrorDialog.set(true);
}
};
}
onFavorite = (uri: string) => {
this.setState({shouldShowSaveBookmarkDialog: true, saveBookmarkURI: uri});
};
function onFavorite(uri: string) {
// TODO: why does this need a dialog?
shouldShowSaveBookmarkDialog.set(true);
saveBookmarkURI.set(uri);
}
addBookmark = (bookmark: Bookmark) => {
function addBookmark(bookmark: Bookmark) {
const newBookmark = {
uri: bookmark.uri,
commonName: bookmark.commonName,
};
bookmarks.update((draft) => {
draft.set(newBookmark.uri, newBookmark);
});
writeBookmarkToDB(newBookmark);
const newMapRef = this.props.persistedState.bookmarks;
newMapRef.set(newBookmark.uri, newBookmark);
this.props.setPersistedState({
bookmarks: newMapRef,
bookmarksProvider: bookmarksToAutoCompleteProvider(newMapRef),
});
};
removeBookmark = (uri: string) => {
removeBookmark(uri);
const newMapRef = this.props.persistedState.bookmarks;
newMapRef.delete(uri);
this.props.setPersistedState({
bookmarks: newMapRef,
bookmarksProvider: bookmarksToAutoCompleteProvider(newMapRef),
});
};
render() {
const {
saveBookmarkURI,
shouldShowSaveBookmarkDialog,
shouldShowURIErrorDialog,
requiredParameters,
} = this.state;
const {
bookmarks,
bookmarksProvider,
currentURI,
appMatchPatternsProvider,
navigationEvents,
} = this.props.persistedState;
const autoCompleteProviders = [bookmarksProvider, appMatchPatternsProvider];
return (
<FlexColumn grow>
<SearchBar
providers={autoCompleteProviders}
bookmarks={bookmarks}
onNavigate={this.navigateTo}
onFavorite={this.onFavorite}
uriFromAbove={currentURI}
/>
<Timeline
bookmarks={bookmarks}
events={navigationEvents}
onNavigate={this.navigateTo}
onFavorite={this.onFavorite}
/>
<BookmarksSidebar
bookmarks={bookmarks}
onRemove={this.removeBookmark}
onNavigate={this.navigateTo}
/>
<SaveBookmarkDialog
shouldShow={shouldShowSaveBookmarkDialog}
uri={saveBookmarkURI}
onHide={() => this.setState({shouldShowSaveBookmarkDialog: false})}
edit={
saveBookmarkURI != null ? bookmarks.has(saveBookmarkURI) : false
}
onSubmit={this.addBookmark}
onRemove={this.removeBookmark}
/>
<RequiredParametersDialog
shouldShow={shouldShowURIErrorDialog}
onHide={() => this.setState({shouldShowURIErrorDialog: false})}
uri={currentURI}
requiredParameters={requiredParameters}
onSubmit={this.navigateTo}
/>
</FlexColumn>
);
}
function removeBookmark(uri: string) {
bookmarks.update((draft) => {
draft.delete(uri);
});
removeBookmarkFromDB(uri);
}
return {
navigateTo,
onFavorite,
addBookmark,
removeBookmark,
bookmarks,
saveBookmarkURI,
shouldShowSaveBookmarkDialog,
shouldShowURIErrorDialog,
requiredParameters,
appMatchPatterns,
navigationEvents,
currentURI,
};
}
export function Component() {
const instance = usePlugin(plugin);
const bookmarks = useValue(instance.bookmarks);
const appMatchPatterns = useValue(instance.appMatchPatterns);
const saveBookmarkURI = useValue(instance.saveBookmarkURI);
const shouldShowSaveBookmarkDialog = useValue(
instance.shouldShowSaveBookmarkDialog,
);
const shouldShowURIErrorDialog = useValue(instance.shouldShowURIErrorDialog);
const requiredParameters = useValue(instance.requiredParameters);
const currentURI = useValue(instance.currentURI);
const navigationEvents = useValue(instance.navigationEvents);
const autoCompleteProviders = useMemo(
() => [
bookmarksToAutoCompleteProvider(bookmarks),
appMatchPatternsToAutoCompleteProvider(appMatchPatterns),
],
[bookmarks, appMatchPatterns],
);
return (
<Layout.Container>
<SearchBar
providers={autoCompleteProviders}
bookmarks={bookmarks}
onNavigate={instance.navigateTo}
onFavorite={instance.onFavorite}
uriFromAbove={currentURI}
/>
<Timeline
bookmarks={bookmarks}
events={navigationEvents}
onNavigate={instance.navigateTo}
onFavorite={instance.onFavorite}
/>
<BookmarksSidebar
bookmarks={bookmarks}
onRemove={instance.removeBookmark}
onNavigate={instance.navigateTo}
/>
<SaveBookmarkDialog
shouldShow={shouldShowSaveBookmarkDialog}
uri={saveBookmarkURI}
onHide={() => {
instance.shouldShowSaveBookmarkDialog.set(false);
}}
edit={saveBookmarkURI != null ? bookmarks.has(saveBookmarkURI) : false}
onSubmit={instance.addBookmark}
onRemove={instance.removeBookmark}
/>
<RequiredParametersDialog
shouldShow={shouldShowURIErrorDialog}
onHide={() => {
instance.shouldShowURIErrorDialog.set(false);
}}
uri={currentURI}
requiredParameters={requiredParameters}
onSubmit={instance.navigateTo}
/>
</Layout.Container>
);
}
/* @scarf-info: do not remove, more info: https://fburl.com/scarf */

View File

@@ -13,5 +13,8 @@
"icon": "directions",
"bugs": {
"email": "beneloca@fb.com"
},
"peerDependencies": {
"flipper-plugin": "0.64.0"
}
}

View File

@@ -9,20 +9,11 @@
export type URI = string;
export type State = {
shouldShowSaveBookmarkDialog: boolean;
shouldShowURIErrorDialog: boolean;
saveBookmarkURI: URI | null;
requiredParameters: Array<string>;
};
export type PersistedState = {
bookmarks: Map<URI, Bookmark>;
navigationEvents: Array<NavigationEvent>;
bookmarksProvider: AutoCompleteProvider;
appMatchPatterns: Array<AppMatchPattern>;
appMatchPatternsProvider: AutoCompleteProvider;
currentURI: string;
export type RawNavigationEvent = {
date: string | undefined;
uri: URI | undefined;
class: string | undefined;
screenshot: string | undefined;
};
export type NavigationEvent = {

View File

@@ -88,7 +88,7 @@ export const readBookmarksFromDB: () => Promise<Map<string, Bookmark>> = () => {
});
};
export const removeBookmark: (uri: string) => Promise<void> = (uri) => {
export const removeBookmarkFromDB: (uri: string) => Promise<void> = (uri) => {
return new Promise<void>((resolve, reject) => {
openNavigationPluginDB()
.then((db: IDBDatabase) => {