diff --git a/web/calendar/day.react.js b/web/calendar/day.react.js index a5889a674..1380233e9 100644 --- a/web/calendar/day.react.js +++ b/web/calendar/day.react.js @@ -1,294 +1,289 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import _some from 'lodash/fp/some'; -import PropTypes from 'prop-types'; import * as React from 'react'; +import { useDispatch } from 'react-redux'; import { createLocalEntry, createLocalEntryActionType, } from 'lib/actions/entry-actions'; -import { onScreenThreadInfos } from 'lib/selectors/thread-selectors'; +import { onScreenThreadInfos as onScreenThreadInfosSelector } from 'lib/selectors/thread-selectors'; import { entryKey } from 'lib/shared/entry-utils'; import type { EntryInfo } from 'lib/types/entry-types'; -import { entryInfoPropType } from 'lib/types/entry-types'; +import type { Dispatch } from 'lib/types/redux-types'; import type { ThreadInfo } from 'lib/types/thread-types'; -import { threadInfoPropType } from 'lib/types/thread-types'; import { dateString, dateFromString, currentDateInTimeZone, } from 'lib/utils/date-utils'; -import { connect } from 'lib/utils/redux-utils'; import LogInFirstModal from '../modals/account/log-in-first-modal.react'; import HistoryModal from '../modals/history/history-modal.react'; -import type { AppState } from '../redux/redux-setup'; +import { useSelector } from '../redux/redux-utils'; import { htmlTargetFromEvent } from '../vector-utils'; import { AddVector, HistoryVector } from '../vectors.react'; import css from './calendar.css'; import type { InnerEntry } from './entry.react'; import Entry from './entry.react'; import ThreadPicker from './thread-picker.react'; -type Props = { - dayString: string, - entryInfos: EntryInfo[], - setModal: (modal: ?React.Node) => void, - startingTabIndex: number, - // Redux state - onScreenThreadInfos: ThreadInfo[], - viewerID: ?string, - loggedIn: boolean, - nextLocalID: number, - timeZone: ?string, - // Redux dispatch functions - dispatchActionPayload: (actionType: string, payload: *) => void, -}; -type State = { - pickerOpen: boolean, - hovered: boolean, -}; +type BaseProps = {| + +dayString: string, + +entryInfos: $ReadOnlyArray, + +setModal: (modal: ?React.Node) => void, + +startingTabIndex: number, +|}; +type Props = {| + ...BaseProps, + +onScreenThreadInfos: $ReadOnlyArray, + +viewerID: ?string, + +loggedIn: boolean, + +nextLocalID: number, + +timeZone: ?string, + +dispatch: Dispatch, +|}; +type State = {| + +pickerOpen: boolean, + +hovered: boolean, +|}; class Day extends React.PureComponent { - static propTypes = { - dayString: PropTypes.string.isRequired, - entryInfos: PropTypes.arrayOf(entryInfoPropType).isRequired, - setModal: PropTypes.func.isRequired, - startingTabIndex: PropTypes.number.isRequired, - onScreenThreadInfos: PropTypes.arrayOf(threadInfoPropType).isRequired, - viewerID: PropTypes.string, - loggedIn: PropTypes.bool.isRequired, - nextLocalID: PropTypes.number.isRequired, - timeZone: PropTypes.string, - dispatchActionPayload: PropTypes.func.isRequired, - }; state: State = { pickerOpen: false, hovered: false, }; entryContainer: ?HTMLDivElement; entryContainerSpacer: ?HTMLDivElement; actionLinks: ?HTMLDivElement; entries: Map = new Map(); static getDerivedStateFromProps(props: Props) { if (props.onScreenThreadInfos.length === 0) { return { pickerOpen: false }; } return null; } componentDidUpdate(prevProps: Props) { if (this.props.entryInfos.length > prevProps.entryInfos.length) { invariant(this.entryContainer, 'entryContainer ref not set'); this.entryContainer.scrollTop = this.entryContainer.scrollHeight; } } render() { const now = currentDateInTimeZone(this.props.timeZone); const isToday = dateString(now) === this.props.dayString; const tdClasses = classNames(css.day, { [css.currentDay]: isToday }); let actionLinks = null; const hovered = this.state.hovered; if (hovered) { const actionLinksClassName = `${css.actionLinks} ${css.dayActionLinks}`; actionLinks = (
Add History
); } const entries = this.props.entryInfos .filter((entryInfo) => _some(['id', entryInfo.threadID])(this.props.onScreenThreadInfos), ) .map((entryInfo, i) => { const key = entryKey(entryInfo); return ( ); }); let threadPicker = null; if (this.state.pickerOpen) { invariant( this.props.onScreenThreadInfos.length > 0, 'onScreenThreadInfos should exist if pickerOpen', ); threadPicker = ( ); } const entryContainerClasses = classNames(css.entryContainer, { [css.focusedEntryContainer]: hovered, }); const date = dateFromString(this.props.dayString); return (

{date.getDate()}

{entries}
{actionLinks} {threadPicker} ); } actionLinksRef = (actionLinks: ?HTMLDivElement) => { this.actionLinks = actionLinks; }; entryContainerRef = (entryContainer: ?HTMLDivElement) => { this.entryContainer = entryContainer; }; entryContainerSpacerRef = (entryContainerSpacer: ?HTMLDivElement) => { this.entryContainerSpacer = entryContainerSpacer; }; entryRef = (key: string, entry: InnerEntry) => { this.entries.set(key, entry); }; closePicker = () => { this.setState({ pickerOpen: false }); }; onMouseEnter = () => { this.setState({ hovered: true }); }; onMouseLeave = () => { this.setState({ hovered: false }); }; onClick = (event: SyntheticEvent) => { const target = htmlTargetFromEvent(event); invariant( this.entryContainer instanceof HTMLDivElement, "entryContainer isn't div", ); invariant( this.entryContainerSpacer instanceof HTMLDivElement, "entryContainerSpacer isn't div", ); if ( target === this.entryContainer || target === this.entryContainerSpacer || (this.actionLinks && target === this.actionLinks) ) { this.onAddEntry(event); } }; onAddEntry = (event: SyntheticEvent<*>) => { event.preventDefault(); invariant( this.props.onScreenThreadInfos.length > 0, "onAddEntry shouldn't be clicked if no onScreenThreadInfos", ); if (this.props.onScreenThreadInfos.length === 1) { this.createNewEntry(this.props.onScreenThreadInfos[0].id); } else if (this.props.onScreenThreadInfos.length > 1) { this.setState({ pickerOpen: true }); } }; createNewEntry = (threadID: string) => { if (!this.props.loggedIn) { this.props.setModal( , ); return; } const viewerID = this.props.viewerID; invariant(viewerID, 'should have viewerID in order to create thread'); - this.props.dispatchActionPayload( - createLocalEntryActionType, - createLocalEntry( + this.props.dispatch({ + type: createLocalEntryActionType, + payload: createLocalEntry( threadID, this.props.nextLocalID, this.props.dayString, viewerID, ), - ); + }); }; onHistory = (event: SyntheticEvent) => { event.preventDefault(); this.props.setModal( , ); }; focusOnFirstEntryNewerThan = (time: number) => { const entryInfo = this.props.entryInfos.find( (candidate) => candidate.creationTime > time, ); if (entryInfo) { const entry = this.entries.get(entryKey(entryInfo)); invariant(entry, 'entry for entryinfo should be defined'); entry.focus(); } }; clearModal = () => { this.props.setModal(null); }; } -export default connect( - (state: AppState) => ({ - onScreenThreadInfos: onScreenThreadInfos(state), - viewerID: state.currentUserInfo && state.currentUserInfo.id, - loggedIn: !!( - state.currentUserInfo && - !state.currentUserInfo.anonymous && - true - ), - nextLocalID: state.nextLocalID, - timeZone: state.timeZone, - }), - null, - true, -)(Day); +export default React.memo(function ConnectedDay(props: BaseProps) { + const onScreenThreadInfos = useSelector(onScreenThreadInfosSelector); + const viewerID = useSelector((state) => state.currentUserInfo?.id); + const loggedIn = useSelector( + (state) => + !!(state.currentUserInfo && !state.currentUserInfo.anonymous && true), + ); + const nextLocalID = useSelector((state) => state.nextLocalID); + const timeZone = useSelector((state) => state.timeZone); + const dispatch = useDispatch(); + + return ( + + ); +});