diff --git a/web/modals/threads/thread-picker-modal.css b/web/modals/threads/thread-picker-modal.css new file mode 100644 --- /dev/null +++ b/web/modals/threads/thread-picker-modal.css @@ -0,0 +1,41 @@ +div.container { + display: flex; + flex-direction: column; + overflow: hidden; + margin: 16px; +} + +div.contentContainer { + overflow: scroll; + height: 448px; +} + +div.threadPickerOptionContainer { + display: flex; + align-items: center; + padding: 12px 16px; + cursor: pointer; +} + +div.threadPickerOptionContainer:hover { + background-color: var(--thread-hover-bg); + border-radius: 8px; +} + +div.threadSplotch { + min-width: 40px; + height: 40px; + border-radius: 10px; +} + +div.threadNameText { + color: var(--shades-white-100); + margin-left: 16px; +} + +div.noResultsText { + text-align: center; + color: var(--shades-white-100); + margin-top: 24px; + font-weight: 500; +} diff --git a/web/modals/threads/thread-picker-modal.react.js b/web/modals/threads/thread-picker-modal.react.js new file mode 100644 --- /dev/null +++ b/web/modals/threads/thread-picker-modal.react.js @@ -0,0 +1,134 @@ +// @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 Search from '../../components/search.react'; +import { useSelector } from '../../redux/redux-utils'; +import Modal, { type ModalOverridableProps } from '../modal.react'; +import css from './thread-picker-modal.css'; + +type OptionProps = { + +threadInfo: ThreadInfo, + +createNewEntry: (threadID: string) => void, + +onCloseModal: () => void, +}; + +function ThreadPickerOption(props: OptionProps) { + const { threadInfo, createNewEntry, onCloseModal } = props; + + const onClickThreadOption = React.useCallback(() => { + createNewEntry(threadInfo.id); + onCloseModal(); + }, [threadInfo.id, createNewEntry, onCloseModal]); + + const splotchColorStyle = React.useMemo( + () => ({ + backgroundColor: `#${threadInfo.color}`, + }), + [threadInfo.color], + ); + + return ( +
+
+
{threadInfo.uiName}
+
+ ); +} + +type Props = { + ...ModalOverridableProps, + +createNewEntry: (threadID: string) => void, +}; + +function ThreadPickerModal(props: Props): React.Node { + const { createNewEntry, ...modalProps } = 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 [searchText, setSearchText] = React.useState(''); + const [searchResults, setSearchResults] = React.useState>( + new Set(), + ); + + 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 threadPickerContent = React.useMemo(() => { + const options = threads.map(threadInfo => ( + + )); + + if (options.length === 0 && searchText.length > 0) { + return ( +
No results for {searchText}
+ ); + } else { + return options; + } + }, [threads, createNewEntry, modalProps.onClose, searchText]); + + return ( + +
+ +
{threadPickerContent}
+
+
+ ); +} + +export default ThreadPickerModal;