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( | |||||
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(() => { | |||||
return searchIndexFromUserInfos(filteredServerUserInfos); | |||||
}, [filteredServerUserInfos]); | |||||
michal: This can be simplified. | |||||
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, | ||||
), | ), | ||||
[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 |
This can be simplified.