diff --git a/lib/types/nav-types.js b/lib/types/nav-types.js index ae392a925..0c2e53a96 100644 --- a/lib/types/nav-types.js +++ b/lib/types/nav-types.js @@ -1,7 +1,36 @@ // @flow +import t from 'tcomb'; +import type { TEnums } from 'tcomb'; + export type BaseNavInfo = { +startDate: string, +endDate: string, ... }; +export type WebNavigationTab = 'calendar' | 'chat' | 'settings'; +export const webNavigationTabValidator: TEnums = t.enums.of([ + 'calendar', + 'chat', + 'settings', +]); +export type WebLoginMethod = 'form' | 'qr-code'; +export const webLoginMethodValidator: TEnums = t.enums.of(['form', 'qr-code']); +export type WebNavigationSettingsSection = + | 'account' + | 'friend-list' + | 'block-list' + | 'keyservers' + | 'danger-zone'; +export const webNavigationSettingsSectionValidator: TEnums = t.enums.of([ + 'account', + 'friend-list', + 'block-list', + 'keyservers', + 'danger-zone', +]); +export type WebNavigationChatMode = 'view' | 'create'; +export const webNavigationChatModeValidator: TEnums = t.enums.of([ + 'view', + 'create', +]); diff --git a/web/app-list/app-list-item.react.js b/web/app-list/app-list-item.react.js index a912c13f0..81a98042a 100644 --- a/web/app-list/app-list-item.react.js +++ b/web/app-list/app-list-item.react.js @@ -1,39 +1,39 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; import SWMansionIcon, { type Icon, } from 'lib/components/SWMansionIcon.react.js'; +import type { WebNavigationTab } from 'lib/types/nav-types.js'; import css from './app-list-item.css'; import { useSelector } from '../redux/redux-utils.js'; import { navTabSelector } from '../selectors/nav-selectors.js'; -import type { WebNavigationTab } from '../types/nav-types.js'; type Props = { +id: WebNavigationTab, +name: string, +icon: Icon, +onClick: () => mixed, }; function AppListItem(props: Props): React.Node { const { id, name, icon, onClick } = props; const currentSelectedApp = useSelector(navTabSelector); const className = classNames(css.container, { [css.selected]: currentSelectedApp === id, }); return (
{name}
); } export default AppListItem; diff --git a/web/selectors/nav-selectors.js b/web/selectors/nav-selectors.js index 39f32b5af..124fd5709 100644 --- a/web/selectors/nav-selectors.js +++ b/web/selectors/nav-selectors.js @@ -1,140 +1,140 @@ // @flow import invariant from 'invariant'; import { createSelector } from 'reselect'; import { nonThreadCalendarFiltersSelector } from 'lib/selectors/calendar-filter-selectors.js'; import { currentCalendarQuery } from 'lib/selectors/nav-selectors.js'; import type { CalendarQuery } from 'lib/types/entry-types.js'; import type { CalendarFilter } from 'lib/types/filter-types.js'; +import type { + WebNavigationSettingsSection, + WebNavigationTab, +} from 'lib/types/nav-types.js'; import type { AppState } from '../redux/redux-setup.js'; -import { - type WebNavigationTab, - type WebNavigationSettingsSection, -} from '../types/nav-types.js'; 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 navTabSelector(state: AppState): WebNavigationTab { return state.navInfo.tab; } function navSettingsSectionSelector( state: AppState, ): ?WebNavigationSettingsSection { return state.navInfo.settingsSection; } export { yearExtractor, yearAssertingSelector, monthExtractor, monthAssertingSelector, activeThreadSelector, webCalendarQuery, nonThreadCalendarQuery, navTabSelector, navSettingsSectionSelector, }; diff --git a/web/settings/user-settings-list-item.react.js b/web/settings/user-settings-list-item.react.js index e240f7693..47a801e35 100644 --- a/web/settings/user-settings-list-item.react.js +++ b/web/settings/user-settings-list-item.react.js @@ -1,38 +1,39 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; +import type { WebNavigationSettingsSection } from 'lib/types/nav-types.js'; + import css from './user-settings-list-item.css'; import { useSelector } from '../redux/redux-utils.js'; import { navSettingsSectionSelector } from '../selectors/nav-selectors.js'; -import type { WebNavigationSettingsSection } from '../types/nav-types.js'; type Props = { +id: WebNavigationSettingsSection, +name: string, +onClick: () => mixed, }; function UserSettingsListItem(props: Props): React.Node { const { id, name, onClick } = props; const currentSelectedSettings = useSelector(navSettingsSectionSelector); const className = classNames(css.container, { [css.selected]: currentSelectedSettings === id, }); const userSettingsListItem = React.useMemo( () => (
{name}
), [className, name, onClick], ); return userSettingsListItem; } export default UserSettingsListItem; diff --git a/web/sidebar/community-drawer-item-community-handlers.react.js b/web/sidebar/community-drawer-item-community-handlers.react.js index 5da84351f..420561a13 100644 --- a/web/sidebar/community-drawer-item-community-handlers.react.js +++ b/web/sidebar/community-drawer-item-community-handlers.react.js @@ -1,116 +1,116 @@ // @flow import * as React from 'react'; import { clearChatCommunityFilter, updateCalendarCommunityFilter, updateChatCommunityFilter, } from 'lib/actions/community-actions.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; +import type { WebNavigationTab } from 'lib/types/nav-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import type { CommunityDrawerItemCommunityHandler } from './community-drawer-item-handler.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useCommunityIsPickedCalendar } from '../selectors/calendar-selectors.js'; import { useOnClickThread, useThreadIsActive, } from '../selectors/thread-selectors.js'; -import type { WebNavigationTab } from '../types/nav-types.js'; export type HandlerProps = { +setHandler: (handler: CommunityDrawerItemCommunityHandler) => void, +threadInfo: ThreadInfo, }; function ChatDrawerItemCommunityHandler(props: HandlerProps): React.Node { const { setHandler, threadInfo } = props; const onClickThread = useOnClickThread(threadInfo); const isActive = useThreadIsActive(threadInfo.id); const dispatch = useDispatch(); const openCommunityID = useSelector(state => state.communityPickerStore.chat); const expanded = openCommunityID === threadInfo.id; const onClick = React.useCallback( (event: SyntheticEvent) => { if (!isActive) { onClickThread(event); } if (openCommunityID === threadInfo.id && isActive) { dispatch({ type: clearChatCommunityFilter, }); return; } const community = threadInfo.community ?? threadInfo.id; dispatch({ type: updateChatCommunityFilter, payload: community, }); }, [ dispatch, isActive, onClickThread, openCommunityID, threadInfo.community, threadInfo.id, ], ); const handler = React.useMemo( () => ({ onClick, isActive, expanded }), [expanded, isActive, onClick], ); React.useEffect(() => { setHandler(handler); }, [handler, setHandler]); return null; } function CalendarDrawerItemCommunityHandler(props: HandlerProps): React.Node { const { setHandler, threadInfo } = props; const dispatch = useDispatch(); const onClick = React.useCallback(() => { dispatch({ type: updateCalendarCommunityFilter, payload: threadInfo.id, }); }, [dispatch, threadInfo.id]); const isActive = useCommunityIsPickedCalendar(threadInfo.id); const expanded = false; const handler = React.useMemo( () => ({ onClick, isActive, expanded }), [onClick, isActive, expanded], ); React.useEffect(() => { setHandler(handler); }, [handler, setHandler]); return null; } const communityDrawerItemCommunityHandlers: { +[tab: WebNavigationTab]: React.ComponentType, } = Object.freeze({ chat: ChatDrawerItemCommunityHandler, calendar: CalendarDrawerItemCommunityHandler, }); function getCommunityDrawerItemCommunityHandler( tab: WebNavigationTab, ): React.ComponentType { return ( communityDrawerItemCommunityHandlers[tab] ?? ChatDrawerItemCommunityHandler ); } export { getCommunityDrawerItemCommunityHandler }; diff --git a/web/sidebar/community-drawer-item-handlers.react.js b/web/sidebar/community-drawer-item-handlers.react.js index 99941e084..1100a2deb 100644 --- a/web/sidebar/community-drawer-item-handlers.react.js +++ b/web/sidebar/community-drawer-item-handlers.react.js @@ -1,74 +1,74 @@ // @flow import * as React from 'react'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; +import type { WebNavigationTab } from 'lib/types/nav-types.js'; import type { CommunityDrawerItemHandler } from './community-drawer-item-handler.react.js'; import { useCommunityIsPickedCalendar } from '../selectors/calendar-selectors.js'; import { useOnClickThread, useThreadIsActive, } from '../selectors/thread-selectors.js'; -import type { WebNavigationTab } from '../types/nav-types.js'; export type HandlerProps = { +setHandler: (handler: CommunityDrawerItemHandler) => void, +threadInfo: ThreadInfo, }; function ChatDrawerItemHandler(props: HandlerProps): React.Node { const { setHandler, threadInfo } = props; const onClick = useOnClickThread(threadInfo); const isActive = useThreadIsActive(threadInfo.id); const [expanded, setExpanded] = React.useState(false); const toggleExpanded = React.useCallback(() => { setExpanded(isExpanded => !isExpanded); }, []); const handler = React.useMemo( () => ({ onClick, isActive, expanded, toggleExpanded }), [expanded, isActive, onClick, toggleExpanded], ); React.useEffect(() => { setHandler(handler); }, [handler, setHandler]); return null; } const onClick = () => {}; const expanded = false; const toggleExpanded = () => {}; function CalendarDrawerItemHandler(props: HandlerProps): React.Node { const { setHandler, threadInfo } = props; const isActive = useCommunityIsPickedCalendar(threadInfo.id); const handler = React.useMemo( () => ({ onClick, isActive, expanded, toggleExpanded }), [isActive], ); React.useEffect(() => { setHandler(handler); }, [handler, setHandler]); return null; } const communityDrawerItemHandlers: { +[tab: WebNavigationTab]: React.ComponentType, } = Object.freeze({ chat: ChatDrawerItemHandler, calendar: CalendarDrawerItemHandler, }); function getCommunityDrawerItemHandler( tab: WebNavigationTab, ): React.ComponentType { return communityDrawerItemHandlers[tab] ?? ChatDrawerItemHandler; } export { getCommunityDrawerItemHandler }; diff --git a/web/sidebar/community-drawer-item.react.js b/web/sidebar/community-drawer-item.react.js index d4205cb3a..f6b01f0dc 100644 --- a/web/sidebar/community-drawer-item.react.js +++ b/web/sidebar/community-drawer-item.react.js @@ -1,118 +1,118 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; +import type { WebNavigationTab } from 'lib/types/nav-types.js'; import type { CommunityDrawerItemData } from 'lib/utils/drawer-utils.react.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js'; import type { CommunityDrawerItemHandler } from './community-drawer-item-handler.react.js'; import type { HandlerProps } from './community-drawer-item-handlers.react.js'; import { getCommunityDrawerItemHandler } from './community-drawer-item-handlers.react.js'; import css from './community-drawer-item.css'; import { getChildren, getExpandButton, } from './community-drawer-utils.react.js'; import ThreadAvatar from '../avatars/thread-avatar.react.js'; -import type { WebNavigationTab } from '../types/nav-types.js'; export type DrawerItemProps = { +itemData: CommunityDrawerItemData, +paddingLeft: number, +expandable?: boolean, +handlerType: WebNavigationTab, }; function CommunityDrawerItem(props: DrawerItemProps): React.Node { const { itemData: { threadInfo, itemChildren, hasSubchannelsButton, labelStyle }, paddingLeft, expandable = true, handlerType, } = props; const [handler, setHandler] = React.useState({ onClick: () => {}, expanded: false, toggleExpanded: () => {}, isActive: false, }); const Handler = getCommunityDrawerItemHandler(handlerType); const children = React.useMemo( () => getChildren({ expanded: handler.expanded, hasSubchannelsButton, itemChildren, paddingLeft, threadInfo, expandable, handlerType, }), [ handler.expanded, hasSubchannelsButton, itemChildren, paddingLeft, threadInfo, expandable, handlerType, ], ); const itemExpandButton = React.useMemo( () => getExpandButton({ expandable, childrenLength: itemChildren.length, hasSubchannelsButton, onExpandToggled: handler.toggleExpanded, expanded: handler.expanded, }), [ expandable, itemChildren.length, hasSubchannelsButton, handler.toggleExpanded, handler.expanded, ], ); const { uiName } = useResolvedThreadInfo(threadInfo); const titleLabel = classnames({ [css[labelStyle]]: true, [css.activeTitle]: handler.isActive, }); const style = React.useMemo(() => ({ paddingLeft }), [paddingLeft]); return ( <>
{itemExpandButton}
{uiName}
{children}
); } export type CommunityDrawerItemChatProps = { +itemData: CommunityDrawerItemData, +paddingLeft: number, +expandable?: boolean, +handler: React.ComponentType, }; const MemoizedCommunityDrawerItem: React.ComponentType = React.memo(CommunityDrawerItem); export default MemoizedCommunityDrawerItem; diff --git a/web/sidebar/community-drawer-utils.react.js b/web/sidebar/community-drawer-utils.react.js index 8645ff2f8..b6836795c 100644 --- a/web/sidebar/community-drawer-utils.react.js +++ b/web/sidebar/community-drawer-utils.react.js @@ -1,89 +1,89 @@ // @flow import * as React from 'react'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; +import type { WebNavigationTab } from 'lib/types/nav-types.js'; import type { CommunityDrawerItemData } from 'lib/utils/drawer-utils.react'; import css from './community-drawer-item.css'; import CommunityDrawerItemChat from './community-drawer-item.react.js'; import { ExpandButton } from './expand-buttons.react.js'; import SubchannelsButton from './subchannels-button.react.js'; -import type { WebNavigationTab } from '../types/nav-types.js'; const indentation = 14; const subchannelsButtonIndentation = 24; function getChildren({ expanded, hasSubchannelsButton, itemChildren, paddingLeft, threadInfo, expandable, handlerType, }: { expanded: boolean, hasSubchannelsButton: boolean, itemChildren: $ReadOnlyArray>, paddingLeft: number, threadInfo: ThreadInfo, expandable: boolean, handlerType: WebNavigationTab, }): React.Node { if (!expanded) { return null; } if (hasSubchannelsButton) { const buttonPaddingLeft = paddingLeft + subchannelsButtonIndentation; return (
); } return itemChildren.map(item => ( )); } function getExpandButton({ expandable, childrenLength, hasSubchannelsButton, onExpandToggled, expanded, }: { +expandable: boolean, +childrenLength: ?number, +hasSubchannelsButton: boolean, +onExpandToggled?: ?() => ?void, +expanded: boolean, }): React.Node { if (!expandable) { return null; } if (childrenLength === 0 && !hasSubchannelsButton) { return (
); } return (
); } export { getChildren, getExpandButton }; diff --git a/web/types/nav-types.js b/web/types/nav-types.js index 7b59b5249..261cfd8df 100644 --- a/web/types/nav-types.js +++ b/web/types/nav-types.js @@ -1,62 +1,50 @@ // @flow import type { TInterface } from 'tcomb'; import t from 'tcomb'; import { threadInfoValidator } from 'lib/permissions/minimally-encoded-thread-permissions-validators.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; -import { type BaseNavInfo } from 'lib/types/nav-types.js'; +import { + type BaseNavInfo, + type WebLoginMethod, + type WebNavigationChatMode, + type WebNavigationSettingsSection, + type WebNavigationTab, + webLoginMethodValidator, + webNavigationChatModeValidator, + webNavigationSettingsSectionValidator, + webNavigationTabValidator, +} from 'lib/types/nav-types.js'; import { type AccountUserInfo, accountUserInfoValidator, } from 'lib/types/user-types.js'; import { tID, tShape } from 'lib/utils/validation-utils.js'; -export type WebNavigationTab = 'calendar' | 'chat' | 'settings'; -const webNavigationTabValidator = t.enums.of(['calendar', 'chat', 'settings']); -export type WebLoginMethod = 'form' | 'qr-code'; -const webLoginMethodValidator = t.enums.of(['form', 'qr-code']); - -export type WebNavigationSettingsSection = - | 'account' - | 'friend-list' - | 'block-list' - | 'keyservers' - | 'danger-zone'; -const webNavigationSettingsSectionValidator = t.enums.of([ - 'account', - 'friend-list', - 'block-list', - 'keyservers', - 'danger-zone', -]); - -export type WebNavigationChatMode = 'view' | 'create'; -const webNavigationChatModeValidator = t.enums.of(['view', 'create']); - export type WebNavInfo = { ...$Exact, +tab: WebNavigationTab, +activeChatThreadID: ?string, +pendingThread?: ThreadInfo, +settingsSection?: WebNavigationSettingsSection, +selectedUserList?: $ReadOnlyArray, +chatMode?: WebNavigationChatMode, +inviteSecret?: ?string, +loginMethod?: WebLoginMethod, }; export const webNavInfoValidator: TInterface = tShape< $Exact, >({ startDate: t.String, endDate: t.String, tab: webNavigationTabValidator, activeChatThreadID: t.maybe(tID), pendingThread: t.maybe(threadInfoValidator), settingsSection: t.maybe(webNavigationSettingsSectionValidator), selectedUserList: t.maybe(t.list(accountUserInfoValidator)), chatMode: t.maybe(webNavigationChatModeValidator), inviteSecret: t.maybe(t.String), loginMethod: t.maybe(webLoginMethodValidator), });