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 ;
}, [
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 };