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 { userSearchIndexForPotentialMembers } from 'lib/selectors/user-selectors'; | import { searchUsers } from 'lib/actions/user-actions'; | ||||
import { filterPotentialMembers } from 'lib/selectors/user-selectors'; | |||||
import SearchIndex from 'lib/shared/search-index'; | |||||
import { getPotentialMemberItems } from 'lib/shared/search-utils'; | import { getPotentialMemberItems } from 'lib/shared/search-utils'; | ||||
import { threadIsPending } from 'lib/shared/thread-utils'; | import { threadIsPending } from 'lib/shared/thread-utils'; | ||||
import type { AccountUserInfo, UserListItem } from 'lib/types/user-types'; | import type { AccountUserInfo, UserListItem } from 'lib/types/user-types'; | ||||
import { useServerCall } from 'lib/utils/action-utils'; | |||||
import Button from '../components/button.react'; | import Button from '../components/button.react'; | ||||
import Label from '../components/label.react'; | import Label from '../components/label.react'; | ||||
import Search from '../components/search.react'; | import Search from '../components/search.react'; | ||||
import type { InputState } from '../input/input-state'; | import type { InputState } from '../input/input-state'; | ||||
import { updateNavInfoActionType } from '../redux/action-types'; | import { updateNavInfoActionType } from '../redux/action-types'; | ||||
import { useSelector } from '../redux/redux-utils'; | import { useSelector } from '../redux/redux-utils'; | ||||
import SWMansionIcon from '../SWMansionIcon.react'; | import SWMansionIcon from '../SWMansionIcon.react'; | ||||
import css from './chat-thread-composer.css'; | import css from './chat-thread-composer.css'; | ||||
type Props = { | type Props = { | ||||
+userInfoInputArray: $ReadOnlyArray<AccountUserInfo>, | +userInfoInputArray: $ReadOnlyArray<AccountUserInfo>, | ||||
+setUserInfoInputArray: ( | |||||
($ReadOnlyArray<AccountUserInfo>) => $ReadOnlyArray<AccountUserInfo>, | |||||
) => void, | |||||
tomek: We have `SetState<T>` type that can be used here | |||||
+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 userSearchIndex = useSelector(userSearchIndexForPotentialMembers); | const viewerID = useSelector(state => state.currentUserInfo?.id); | ||||
const [serverSearchUserInfos, setServerSearchUserInfos] = React.useState<{ | |||||
[id: string]: AccountUserInfo, | |||||
}>({}); | |||||
const callSearchUsers = useServerCall(searchUsers); | |||||
React.useEffect(() => { | |||||
(async () => { | |||||
if (usernameInputText.length === 0) { | |||||
setServerSearchUserInfos({}); | |||||
} else { | |||||
const { userInfos: serverUserInfos } = await callSearchUsers( | |||||
tomekUnsubmitted Not Done Inline ActionsIn this effect we're calling a server function. We should make sure that we do this only when necessary - when search text is changed. Currently we would do this also when e.g. userInfos is changed. So we should make this effect smaller and reduce its dependencies, and then use some state and memos to compute the desired value. tomek: In this effect we're calling a server function. We should make sure that we do this only when… | |||||
usernameInputText, | |||||
); | |||||
const result = {}; | |||||
for (const user of serverUserInfos) { | |||||
if (!(user.id in userInfos)) { | |||||
result[user.id] = user; | |||||
} | |||||
} | |||||
const potentialMembers = filterPotentialMembers(result, viewerID); | |||||
setServerSearchUserInfos(potentialMembers); | |||||
} | |||||
})(); | |||||
}, [userInfos, callSearchUsers, usernameInputText, viewerID]); | |||||
const { | |||||
mergedUserInfos, | |||||
userSearchIndex, | |||||
}: { | |||||
mergedUserInfos: { [id: string]: AccountUserInfo }, | |||||
userSearchIndex: SearchIndex, | |||||
} = React.useMemo(() => { | |||||
const bothUserInfos = { ...serverSearchUserInfos, ...otherUserInfos }; | |||||
const searchIndex = new SearchIndex(); | |||||
for (const id in bothUserInfos) { | |||||
searchIndex.addEntry(id, bothUserInfos[id].username); | |||||
} | |||||
tomekUnsubmitted Not Done Inline ActionsAre you really sure it is necessary to construct this index by hand and to create it from scratch after any change to the dependencies? I guess we could use an existing selector that returns an index. tomek: Are you really sure it is necessary to construct this index by hand and to create it from… | |||||
return { | |||||
mergedUserInfos: bothUserInfos, | |||||
userSearchIndex: searchIndex, | |||||
}; | |||||
}, [serverSearchUserInfos, otherUserInfos]); | |||||
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, | ||||
userInfoInputIDs, | userInfoInputIDs, | ||||
), | ), | ||||
[usernameInputText, otherUserInfos, userSearchIndex, userInfoInputIDs], | [usernameInputText, mergedUserInfos, userSearchIndex, userInfoInputIDs], | ||||
); | ); | ||||
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 ( | ||||
!userListItems.length || | !userListItems.length || | ||||
(!usernameInputText && userInfoInputArray.length) | (!usernameInputText && userInfoInputArray.length) | ||||
) { | ) { | ||||
return null; | return null; | ||||
} | } | ||||
return ( | return ( | ||||
<ul className={css.searchResultsContainer}> | <ul className={css.searchResultsContainer}> | ||||
{userListItems.map((userSearchResult: UserListItem) => ( | {userListItems.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, | ||||
userListItems, | userListItems, | ||||
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 64 Lines • Show Last 20 Lines |
We have SetState<T> type that can be used here