Changeset View
Changeset View
Standalone View
Standalone View
web/chat/chat-message-list-container.react.js
Show All 11 Lines | |||||
import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors'; | import { userInfoSelectorForPotentialMembers } from 'lib/selectors/user-selectors'; | ||||
import { | import { | ||||
useWatchThread, | useWatchThread, | ||||
useExistingThreadInfoFinder, | useExistingThreadInfoFinder, | ||||
createPendingThread, | createPendingThread, | ||||
threadIsPending, | threadIsPending, | ||||
} from 'lib/shared/thread-utils'; | } from 'lib/shared/thread-utils'; | ||||
import { threadTypes } from 'lib/types/thread-types'; | import { threadTypes } from 'lib/types/thread-types'; | ||||
import type { AccountUserInfo } from 'lib/types/user-types'; | |||||
import { InputStateContext } from '../input/input-state'; | import { InputStateContext } 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 ChatInputBar from './chat-input-bar.react'; | import ChatInputBar from './chat-input-bar.react'; | ||||
import css from './chat-message-list-container.css'; | import css from './chat-message-list-container.css'; | ||||
import ChatMessageList from './chat-message-list.react'; | import ChatMessageList from './chat-message-list.react'; | ||||
import ChatThreadComposer from './chat-thread-composer.react'; | import ChatThreadComposer from './chat-thread-composer.react'; | ||||
import ThreadTopBar from './thread-top-bar.react'; | import ThreadTopBar from './thread-top-bar.react'; | ||||
type Props = { | type Props = { | ||||
+activeChatThreadID: string, | +activeChatThreadID: string, | ||||
}; | }; | ||||
function ChatMessageListContainer(props: Props): React.Node { | function ChatMessageListContainer(props: Props): React.Node { | ||||
const { activeChatThreadID } = props; | const { activeChatThreadID } = props; | ||||
const isChatCreation = | const isChatCreation = | ||||
useSelector(state => state.navInfo.chatMode) === 'create'; | useSelector(state => state.navInfo.chatMode) === 'create'; | ||||
const selectedUserIDs = useSelector(state => state.navInfo.selectedUserList); | const selectedUserIDs = useSelector( | ||||
state => state.navInfo.selectedUserList ?? [], | |||||
); | |||||
const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); | const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers); | ||||
const userInfoInputArray: $ReadOnlyArray<AccountUserInfo> = React.useMemo( | const [userInfoInputArray, setUserInfoInputArray] = React.useState(() => | ||||
() => selectedUserIDs?.map(id => otherUserInfos[id]).filter(Boolean) ?? [], | selectedUserIDs.map(id => otherUserInfos[id]).filter(Boolean), | ||||
[otherUserInfos, selectedUserIDs], | |||||
); | ); | ||||
React.useEffect(() => { | |||||
if (!isChatCreation) { | |||||
setUserInfoInputArray([]); | |||||
} | |||||
}, [isChatCreation]); | |||||
michal: We need to store usernames alongside the userIDs (because we might not have some of these users… | |||||
const viewerID = useSelector(state => state.currentUserInfo?.id); | const viewerID = useSelector(state => state.currentUserInfo?.id); | ||||
invariant(viewerID, 'should be set'); | invariant(viewerID, 'should be set'); | ||||
const pendingPrivateThread = React.useRef( | const pendingPrivateThread = React.useRef( | ||||
createPendingThread({ | createPendingThread({ | ||||
viewerID, | viewerID, | ||||
threadType: threadTypes.PRIVATE, | threadType: threadTypes.PRIVATE, | ||||
}), | }), | ||||
▲ Show 20 Lines • Show All 47 Lines • ▼ Show 20 Lines | const threadInfo = React.useMemo(() => { | ||||
existingThreadInfoFinderForCreatingThread, | existingThreadInfoFinderForCreatingThread, | ||||
isChatCreation, | isChatCreation, | ||||
userInfoInputArray, | userInfoInputArray, | ||||
pendingNewThread, | pendingNewThread, | ||||
]); | ]); | ||||
invariant(threadInfo, 'ThreadInfo should be set'); | invariant(threadInfo, 'ThreadInfo should be set'); | ||||
const dispatch = useDispatch(); | const dispatch = useDispatch(); | ||||
// The effect removes members from list in navInfo | |||||
// if some of the user IDs don't exist in redux store | |||||
React.useEffect(() => { | React.useEffect(() => { | ||||
if (!isChatCreation) { | if (isChatCreation) { | ||||
return; | let payload = {}; | ||||
tomekUnsubmitted Done Inline ActionsWe should exit early tomek: We should exit early | |||||
} | |||||
const existingSelectedUsersSet = new Set( | const newSelectedUserIDs = userInfoInputArray.map(user => user.id); | ||||
userInfoInputArray.map(userInfo => userInfo.id), | if (!_isEqual(new Set(selectedUserIDs), new Set(newSelectedUserIDs))) { | ||||
tomekUnsubmitted Done Inline ActionsIt might be a good idea to extract some of these to separate memos so that we don't recompute them on every effect call tomek: It might be a good idea to extract some of these to separate memos so that we don't recompute… | |||||
); | payload = { | ||||
if ( | ...payload, | ||||
selectedUserIDs?.length !== existingSelectedUsersSet.size || | selectedUserList: newSelectedUserIDs, | ||||
!_isEqual(new Set(selectedUserIDs), existingSelectedUsersSet) | }; | ||||
) { | |||||
dispatch({ | |||||
type: updateNavInfoActionType, | |||||
payload: { | |||||
selectedUserList: Array.from(existingSelectedUsersSet), | |||||
}, | |||||
}); | |||||
} | } | ||||
}, [ | |||||
dispatch, | |||||
isChatCreation, | |||||
otherUserInfos, | |||||
selectedUserIDs, | |||||
userInfoInputArray, | |||||
]); | |||||
React.useEffect(() => { | if (activeChatThreadID !== threadInfo?.id) { | ||||
if (isChatCreation && activeChatThreadID !== threadInfo?.id) { | payload = { | ||||
let payload = { | ...payload, | ||||
activeChatThreadID: threadInfo?.id, | activeChatThreadID: threadInfo?.id, | ||||
}; | }; | ||||
if (threadIsPending(threadInfo?.id)) { | if (threadIsPending(threadInfo?.id)) { | ||||
payload = { | payload = { | ||||
...payload, | ...payload, | ||||
pendingThread: threadInfo, | pendingThread: threadInfo, | ||||
}; | }; | ||||
} | } | ||||
} | |||||
dispatch({ | dispatch({ | ||||
type: updateNavInfoActionType, | type: updateNavInfoActionType, | ||||
payload, | payload, | ||||
}); | }); | ||||
tomekUnsubmitted Done Inline ActionsIs it possible for payload to be empty here? tomek: Is it possible for `payload` to be empty here? | |||||
michalUnsubmitted Done Inline ActionsGood point. When we add a new user the newSelectedUserIDs changes which triggers this dispatch and updates the navInfo. Then the userEffect runs again because selectedUserIDs is updated but now it's equal to newSelectedUserIDs and the payload is empty. Should just surround the dispatch with an if? michal: Good point. When we add a new user the `newSelectedUserIDs` changes which triggers this… | |||||
tomekUnsubmitted Not Done Inline ActionsYes, I think so tomek: Yes, I think so | |||||
} | } | ||||
}, [activeChatThreadID, dispatch, isChatCreation, threadInfo]); | }, [ | ||||
activeChatThreadID, | |||||
dispatch, | |||||
isChatCreation, | |||||
selectedUserIDs, | |||||
threadInfo, | |||||
userInfoInputArray, | |||||
]); | |||||
michalUnsubmitted Done Inline ActionsThis useEffect combines the two previous ones. michal: This `useEffect` combines the two previous ones. | |||||
const inputState = React.useContext(InputStateContext); | const inputState = React.useContext(InputStateContext); | ||||
invariant(inputState, 'InputState should be set'); | invariant(inputState, 'InputState should be set'); | ||||
const [{ isActive }, connectDropTarget] = useDrop({ | const [{ isActive }, connectDropTarget] = useDrop({ | ||||
accept: NativeTypes.FILE, | accept: NativeTypes.FILE, | ||||
drop: item => { | drop: item => { | ||||
const { files } = item; | const { files } = item; | ||||
if (inputState && files.length > 0) { | if (inputState && files.length > 0) { | ||||
▲ Show 20 Lines • Show All 58 Lines • ▼ Show 20 Lines | if (!isChatCreation) { | ||||
{topBar} | {topBar} | ||||
{messageListAndInput} | {messageListAndInput} | ||||
</> | </> | ||||
); | ); | ||||
} | } | ||||
const chatUserSelection = ( | const chatUserSelection = ( | ||||
<ChatThreadComposer | <ChatThreadComposer | ||||
userInfoInputArray={userInfoInputArray} | userInfoInputArray={userInfoInputArray} | ||||
setUserInfoInputArray={setUserInfoInputArray} | |||||
otherUserInfos={otherUserInfos} | otherUserInfos={otherUserInfos} | ||||
threadID={threadInfo.id} | threadID={threadInfo.id} | ||||
inputState={inputState} | inputState={inputState} | ||||
/> | /> | ||||
); | ); | ||||
if (!userInfoInputArray.length) { | if (!userInfoInputArray.length) { | ||||
return chatUserSelection; | return chatUserSelection; | ||||
Show All 24 Lines |
We need to store usernames alongside the userIDs (because we might not have some of these users in userStore so we can't access their usernames later).
Because of this, we need to stop keeping the state in navInfo.selectedUserList (we only store ids there). Now the navInfo is only used for the initialization, and later it's only updated to reflect the state in userInfoInputArray.