From d782f19001b787bd206ab0c39e4d5aa39c5de02f Mon Sep 17 00:00:00 2001 From: Anton Nikolaev Date: Wed, 21 Jul 2021 07:23:48 -0700 Subject: [PATCH] Refactor plugin to make it fast refreshable Summary: Refactored Navigation plugin to make it fast-refreshable: moved the main component into a separate file and exported all components as named functions. Without these changes every change of UI triggered full reload. Reviewed By: timur-valiev Differential Revision: D29814077 fbshipit-source-id: 5285bdc5f14a5163f9501c0d45a3affefb08fc8e --- .../plugins/public/navigation/Component.tsx | 78 ++++++ .../components/AutoCompleteSheet.tsx | 4 +- .../components/BookmarksSidebar.tsx | 4 +- .../navigation/components/FavoriteButton.tsx | 4 +- .../navigation/components/IconButton.tsx | 8 +- .../components/NavigationInfoBox.tsx | 4 +- .../components/RequiredParametersDialog.tsx | 4 +- .../components/SaveBookmarkDialog.tsx | 4 +- .../navigation/components/SearchBar.tsx | 4 +- .../public/navigation/components/Timeline.tsx | 4 +- .../public/navigation/components/index.tsx | 18 +- desktop/plugins/public/navigation/index.tsx | 247 +----------------- desktop/plugins/public/navigation/plugin.tsx | 187 +++++++++++++ 13 files changed, 295 insertions(+), 275 deletions(-) create mode 100644 desktop/plugins/public/navigation/Component.tsx create mode 100644 desktop/plugins/public/navigation/plugin.tsx diff --git a/desktop/plugins/public/navigation/Component.tsx b/desktop/plugins/public/navigation/Component.tsx new file mode 100644 index 000000000..347a18753 --- /dev/null +++ b/desktop/plugins/public/navigation/Component.tsx @@ -0,0 +1,78 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +import { + BookmarksSidebar, + SaveBookmarkDialog, + SearchBar, + Timeline, +} from './components'; +import { + appMatchPatternsToAutoCompleteProvider, + bookmarksToAutoCompleteProvider, +} from './util/autoCompleteProvider'; +import React, {useMemo} from 'react'; +import {useValue, usePlugin, Layout} from 'flipper-plugin'; +import {plugin} from './plugin'; + +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 currentURI = useValue(instance.currentURI); + const navigationEvents = useValue(instance.navigationEvents); + + const autoCompleteProviders = useMemo( + () => [ + bookmarksToAutoCompleteProvider(bookmarks), + appMatchPatternsToAutoCompleteProvider(appMatchPatterns), + ], + [bookmarks, appMatchPatterns], + ); + return ( + + + + + { + instance.shouldShowSaveBookmarkDialog.set(false); + }} + edit={saveBookmarkURI != null ? bookmarks.has(saveBookmarkURI) : false} + onSubmit={instance.addBookmark} + onRemove={instance.removeBookmark} + /> + + ); +} + +/* @scarf-info: do not remove, more info: https://fburl.com/scarf */ +/* @scarf-generated: flipper-plugin index.js.template 0bfa32e5-fb15-4705-81f8-86260a1f3f8e */ diff --git a/desktop/plugins/public/navigation/components/AutoCompleteSheet.tsx b/desktop/plugins/public/navigation/components/AutoCompleteSheet.tsx index 83e2aaa2e..c7bd30f73 100644 --- a/desktop/plugins/public/navigation/components/AutoCompleteSheet.tsx +++ b/desktop/plugins/public/navigation/components/AutoCompleteSheet.tsx @@ -50,7 +50,7 @@ const SheetItemIcon = styled.span({ padding: 8, }); -export default (props: Props) => { +export function AutoCompleteSheet(props: Props) { const {providers, onHighlighted, onNavigate, query} = props; const lineItems = filterProvidersToLineItems(providers, query, MAX_ITEMS); lineItems.unshift({uri: query, matchPattern: query, icon: 'send'}); @@ -70,4 +70,4 @@ export default (props: Props) => { ))} ); -}; +} diff --git a/desktop/plugins/public/navigation/components/BookmarksSidebar.tsx b/desktop/plugins/public/navigation/components/BookmarksSidebar.tsx index a7f9fc3f7..07c3a0bcb 100644 --- a/desktop/plugins/public/navigation/components/BookmarksSidebar.tsx +++ b/desktop/plugins/public/navigation/components/BookmarksSidebar.tsx @@ -78,7 +78,7 @@ const alphabetizeBookmarkCompare = (b1: Bookmark, b2: Bookmark) => { return b1.uri < b2.uri ? -1 : b1.uri > b2.uri ? 1 : 0; }; -export default (props: Props) => { +export function BookmarksSidebar(props: Props) { const {bookmarks, onNavigate, onRemove} = props; return ( @@ -119,4 +119,4 @@ export default (props: Props) => { ); -}; +} diff --git a/desktop/plugins/public/navigation/components/FavoriteButton.tsx b/desktop/plugins/public/navigation/components/FavoriteButton.tsx index 613e09eac..287ff368b 100644 --- a/desktop/plugins/public/navigation/components/FavoriteButton.tsx +++ b/desktop/plugins/public/navigation/components/FavoriteButton.tsx @@ -27,7 +27,7 @@ const FavoriteButtonContainer = styled.div({ }, }); -export default (props: Props) => { +export function FavoriteButton(props: Props) { const {highlighted, onClick, ...iconButtonProps} = props; return ( @@ -42,4 +42,4 @@ export default (props: Props) => { ); -}; +} diff --git a/desktop/plugins/public/navigation/components/IconButton.tsx b/desktop/plugins/public/navigation/components/IconButton.tsx index 1a8a067dc..f21caf849 100644 --- a/desktop/plugins/public/navigation/components/IconButton.tsx +++ b/desktop/plugins/public/navigation/components/IconButton.tsx @@ -43,23 +43,23 @@ const RippleEffect = styled.div({ }, }); -const IconButton = styled.div({ +const IconButtonContainer = styled.div({ ':active': { animation: `${shrinkAnimation} .25s ease forwards`, }, }); -export default function (props: Props) { +export function IconButton(props: Props) { return ( - + - + ); } diff --git a/desktop/plugins/public/navigation/components/NavigationInfoBox.tsx b/desktop/plugins/public/navigation/components/NavigationInfoBox.tsx index e8cbf4fe6..6639f1a5f 100644 --- a/desktop/plugins/public/navigation/components/NavigationInfoBox.tsx +++ b/desktop/plugins/public/navigation/components/NavigationInfoBox.tsx @@ -160,7 +160,7 @@ const buildParameterTable = (parameters: Map) => { ); }; -export default (props: Props) => { +export function NavigationInfoBox(props: Props) { const { uri, isBookmarked, @@ -238,4 +238,4 @@ export default (props: Props) => { ); } -}; +} diff --git a/desktop/plugins/public/navigation/components/RequiredParametersDialog.tsx b/desktop/plugins/public/navigation/components/RequiredParametersDialog.tsx index b409915bd..4444dc6b7 100644 --- a/desktop/plugins/public/navigation/components/RequiredParametersDialog.tsx +++ b/desktop/plugins/public/navigation/components/RequiredParametersDialog.tsx @@ -28,7 +28,7 @@ type Props = { onSubmit: (uri: URI) => void; }; -export default (props: Props) => { +export function RequiredParametersDialog(props: Props) { const {onHide, onSubmit, uri, requiredParameters} = props; const {isValid, values, setValuesArray} = useRequiredParameterFormValidator(requiredParameters); @@ -95,4 +95,4 @@ export default (props: Props) => { ); -}; +} diff --git a/desktop/plugins/public/navigation/components/SaveBookmarkDialog.tsx b/desktop/plugins/public/navigation/components/SaveBookmarkDialog.tsx index 830097c2a..f3a01515c 100644 --- a/desktop/plugins/public/navigation/components/SaveBookmarkDialog.tsx +++ b/desktop/plugins/public/navigation/components/SaveBookmarkDialog.tsx @@ -48,7 +48,7 @@ const NameInput = styled(Input)({ height: 30, }); -export default (props: Props) => { +export function SaveBookmarkDialog(props: Props) { const {edit, shouldShow, onHide, onRemove, onSubmit, uri} = props; const [commonName, setCommonName] = useState(''); if (uri == null || !shouldShow) { @@ -114,4 +114,4 @@ export default (props: Props) => { ); } -}; +} diff --git a/desktop/plugins/public/navigation/components/SearchBar.tsx b/desktop/plugins/public/navigation/components/SearchBar.tsx index 07469a6cb..7019331ae 100644 --- a/desktop/plugins/public/navigation/components/SearchBar.tsx +++ b/desktop/plugins/public/navigation/components/SearchBar.tsx @@ -57,7 +57,7 @@ const SearchInputContainer = styled.div({ position: 'relative', }); -class SearchBar extends Component { +export class SearchBar extends Component { state = { inputFocused: false, autoCompleteSheetOpen: false, @@ -158,5 +158,3 @@ class SearchBar extends Component { ); } } - -export default SearchBar; diff --git a/desktop/plugins/public/navigation/components/Timeline.tsx b/desktop/plugins/public/navigation/components/Timeline.tsx index 216f3e98e..f79d1b9ba 100644 --- a/desktop/plugins/public/navigation/components/Timeline.tsx +++ b/desktop/plugins/public/navigation/components/Timeline.tsx @@ -58,7 +58,7 @@ const NoData = styled(FlexCenter)({ color: theme.textColorSecondary, }); -export default (props: Props) => { +export function Timeline(props: Props) { const {bookmarks, events, onNavigate, onFavorite} = props; const timelineRef = useRef(null); return events.length === 0 ? ( @@ -92,4 +92,4 @@ export default (props: Props) => { ); -}; +} diff --git a/desktop/plugins/public/navigation/components/index.tsx b/desktop/plugins/public/navigation/components/index.tsx index 8d55be28d..62dca3bd7 100644 --- a/desktop/plugins/public/navigation/components/index.tsx +++ b/desktop/plugins/public/navigation/components/index.tsx @@ -7,12 +7,12 @@ * @format */ -export {default as AutoCompleteSheet} from './AutoCompleteSheet'; -export {default as BookmarksSidebar} from './BookmarksSidebar'; -export {default as FavoriteButton} from './FavoriteButton'; -export {default as IconButton} from './IconButton'; -export {default as NavigationInfoBox} from './NavigationInfoBox'; -export {default as RequiredParametersDialog} from './RequiredParametersDialog'; -export {default as SaveBookmarkDialog} from './SaveBookmarkDialog'; -export {default as SearchBar} from './SearchBar'; -export {default as Timeline} from './Timeline'; +export {AutoCompleteSheet} from './AutoCompleteSheet'; +export {BookmarksSidebar} from './BookmarksSidebar'; +export {FavoriteButton} from './FavoriteButton'; +export {IconButton} from './IconButton'; +export {NavigationInfoBox} from './NavigationInfoBox'; +export {RequiredParametersDialog} from './RequiredParametersDialog'; +export {SaveBookmarkDialog} from './SaveBookmarkDialog'; +export {SearchBar} from './SearchBar'; +export {Timeline} from './Timeline'; diff --git a/desktop/plugins/public/navigation/index.tsx b/desktop/plugins/public/navigation/index.tsx index 4f97fe404..fa2ff8b8a 100644 --- a/desktop/plugins/public/navigation/index.tsx +++ b/desktop/plugins/public/navigation/index.tsx @@ -8,248 +8,5 @@ * @flow strict-local */ -import {bufferToBlob} from 'flipper'; -import { - BookmarksSidebar, - SaveBookmarkDialog, - SearchBar, - Timeline, - RequiredParametersDialog, -} from './components'; -import { - removeBookmarkFromDB, - readBookmarksFromDB, - writeBookmarkToDB, -} from './util/indexedDB'; -import { - appMatchPatternsToAutoCompleteProvider, - bookmarksToAutoCompleteProvider, -} from './util/autoCompleteProvider'; -import {getAppMatchPatterns} from './util/appMatchPatterns'; -import {getRequiredParameters, filterOptionalParameters} from './util/uri'; -import { - Bookmark, - NavigationEvent, - AppMatchPattern, - URI, - RawNavigationEvent, -} from './types'; -import React, {useMemo} from 'react'; -import { - PluginClient, - createState, - useValue, - usePlugin, - Layout, - renderReactRoot, -} from 'flipper-plugin'; - -export type State = { - shouldShowSaveBookmarkDialog: boolean; - shouldShowURIErrorDialog: boolean; - saveBookmarkURI: URI | null; - requiredParameters: Array; -}; - -type Events = { - nav_event: RawNavigationEvent; -}; - -type Methods = { - navigate_to(params: {url: string}): Promise; -}; - -export type NavigationPlugin = ReturnType; - -export function plugin(client: PluginClient) { - const bookmarks = createState(new Map(), { - persist: 'bookmarks', - }); - const navigationEvents = createState([], { - persist: 'navigationEvents', - }); - const appMatchPatterns = createState([], { - persist: 'appMatchPatterns', - }); - const currentURI = createState(''); - const shouldShowSaveBookmarkDialog = createState(false); - const saveBookmarkURI = createState(null); - - 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); - }); - - 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); - 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 - client.send('navigate_to', { - url: filterOptionalParameters(filteredQuery), - }); - } else { - client.device.realDevice.navigateToLocation( - filterOptionalParameters(filteredQuery), - ); - } - } else { - renderReactRoot((unmount) => ( - - )); - } - } - - function onFavorite(uri: string) { - shouldShowSaveBookmarkDialog.set(true); - saveBookmarkURI.set(uri); - } - - function addBookmark(bookmark: Bookmark) { - const newBookmark = { - uri: bookmark.uri, - commonName: bookmark.commonName, - }; - - bookmarks.update((draft) => { - draft.set(newBookmark.uri, newBookmark); - }); - writeBookmarkToDB(newBookmark); - } - - function removeBookmark(uri: string) { - bookmarks.update((draft) => { - draft.delete(uri); - }); - removeBookmarkFromDB(uri); - } - - return { - navigateTo, - onFavorite, - addBookmark, - removeBookmark, - bookmarks, - saveBookmarkURI, - shouldShowSaveBookmarkDialog, - appMatchPatterns, - navigationEvents, - currentURI, - getAutoCompleteAppMatchPatterns( - query: string, - bookmarks: Map, - appMatchPatterns: AppMatchPattern[], - limit: number, - ): AppMatchPattern[] { - const q = query.toLowerCase(); - const results: AppMatchPattern[] = []; - for (const item of appMatchPatterns) { - if ( - !bookmarks.has(item.pattern) && - (item.className.toLowerCase().includes(q) || - item.pattern.toLowerCase().includes(q)) - ) { - results.push(item); - if (--limit < 1) break; - } - } - return results; - }, - }; -} - -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 currentURI = useValue(instance.currentURI); - const navigationEvents = useValue(instance.navigationEvents); - - const autoCompleteProviders = useMemo( - () => [ - bookmarksToAutoCompleteProvider(bookmarks), - appMatchPatternsToAutoCompleteProvider(appMatchPatterns), - ], - [bookmarks, appMatchPatterns], - ); - return ( - - - - - { - instance.shouldShowSaveBookmarkDialog.set(false); - }} - edit={saveBookmarkURI != null ? bookmarks.has(saveBookmarkURI) : false} - onSubmit={instance.addBookmark} - onRemove={instance.removeBookmark} - /> - - ); -} - -/* @scarf-info: do not remove, more info: https://fburl.com/scarf */ -/* @scarf-generated: flipper-plugin index.js.template 0bfa32e5-fb15-4705-81f8-86260a1f3f8e */ +export {plugin, NavigationPlugin} from './plugin'; +export {Component} from './Component'; diff --git a/desktop/plugins/public/navigation/plugin.tsx b/desktop/plugins/public/navigation/plugin.tsx new file mode 100644 index 000000000..48b5e52dd --- /dev/null +++ b/desktop/plugins/public/navigation/plugin.tsx @@ -0,0 +1,187 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +import {bufferToBlob} from 'flipper'; +import {RequiredParametersDialog} from './components'; +import { + removeBookmarkFromDB, + readBookmarksFromDB, + writeBookmarkToDB, +} from './util/indexedDB'; +import {} from './util/autoCompleteProvider'; +import {getAppMatchPatterns} from './util/appMatchPatterns'; +import {getRequiredParameters, filterOptionalParameters} from './util/uri'; +import { + Bookmark, + NavigationEvent, + AppMatchPattern, + URI, + RawNavigationEvent, +} from './types'; +import React from 'react'; +import {PluginClient, createState, renderReactRoot} from 'flipper-plugin'; + +export type State = { + shouldShowSaveBookmarkDialog: boolean; + shouldShowURIErrorDialog: boolean; + saveBookmarkURI: URI | null; + requiredParameters: Array; +}; + +type Events = { + nav_event: RawNavigationEvent; +}; + +type Methods = { + navigate_to(params: {url: string}): Promise; +}; + +export type NavigationPlugin = ReturnType; + +export function plugin(client: PluginClient) { + const bookmarks = createState(new Map(), { + persist: 'bookmarks', + }); + const navigationEvents = createState([], { + persist: 'navigationEvents', + }); + const appMatchPatterns = createState([], { + persist: 'appMatchPatterns', + }); + const currentURI = createState(''); + const shouldShowSaveBookmarkDialog = createState(false); + const saveBookmarkURI = createState(null); + + 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); + }); + + 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); + 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 + client.send('navigate_to', { + url: filterOptionalParameters(filteredQuery), + }); + } else { + client.device.realDevice.navigateToLocation( + filterOptionalParameters(filteredQuery), + ); + } + } else { + renderReactRoot((unmount) => ( + + )); + } + } + + function onFavorite(uri: string) { + shouldShowSaveBookmarkDialog.set(true); + saveBookmarkURI.set(uri); + } + + function addBookmark(bookmark: Bookmark) { + const newBookmark = { + uri: bookmark.uri, + commonName: bookmark.commonName, + }; + + bookmarks.update((draft) => { + draft.set(newBookmark.uri, newBookmark); + }); + writeBookmarkToDB(newBookmark); + } + + function removeBookmark(uri: string) { + bookmarks.update((draft) => { + draft.delete(uri); + }); + removeBookmarkFromDB(uri); + } + + return { + navigateTo, + onFavorite, + addBookmark, + removeBookmark, + bookmarks, + saveBookmarkURI, + shouldShowSaveBookmarkDialog, + appMatchPatterns, + navigationEvents, + currentURI, + getAutoCompleteAppMatchPatterns( + query: string, + bookmarks: Map, + appMatchPatterns: AppMatchPattern[], + limit: number, + ): AppMatchPattern[] { + const q = query.toLowerCase(); + const results: AppMatchPattern[] = []; + for (const item of appMatchPatterns) { + if ( + !bookmarks.has(item.pattern) && + (item.className.toLowerCase().includes(q) || + item.pattern.toLowerCase().includes(q)) + ) { + results.push(item); + if (--limit < 1) break; + } + } + return results; + }, + }; +} + +/* @scarf-info: do not remove, more info: https://fburl.com/scarf */ +/* @scarf-generated: flipper-plugin index.js.template 0bfa32e5-fb15-4705-81f8-86260a1f3f8e */