diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js index 2c23db12c..3f891de7c 100644 --- a/web/chat/chat-thread-list-item.react.js +++ b/web/chat/chat-thread-list-item.react.js @@ -1,75 +1,63 @@ // @flow import type { ChatThreadItem } from 'lib/selectors/chat-selectors'; -import { updateNavInfoActionType } from '../redux/redux-setup'; import * as React from 'react'; import classNames from 'classnames'; -import { useDispatch } from 'react-redux'; import { shortAbsoluteDate } from 'lib/utils/date-utils'; import css from './chat-thread-list.css'; import MessagePreview from './message-preview.react'; import ChatThreadListItemMenu from './chat-thread-list-item-menu.react'; import { useSelector } from '../redux/redux-utils'; +import { + useOnClickThread, + useThreadIsActive, +} from '../selectors/nav-selectors'; type Props = {| +item: ChatThreadItem, |}; function ChatThreadListItem(props: Props) { const { item } = props; const threadID = item.threadInfo.id; - const dispatch = useDispatch(); - const onClick = React.useCallback( - (event: SyntheticEvent) => { - event.preventDefault(); - dispatch({ - type: updateNavInfoActionType, - payload: { - activeChatThreadID: threadID, - }, - }); - }, - [dispatch, threadID], - ); + const onClick = useOnClickThread(threadID); const timeZone = useSelector((state) => state.timeZone); const lastActivity = shortAbsoluteDate(item.lastUpdatedTime, timeZone); - const active = useSelector( - (state) => threadID === state.navInfo.activeChatThreadID, - ); + const active = useThreadIsActive(threadID); const activeStyle = active ? css.activeThread : null; const colorSplotchStyle = { backgroundColor: `#${item.threadInfo.color}` }; const unread = item.threadInfo.currentUser.unread; return (
{item.threadInfo.uiName}
{lastActivity}
); } export default ChatThreadListItem; diff --git a/web/selectors/nav-selectors.js b/web/selectors/nav-selectors.js index 378877c3b..c668bb582 100644 --- a/web/selectors/nav-selectors.js +++ b/web/selectors/nav-selectors.js @@ -1,124 +1,151 @@ // @flow import type { AppState } from '../redux/redux-setup'; import type { CalendarFilter } from 'lib/types/filter-types'; import type { CalendarQuery } from 'lib/types/entry-types'; +import * as React from 'react'; import { createSelector } from 'reselect'; import invariant from 'invariant'; +import { useDispatch } from 'react-redux'; import { currentCalendarQuery } from 'lib/selectors/nav-selectors'; import { nonThreadCalendarFiltersSelector } from 'lib/selectors/calendar-filter-selectors'; +import { useSelector } from '../redux/redux-utils'; +import { updateNavInfoActionType } from '../redux/redux-setup'; + 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(threadID: string) { + const dispatch = useDispatch(); + return React.useCallback( + (event: SyntheticEvent) => { + event.preventDefault(); + dispatch({ + type: updateNavInfoActionType, + payload: { + activeChatThreadID: threadID, + }, + }); + }, + [dispatch, threadID], + ); +} + +function useThreadIsActive(threadID: string) { + return useSelector((state) => threadID === state.navInfo.activeChatThreadID); +} + export { yearExtractor, yearAssertingSelector, monthExtractor, monthAssertingSelector, activeThreadSelector, webCalendarQuery, nonThreadCalendarQuery, + useOnClickThread, + useThreadIsActive, };