diff --git a/web/app-list/app-list-item.react.js b/web/app-list/app-list-item.react.js index 4cdb67477..a912c13f0 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 css from './app-list-item.css'; import { useSelector } from '../redux/redux-utils.js'; import { navTabSelector } from '../selectors/nav-selectors.js'; -import type { NavigationTab } from '../types/nav-types.js'; +import type { WebNavigationTab } from '../types/nav-types.js'; type Props = { - +id: NavigationTab, + +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 571f2b4cb..c86996cd5 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 { AppState } from '../redux/redux-setup.js'; import { - type NavigationTab, + type WebNavigationTab, type NavigationSettingsSection, } 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): NavigationTab { +function navTabSelector(state: AppState): WebNavigationTab { return state.navInfo.tab; } function navSettingsSectionSelector( state: AppState, ): ?NavigationSettingsSection { return state.navInfo.settingsSection; } export { yearExtractor, yearAssertingSelector, monthExtractor, monthAssertingSelector, activeThreadSelector, webCalendarQuery, nonThreadCalendarQuery, navTabSelector, navSettingsSectionSelector, }; diff --git a/web/sidebar/community-drawer-item-community-handlers.react.js b/web/sidebar/community-drawer-item-community-handlers.react.js index d3082a8b2..5da84351f 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 { 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 { NavigationTab } from '../types/nav-types.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: NavigationTab]: React.ComponentType, + +[tab: WebNavigationTab]: React.ComponentType, } = Object.freeze({ chat: ChatDrawerItemCommunityHandler, calendar: CalendarDrawerItemCommunityHandler, }); function getCommunityDrawerItemCommunityHandler( - tab: NavigationTab, + 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 97c312567..99941e084 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 { 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 { NavigationTab } from '../types/nav-types.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: NavigationTab]: React.ComponentType, + +[tab: WebNavigationTab]: React.ComponentType, } = Object.freeze({ chat: ChatDrawerItemHandler, calendar: CalendarDrawerItemHandler, }); function getCommunityDrawerItemHandler( - tab: NavigationTab, + 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 3b5982241..d4205cb3a 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 { 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 { NavigationTab } from '../types/nav-types.js'; +import type { WebNavigationTab } from '../types/nav-types.js'; export type DrawerItemProps = { +itemData: CommunityDrawerItemData, +paddingLeft: number, +expandable?: boolean, - +handlerType: NavigationTab, + +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 fec27951a..8645ff2f8 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 { 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 { NavigationTab } from '../types/nav-types.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: NavigationTab, + 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 689a2f34e..04401dca6 100644 --- a/web/types/nav-types.js +++ b/web/types/nav-types.js @@ -1,60 +1,60 @@ // @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 AccountUserInfo, accountUserInfoValidator, } from 'lib/types/user-types.js'; import { tID, tShape } from 'lib/utils/validation-utils.js'; -export type NavigationTab = 'calendar' | 'chat' | 'settings'; -const navigationTabValidator = t.enums.of(['calendar', 'chat', 'settings']); +export type WebNavigationTab = 'calendar' | 'chat' | 'settings'; +const webNavigationTabValidator = t.enums.of(['calendar', 'chat', 'settings']); export type LoginMethod = 'form' | 'qr-code'; const loginMethodValidator = t.enums.of(['form', 'qr-code']); export type NavigationSettingsSection = | 'account' | 'friend-list' | 'block-list' | 'keyservers' | 'danger-zone'; const navigationSettingsSectionValidator = t.enums.of([ 'account', 'friend-list', 'block-list', 'keyservers', 'danger-zone', ]); export type NavigationChatMode = 'view' | 'create'; const navigationChatModeValidator = t.enums.of(['view', 'create']); export type NavInfo = { ...$Exact, - +tab: NavigationTab, + +tab: WebNavigationTab, +activeChatThreadID: ?string, +pendingThread?: ThreadInfo, +settingsSection?: NavigationSettingsSection, +selectedUserList?: $ReadOnlyArray, +chatMode?: NavigationChatMode, +inviteSecret?: ?string, +loginMethod?: LoginMethod, }; export const navInfoValidator: TInterface = tShape<$Exact>({ startDate: t.String, endDate: t.String, - tab: navigationTabValidator, + tab: webNavigationTabValidator, activeChatThreadID: t.maybe(tID), pendingThread: t.maybe(threadInfoValidator), settingsSection: t.maybe(navigationSettingsSectionValidator), selectedUserList: t.maybe(t.list(accountUserInfoValidator)), chatMode: t.maybe(navigationChatModeValidator), inviteSecret: t.maybe(t.String), loginMethod: t.maybe(loginMethodValidator), });