diff --git a/web/chat/chat-message-list-container.react.js b/web/chat/chat-message-list-container.react.js index 419e24efe..cfc49ecf4 100644 --- a/web/chat/chat-message-list-container.react.js +++ b/web/chat/chat-message-list-container.react.js @@ -1,161 +1,153 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import * as React from 'react'; import { useDrop } from 'react-dnd'; import { NativeTypes } from 'react-dnd-html5-backend'; import { threadIsPending } from 'lib/shared/thread-utils.js'; import { useWatchThread } from 'lib/shared/watch-thread-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import ChatInputBar from './chat-input-bar.react.js'; import css from './chat-message-list-container.css'; import ChatMessageList from './chat-message-list.react.js'; import ChatThreadComposer from './chat-thread-composer.react.js'; import ThreadTopBar from './thread-top-bar.react.js'; import { InputStateContext } from '../input/input-state.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useThreadInfoForPossiblyPendingThread, useInfosForPendingThread, } from '../utils/thread-utils.js'; type Props = { +activeChatThreadID: ?string, }; function ChatMessageListContainer(props: Props): React.Node { const { activeChatThreadID } = props; - const { isChatCreation, selectedUserInfos, otherUserInfos } = - useInfosForPendingThread(); + const { isChatCreation, selectedUserInfos } = useInfosForPendingThread(); const threadInfo = useThreadInfoForPossiblyPendingThread(activeChatThreadID); invariant(threadInfo, 'ThreadInfo should be set'); const dispatch = useDispatch(); React.useEffect(() => { if (isChatCreation && activeChatThreadID !== threadInfo.id) { let payload = { activeChatThreadID: threadInfo.id, }; if (threadIsPending(threadInfo.id)) { payload = { ...payload, pendingThread: threadInfo, }; } dispatch({ type: updateNavInfoActionType, payload, }); } }, [activeChatThreadID, dispatch, isChatCreation, threadInfo]); const inputState = React.useContext(InputStateContext); invariant(inputState, 'InputState should be set'); const [{ isActive }, connectDropTarget] = useDrop({ accept: NativeTypes.FILE, drop: item => { const { files } = item; if (inputState && files.length > 0) { void inputState.appendFiles(threadInfo, files); } }, collect: monitor => ({ isActive: monitor.isOver() && monitor.canDrop(), }), }); useWatchThread(threadInfo); const containerStyle = classNames({ [css.container]: true, [css.activeContainer]: isActive, }); const containerRef = React.useRef(); const onPaste = React.useCallback( (e: ClipboardEvent) => { if (!inputState) { return; } const { clipboardData } = e; if (!clipboardData) { return; } const { files } = clipboardData; if (files.length === 0) { return; } e.preventDefault(); void inputState.appendFiles(threadInfo, [...files]); }, [inputState, threadInfo], ); React.useEffect(() => { const currentContainerRef = containerRef.current; if (!currentContainerRef) { return undefined; } currentContainerRef.addEventListener('paste', onPaste); return () => { currentContainerRef.removeEventListener('paste', onPaste); }; }, [onPaste]); const content = React.useMemo(() => { const topBar = ; const messageListAndInput = ( <> ); if (!isChatCreation) { return ( <> {topBar} {messageListAndInput} ); } const chatUserSelection = ( ); if (!selectedUserInfos.length) { return chatUserSelection; } return ( <> {topBar} {chatUserSelection} {messageListAndInput} ); - }, [ - inputState, - isChatCreation, - otherUserInfos, - selectedUserInfos, - threadInfo, - ]); + }, [inputState, isChatCreation, selectedUserInfos, threadInfo]); return connectDropTarget(
{content}
, ); } export default ChatMessageListContainer; diff --git a/web/chat/chat-thread-composer.react.js b/web/chat/chat-thread-composer.react.js index 369983896..72de07cc3 100644 --- a/web/chat/chat-thread-composer.react.js +++ b/web/chat/chat-thread-composer.react.js @@ -1,262 +1,263 @@ // @flow import classNames from 'classnames'; import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/swmansion-icon.react.js'; import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js'; import { useENSNames } from 'lib/hooks/ens-cache.js'; import { useUsersSupportThickThreads } from 'lib/hooks/user-identities-hooks.js'; +import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors.js'; import { usePotentialMemberItems, useSearchUsers, notFriendNotice, } from 'lib/shared/search-utils.js'; import { createPendingThread, threadIsPending, useExistingThreadInfoFinder, } from 'lib/shared/thread-utils.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import type { AccountUserInfo, UserListItem } from 'lib/types/user-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import css from './chat-thread-composer.css'; import UserAvatar from '../avatars/user-avatar.react.js'; import Button from '../components/button.react.js'; import Label from '../components/label.react.js'; import Search from '../components/search.react.js'; import type { InputState } from '../input/input-state.js'; import Alert from '../modals/alert.react.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; type Props = { +userInfoInputArray: $ReadOnlyArray, - +otherUserInfos: { [id: string]: AccountUserInfo }, +threadID: string, +inputState: InputState, }; type ActiveThreadBehavior = | 'reset-active-thread-if-pending' | 'keep-active-thread'; function ChatThreadComposer(props: Props): React.Node { - const { userInfoInputArray, otherUserInfos, threadID, inputState } = props; + const { userInfoInputArray, threadID, inputState } = props; const [usernameInputText, setUsernameInputText] = React.useState(''); const dispatch = useDispatch(); const userInfoInputIDs = React.useMemo( () => userInfoInputArray.map(userInfo => userInfo.id), [userInfoInputArray], ); const searchResults = useSearchUsers(usernameInputText); const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos); + const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); const userListItems = usePotentialMemberItems({ text: usernameInputText, userInfos: otherUserInfos, auxUserInfos, excludeUserIDs: userInfoInputIDs, includeServerSearchUsers: searchResults, }); const userListItemsWithENSNames = useENSNames(userListItems); const { pushModal } = useModalContext(); const loggedInUserInfo = useLoggedInUserInfo(); invariant(loggedInUserInfo, 'loggedInUserInfo should be set'); const pendingPrivateThread = React.useRef( createPendingThread({ viewerID: loggedInUserInfo.id, threadType: threadTypes.GENESIS_PRIVATE, members: [loggedInUserInfo], }), ); const existingThreadInfoFinderForCreatingThread = useExistingThreadInfoFinder( pendingPrivateThread.current, ); const checkUsersThickThreadSupport = useUsersSupportThickThreads(); const onSelectUserFromSearch = React.useCallback( async (userListItem: UserListItem) => { const { alert, notice, disabled, ...user } = userListItem; setUsernameInputText(''); if (!alert) { dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: [...userInfoInputArray, user], }, }); } else if ( notice === notFriendNotice && userInfoInputArray.length === 0 ) { const newUserInfoInputArray = [ { id: userListItem.id, username: userListItem.username }, ]; const usersSupportingThickThreads = await checkUsersThickThreadSupport( newUserInfoInputArray.map(userInfo => userInfo.id), ); const threadInfo = existingThreadInfoFinderForCreatingThread({ searching: true, userInfoInputArray: newUserInfoInputArray, allUsersSupportThickThreads: newUserInfoInputArray.every(userInfo => usersSupportingThickThreads.has(userInfo.id), ), }); dispatch({ type: updateNavInfoActionType, payload: { chatMode: 'view', activeChatThreadID: threadInfo?.id, pendingThread: threadInfo, }, }); } else { pushModal({alert.text}); } }, [ checkUsersThickThreadSupport, dispatch, existingThreadInfoFinderForCreatingThread, pushModal, userInfoInputArray, ], ); const onRemoveUserFromSelected = React.useCallback( (userID: string) => { const newSelectedUserList = userInfoInputArray.filter( ({ id }) => userID !== id, ); if (_isEqual(userInfoInputArray)(newSelectedUserList)) { return; } dispatch({ type: updateNavInfoActionType, payload: { selectedUserList: newSelectedUserList, }, }); }, [dispatch, userInfoInputArray], ); const userSearchResultList = React.useMemo(() => { if ( !userListItemsWithENSNames.length || (!usernameInputText && userInfoInputArray.length) ) { return null; } const userItems = userListItemsWithENSNames.map( (userSearchResult: UserListItem) => { return (
  • ); }, ); return
      {userItems}
    ; }, [ onSelectUserFromSearch, userInfoInputArray.length, userListItemsWithENSNames, usernameInputText, ]); const hideSearch = React.useCallback( (threadBehavior: ActiveThreadBehavior = 'keep-active-thread') => { dispatch({ type: updateNavInfoActionType, payload: { chatMode: 'view', activeChatThreadID: threadBehavior === 'keep-active-thread' || !threadIsPending(threadID) ? threadID : null, }, }); }, [dispatch, threadID], ); const onCloseSearch = React.useCallback(() => { hideSearch('reset-active-thread-if-pending'); }, [hideSearch]); const userInfoInputArrayWithENSNames = useENSNames(userInfoInputArray); const tagsList = React.useMemo(() => { if (!userInfoInputArrayWithENSNames?.length) { return null; } const labels = userInfoInputArrayWithENSNames.map(user => { return ( ); }); return
    {labels}
    ; }, [userInfoInputArrayWithENSNames, onRemoveUserFromSelected]); React.useEffect(() => { if (!inputState) { return undefined; } inputState.registerSendCallback(hideSearch); return () => inputState.unregisterSendCallback(hideSearch); }, [hideSearch, inputState]); const threadSearchContainerStyles = classNames(css.threadSearchContainer, { [css.fullHeight]: !userInfoInputArray.length, }); return (
    {tagsList} {userSearchResultList}
    ); } export default ChatThreadComposer; diff --git a/web/utils/thread-utils.js b/web/utils/thread-utils.js index 706ec6ccf..1764b3239 100644 --- a/web/utils/thread-utils.js +++ b/web/utils/thread-utils.js @@ -1,131 +1,127 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js'; import { useUsersSupportThickThreads } from 'lib/hooks/user-identities-hooks.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; -import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors.js'; import { createPendingThread, useExistingThreadInfoFinder, } from 'lib/shared/thread-utils.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import type { AccountUserInfo } from 'lib/types/user-types.js'; import { useSelector } from '../redux/redux-utils.js'; type InfosForPendingThread = { +isChatCreation: boolean, +selectedUserInfos: $ReadOnlyArray, - +otherUserInfos: { [id: string]: AccountUserInfo }, }; function useInfosForPendingThread(): InfosForPendingThread { const isChatCreation = useSelector( state => state.navInfo.chatMode === 'create', ); const selectedUserInfos = useSelector( state => state.navInfo.selectedUserList ?? [], ); - const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); return { isChatCreation, selectedUserInfos, - otherUserInfos, }; } function useThreadInfoForPossiblyPendingThread( activeChatThreadID: ?string, ): ?ThreadInfo { const { isChatCreation, selectedUserInfos } = useInfosForPendingThread(); const loggedInUserInfo = useLoggedInUserInfo(); invariant(loggedInUserInfo, 'loggedInUserInfo should be set'); const pendingPrivateThread = React.useRef( createPendingThread({ viewerID: loggedInUserInfo.id, threadType: threadTypes.GENESIS_PRIVATE, members: [loggedInUserInfo], }), ); const newThreadID = 'pending/new_thread'; const pendingNewThread = React.useMemo( () => ({ ...createPendingThread({ viewerID: loggedInUserInfo.id, threadType: threadTypes.PRIVATE, members: [loggedInUserInfo], name: 'New thread', }), id: newThreadID, }), [loggedInUserInfo], ); const existingThreadInfoFinderForCreatingThread = useExistingThreadInfoFinder( pendingPrivateThread.current, ); const baseThreadInfo = useSelector(state => { if (activeChatThreadID) { const activeThreadInfo = threadInfoSelector(state)[activeChatThreadID]; if (activeThreadInfo) { return activeThreadInfo; } } return state.navInfo.pendingThread; }); const existingThreadInfoFinder = useExistingThreadInfoFinder(baseThreadInfo); const checkUsersThickThreadSupport = useUsersSupportThickThreads(); const [allUsersSupportThickThreads, setAllUsersSupportThickThreads] = React.useState(false); React.useEffect(() => { void (async () => { const usersSupportingThickThreads = await checkUsersThickThreadSupport( selectedUserInfos.map(user => user.id), ); setAllUsersSupportThickThreads( selectedUserInfos.every(userInfo => usersSupportingThickThreads.has(userInfo.id), ), ); })(); }, [checkUsersThickThreadSupport, selectedUserInfos]); const threadInfo = React.useMemo(() => { if (isChatCreation) { if (selectedUserInfos.length === 0) { return pendingNewThread; } return existingThreadInfoFinderForCreatingThread({ searching: true, userInfoInputArray: selectedUserInfos, allUsersSupportThickThreads, }); } return existingThreadInfoFinder({ searching: false, userInfoInputArray: [], allUsersSupportThickThreads: true, }); }, [ allUsersSupportThickThreads, existingThreadInfoFinder, existingThreadInfoFinderForCreatingThread, isChatCreation, pendingNewThread, selectedUserInfos, ]); return threadInfo; } export { useThreadInfoForPossiblyPendingThread, useInfosForPendingThread };