diff --git a/web/selectors/nav-selectors.js b/web/selectors/nav-selectors.js index 3e71299a2..a2b56399d 100644 --- a/web/selectors/nav-selectors.js +++ b/web/selectors/nav-selectors.js @@ -1,205 +1,221 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { createSelector } from 'reselect'; import { nonThreadCalendarFiltersSelector } from 'lib/selectors/calendar-filter-selectors'; import { currentCalendarQuery } from 'lib/selectors/nav-selectors'; import { createPendingSidebar } from 'lib/shared/thread-utils'; import type { CalendarQuery } from 'lib/types/entry-types'; import type { CalendarFilter } from 'lib/types/filter-types'; import type { ComposableMessageInfo, RobotextMessageInfo, } from 'lib/types/message-types'; import type { ThreadInfo } from 'lib/types/thread-types'; import { getDefaultTextMessageRules } from '../markdown/rules.react'; import type { AppState } from '../redux/redux-setup'; import { useSelector } from '../redux/redux-utils'; -import { updateNavInfoActionType } from '../types/nav-types'; +import { + type NavigationTab, + type NavigationSettingsSection, + updateNavInfoActionType, +} from '../types/nav-types'; const dateExtractionRegex = /^([0-9]{4})-([0-9]{2})-[0-9]{2}$/; function yearExtractor(startDate: string, endDate: string): ?number { const startDateResults = dateExtractionRegex.exec(startDate); const endDateResults = dateExtractionRegex.exec(endDate); if ( !startDateResults || !startDateResults[1] || !endDateResults || !endDateResults[1] || startDateResults[1] !== endDateResults[1] ) { return null; } return parseInt(startDateResults[1], 10); } function yearAssertingExtractor(startDate: string, endDate: string): number { const result = yearExtractor(startDate, endDate); invariant( result !== null && result !== undefined, `${startDate} and ${endDate} aren't in the same year`, ); return result; } const yearAssertingSelector: (state: AppState) => number = createSelector( (state: AppState) => state.navInfo.startDate, (state: AppState) => state.navInfo.endDate, yearAssertingExtractor, ); // 1-indexed function monthExtractor(startDate: string, endDate: string): ?number { const startDateResults = dateExtractionRegex.exec(startDate); const endDateResults = dateExtractionRegex.exec(endDate); if ( !startDateResults || !startDateResults[1] || !startDateResults[2] || !endDateResults || !endDateResults[1] || !endDateResults[2] || startDateResults[1] !== endDateResults[1] || startDateResults[2] !== endDateResults[2] ) { return null; } return parseInt(startDateResults[2], 10); } // 1-indexed function monthAssertingExtractor(startDate: string, endDate: string): number { const result = monthExtractor(startDate, endDate); invariant( result !== null && result !== undefined, `${startDate} and ${endDate} aren't in the same month`, ); return result; } // 1-indexed const monthAssertingSelector: (state: AppState) => number = createSelector( (state: AppState) => state.navInfo.startDate, (state: AppState) => state.navInfo.endDate, monthAssertingExtractor, ); function activeThreadSelector(state: AppState): ?string { return state.navInfo.tab === 'chat' ? state.navInfo.activeChatThreadID : null; } const webCalendarQuery: ( state: AppState, ) => () => CalendarQuery = createSelector( currentCalendarQuery, (state: AppState) => state.navInfo.tab === 'calendar', ( calendarQuery: (calendarActive: boolean) => CalendarQuery, calendarActive: boolean, ) => () => calendarQuery(calendarActive), ); const nonThreadCalendarQuery: ( state: AppState, ) => () => CalendarQuery = createSelector( webCalendarQuery, nonThreadCalendarFiltersSelector, ( calendarQuery: () => CalendarQuery, filters: $ReadOnlyArray, ) => { return (): CalendarQuery => { const query = calendarQuery(); return { startDate: query.startDate, endDate: query.endDate, filters, }; }; }, ); function useOnClickThread( thread: ?ThreadInfo, ): (event: SyntheticEvent) => void { const dispatch = useDispatch(); return React.useCallback( (event: SyntheticEvent) => { invariant( thread?.id, 'useOnClickThread should be called with threadID set', ); event.preventDefault(); const { id: threadID } = thread; if (threadID.includes('pending')) { dispatch({ type: updateNavInfoActionType, payload: { activeChatThreadID: threadID, pendingThread: thread, }, }); } else { dispatch({ type: updateNavInfoActionType, payload: { activeChatThreadID: threadID, }, }); } }, [dispatch, thread], ); } function useThreadIsActive(threadID: string): boolean { return useSelector(state => threadID === state.navInfo.activeChatThreadID); } function useOnClickPendingSidebar( messageInfo: ComposableMessageInfo | RobotextMessageInfo, threadInfo: ThreadInfo, ): (event: SyntheticEvent) => void { const dispatch = useDispatch(); const viewerID = useSelector(state => state.currentUserInfo?.id); return React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); if (!viewerID) { return; } const pendingSidebarInfo = createPendingSidebar( messageInfo, threadInfo, viewerID, getDefaultTextMessageRules().simpleMarkdownRules, ); dispatch({ type: updateNavInfoActionType, payload: { activeChatThreadID: pendingSidebarInfo.id, pendingThread: pendingSidebarInfo, }, }); }, [viewerID, messageInfo, threadInfo, dispatch], ); } +function navTabSelector(state: AppState): NavigationTab { + return state.navInfo.tab; +} + +function navSettingsSectionSelector( + state: AppState, +): ?NavigationSettingsSection { + return state.navInfo.settingsSection; +} + export { yearExtractor, yearAssertingSelector, monthExtractor, monthAssertingSelector, activeThreadSelector, webCalendarQuery, nonThreadCalendarQuery, useOnClickThread, useThreadIsActive, useOnClickPendingSidebar, + navTabSelector, + navSettingsSectionSelector, }; diff --git a/web/sidebar/app-switcher.react.js b/web/sidebar/app-switcher.react.js index 257b223f2..27216a082 100644 --- a/web/sidebar/app-switcher.react.js +++ b/web/sidebar/app-switcher.react.js @@ -1,133 +1,134 @@ // @flow import * as React from 'react'; import { useDispatch } from 'react-redux'; import { mostRecentlyReadThreadSelector, unreadCount, } from 'lib/selectors/thread-selectors'; import { useSelector } from '../redux/redux-utils'; +import { navTabSelector } from '../selectors/nav-selectors.js'; import SWMansionIcon from '../SWMansionIcon.react'; import { updateNavInfoActionType } from '../types/nav-types'; import css from './left-layout-aside.css'; import NavigationPanel from './navigation-panel.react'; function AppSwitcher(): React.Node { const activeChatThreadID = useSelector( state => state.navInfo.activeChatThreadID, ); const mostRecentlyReadThread = useSelector(mostRecentlyReadThreadSelector); const isActiveThreadCurrentlyUnread = useSelector( state => !activeChatThreadID || !!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread, ); const dispatch = useDispatch(); const onClickChat = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); dispatch({ type: updateNavInfoActionType, payload: { tab: 'chat', activeChatThreadID: isActiveThreadCurrentlyUnread ? mostRecentlyReadThread : activeChatThreadID, }, }); }, [ dispatch, isActiveThreadCurrentlyUnread, mostRecentlyReadThread, activeChatThreadID, ], ); const boundUnreadCount = useSelector(unreadCount); let chatBadge = null; if (boundUnreadCount > 0) { chatBadge = {boundUnreadCount}; } const chatNavigationItem = React.useMemo( () => (
{chatBadge}

Chat

), [chatBadge, onClickChat], ); const onClickCalendar = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); dispatch({ type: updateNavInfoActionType, payload: { tab: 'calendar' }, }); }, [dispatch], ); const isCalendarEnabled = useSelector(state => state.enabledApps.calendar); const calendarNavigationItem = React.useMemo(() => { if (!isCalendarEnabled) { return null; } return (

Calendar

); }, [isCalendarEnabled, onClickCalendar]); const onClickApps = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); dispatch({ type: updateNavInfoActionType, payload: { tab: 'apps', }, }); }, [dispatch], ); const appNavigationItem = React.useMemo( () => (

Apps

), [onClickApps], ); return ( - + {chatNavigationItem} {calendarNavigationItem} {appNavigationItem} ); } export default AppSwitcher; diff --git a/web/sidebar/navigation-panel.react.js b/web/sidebar/navigation-panel.react.js index dafadf9d2..e566183df 100644 --- a/web/sidebar/navigation-panel.react.js +++ b/web/sidebar/navigation-panel.react.js @@ -1,58 +1,59 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; +import type { AppState } from '../redux/redux-setup.js'; import { useSelector } from '../redux/redux-utils'; -import type { NavigationTab } from '../types/nav-types'; import css from './left-layout-aside.css'; type NavigationPanelItemProps = { - +tab: NavigationTab, + +tab: string, +children: React.Node, }; function NavigationPanelItem(props: NavigationPanelItemProps): React.Node { const { children } = props; return children; } -type NavigationPanelContainerProps = { +type NavigationPanelContainerProps = { + +tabSelector: AppState => T, +children: React.ChildrenArray>, }; -function NavigationPanelContainer( - props: NavigationPanelContainerProps, +function NavigationPanelContainer( + props: NavigationPanelContainerProps, ): React.Node { - const { children } = props; - const navInfo = useSelector(state => state.navInfo); + const { children, tabSelector } = props; + const currentTab = useSelector(tabSelector); const items = React.useMemo( () => React.Children.map(children, child => { if (!child) { return null; } return (
{child}
); }), - [children, navInfo.tab], + [children, currentTab], ); return
{items}
; } const NavigationPanel = { Item: NavigationPanelItem, Container: NavigationPanelContainer, }; export default NavigationPanel; diff --git a/web/sidebar/settings-switcher.react.js b/web/sidebar/settings-switcher.react.js index 2902e2848..60e4c30d1 100644 --- a/web/sidebar/settings-switcher.react.js +++ b/web/sidebar/settings-switcher.react.js @@ -1,41 +1,42 @@ // @flow import * as React from 'react'; import { useDispatch } from 'react-redux'; +import { navSettingsSectionSelector } from '../selectors/nav-selectors.js'; import { updateNavInfoActionType } from '../types/nav-types'; import css from './left-layout-aside.css'; import NavigationPanel from './navigation-panel.react'; function SettingsSwitcher(): React.Node { const dispatch = useDispatch(); const onClickAccountSettings = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); dispatch({ type: updateNavInfoActionType, payload: { tab: 'settings', settingsSection: 'account' }, }); }, [dispatch], ); const accountSettingsNavigationItem = React.useMemo( () => (

My Account

), [onClickAccountSettings], ); return ( - - + + {accountSettingsNavigationItem} ); } export default SettingsSwitcher; diff --git a/web/types/nav-types.js b/web/types/nav-types.js index cfc78d5b0..8e4835b69 100644 --- a/web/types/nav-types.js +++ b/web/types/nav-types.js @@ -1,16 +1,18 @@ // @flow import type { BaseNavInfo } from 'lib/types/nav-types'; import type { ThreadInfo } from 'lib/types/thread-types'; export type NavigationTab = 'calendar' | 'chat' | 'apps' | 'settings'; +export type NavigationSettingsSection = 'account'; + export type NavInfo = { ...$Exact, +tab: NavigationTab, +activeChatThreadID: ?string, +pendingThread?: ThreadInfo, - +settingsSection?: 'account', + +settingsSection?: NavigationSettingsSection, }; export const updateNavInfoActionType = 'UPDATE_NAV_INFO';