diff --git a/web/calendar/day.react.js b/web/calendar/day.react.js index 4b5085c81..61ad7db81 100644 --- a/web/calendar/day.react.js +++ b/web/calendar/day.react.js @@ -1,282 +1,262 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import _some from 'lodash/fp/some'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { createLocalEntry, createLocalEntryActionType, } from 'lib/actions/entry-actions'; 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 type { Dispatch } from 'lib/types/redux-types'; import type { ThreadInfo } from 'lib/types/thread-types'; import { dateString, dateFromString, currentDateInTimeZone, } from 'lib/utils/date-utils'; import LogInFirstModal from '../modals/account/log-in-first-modal.react'; import HistoryModal from '../modals/history/history-modal.react'; import { useModalContext } from '../modals/modal-provider.react'; +import ThreadPickerModal from '../modals/threads/thread-picker-modal.react'; 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 BaseProps = { +dayString: string, +entryInfos: $ReadOnlyArray, +startingTabIndex: number, }; type Props = { ...BaseProps, +onScreenThreadInfos: $ReadOnlyArray, +viewerID: ?string, +loggedIn: boolean, +nextLocalID: number, +timeZone: ?string, +dispatch: Dispatch, +pushModal: (modal: React.Node) => void, + +popModal: () => void, }; type State = { - +pickerOpen: boolean, +hovered: boolean, }; class Day extends React.PureComponent { 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 }); + this.props.pushModal( + , + ); } }; createNewEntry = (threadID: string) => { if (!this.props.loggedIn) { this.props.pushModal(); return; } const viewerID = this.props.viewerID; invariant(viewerID, 'should have viewerID in order to create thread'); this.props.dispatch({ type: createLocalEntryActionType, payload: createLocalEntry( threadID, this.props.nextLocalID, this.props.dayString, viewerID, ), }); }; onHistory = (event: SyntheticEvent) => { event.preventDefault(); this.props.pushModal( , ); }; 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(); } }; } const ConnectedDay: React.ComponentType = React.memo( function ConnectedDay(props) { 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(); - const modalContext = useModalContext(); + const { pushModal, popModal } = useModalContext(); return ( ); }, ); export default ConnectedDay; diff --git a/web/calendar/thread-picker.css b/web/calendar/thread-picker.css deleted file mode 100644 index 470affafe..000000000 --- a/web/calendar/thread-picker.css +++ /dev/null @@ -1,45 +0,0 @@ -div.container { - position: absolute; - left: 4px; - bottom: 20px; - box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.3); - z-index: 1; - font-weight: 600; - background-color: white; - border-radius: 3px; - white-space: nowrap; - line-height: normal; - font-size: 12px; - cursor: pointer; - outline: none; -} -span.thread { - color: black; - display: inline-block; - overflow: hidden; - text-overflow: ellipsis; - width: 110px; -} -div.container > div.option { - border-bottom: 1px solid #d3d3d3; - padding: 1px 10px 1px 25px; - position: relative; -} -div.container > div.option:hover { - background-color: #f2f2f2; -} -div.container > div:last-child { - border-bottom: none; -} -div.colorPreview { - width: 12px; - height: 12px; - left: 6px; - position: absolute; - top: 4px; - border: 1px solid lightgray; - border-radius: 2px; -} -div.threadName { - padding-top: 1px; -} diff --git a/web/calendar/thread-picker.react.js b/web/calendar/thread-picker.react.js deleted file mode 100644 index 2a58807f9..000000000 --- a/web/calendar/thread-picker.react.js +++ /dev/null @@ -1,144 +0,0 @@ -// @flow - -import invariant from 'invariant'; -import * as React from 'react'; -import { createSelector } from 'reselect'; - -import { threadSearchIndex } from 'lib/selectors/nav-selectors'; -import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors'; -import type { ThreadInfo } from 'lib/types/thread-types'; - -import { useSelector } from '../redux/redux-utils'; -import { htmlTargetFromEvent } from '../vector-utils'; -import css from './thread-picker.css'; - -type OptionProps = { - +threadInfo: ThreadInfo, - +createNewEntry: (threadID: string) => void, -}; -function ThreadPickerOption(props: OptionProps) { - const { threadInfo, createNewEntry } = props; - const onClick = React.useCallback(() => createNewEntry(threadInfo.id), [ - threadInfo.id, - createNewEntry, - ]); - const colorStyle = { backgroundColor: `#${props.threadInfo.color}` }; - - return ( -
- -
- {props.threadInfo.uiName} - -
- ); -} - -type Props = { - +createNewEntry: (threadID: string) => void, - +closePicker: () => void, -}; - -function ThreadPicker(props: Props): React.Node { - const { closePicker, createNewEntry } = props; - - const onScreenThreadInfos = useSelector(onScreenEntryEditableThreadInfos); - const searchIndex = useSelector(state => threadSearchIndex(state)); - - invariant( - onScreenThreadInfos.length > 0, - "ThreadPicker can't be open when onScreenThreadInfos is empty", - ); - - const pickerDivRef = React.useRef(null); - - React.useLayoutEffect(() => { - invariant(pickerDivRef, 'pickerDivRef must be set'); - const { current } = pickerDivRef; - current?.focus(); - }, []); - - const [searchText, setSearchText] = React.useState(''); - const [searchResults, setSearchResults] = React.useState>( - new Set(), - ); - - const onPickerKeyDown = React.useCallback( - (event: SyntheticKeyboardEvent) => { - if (event.keyCode === 27) { - // esc - closePicker(); - } - }, - [closePicker], - ); - - const onMouseDown = React.useCallback( - (event: SyntheticEvent) => { - const target = htmlTargetFromEvent(event); - invariant(pickerDivRef, 'pickerDivRef must be set'); - if (pickerDivRef.current?.contains(target)) { - // This prevents onBlur from firing - event.preventDefault(); - } - }, - [], - ); - - // eslint-disable-next-line no-unused-vars - const onChangeSearchText = React.useCallback( - (text: string) => { - const results = searchIndex.getSearchResults(text); - setSearchText(text); - setSearchResults(new Set(results)); - }, - [searchIndex], - ); - - const listDataSelector = createSelector( - state => state.onScreenThreadInfos, - state => state.searchText, - state => state.searchResults, - ( - threadInfos: $ReadOnlyArray, - text: string, - results: Set, - ) => - text - ? threadInfos.filter(threadInfo => results.has(threadInfo.id)) - : [...threadInfos], - ); - - const threads = useSelector(() => - listDataSelector({ - onScreenThreadInfos, - searchText, - searchResults, - }), - ); - - const options = React.useMemo(() => { - return threads.map(threadInfo => ( - - )); - }, [threads, createNewEntry]); - - return ( -
- {options} -
- ); -} - -export default ThreadPicker;