client/src/app/WelcomePanel.jsx
import React, { useState, useEffect, useCallback } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { isSignedIn } from '../users/authentication'
import CloseButton from '../ui/CloseButton'
import {
setWelcomePanelVisible,
setWelcomePanelDismissed
} from '../store/slices/ui'
import { registerKeypress, deregisterKeypress } from './keypress'
import { MODES, getMode } from './mode'
import WelcomeNewStreet from './WelcomePanel/NewStreet'
import WelcomeFirstTimeExistingStreet from './WelcomePanel/FirstTimeExistingStreet'
import WelcomeFirstTimeNewStreet from './WelcomePanel/FirstTimeNewStreet'
import './WelcomePanel.scss'
const WELCOME_NONE = 0
const WELCOME_NEW_STREET = 1
const WELCOME_FIRST_TIME_NEW_STREET = 2
const WELCOME_FIRST_TIME_EXISTING_STREET = 3
// In the past, dismissing the welcome panel would set this flag in
// LocalStorage, and the next time a welcome panel would be shown for a
// returning user, the presence of this flag would show different content (or
// not display the welcome panel at all).
//
// This naming convention caused confusion during the conversion to a
// React function component, because in reality there are two states we care
// about:
// - the welcome panel is dismissed _for the session_ (and will not re-apppear
// _this session_)
// - the user is a first-time user, and dismissing the welcome panel changes
// its content _in the future_
//
// The first value is ephemeral state, ao it is set only for the duration of
// the session and is discarded if the tab is closed. We want to keep using
// `dismissed` to label this state.
//
// The second value is persistent state. This is the one that's saved to
// LocalStorage and is retrieved when the app is loaded to determine what
// type of welcome message will be displayed. The LocalStorage key name
// is now no longer what it means, but for now we keep it for backwards
// compatibility
const LOCAL_STORAGE_RETURNING_USER = 'settings-welcome-dismissed'
function WelcomePanel (props) {
const { readOnly, everythingLoaded } = useSelector((state) => state.app)
const { welcomePanelVisible: isVisible, welcomePanelDismissed: isDismissed } =
useSelector((state) => state.ui)
const dispatch = useDispatch()
const [welcomeType, setWelcomeType] = useState(WELCOME_NONE)
const [isReturningUser, setIsReturningUser] = useState(
getIsReturningUserFromLocalStorage()
)
// Do not show under the following conditions:
// If app is read-only
// If app has not fully loaded yet
// If user has dismissed the panel this session
// If the welcome type is WELCOME_NONE
//
// When rendering, the dispatch call below affects the state of another
// component (`StreetNameplateContainer`), which throws an error in React.
// This is considered a bug, despite the functionality behaving as expected.
// The fix is to wrap this in `useEffect` so that the dispatch call occurs
// after the render is done. For more information see the discussion at
// https://github.com/streetmix/streetmix/issues/2324
useEffect(() => {
if (
!readOnly &&
everythingLoaded &&
!isDismissed &&
welcomeType !== WELCOME_NONE
) {
dispatch(setWelcomePanelVisible())
}
})
const handleWelcomeDismissed = useCallback(() => {
// Certain events will dismiss the welcome panel. If already
// invisible, do nothing.
if (welcomeType === WELCOME_NONE) {
return
}
setWelcomeType(WELCOME_NONE)
setIsReturningUser(true)
setIsReturningUserInLocalStorage()
dispatch(setWelcomePanelDismissed())
}, [welcomeType, dispatch])
// When everything is loaded, determine what type of welcome panel to show
useEffect(() => {
function determineWelcomeType () {
let welcomeType = WELCOME_NONE
if (getMode() === MODES.NEW_STREET) {
if (isSignedIn() || isReturningUser) {
welcomeType = WELCOME_NEW_STREET
} else {
welcomeType = WELCOME_FIRST_TIME_NEW_STREET
}
} else {
if (!isReturningUser) {
welcomeType = WELCOME_FIRST_TIME_EXISTING_STREET
}
}
return welcomeType
}
if (everythingLoaded === true) {
setWelcomeType(determineWelcomeType())
}
}, [everythingLoaded, isReturningUser])
// Set up and tear down when a welcome panel is shown
useEffect(() => {
// Do nothing with this hook if the panel is not visible
if (isVisible === false) return
// Hide welcome panel on certain events
window.addEventListener(
'stmx:receive_gallery_street',
handleWelcomeDismissed
)
window.addEventListener('stmx:save_street', handleWelcomeDismissed)
// Hide welcome panel when someone presses Escape
registerKeypress('esc', handleWelcomeDismissed)
return () => {
// Clean up event listeners
window.removeEventListener(
'stmx:receive_gallery_street',
handleWelcomeDismissed
)
window.removeEventListener('stmx:save_street', handleWelcomeDismissed)
deregisterKeypress('esc', handleWelcomeDismissed)
}
}, [isVisible, dispatch, handleWelcomeDismissed])
// Figure out what to display inside the panel
let welcomeContent
switch (welcomeType) {
case WELCOME_FIRST_TIME_NEW_STREET:
welcomeContent = <WelcomeFirstTimeNewStreet />
break
case WELCOME_FIRST_TIME_EXISTING_STREET:
welcomeContent = <WelcomeFirstTimeExistingStreet />
break
case WELCOME_NEW_STREET:
welcomeContent = <WelcomeNewStreet />
break
case WELCOME_NONE:
default:
welcomeContent = null
break
}
// Do not render if the panel is not visible
if (!isVisible) return null
return (
<div className="welcome-panel-container">
<div className="welcome-panel">
<CloseButton onClick={handleWelcomeDismissed} />
{welcomeContent}
</div>
</div>
)
}
export default WelcomePanel
/**
* When the Welcome Panel is dismissed for the first time we mark this browser
* as a "returning user" so that the message is not geared toward first-time
* users the next time they visit the site.
*/
export function setIsReturningUserInLocalStorage () {
window.localStorage[LOCAL_STORAGE_RETURNING_USER] = 'true'
}
/**
* Retrieves LocalStorage state for whether whether user is a returning user
*/
function getIsReturningUserFromLocalStorage () {
const localSetting = window.localStorage[LOCAL_STORAGE_RETURNING_USER]
if (localSetting) {
return JSON.parse(localSetting)
}
return false
}