diff --git a/src/components/FileExplorer.tsx b/src/components/FileExplorer.tsx index 8f4bb73..60e95ab 100644 --- a/src/components/FileExplorer.tsx +++ b/src/components/FileExplorer.tsx @@ -1,129 +1,91 @@ import { LoadingIndicator } from 'components/LoadingIndicator' import { Node } from 'components/Node' import { SearchBar } from 'components/SearchBar' +import { useConfigs } from 'containers/ConfigsContext' import { connect } from 'driver/connect' import { FileExplorerCore } from 'driver/core' import { ConnectorState, Props } from 'driver/core/FileExplorer' import * as React from 'react' -import { FixedSizeList as List, ListChildComponentProps } from 'react-window' +import { FixedSizeList as List, ListChildComponentProps, ListProps } from 'react-window' import { cx } from 'utils/cx' import { usePrevious } from 'utils/hooks' import { TreeNode, VisibleNodes } from 'utils/VisibleNodesGenerator' import { Icon } from './Icon' import { SizeObserver } from './SizeObserver' -class RawFileExplorer extends React.Component { - static defaultProps: Partial = { - freeze: false, - searchKey: '', - visibleNodes: null, - } +const VisibleNodesContext = React.createContext(null) - componentDidMount() { - const { init, setUpTree, treeData, metaData, compressSingletonFolder, accessToken } = this.props +const RawFileExplorer: React.FC = function RawFileExplorer(props) { + const { visibleNodes, freeze, onNodeClick, searchKey } = props + const { + val: { access_token: accessToken, compressSingletonFolder }, + } = useConfigs() + + React.useEffect(() => { + const { init } = props init() + }, []) + + React.useEffect(() => { + const { setUpTree, treeData, metaData } = props setUpTree({ treeData, metaData, compressSingletonFolder, accessToken }) - const { execAfterRender } = this.props + }, [props.setUpTree, props.treeData, props.metaData, compressSingletonFolder, accessToken]) + + React.useEffect(() => { + const { execAfterRender } = props execAfterRender() - } - - componentDidUpdate(prevProps: Props & ConnectorState) { - if (this.props.treeData !== prevProps.treeData) { - const { setUpTree, treeData, metaData, compressSingletonFolder, accessToken } = this.props - setUpTree({ treeData, metaData, compressSingletonFolder, accessToken }) - } - const { execAfterRender } = this.props - execAfterRender() - } - - renderFiles(visibleNodes: VisibleNodes) { - const { nodes, focusedNode } = visibleNodes - const { searchKey } = this.props - const inSearch = searchKey !== '' - if (inSearch && nodes.length === 0) { - return - } - return ( - - {({ width = 0, height = 0 }) => ( - - )} - - ) - } - - ListV = React.memo<{ - nodes: TreeNode[] - height: number - width: number - focusedNode: TreeNode | null - }>(({ nodes, width, height, focusedNode }) => { - const listRef = React.useRef(null) - React.useEffect(() => { - if (focusedNode && listRef.current) { - listRef.current.scrollToItem(nodes.indexOf(focusedNode), 'smart') - } - }, [listRef.current, focusedNode]) - - const lastNodeLength = usePrevious(nodes.length) - React.useEffect(() => { - if (listRef.current && !focusedNode && lastNodeLength !== nodes.length) { - listRef.current.scrollTo(0) - } - }, [listRef.current, focusedNode, nodes.length]) - return ( - { - const node = nodes[index] - return node && node.path - }} - itemData={{ nodes }} - itemCount={nodes.length} - itemSize={35} - height={height} - width={width} - > - {this.VirtualNode} - - ) }) - VirtualNode = React.memo(({ index, style }) => { - const { visibleNodes, onNodeClick } = this.props - if (!visibleNodes) return null - const { nodes, depths, focusedNode, expandedNodes } = visibleNodes - const node = nodes[index] - return ( - - ) - }) - - private renderActions: React.ComponentProps['renderActions'] = node => { - const { searchKey, goTo } = this.props - return ( - searchKey && ( + const renderActions: React.ComponentProps['renderActions'] = React.useCallback( + node => + searchKey ? ( - ) - ) - } + ) : null, + [searchKey, props.goTo], + ) - revealNode( + const renderNode = React.useCallback( + ({ index, style }: ListChildComponentProps) => ( + + ), + [renderActions, onNodeClick], + ) + + const renderFiles = React.useCallback( + ({ nodes, focusedNode }: VisibleNodes) => { + const inSearch = searchKey !== '' + if (inSearch && nodes.length === 0) { + return + } + return ( + + {({ width = 0, height = 0 }) => ( + + )} + + ) + }, + [searchKey, ListView, renderNode], + ) + + const revealNode = React.useCallback(function revealNode( goTo: (path: string[]) => void, node: TreeNode, ): (event: React.MouseEvent) => void { @@ -132,39 +94,113 @@ class RawFileExplorer extends React.Component { e.preventDefault() goTo(node.path.split('/')) } - } + }, + []) - render() { - const { - stateText, - visibleNodes, - freeze, - handleKeyDown, - search, - toggleShowSettings, - onFocusSearchBar, - searchKey, - } = this.props - return ( + return ( +
- {stateText ? ( - + {props.stateText ? ( + ) : ( visibleNodes && ( - - - {this.renderFiles(visibleNodes)} - + <> + + {renderFiles(visibleNodes)} + ) )}
- ) - } +
+ ) +} + +RawFileExplorer.defaultProps = { + freeze: false, + searchKey: '', + visibleNodes: null, } export const FileExplorer = connect(FileExplorerCore)(RawFileExplorer) + +function VirtualNode({ + index, + style, + onNodeClick, + renderActions, +}: { + index: number + style: React.CSSProperties + onNodeClick: (treeNode: TreeNode) => void + renderActions: ((node: TreeNode) => React.ReactNode) | undefined +}) { + const visibleNodes = React.useContext(VisibleNodesContext) + if (!visibleNodes) return null + const { nodes, depths, focusedNode, expandedNodes } = visibleNodes + const node = nodes[index] + return ( + + ) +} + +function ListView({ + nodes, + width, + height, + focusedNode, + renderNode, +}: { + nodes: TreeNode[] + height: number + width: number + focusedNode: TreeNode | null + renderNode: ListProps['children'] +}) { + const listRef = React.useRef(null) + React.useEffect(() => { + if (focusedNode && listRef.current) { + listRef.current.scrollToItem(nodes.indexOf(focusedNode), 'smart') + } + }, [listRef.current, focusedNode]) + + const lastNodeLength = usePrevious(nodes.length) + React.useEffect(() => { + if (listRef.current && !focusedNode && lastNodeLength !== nodes.length) { + listRef.current.scrollTo(0) + } + }, [listRef.current, focusedNode, nodes.length]) + return ( + { + const node = nodes[index] + return node && node.path + }} + itemData={{ nodes }} + itemCount={nodes.length} + itemSize={35} + height={height} + width={width} + > + {renderNode} + + ) +} diff --git a/src/components/MoreOption.tsx b/src/components/MoreOption.tsx new file mode 100644 index 0000000..65a9d66 --- /dev/null +++ b/src/components/MoreOption.tsx @@ -0,0 +1,52 @@ +import { useConfigs } from 'containers/ConfigsContext' +import * as React from 'react' +import { Config } from 'utils/configHelper' + +export type SimpleField = { + key: keyof Config + label: string + wikiLink?: string + description?: string + overwrite?: Props['overwrite'] +} + +type Props = { + field: SimpleField + onChange?(): void + overwrite?: { + value: (value: T) => boolean + onChange: (checked: boolean) => any + } +} + +export function SimpleFieldInput({ field, overwrite, onChange }: Props) { + const configContext = useConfigs() + const value = configContext.val[field.key] + return ( + + ) +} diff --git a/src/components/SettingsBar.tsx b/src/components/SettingsBar.tsx index f5fa77e..98ac7e1 100644 --- a/src/components/SettingsBar.tsx +++ b/src/components/SettingsBar.tsx @@ -1,10 +1,12 @@ import { raiseError } from 'analytics' import { Icon } from 'components/Icon' +import { useConfigs } from 'containers/ConfigsContext' import { oauth, VERSION } from 'env' import * as React from 'react' -import { Config, configKeys, setOne } from 'utils/configHelper' import { friendlyFormatShortcut, JSONRequest, parseURLSearch } from 'utils/general' +import { useStates } from 'utils/hooks' import * as keyHelper from 'utils/keyHelper' +import { SimpleField, SimpleFieldInput } from './MoreOption' const WIKI_HOME_LINK = 'https://github.com/EnixCoda/Gitako/wiki' const wikiLinks = { @@ -18,348 +20,268 @@ const wikiLinks = { const ACCESS_TOKEN_REGEXP = /^[0-9a-f]{40}$/ type Props = { - accessToken?: string activated: boolean - onAccessTokenChange: (accessToken: string) => void - onShortcutChange: (shortcut: string) => void - setCopyFile: (copyFileButton: Props['copyFileButton']) => void - setCopySnippet: (copySnippetButton: Props['copySnippetButton']) => void - setCompressSingleton: (compressSingletonFolder: Props['compressSingletonFolder']) => void - setIntelligentToggle: (intelligentToggle: Props['intelligentToggle']) => void toggleShowSettings: () => void - toggleShowSideBarShortcut?: string -} & Pick< - Config, - 'compressSingletonFolder' | 'copyFileButton' | 'copySnippetButton' | 'intelligentToggle' -> - -type State = { - accessToken?: string - accessTokenHint: React.ReactNode - shortcutHint: string - toggleShowSideBarShortcut?: string - reloadHint: React.ReactNode - varyOptions: { - key: string - label: string - onChange: (e: React.FormEvent) => Promise | void - getValue: () => boolean - wikiLink?: string - description?: string - }[] } -export class SettingsBar extends React.PureComponent { - state = { - accessToken: '', - accessTokenHint: '', - shortcutHint: '', - toggleShowSideBarShortcut: this.props.toggleShowSideBarShortcut, - reloadHint: '', - varyOptions: [ - { - key: 'compress-singleton', - label: 'Compress singleton folder', - onChange: this.createOnToggleChecked( - configKeys.compressSingletonFolder, - this.props.setCompressSingleton, - ), - getValue: () => this.props.compressSingletonFolder, - wikiLink: wikiLinks.compressSingletonFolder, - }, - { - key: 'copy-file', - label: 'Copy File Shortcut', - onChange: this.createOnToggleChecked(configKeys.copyFileButton, this.props.setCopyFile), - getValue: () => this.props.copyFileButton, - wikiLink: wikiLinks.copyFileButton, - }, - { - key: 'copy-snippet', - label: 'Copy Snippet Shortcut', - onChange: this.createOnToggleChecked( - configKeys.copySnippetButton, - this.props.setCopySnippet, - ), - getValue: () => this.props.copySnippetButton, - wikiLink: wikiLinks.copySnippet, - }, - { - key: 'intelligent-toggle', - label: 'Intelligent Toggle', - onChange: async (e: React.FormEvent) => { - const { checked } = e.currentTarget - const intelligentToggle = checked ? null : true - await setOne(configKeys.intelligentToggle, intelligentToggle) - this.props.setIntelligentToggle(intelligentToggle) - }, - getValue: () => this.props.intelligentToggle === null, - description: `Gitako will open/close automatically according to page content when this is enabled.`, - }, - ], - } +const moreFields: SimpleField[] = [ + { + key: 'compressSingletonFolder', + label: 'Compress singleton folder', + wikiLink: wikiLinks.compressSingletonFolder, + }, + { + key: 'copyFileButton', + label: 'Copy File Shortcut', + wikiLink: wikiLinks.copyFileButton, + }, + { + key: 'copySnippetButton', + label: 'Copy Snippet Shortcut', + wikiLink: wikiLinks.copySnippet, + }, + { + key: 'intelligentToggle', + label: 'Intelligent Toggle', + description: `Gitako will open/close automatically according to page content when this is enabled.`, + overwrite: { + value: enabled => enabled === null, + onChange: checked => (checked ? null : true), + }, + }, +] - componentDidMount() { - if (!this.props.accessToken) this.trySetUpAccessTokenWithCode() - } +function SettingsBarContent() { + const configContext = useConfigs() + const hasAccessToken = Boolean(configContext.val.access_token) + const useAccessToken = useStates('') + const useAccessTokenHint = useStates('') + const useShortcutHint = useStates('') + const useToggleShowSideBarShortcut = useStates(configContext.val.shortcut) + const useReloadHint = useStates('') - componentDidUpdate({ toggleShowSideBarShortcut }: Props) { - if (toggleShowSideBarShortcut !== this.props.toggleShowSideBarShortcut) { - this.setState({ toggleShowSideBarShortcut: this.props.toggleShowSideBarShortcut }) + React.useEffect(() => { + if (!configContext.val.access_token) { + trySetUpAccessTokenWithCode().then(accessToken => { + useAccessToken.set(accessToken) + saveToken('') + }) } - } + }, []) - private async trySetUpAccessTokenWithCode() { - try { - const search = parseURLSearch() - if ('code' in search) { - const res = await JSONRequest('https://github.com/login/oauth/access_token', { - code: search.code, - client_id: oauth.clientId, - client_secret: oauth.clientSecret, - }) - const { access_token: accessToken, scope } = res - if (scope !== 'repo' || !accessToken) { - throw new Error(`Cannot resolve token response: '${JSON.stringify(res)}'`) - } - window.history.pushState({}, 'removed code', window.location.pathname.replace(/#.*$/, '')) - this.setState({ accessToken }, () => this.saveToken('')) - } - } catch (err) { - raiseError(err) - } - } + React.useEffect(() => { + useToggleShowSideBarShortcut.set(configContext.val.shortcut) + }, [configContext.val.shortcut]) - onInputAccessToken = (event: React.FormEvent) => { - const { value } = event.currentTarget - this.setState({ - accessToken: value, - accessTokenHint: ACCESS_TOKEN_REGEXP.test(value) ? '' : 'This token is in unknown format.', - }) - } + const onInputAccessToken = React.useCallback( + ({ currentTarget: { value } }: React.FormEvent) => { + useAccessToken.set(value) + useAccessTokenHint.set( + ACCESS_TOKEN_REGEXP.test(value) ? '' : 'This token is in unknown format.', + ) + }, + [], + ) - onPressAccessToken = (event: React.KeyboardEvent) => { - const { key } = event - if (key === 'Enter') { - this.saveToken() - } - } + const onPressAccessToken = React.useCallback(({ key }: React.KeyboardEvent) => { + if (key === 'Enter') saveToken() + }, []) - saveToken = async ( - hint: State['accessTokenHint'] = ( - - window.location.reload()}> - Reload - {' '} - to activate! - - ), - ) => { - const { onAccessTokenChange } = this.props - const { accessToken } = this.state + const saveToken = React.useCallback(async (hint?: typeof useAccessTokenHint.val) => { + const { val: accessToken } = useAccessToken if (accessToken) { - await setOne(configKeys.accessToken, accessToken) - onAccessTokenChange(accessToken) - this.setState({ - accessToken: '', - accessTokenHint: hint, - }) + configContext.set({ access_token: accessToken }) + useAccessToken.set('') + useAccessTokenHint.set( + hint || ( + + window.location.reload()}> + Reload + {' '} + to activate! + + ), + ) } - } + }, []) - clearToken = async () => { - const { onAccessTokenChange } = this.props - await setOne(configKeys.accessToken, '') - onAccessTokenChange('') - this.setState({ accessToken: '' }) - } + const clearToken = React.useCallback(async () => { + configContext.set({ access_token: '' }) + useAccessToken.set('') + }, []) - saveShortcut = async () => { - const { onShortcutChange } = this.props - const { toggleShowSideBarShortcut } = this.state - await setOne(configKeys.shortcut, toggleShowSideBarShortcut) + const saveShortcut = React.useCallback(async () => { + const { val: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut + configContext.set({ shortcut: toggleShowSideBarShortcut }) if (typeof toggleShowSideBarShortcut === 'string') { - onShortcutChange(toggleShowSideBarShortcut) - this.setState({ - shortcutHint: 'Shortcut is saved!', - }) + useShortcutHint.set('Shortcut is saved!') } - } + }, [useToggleShowSideBarShortcut.val]) - onShortCutInputKeyDown = (e: React.KeyboardEvent) => { + const onShortCutInputKeyDown = React.useCallback((e: React.KeyboardEvent) => { e.preventDefault() + e.stopPropagation() // Clear shortcut with backspace const shortcut = e.key === 'Backspace' ? '' : keyHelper.parseEvent(e) - this.setState({ toggleShowSideBarShortcut: shortcut }) - } + useToggleShowSideBarShortcut.set(shortcut) + }, []) - showReloadHint = () => { - this.setState({ - reloadHint: ( + const showReloadHint = React.useCallback( + () => + useReloadHint.set( Saved,{' '} window.location.reload()}> reload {' '} to apply. - + , ), - }) - } + [], + ) - createOnToggleChecked( - configKey: configKeys, - set: (value: boolean) => void, - ): (e: React.FormEvent) => Promise { - return async e => { - const enabled = e.currentTarget.checked - await setOne(configKey, enabled) - set(enabled) - this.showReloadHint() - } - } + const { val: accessTokenHint } = useAccessTokenHint + const { val: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut + const { val: shortcutHint } = useShortcutHint + const { val: accessToken } = useAccessToken + const { val: reloadHint } = useReloadHint - render() { - const { - accessTokenHint, - toggleShowSideBarShortcut, - shortcutHint, - accessToken, - reloadHint, - varyOptions, - } = this.state - const { toggleShowSettings, activated } = this.props - const hasAccessToken = Boolean(this.props.accessToken) - return ( -
- {activated && ( - -

Settings

-
-
-
-

- Access Token - -  (?) - -

- {!hasAccessToken && ( - { - // use js here to make sure redirect_uri is latest url - const url = `https://github.com/login/oauth/authorize?client_id=${ - oauth.clientId - }&scope=repo&redirect_uri=${encodeURIComponent(window.location.href)}` - window.location.href = url - }} - > - Create with OAuth (recommended) - - )} -
- - {hasAccessToken && !accessToken ? ( - - ) : ( - - )} -
- {accessTokenHint && {accessTokenHint}} -
-
-

Toggle Shortcut

- Set a combination of keys for toggling Gitako sidebar. -
-
- - -
- {shortcutHint && {shortcutHint}} -
-
-

More Options

- {varyOptions.map(option => ( - - -
-
- ))} - {reloadHint &&
{reloadHint}
} -
- -
- - )} -
- - {VERSION} - - {activated ? ( - - ) : ( - + return ( + <> +

Settings

+
+
+
+

+ Access Token{' '} + + (?) + +

+ {!hasAccessToken && ( + { + // use js here to make sure redirect_uri is latest url + const url = `https://github.com/login/oauth/authorize?client_id=${ + oauth.clientId + }&scope=repo&redirect_uri=${encodeURIComponent(window.location.href)}` + window.location.href = url + }} + > + Create with OAuth (recommended) + )} +
+ + {hasAccessToken && !accessToken ? ( + + ) : ( + + )} +
+ {accessTokenHint && {accessTokenHint}} +
+
+

Toggle Shortcut

+ Set a combination of keys for toggling Gitako sidebar. +
+
+ + +
+ {shortcutHint && {shortcutHint}} +
+
+

More Options

+ {moreFields.map(field => ( + + +
+
+ ))} + + {reloadHint &&
{reloadHint}
} +
+
- ) + + ) +} + +export function SettingsBar(props: Props) { + const { toggleShowSettings, activated } = props + return ( +
+ {activated && } +
+ + {VERSION} + + {activated ? ( + + ) : ( + + )} +
+
+ ) +} + +async function trySetUpAccessTokenWithCode() { + try { + const search = parseURLSearch() + if ('code' in search) { + const res = await JSONRequest('https://github.com/login/oauth/access_token', { + code: search.code, + client_id: oauth.clientId, + client_secret: oauth.clientSecret, + }) + const { access_token: accessToken, scope } = res + if (scope !== 'repo' || !accessToken) { + throw new Error(`Cannot resolve token response: '${JSON.stringify(res)}'`) + } + window.history.pushState({}, 'removed code', window.location.pathname.replace(/#.*$/, '')) + return accessToken + } + } catch (err) { + raiseError(err) } } diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 6b4cbbe..3a303c5 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -10,135 +10,92 @@ import { ConnectorState, Props } from 'driver/core/SideBar' import * as React from 'react' import { cx } from 'utils/cx' -class RawGitako extends React.PureComponent { - static defaultProps: Partial = { - baseSize: 260, - shouldShow: false, - showSettings: false, - errorDueToAuth: false, - accessToken: '', - toggleShowSideBarShortcut: '', - compressSingletonFolder: true, - copyFileButton: true, - copySnippetButton: true, - disabled: false, - } - - componentDidMount() { - const { init, useListeners } = this.props +const RawGitako: React.FC = function RawGitako(props) { + React.useEffect(() => { + const { init, useListeners } = props init() useListeners(true) - } + return () => useListeners(false) + }, []) - componentWillUnmount() { - const { useListeners } = this.props - useListeners(false) - } + const accessToken = props.configContext.val.access_token + React.useEffect(() => { + if (accessToken) { + // reload when setting new accessToken + if (accessToken) props.init() + } + }, [accessToken, props.init]) - renderAccessDeniedError() { - return ( -
-
Access Denied
-

- Due to{' '} - - limitation of GitHub - {' '} - or{' '} - - auth needs - - , Gitako needs access token to continue. Please follow the instructions in the settings - panel below. -

-
- ) - } - - renderContent() { - const { - errorDueToAuth, - metaData, - treeData, - showSettings, - toggleShowSettings, - compressSingletonFolder, - accessToken, - } = this.props - return ( -
- {metaData && } - {errorDueToAuth - ? this.renderAccessDeniedError() - : metaData && ( - - )} -
- ) - } - - render() { - const { - baseSize, - error, - shouldShow, - showSettings, - accessToken, - compressSingletonFolder, - copyFileButton, - copySnippetButton, - intelligentToggle, - toggleShowSideBarShortcut, - logoContainerElement, - toggleShowSideBar, - toggleShowSettings, - onShortcutChange, - onAccessTokenChange, - setCompressSingleton, - setCopyFile, - setCopySnippet, - setIntelligentToggle, - } = this.props - return ( -
- - - - -
- {this.renderContent()} - + const { + errorDueToAuth, + metaData, + treeData, + baseSize, + error, + shouldShow, + showSettings, + logoContainerElement, + toggleShowSideBar, + toggleShowSettings, + } = props + return ( +
+ + + + +
+
+ {metaData && } + {errorDueToAuth + ? renderAccessDeniedError() + : metaData && ( + + )}
- -
- ) - } + +
+ +
+ ) +} + +RawGitako.defaultProps = { + baseSize: 260, + shouldShow: false, + showSettings: false, + errorDueToAuth: false, + disabled: false, } export const SideBar = connect(SideBarCore)(RawGitako) + +function renderAccessDeniedError() { + return ( +
+
Access Denied
+

+ Due to{' '} + + limitation of GitHub + {' '} + or{' '} + + auth needs + + , Gitako needs access token to continue. Please follow the instructions in the settings + panel below. +

+
+ ) +} diff --git a/src/containers/ConfigsContext.tsx b/src/containers/ConfigsContext.tsx index 9bfb22f..714ce3a 100644 --- a/src/containers/ConfigsContext.tsx +++ b/src/containers/ConfigsContext.tsx @@ -15,11 +15,12 @@ export function ConfigsContextWrapper(props: React.PropsWithChildren) { configsHelper.get().then(setConfigs) }, []) const set = React.useCallback( - () => (configs: Config) => { - configsHelper.set(configs) - setConfigs(configs) + (updatedConfigs: Partial) => { + const mergedConfigs = { ...configs, ...updatedConfigs } as Config + configsHelper.set(mergedConfigs) + setConfigs(mergedConfigs) }, - [setConfigs], + [configs, setConfigs], ) if (configs === null) return null return ( diff --git a/src/content.less b/src/content.less index 1c6fdbc..b5b97c8 100644 --- a/src/content.less +++ b/src/content.less @@ -473,7 +473,7 @@ color: #6a737d; } } - .placeholder-row { + .header-row { flex-shrink: 0; display: flex; justify-content: space-between; diff --git a/src/driver/connect.ts b/src/driver/connect.ts index 6f6165b..0f31303 100644 --- a/src/driver/connect.ts +++ b/src/driver/connect.ts @@ -94,10 +94,9 @@ function link(instance: React.Component, sources: Sources): Wr } export function connect(mapping: Sources) { - return function linkComponent< - State, - ComponentClass extends React.ComponentClass - >(Component: ComponentClass) { + return function linkComponent>( + Component: ComponentType, + ) { return class ConnectedComponent extends React.PureComponent { static displayName = `Connected(${Component.displayName || Component.name})` static defaultProps = Component.defaultProps diff --git a/src/driver/core/FileExplorer.ts b/src/driver/core/FileExplorer.ts index 4d5a4d4..1373e83 100644 --- a/src/driver/core/FileExplorer.ts +++ b/src/driver/core/FileExplorer.ts @@ -1,6 +1,7 @@ import { GetCreatedMethod, MethodCreator } from 'driver/connect' import * as ini from 'ini' import { Base64 } from 'js-base64' +import { Config } from 'utils/configHelper' import * as DOMHelper from 'utils/DOMHelper' import { findNode, searchKeyToRegexps } from 'utils/general' import * as GitHubHelper from 'utils/GitHubHelper' @@ -13,7 +14,6 @@ export type Props = { treeData?: GitHubHelper.TreeData metaData: GitHubHelper.MetaData freeze: boolean - compressSingletonFolder: boolean accessToken: string | undefined toggleShowSettings: React.MouseEventHandler } @@ -137,7 +137,7 @@ function handleParsed(root: TreeNode, parsed: Parsed) { } export const setUpTree: BoundMethodCreator< - [Pick] + [Pick & Pick] > = dispatch => async ({ treeData, metaData, compressSingletonFolder, accessToken }) => { if (!treeData) return dispatch.call(setStateText, 'Rendering File List...') diff --git a/src/driver/core/SideBar.ts b/src/driver/core/SideBar.ts index 4695afb..6de60da 100644 --- a/src/driver/core/SideBar.ts +++ b/src/driver/core/SideBar.ts @@ -1,6 +1,5 @@ import { ConfigsContextShape } from 'containers/ConfigsContext' import { GetCreatedMethod, MethodCreator } from 'driver/connect' -import { Config } from 'utils/configHelper' import * as DOMHelper from 'utils/DOMHelper' import * as GitHubHelper from 'utils/GitHubHelper' import { MetaData, TreeData } from 'utils/GitHubHelper' @@ -34,20 +33,9 @@ export type ConnectorState = { toggleShowSideBar: GetCreatedMethod toggleShowSettings: GetCreatedMethod useListeners: GetCreatedMethod - onAccessTokenChange: GetCreatedMethod - onShortcutChange: GetCreatedMethod - setCopyFile: GetCreatedMethod - setCopySnippet: GetCreatedMethod - setCompressSingleton: GetCreatedMethod - setIntelligentToggle: GetCreatedMethod } & { baseSize: number - toggleShowSideBarShortcut?: string - accessToken?: string -} & Pick< - Config, - 'compressSingletonFolder' | 'copyFileButton' | 'copySnippetButton' | 'intelligentToggle' - > +} type BoundMethodCreator = MethodCreator @@ -83,8 +71,6 @@ export const init: BoundMethodCreator = dispatch => async () => { const { sideBarWidth, access_token: accessToken, - shortcut, - compressSingletonFolder, copyFileButton, copySnippetButton, intelligentToggle, @@ -92,12 +78,6 @@ export const init: BoundMethodCreator = dispatch => async () => { DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton }) dispatch.set({ baseSize: sideBarWidth, - accessToken, - toggleShowSideBarShortcut: shortcut, - compressSingletonFolder, - copyFileButton, - copySnippetButton, - intelligentToggle, }) if (!metaData.branchName || !metaData.userName) return @@ -178,7 +158,14 @@ export const handleError: BoundMethodCreator<[Error]> = dispatch => async err => } export const onPJAXEnd: BoundMethodCreator = dispatch => () => { - const [{ metaData, copyFileButton, copySnippetButton, intelligentToggle }] = dispatch.get() + const [ + { metaData }, + { + configContext: { + val: { intelligentToggle, copyFileButton, copySnippetButton }, + }, + }, + ] = dispatch.get() DOMHelper.unmountTopProgressBar() DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton }) const mergedMetaData = { ...metaData, ...URLHelper.parse() } @@ -190,22 +177,25 @@ export const onPJAXEnd: BoundMethodCreator = dispatch => () => { } export const onKeyDown: BoundMethodCreator<[KeyboardEvent]> = dispatch => e => { - const [{ toggleShowSideBarShortcut }] = dispatch.get() - if (toggleShowSideBarShortcut) { + const [ + , + { + configContext: { + val: { shortcut }, + }, + }, + ] = dispatch.get() + if (shortcut) { const keys = keyHelper.parseEvent(e) - if (keys === toggleShowSideBarShortcut) { + if (keys === shortcut) { dispatch.call(toggleShowSideBar) } } } export const toggleShowSideBar: BoundMethodCreator = dispatch => () => { - const [{ intelligentToggle, shouldShow }] = dispatch.get() + const [{ shouldShow }] = dispatch.get() dispatch.call(setShouldShow, !shouldShow) - - if (intelligentToggle !== null) { - dispatch.call(setIntelligentToggle, shouldShow) - } } export const setShouldShow: BoundMethodCreator< @@ -229,43 +219,9 @@ export const setShowSettings: BoundMethodCreator< [ConnectorState['showSettings']] > = dispatch => showSettings => dispatch.set({ showSettings }) -export const onAccessTokenChange: BoundMethodCreator< - [ConnectorState['accessToken']] -> = dispatch => accessToken => { - dispatch.set({ accessToken }) - // reload when setting new accessToken - if (accessToken) { - dispatch.call(init) - } -} - -export const onShortcutChange: BoundMethodCreator< - [ConnectorState['toggleShowSideBarShortcut']] -> = dispatch => shortcut => dispatch.set({ toggleShowSideBarShortcut: shortcut }) - export const setMetaData: BoundMethodCreator<[ConnectorState['metaData']]> = dispatch => metaData => dispatch.set({ metaData }) -export const setCompressSingleton: BoundMethodCreator< - [ConnectorState['compressSingletonFolder']] -> = dispatch => compressSingletonFolder => dispatch.set({ compressSingletonFolder }) - -export const setCopyFile: BoundMethodCreator< - [ConnectorState['copyFileButton']] -> = dispatch => copyFileButton => dispatch.set({ copyFileButton }) - -export const setCopySnippet: BoundMethodCreator< - [ConnectorState['copySnippetButton']] -> = dispatch => copySnippetButton => dispatch.set({ copySnippetButton }) - -export const setIntelligentToggle: BoundMethodCreator< - [ConnectorState['intelligentToggle']] -> = dispatch => intelligentToggle => { - const [, { configContext }] = dispatch.get() - configContext.set({ intelligentToggle }) - dispatch.set({ intelligentToggle }) -} - export const useListeners: BoundMethodCreator<[boolean]> = dispatch => { const $onPJAXEnd = () => dispatch.call(onPJAXEnd) const $onKeyDown = (e: KeyboardEvent) => dispatch.call(onKeyDown, e) diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index 258481d..8f24fde 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -54,3 +54,17 @@ export function useStates( const [val, set] = React.useState(initialState) return { val, set } } + +export function useAsyncMemo( + factory: (dependencies: D) => T | Promise, + deps: D, + initialValue: T, +): T { + const firstTime = React.useRef(true) + const state = useStates(() => initialValue) + React.useEffect(() => { + if (firstTime.current) firstTime.current = false + Promise.resolve(factory(deps)).then(consumed => state.set(() => consumed)) + }, deps) + return state.val +} diff --git a/tsconfig.json b/tsconfig.json index e181701..08918c4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,4 @@ { - "files": ["src/content.tsx"], "compilerOptions": { "target": "es2016", "outDir": "dist",