diff --git a/src/App.js b/src/App.js index af0f0d854..838ee0466 100644 --- a/src/App.js +++ b/src/App.js @@ -18,6 +18,7 @@ import Notify from './components/notify/Notify.js' import Connected from './components/connected/Connected.js' import TourHelper from './components/tour/TourHelper.js' import FilesExploreForm from './files/explore-form/files-explore-form.tsx' +import { useTours } from './contexts/tours-context' export class App extends Component { static propTypes = { @@ -124,20 +125,27 @@ const dropCollect = (connect, monitor) => ({ canDrop: monitor.canDrop() }) +const AppWithTooltip = (props) => { + const { tooltip: showTooltip, disableTooltip } = useTours() + return ( + + ) +} + export const AppWithDropTarget = DropTarget(NativeTypes.FILE, dropTarget, dropCollect)(App) export default connect( 'selectRoute', 'selectRouteInfo', 'selectIpfsReady', - 'selectShowTooltip', 'doFilesNavigateTo', 'doUpdateUrl', 'doUpdateHash', 'doSetupLocalStorage', 'doTryInitIpfs', 'doFilesWrite', - 'doDisableTooltip', 'selectFilesPathInfo', - withTranslation('app')(AppWithDropTarget) + withTranslation('app')(DropTarget(NativeTypes.FILE, dropTarget, dropCollect)(AppWithTooltip)) ) diff --git a/src/bundles/index.js b/src/bundles/index.js index 5e6f8312d..cece2a6b8 100644 --- a/src/bundles/index.js +++ b/src/bundles/index.js @@ -11,7 +11,6 @@ import redirectsBundle from './redirects.js' import filesBundle from './files/index.js' import configBundle from './config.js' import configSaveBundle from './config-save.js' -import toursBundle from './tours.js' import notifyBundle from './notify.js' import connectedBundle from './connected.js' import retryInitBundle from './retry-init.js' @@ -32,7 +31,6 @@ export default composeBundles( ipfsProvider, routesBundle, redirectsBundle, - toursBundle, filesBundle(), configBundle, configSaveBundle, diff --git a/src/bundles/tours.js b/src/bundles/tours.js deleted file mode 100644 index 046123384..000000000 --- a/src/bundles/tours.js +++ /dev/null @@ -1,49 +0,0 @@ -import root from 'window-or-global' - -const toursBundle = { - name: 'tours', - - init: (store) => { - const tourTooltip = root.localStorage.getItem('tourTooltip') - - if (tourTooltip) { - store.doDisableTooltip() - } - }, - - reducer: (state = { enabled: false, tooltip: true }, action) => { - if (action.type === 'TOURS_ENABLE') { - return { ...state, enabled: true } - } - - if (action.type === 'TOURS_DISABLE') { - return { ...state, enabled: false } - } - - if (action.type === 'TOURS_TOOLTIP_DISABLE') { - return { ...state, tooltip: false } - } - - return state - }, - - doDisableTooltip: () => ({ dispatch }) => { - root.localStorage.setItem('tourTooltip', false) - dispatch({ type: 'TOURS_TOOLTIP_DISABLE' }) - }, - - doEnableTours: () => ({ dispatch }) => { - dispatch({ type: 'TOURS_ENABLE' }) - }, - - doDisableTours: () => ({ dispatch }) => { - dispatch({ type: 'TOURS_DISABLE' }) - }, - - selectTours: state => state.tours, - - selectToursEnabled: state => state.tours.enabled, - - selectShowTooltip: state => state.tours.tooltip -} -export default toursBundle diff --git a/src/components/tour/TourHelper.js b/src/components/tour/TourHelper.js index 91417c0b5..fc953188a 100644 --- a/src/components/tour/TourHelper.js +++ b/src/components/tour/TourHelper.js @@ -1,10 +1,11 @@ import React from 'react' -import { connect } from 'redux-bundler-react' import { withTranslation } from 'react-i18next' +import { useTours } from '../../contexts/tours-context' -export const TourHelper = ({ doEnableTours, className = '', size = 23, t }) => { +export const TourHelper = ({ className = '', size = 23, t }) => { + const { enableTours } = useTours() const handleClick = () => { - doEnableTours() + enableTours() } return ( @@ -16,7 +17,4 @@ export const TourHelper = ({ doEnableTours, className = '', size = 23, t }) => { ) } -export default connect( - 'doEnableTours', - withTranslation('app')(TourHelper) -) +export default withTranslation('app')(TourHelper) diff --git a/src/components/tour/withTour.js b/src/components/tour/withTour.js index e2316d822..62ae62cdd 100644 --- a/src/components/tour/withTour.js +++ b/src/components/tour/withTour.js @@ -1,31 +1,34 @@ -import React from 'react' -import { connect } from 'redux-bundler-react' +import React, { useCallback } from 'react' import { STATUS } from 'react-joyride' +import { useTours } from '../../contexts/tours-context' /** * @param {React.ComponentType} WrappedComponent * @returns {React.ComponentType} */ const withTour = WrappedComponent => { - class WithTour extends React.Component { - /** - * @param {import('react-joyride').CallBackProps} data - */ - handleJoyrideCallback = (data) => { - const { doDisableTours } = this.props - const { action, status } = data + /** + * @param {Object} props + */ + const WithTour = (props) => { + const { disableTours } = useTours() + const handleJoyrideCallback = useCallback( + /** + * @param {import('react-joyride').CallBackProps} data + */ + (data) => { + const { action, status } = data - if (action === 'close' || status === STATUS.FINISHED) { - doDisableTours() - } - } + if (action === 'close' || status === STATUS.FINISHED) { + disableTours() + } + }, [disableTours] + ) - render () { - return - } + return } - return connect('doDisableTours', WithTour) + return WithTour } export default withTour diff --git a/src/contexts/tours-context.tsx b/src/contexts/tours-context.tsx new file mode 100644 index 000000000..5aa3dd2fd --- /dev/null +++ b/src/contexts/tours-context.tsx @@ -0,0 +1,94 @@ +import React, { createContext, useContext, useReducer, useCallback, useEffect } from 'react' +import { useBridgeContext } from '../helpers/context-bridge' +// Replace window-or-global with direct window check +const root = typeof window !== 'undefined' ? window : global + +interface ToursState { + enabled: boolean + tooltip: boolean +} + +interface ToursContextValue { + enabled: boolean + tooltip: boolean + enableTours: () => void + disableTours: () => void + disableTooltip: () => void +} + +const ToursContext = createContext(undefined) + +type ToursAction = | { type: 'TOURS_ENABLE' } + | { type: 'TOURS_DISABLE' } + | { type: 'TOURS_TOOLTIP_DISABLE' } + +const toursReducer = (state: ToursState, action: ToursAction): ToursState => { + switch (action.type) { + case 'TOURS_ENABLE': + return { ...state, enabled: true } + case 'TOURS_DISABLE': + return { ...state, enabled: false } + case 'TOURS_TOOLTIP_DISABLE': + return { ...state, tooltip: false } + default: + return state + } +} + +const initialState: ToursState = { enabled: false, tooltip: true } + +export const ToursProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [state, dispatch] = useReducer(toursReducer, initialState) + + // Initialize from localStorage (equivalent to bundle's init function) + useEffect(() => { + const tourTooltip = root.localStorage.getItem('tourTooltip') + if (tourTooltip) { + dispatch({ type: 'TOURS_TOOLTIP_DISABLE' }) + } + }, []) + + const enableTours = useCallback(() => { + dispatch({ type: 'TOURS_ENABLE' }) + }, []) + + const disableTours = useCallback(() => { + dispatch({ type: 'TOURS_DISABLE' }) + }, []) + + const disableTooltip = useCallback(() => { + root.localStorage.setItem('tourTooltip', 'false') + dispatch({ type: 'TOURS_TOOLTIP_DISABLE' }) + }, []) + + const contextValue: ToursContextValue = { + enabled: state.enabled, + tooltip: state.tooltip, + enableTours, + disableTours, + disableTooltip + } + + // Bridge to redux bundles that might still need tour state + useBridgeContext('tours', { + enabled: state.enabled, + tooltip: state.tooltip, + doEnableTours: enableTours, + doDisableTours: disableTours, + doDisableTooltip: disableTooltip + }) + + return ( + + {children} + + ) +} + +export const useTours = (): ToursContextValue => { + const context = useContext(ToursContext) + if (!context) { + throw new Error('useTours must be used within ToursProvider') + } + return context +} diff --git a/src/explore/ExploreContainer.js b/src/explore/ExploreContainer.js index 977715029..a09ee816d 100644 --- a/src/explore/ExploreContainer.js +++ b/src/explore/ExploreContainer.js @@ -1,18 +1,15 @@ import React from 'react' import { connect } from 'redux-bundler-react' import { ExplorePage } from 'ipld-explorer-components/pages' -import withTour from '../components/tour/withTour.js' +import { useTours } from '../contexts/tours-context' + +const ExploreContainer = ({ availableGatewayUrl, publicGateway }) => { + const { enabled, handleJoyrideCallback } = useTours() -const ExploreContainer = ({ - toursEnabled, - handleJoyrideCallback, - availableGatewayUrl, - publicGateway -}) => { return (
( - -) +const StartExploringContainer = () => { + const { enabled, handleJoyrideCallback } = useTours() + return ( + + ) +} -export default connect( - 'selectToursEnabled', - withTour(StartExploringContainer) -) +export default StartExploringContainer diff --git a/src/files/FilesPage.js b/src/files/FilesPage.js index ede996fd1..e5b39b2d2 100644 --- a/src/files/FilesPage.js +++ b/src/files/FilesPage.js @@ -3,12 +3,12 @@ import { findDOMNode } from 'react-dom' import { Helmet } from 'react-helmet' import { connect } from 'redux-bundler-react' import { withTranslation, Trans } from 'react-i18next' -import ReactJoyride from 'react-joyride' +import ReactJoyride, { STATUS } from 'react-joyride' +import { useTours } from '../contexts/tours-context' // Lib import { filesTour } from '../lib/tours.js' // Components import ContextMenu from './context-menu/ContextMenu.js' -import withTour from '../components/tour/withTour.js' import InfoBoxes from './info-boxes/InfoBoxes.js' import FilePreview from './file-preview/FilePreview.js' import FilesList from './files-list/FilesList.js' @@ -30,8 +30,15 @@ const FilesPage = ({ doFetchPinningServices, doFilesFetch, doPinsFetch, doFilesSizeGet, doFilesDownloadLink, doFilesDownloadCarLink, doFilesWrite, doAddCarFile, doFilesBulkCidImport, doFilesAddPath, doUpdateHash, doFilesUpdateSorting, doFilesNavigateTo, doFilesMove, doSetCliOptions, doFetchRemotePins, remotePins, pendingPins, failedPins, ipfsProvider, ipfsConnected, doFilesMakeDir, doFilesShareLink, doFilesCopyCidProvide, doFilesDelete, doSetPinning, onRemotePinClick, doPublishIpnsKey, - files, filesPathInfo, pinningServices, toursEnabled, handleJoyrideCallback, isCliTutorModeEnabled, cliOptions, t + files, filesPathInfo, pinningServices, isCliTutorModeEnabled, cliOptions, t }) => { + const { enabled: toursEnabled, disableTours } = useTours() + const handleJoyrideCallback = useCallback((data) => { + const { action, status } = data + if (action === 'close' || status === STATUS.FINISHED) { + disableTours() + } + }, [disableTours]) const { doExploreUserProvidedPath } = useExplore() const contextMenuRef = useRef() const [modals, setModals] = useState({ show: null, files: null }) @@ -435,7 +442,6 @@ export default connect( 'doFilesNavigateTo', 'doFilesUpdateSorting', 'selectFilesSorting', - 'selectToursEnabled', 'doFilesWrite', 'doFilesBulkCidImport', 'doFilesDownloadLink', @@ -448,5 +454,5 @@ export default connect( 'selectCliOptions', 'doSetPinning', 'doPublishIpnsKey', - withTour(withTranslation('files')(FilesPage)) + withTranslation('files')(FilesPage) ) diff --git a/src/index.js b/src/index.js index 5ac1181b2..8488389d0 100644 --- a/src/index.js +++ b/src/index.js @@ -12,6 +12,7 @@ import { DndProvider } from 'react-dnd' import DndBackend from './lib/dnd-backend.js' import { HeliaProvider, ExploreProvider } from 'ipld-explorer-components/providers' import { ContextBridgeProvider } from './helpers/context-bridge.jsx' +import { ToursProvider } from './contexts/tours-context' const appVersion = process.env.REACT_APP_VERSION const gitRevision = process.env.REACT_APP_GIT_REV @@ -35,6 +36,7 @@ async function render () { ReactDOM.render( + @@ -44,6 +46,7 @@ async function render () { + , document.getElementById('root') diff --git a/src/peers/PeersPage.js b/src/peers/PeersPage.js index 4f2bcc266..b9b749325 100644 --- a/src/peers/PeersPage.js +++ b/src/peers/PeersPage.js @@ -1,11 +1,11 @@ -import React from 'react' +import React, { useCallback } from 'react' import { connect } from 'redux-bundler-react' import { Helmet } from 'react-helmet' import { withTranslation } from 'react-i18next' -import ReactJoyride from 'react-joyride' -import withTour from '../components/tour/withTour.js' +import ReactJoyride, { STATUS } from 'react-joyride' import { peersTour } from '../lib/tours.js' import { getJoyrideLocales } from '../helpers/i8n.js' +import { useTours } from '../contexts/tours-context' // Components import Box from '../components/box/Box.js' @@ -15,7 +15,15 @@ import AddConnection from './AddConnection/AddConnection.js' import CliTutorMode from '../components/cli-tutor-mode/CliTutorMode.js' import { cliCmdKeys, cliCommandList } from '../bundles/files/consts.js' -const PeersPage = ({ t, toursEnabled, handleJoyrideCallback }) => ( +const PeersPage = ({ t }) => { + const { enabled: toursEnabled, disableTours } = useTours() + const handleJoyrideCallback = useCallback((data) => { + const { action, status } = data + if (action === 'close' || status === STATUS.FINISHED) { + disableTours() + } + }, [disableTours]) + return (
{t('title')} | IPFS @@ -41,10 +49,10 @@ const PeersPage = ({ t, toursEnabled, handleJoyrideCallback }) => ( locale={getJoyrideLocales(t)} showProgress />
-) + ) +} export default connect( - 'selectToursEnabled', 'selectIsCliTutorModeEnabled', - withTour(withTranslation('peers')(PeersPage)) + withTranslation('peers')(PeersPage) ) diff --git a/src/settings/SettingsPage.js b/src/settings/SettingsPage.js index fc52316cc..ca356c09f 100644 --- a/src/settings/SettingsPage.js +++ b/src/settings/SettingsPage.js @@ -1,12 +1,12 @@ -import React from 'react' +import React, { useCallback } from 'react' import { Helmet } from 'react-helmet' import { connect } from 'redux-bundler-react' import { withTranslation, Trans } from 'react-i18next' -import ReactJoyride from 'react-joyride' +import ReactJoyride, { STATUS } from 'react-joyride' // Tour import { settingsTour } from '../lib/tours.js' -import withTour from '../components/tour/withTour.js' import { getJoyrideLocales } from '../helpers/i8n.js' +import { useTours } from '../contexts/tours-context' // Components import Tick from '../icons/GlyphSmallTick.js' import Box from '../components/box/Box.js' @@ -375,6 +375,24 @@ export class SettingsPageContainer extends React.Component { export const TranslatedSettingsPage = withTranslation('settings')(SettingsPageContainer) +const SettingsPageWithTours = (props) => { + const { enabled: toursEnabled, disableTours, disableTooltip } = useTours() + const handleJoyrideCallback = useCallback((data) => { + const { action, status } = data + if (action === 'close' || status === STATUS.FINISHED) { + disableTours() + } + }, [disableTours]) + return ( + + ) +} + export default connect( 'selectConfig', 'selectIpfsConnected', @@ -385,7 +403,6 @@ export default connect( 'selectConfigSaveLastSuccess', 'selectConfigSaveLastError', 'selectIsIpfsDesktop', - 'selectToursEnabled', 'selectShowAnalyticsComponents', 'selectAnalyticsEnabled', 'selectArePinningServicesSupported', @@ -393,5 +410,5 @@ export default connect( 'doSaveConfig', 'selectIsCliTutorModeEnabled', 'doToggleCliTutorMode', - withTour(TranslatedSettingsPage) + SettingsPageWithTours ) diff --git a/src/status/StatusPage.js b/src/status/StatusPage.js index 214c5884e..d4c309b57 100644 --- a/src/status/StatusPage.js +++ b/src/status/StatusPage.js @@ -1,8 +1,8 @@ -import React from 'react' +import React, { useCallback } from 'react' import { Helmet } from 'react-helmet' import { withTranslation, Trans } from 'react-i18next' import { connect } from 'redux-bundler-react' -import ReactJoyride from 'react-joyride' +import ReactJoyride, { STATUS } from 'react-joyride' import { IdentityProvider } from '../contexts/identity-context.jsx' import StatusConnected from './StatusConnected.js' import BandwidthStatsDisabled from './BandwidthStatsDisabled.js' @@ -15,7 +15,7 @@ import Box from '../components/box/Box.js' import AnalyticsBanner from '../components/analytics-banner/AnalyticsBanner.js' import { statusTour } from '../lib/tours.js' import { getJoyrideLocales } from '../helpers/i8n.js' -import withTour from '../components/tour/withTour.js' +import { useTours } from '../contexts/tours-context' const StatusPage = ({ t, @@ -25,10 +25,15 @@ const StatusPage = ({ doEnableAnalytics, doDisableAnalytics, doToggleShowAnalyticsBanner, - toursEnabled, - handleJoyrideCallback, nodeBandwidthEnabled }) => { + const { enabled, disableTours } = useTours() + const handleJoyrideCallback = useCallback((data) => { + const { action, status } = data + if (action === 'close' || status === STATUS.FINISHED) { + disableTours() + } + }, [disableTours]) return (
@@ -79,7 +84,7 @@ const StatusPage = ({ : } void} props.handleJoyrideCallback */ -const WelcomePage = ({ t, ipfsInitFailed, ipfsConnected, ipfsReady, toursEnabled, handleJoyrideCallback }) => { +const WelcomePage = ({ t, ipfsInitFailed, ipfsConnected, ipfsReady }) => { + const { enabled: toursEnabled, disableTours } = useTours() const isSameOrigin = useBridgeSelector('selectIsSameOrigin') + const handleJoyrideCallback = useCallback( + /** + * @param {import('react-joyride').CallBackProps} data + */ + (data) => { + const { action, status } = data + if (action === 'close' || status === STATUS.FINISHED) { + disableTours() + } + }, [disableTours]) if (!ipfsInitFailed && !ipfsReady) { return @@ -83,6 +94,5 @@ export default connect( 'selectIpfsInitFailed', 'selectIpfsConnected', 'selectIpfsReady', - 'selectToursEnabled', - withTour(withTranslation('welcome')(WelcomePage)) + withTranslation('welcome')(WelcomePage) )