diff --git a/web/chat/chat-thread-composer.css b/web/chat/chat-thread-composer.css new file mode 100644 --- /dev/null +++ b/web/chat/chat-thread-composer.css @@ -0,0 +1,71 @@ +div.threadSearchContainer { + background-color: var(--thread-creation-search-container-bg); + color: var(--fg); + display: flex; + flex-direction: column; + max-height: 50%; + overflow: auto; + flex-shrink: 0; +} + +div.fullHeight { + height: 100%; +} + +div.userSelectedTags { + display: flex; + flex-wrap: wrap; + flex-direction: row; + align-items: center; + gap: 4px; + padding: 4px 12px; + margin-bottom: 8px; +} + +div.searchRow { + display: flex; + flex-direction: row; + align-items: center; + margin-right: 8px; +} + +div.searchField { + flex-grow: 1; +} + +div.closeSearch { + cursor: pointer; + display: flex; + align-items: center; + color: var(--thread-creation-close-search-color); + margin: 0 8px; +} + +ul.searchResultsContainer { + display: flex; + flex-direction: column; + overflow: auto; + padding: 0px 12px 8px 12px; +} + +li.searchResultsItem { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + cursor: pointer; +} + +li.searchResultsItem:hover { + background-color: var(--thread-creation-search-item-bg-hover); +} + +div.userName { + color: var(--fg); +} + +div.userInfo { + font-style: italic; + color: var(--thread-creation-search-item-info-color); +} diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js new file mode 100644 --- /dev/null +++ b/web/chat/chat-thread-composer.react.js @@ -0,0 +1,120 @@ +// @flow +import classNames from 'classnames'; +import * as React from 'react'; + +import { userSearchIndexForPotentialMembers } from 'lib/selectors/user-selectors'; +import { getPotentialMemberItems } from 'lib/shared/search-utils'; +import type { AccountUserInfo, UserListItem } from 'lib/types/user-types'; + +import Label from '../components/label.react'; +import Search from '../components/search.react'; +import type { InputState } from '../input/input-state'; +import { useSelector } from '../redux/redux-utils'; +import SWMansionIcon from '../SWMansionIcon.react'; +import css from './chat-thread-composer.css'; + +type Props = { + +userInfoInputArray: $ReadOnlyArray<AccountUserInfo>, + +otherUserInfos: { [id: string]: AccountUserInfo }, + +threadID: string, + +isThreadSelected: boolean, + +inputState: InputState, +}; + +function ChatThreadComposer(props: Props): React.Node { + const { userInfoInputArray, otherUserInfos, isThreadSelected } = props; + + const [usernameInputText, setUsernameInputText] = React.useState(''); + + const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); + + const userListItems = React.useMemo( + () => + getPotentialMemberItems( + usernameInputText, + otherUserInfos, + userSearchIndex, + userInfoInputArray.map(userInfo => userInfo.id), + ), + [usernameInputText, otherUserInfos, userSearchIndex, userInfoInputArray], + ); + + // eslint-disable-next-line no-unused-vars + const onSelectUserFromSearch = React.useCallback((id: string) => {}, []); + // eslint-disable-next-line no-unused-vars + const onRemoveUserFromSelected = React.useCallback((id: string) => {}, []); + + const userSearchResultList = React.useMemo(() => { + if ( + !userListItems.length || + (!usernameInputText.length && userInfoInputArray.length) + ) { + return null; + } + + return ( + <ul className={css.searchResultsContainer}> + {userListItems.map((userSearchResult: UserListItem) => ( + <li + className={css.searchResultsItem} + key={userSearchResult.id} + onClick={() => onSelectUserFromSearch(userSearchResult.id)} + > + <div className={css.userName}>{userSearchResult.username}</div> + <div className={css.userInfo}>{userSearchResult.alertTitle}</div> + </li> + ))} + </ul> + ); + }, [ + onSelectUserFromSearch, + userInfoInputArray.length, + userListItems, + usernameInputText.length, + ]); + + const hideSearch = React.useCallback(() => {}, []); + + const tagsList = React.useMemo(() => { + if (!userInfoInputArray?.length) { + return null; + } + const labels = userInfoInputArray.map(user => { + return ( + <Label key={user.id} onClose={() => onRemoveUserFromSelected(user.id)}> + {user.username} + </Label> + ); + }); + return <div className={css.userSelectedTags}>{labels}</div>; + }, [userInfoInputArray, onRemoveUserFromSelected]); + + const threadSearchContainerStyles = React.useMemo( + () => + classNames(css.threadSearchContainer, { + [css.fullHeight]: !isThreadSelected, + }), + [isThreadSelected], + ); + + return ( + <div className={threadSearchContainerStyles}> + <div className={css.searchRow}> + <div className={css.searchField}> + <Search + onChangeText={setUsernameInputText} + searchText={usernameInputText} + placeholder="Select users for thread" + /> + </div> + <div className={css.closeSearch} onClick={hideSearch}> + <SWMansionIcon size={25} icon="cross" /> + </div> + </div> + {tagsList} + {userSearchResultList} + </div> + ); +} + +export default ChatThreadComposer; diff --git a/web/theme.css b/web/theme.css --- a/web/theme.css +++ b/web/theme.css @@ -169,4 +169,8 @@ --notification-settings-option-invalid-selected-color: var(--shades-black-60); --danger-zone-subheading-color: var(--shades-white-60); --danger-zone-explanation-color: var(--shades-black-60); + --thread-creation-search-container-bg: var(--shades-black-90); + --thread-creation-close-search-color: var(--shades-black-60); + --thread-creation-search-item-bg-hover: var(--shades-black-80); + --thread-creation-search-item-info-color: var(--shades-black-60); }