Changeset View
Changeset View
Standalone View
Standalone View
web/chat/chat-thread-composer.react.js
// @flow | // @flow | ||||
import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { useDispatch } from 'react-redux'; | import { useDispatch } from 'react-redux'; | ||||
import { searchUsers } from 'lib/actions/user-actions.js'; | |||||
import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; | import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; | ||||
import { useENSNames } from 'lib/hooks/ens-cache.js'; | import { useENSNames } from 'lib/hooks/ens-cache.js'; | ||||
import { userSearchIndexForPotentialMembers } from 'lib/selectors/user-selectors.js'; | import { | ||||
filterPotentialMembers, | |||||
userSearchIndexForPotentialMembers, | |||||
searchIndexFromUserInfos, | |||||
} from 'lib/selectors/user-selectors.js'; | |||||
import { getPotentialMemberItems } from 'lib/shared/search-utils.js'; | import { getPotentialMemberItems } from 'lib/shared/search-utils.js'; | ||||
import { threadIsPending } from 'lib/shared/thread-utils.js'; | import { threadIsPending } from 'lib/shared/thread-utils.js'; | ||||
import type { AccountUserInfo, UserListItem } from 'lib/types/user-types.js'; | import type { SetState } from 'lib/types/hook-types.js'; | ||||
import type { | |||||
AccountUserInfo, | |||||
UserListItem, | |||||
GlobalAccountUserInfo, | |||||
} from 'lib/types/user-types.js'; | |||||
import { useServerCall } from 'lib/utils/action-utils.js'; | |||||
import css from './chat-thread-composer.css'; | import css from './chat-thread-composer.css'; | ||||
import Button from '../components/button.react.js'; | import Button from '../components/button.react.js'; | ||||
import Label from '../components/label.react.js'; | import Label from '../components/label.react.js'; | ||||
import Search from '../components/search.react.js'; | import Search from '../components/search.react.js'; | ||||
import type { InputState } from '../input/input-state.js'; | import type { InputState } from '../input/input-state.js'; | ||||
import { updateNavInfoActionType } from '../redux/action-types.js'; | import { updateNavInfoActionType } from '../redux/action-types.js'; | ||||
import { useSelector } from '../redux/redux-utils.js'; | import { useSelector } from '../redux/redux-utils.js'; | ||||
type Props = { | type Props = { | ||||
+userInfoInputArray: $ReadOnlyArray<AccountUserInfo>, | +userInfoInputArray: $ReadOnlyArray<AccountUserInfo>, | ||||
+setUserInfoInputArray: SetState<$ReadOnlyArray<AccountUserInfo>>, | |||||
+otherUserInfos: { [id: string]: AccountUserInfo }, | +otherUserInfos: { [id: string]: AccountUserInfo }, | ||||
+threadID: string, | +threadID: string, | ||||
+inputState: InputState, | +inputState: InputState, | ||||
}; | }; | ||||
type ActiveThreadBehavior = | type ActiveThreadBehavior = | ||||
| 'reset-active-thread-if-pending' | | 'reset-active-thread-if-pending' | ||||
| 'keep-active-thread'; | | 'keep-active-thread'; | ||||
function ChatThreadComposer(props: Props): React.Node { | function ChatThreadComposer(props: Props): React.Node { | ||||
const { userInfoInputArray, otherUserInfos, threadID, inputState } = props; | const { | ||||
userInfoInputArray, | |||||
setUserInfoInputArray, | |||||
otherUserInfos, | |||||
threadID, | |||||
inputState, | |||||
} = props; | |||||
const [usernameInputText, setUsernameInputText] = React.useState(''); | const [usernameInputText, setUsernameInputText] = React.useState(''); | ||||
const dispatch = useDispatch(); | const userInfos = useSelector(state => state.userStore.userInfos); | ||||
const viewerID = useSelector(state => state.currentUserInfo?.id); | |||||
const [serverSearchUserInfos, setServerSearchUserInfos] = React.useState< | |||||
$ReadOnlyArray<GlobalAccountUserInfo>, | |||||
>([]); | |||||
const callSearchUsers = useServerCall(searchUsers); | |||||
React.useEffect(() => { | |||||
(async () => { | |||||
if (usernameInputText.length === 0) { | |||||
setServerSearchUserInfos([]); | |||||
} else { | |||||
const { userInfos: serverUserInfos } = await callSearchUsers( | |||||
tomek: We should avoid calling server directly. A better approach is to do it indirectly by using… | |||||
usernameInputText, | |||||
); | |||||
setServerSearchUserInfos(serverUserInfos); | |||||
} | |||||
})(); | |||||
}, [callSearchUsers, usernameInputText]); | |||||
const filteredServerUserInfos = React.useMemo(() => { | |||||
const result = {}; | |||||
for (const user of serverSearchUserInfos) { | |||||
if (!(user.id in userInfos)) { | |||||
result[user.id] = user; | |||||
} | |||||
} | |||||
return filterPotentialMembers(result, viewerID); | |||||
}, [serverSearchUserInfos, userInfos, viewerID]); | |||||
const mergedUserInfos = React.useMemo( | |||||
() => ({ ...filteredServerUserInfos, ...otherUserInfos }), | |||||
[filteredServerUserInfos, otherUserInfos], | |||||
); | |||||
const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); | const userSearchIndex = useSelector(userSearchIndexForPotentialMembers); | ||||
const filteredServerUsersSearchIndex = React.useMemo( | |||||
() => searchIndexFromUserInfos(filteredServerUserInfos), | |||||
[filteredServerUserInfos], | |||||
); | |||||
const userInfoInputIDs = React.useMemo( | const userInfoInputIDs = React.useMemo( | ||||
() => userInfoInputArray.map(userInfo => userInfo.id), | () => userInfoInputArray.map(userInfo => userInfo.id), | ||||
[userInfoInputArray], | [userInfoInputArray], | ||||
); | ); | ||||
const userListItems = React.useMemo( | const userListItems = React.useMemo( | ||||
() => | () => | ||||
getPotentialMemberItems( | getPotentialMemberItems( | ||||
usernameInputText, | usernameInputText, | ||||
otherUserInfos, | mergedUserInfos, | ||||
userSearchIndex, | [userSearchIndex, filteredServerUsersSearchIndex], | ||||
userInfoInputIDs, | userInfoInputIDs, | ||||
tomekUnsubmitted Not Done Inline ActionsIt is confusing to me why do we need to modify this function add accept two search indexes. Instead of calling the server, creating index based on response, then searching using this index, we could just use what server returned to us. tomek: It is confusing to me why do we need to modify this function add accept two search indexes. | |||||
), | ), | ||||
[usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs], | [ | ||||
usernameInputText, | |||||
mergedUserInfos, | |||||
userSearchIndex, | |||||
filteredServerUsersSearchIndex, | |||||
userInfoInputIDs, | |||||
], | |||||
); | ); | ||||
const userListItemsWithENSNames = useENSNames(userListItems); | const userListItemsWithENSNames = useENSNames(userListItems); | ||||
const onSelectUserFromSearch = React.useCallback( | const onSelectUserFromSearch = React.useCallback( | ||||
(id: string) => { | (id: string, username: string) => { | ||||
const selectedUserIDs = userInfoInputArray.map(user => user.id); | setUserInfoInputArray(previousUserInfoInputArray => [ | ||||
dispatch({ | ...previousUserInfoInputArray, | ||||
type: updateNavInfoActionType, | { id, username }, | ||||
payload: { | ]); | ||||
selectedUserList: [...selectedUserIDs, id], | |||||
}, | |||||
}); | |||||
setUsernameInputText(''); | setUsernameInputText(''); | ||||
}, | }, | ||||
[dispatch, userInfoInputArray], | [setUserInfoInputArray], | ||||
); | ); | ||||
const onRemoveUserFromSelected = React.useCallback( | const onRemoveUserFromSelected = React.useCallback( | ||||
(id: string) => { | (id: string) => { | ||||
const selectedUserIDs = userInfoInputArray.map(user => user.id); | setUserInfoInputArray(previousUserInfoInputArray => | ||||
if (!selectedUserIDs.includes(id)) { | previousUserInfoInputArray.filter(user => user.id !== id), | ||||
return; | ); | ||||
} | |||||
dispatch({ | |||||
type: updateNavInfoActionType, | |||||
payload: { | |||||
selectedUserList: selectedUserIDs.filter(userID => userID !== id), | |||||
}, | |||||
}); | |||||
}, | }, | ||||
[dispatch, userInfoInputArray], | [setUserInfoInputArray], | ||||
); | ); | ||||
const userSearchResultList = React.useMemo(() => { | const userSearchResultList = React.useMemo(() => { | ||||
if ( | if ( | ||||
!userListItemsWithENSNames.length || | !userListItemsWithENSNames.length || | ||||
(!usernameInputText && userInfoInputArray.length) | (!usernameInputText && userInfoInputArray.length) | ||||
) { | ) { | ||||
return null; | return null; | ||||
} | } | ||||
return ( | return ( | ||||
<ul className={css.searchResultsContainer}> | <ul className={css.searchResultsContainer}> | ||||
{userListItemsWithENSNames.map((userSearchResult: UserListItem) => ( | {userListItemsWithENSNames.map((userSearchResult: UserListItem) => ( | ||||
<li key={userSearchResult.id} className={css.searchResultsItem}> | <li key={userSearchResult.id} className={css.searchResultsItem}> | ||||
<Button | <Button | ||||
variant="text" | variant="text" | ||||
onClick={() => onSelectUserFromSearch(userSearchResult.id)} | onClick={() => | ||||
onSelectUserFromSearch( | |||||
userSearchResult.id, | |||||
userSearchResult.username, | |||||
) | |||||
} | |||||
className={css.searchResultsButton} | className={css.searchResultsButton} | ||||
> | > | ||||
<div className={css.userName}>{userSearchResult.username}</div> | <div className={css.userName}>{userSearchResult.username}</div> | ||||
<div className={css.userInfo}>{userSearchResult.alertTitle}</div> | <div className={css.userInfo}>{userSearchResult.alertTitle}</div> | ||||
</Button> | </Button> | ||||
</li> | </li> | ||||
))} | ))} | ||||
</ul> | </ul> | ||||
); | ); | ||||
}, [ | }, [ | ||||
onSelectUserFromSearch, | onSelectUserFromSearch, | ||||
userInfoInputArray.length, | userInfoInputArray.length, | ||||
userListItemsWithENSNames, | userListItemsWithENSNames, | ||||
usernameInputText, | usernameInputText, | ||||
]); | ]); | ||||
const dispatch = useDispatch(); | |||||
const hideSearch = React.useCallback( | const hideSearch = React.useCallback( | ||||
(threadBehavior: ActiveThreadBehavior = 'keep-active-thread') => { | (threadBehavior: ActiveThreadBehavior = 'keep-active-thread') => { | ||||
dispatch({ | dispatch({ | ||||
type: updateNavInfoActionType, | type: updateNavInfoActionType, | ||||
payload: { | payload: { | ||||
chatMode: 'view', | chatMode: 'view', | ||||
activeChatThreadID: | activeChatThreadID: | ||||
threadBehavior === 'keep-active-thread' || | threadBehavior === 'keep-active-thread' || | ||||
▲ Show 20 Lines • Show All 61 Lines • Show Last 20 Lines |
We should avoid calling server directly. A better approach is to do it indirectly by using useDispatchActionPromise hook.