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.threadSwatch { + 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,133 @@ +// @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 onClick = React.useCallback(() => { + onCloseModal(); + createNewEntry(threadInfo.id); + }, [threadInfo.id, createNewEntry, onCloseModal]); + + const swatchColorStyle = React.useMemo( + () => ({ + backgroundColor: `#${threadInfo.color}`, + }), + [threadInfo.color], + ); + + return ( + <div + key={threadInfo.id} + className={css.threadPickerOptionContainer} + onClick={onClick} + > + <div style={swatchColorStyle} className={css.threadSwatch} /> + <div className={css.threadNameText}>{threadInfo.uiName}</div> + </div> + ); +} + +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<string>(''); + const [searchResults, setSearchResults] = React.useState<Set<string>>( + 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<ThreadInfo>, + text: string, + results: Set<string>, + ) => + 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 => ( + <ThreadPickerOption + threadInfo={threadInfo} + createNewEntry={createNewEntry} + key={threadInfo.id} + onCloseModal={modalProps.onClose} + /> + )); + + if (options.length === 0 && searchText.length > 0) { + return ( + <div className={css.noResultsText}>No results for {searchText}</div> + ); + } else { + return options; + } + }, [threads, createNewEntry, modalProps.onClose, searchText]); + + return ( + <Modal {...modalProps} size="large"> + <div className={css.container}> + <Search + onChangeText={onChangeSearchText} + searchText={searchText} + placeholder="Search chats" + /> + <div className={css.contentContainer}>{threadPickerContent}</div> + </div> + </Modal> + ); +} + +export default ThreadPickerModal;