whole flow with keyboard shortcuts looks good
This commit is contained in:
@@ -1,26 +1,34 @@
|
|||||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import { FiPlusSquare, FiUsers, FiXSquare } from 'react-icons/fi';
|
import { FiPlusSquare, FiUsers, FiX, FiXSquare } from 'react-icons/fi';
|
||||||
import { useAsyncFn, useDebounce } from 'react-use';
|
import { useAsyncFn, useDebounce, useKeyPressEvent } from 'react-use';
|
||||||
import { userSearch } from '../../../../api/NirvanaApi';
|
import { createLine, userSearch } from '../../../../api/NirvanaApi';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { Avatar, Skeleton, Spin } from 'antd';
|
import { Avatar, Divider, Skeleton, Spin } from 'antd';
|
||||||
import { User } from '@nirvana/core/models/user.model';
|
import { User } from '@nirvana/core/models/user.model';
|
||||||
|
import CreateLineRequest from '@nirvana/core/requests/createLine.request';
|
||||||
|
|
||||||
export default function NewChannelForm() {
|
export default function NewChannelForm({ handleClose }: { handleClose: () => void }) {
|
||||||
const [peopleSearchQuery, setPeopleSearchQuery] = useState<string>('');
|
const [peopleSearchQuery, setPeopleSearchQuery] = useState<string>('');
|
||||||
|
|
||||||
const [userSearchRes, fetchUsers] = useAsyncFn(userSearch);
|
const [userSearchRes, fetchUsers] = useAsyncFn(userSearch);
|
||||||
|
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
|
||||||
|
|
||||||
const [isSearchingUsers, setSearchingUsers] = useState<boolean>(false);
|
const [isSearchingUsers, setSearchingUsers] = useState<boolean>(false);
|
||||||
|
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [createChannelRes, triggerCreateChannel] = useAsyncFn(createLine);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchInputRef) searchInputRef.current.focus();
|
if (searchInputRef) searchInputRef.current.focus();
|
||||||
}, [searchInputRef]);
|
}, [searchInputRef]);
|
||||||
|
|
||||||
const [_, cancel] = useDebounce(
|
const [_, cancel] = useDebounce(
|
||||||
async () => {
|
async () => {
|
||||||
|
if (!peopleSearchQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetchUsers(peopleSearchQuery);
|
await fetchUsers(peopleSearchQuery);
|
||||||
|
|
||||||
@@ -42,17 +50,15 @@ export default function NewChannelForm() {
|
|||||||
[setPeopleSearchQuery, setSearchingUsers],
|
[setPeopleSearchQuery, setSearchingUsers],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
|
|
||||||
|
|
||||||
// ensuring that we haven't already selected this user
|
// ensuring that we haven't already selected this user
|
||||||
// changing search results so that we don't see the selected user in the search results anymore
|
// changing search results so that we don't see the selected user in the search results anymore
|
||||||
const addUser = useCallback(
|
const addUser = useCallback(
|
||||||
(newUser: User) => {
|
(newUser: User) => {
|
||||||
setSelectedUsers((prevUsers) => {
|
setSelectedUsers((prevUsers) => {
|
||||||
if (prevUsers.find((currUser) => currUser.email === newUser.email)) {
|
if (prevUsers.find((currUser) => currUser.email === newUser.email)) {
|
||||||
return [...prevUsers, newUser];
|
return prevUsers;
|
||||||
}
|
}
|
||||||
return prevUsers;
|
return [...prevUsers, newUser];
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userSearchRes.value?.users) {
|
if (userSearchRes.value?.users) {
|
||||||
@@ -70,8 +76,49 @@ export default function NewChannelForm() {
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// TODO: prevent creating one-on-one line if already exists with x person?
|
||||||
|
// ensure that we don't have a one on one chat already with x person if it's one person selected
|
||||||
|
|
||||||
|
// upon success,
|
||||||
|
// make sure that the list of lines updates for this client and others so that it shows this new line
|
||||||
|
// select the line so that it shows up in the line details for this client
|
||||||
|
const handleCreateChannel = useCallback(async () => {
|
||||||
|
console.log('trying to create line now!');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!selectedUsers?.length) {
|
||||||
|
toast.error('you must select at least one person');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedMemberIds = selectedUsers.map((selectedPerson) =>
|
||||||
|
selectedPerson._id.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await triggerCreateChannel(new CreateLineRequest(selectedMemberIds));
|
||||||
|
|
||||||
|
toast.success('created channel!');
|
||||||
|
|
||||||
|
// handle close once the new line is created
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error);
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
console.log('done');
|
||||||
|
}
|
||||||
|
}, [handleClose, selectedUsers, triggerCreateChannel]);
|
||||||
|
|
||||||
|
useKeyPressEvent('Enter', handleCreateChannel);
|
||||||
|
useKeyPressEvent('Escape', handleClose);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 items-center pt-10 bg-white">
|
<div className="flex flex-col flex-1 items-center pt-10 bg-white relative">
|
||||||
|
<span className="flex flex-col items-center gap-2 absolute top-5 right-5 cursor-pointer">
|
||||||
|
<FiX onClick={handleClose} className="text-gray-300 text-xl" />
|
||||||
|
<span className="text-gray-300 text-xs p-1 bg-gray-100">`esc`</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 max-w-lg w-full">
|
<div className="flex flex-col gap-2 max-w-lg w-full">
|
||||||
{/* people search */}
|
{/* people search */}
|
||||||
<span className="text-gray-500">People</span>
|
<span className="text-gray-500">People</span>
|
||||||
@@ -89,9 +136,10 @@ export default function NewChannelForm() {
|
|||||||
{/* dropdown search results */}
|
{/* dropdown search results */}
|
||||||
<div className="flex flex-col shadow-lg max-h-[500px] overflow-auto">
|
<div className="flex flex-col shadow-lg max-h-[500px] overflow-auto">
|
||||||
{(userSearchRes.loading || isSearchingUsers) && <Spin />}
|
{(userSearchRes.loading || isSearchingUsers) && <Spin />}
|
||||||
{(!userSearchRes.value || userSearchRes.value?.users.length === 0) && (
|
{(!userSearchRes.value || userSearchRes.value?.users.length === 0) &&
|
||||||
<span className="p-10 text-gray-300">{`Can't find someone? Invite them and tell them the secret passcode!`}</span>
|
selectedUsers?.length === 0 && (
|
||||||
)}
|
<span className="p-10 text-gray-300">{`Can't find someone? Invite them and tell them the secret passcode!`}</span>
|
||||||
|
)}
|
||||||
{userSearchRes.value?.users.map((searchedUser) => {
|
{userSearchRes.value?.users.map((searchedUser) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -139,6 +187,21 @@ export default function NewChannelForm() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-end items-start gap-2">
|
||||||
|
<button onClick={handleClose} className="p-2 text-gray-300 hover:bg-gray-100">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span className="flex flex-col items-center gap-2">
|
||||||
|
<button onClick={handleCreateChannel} className="p-2 bg-gray-800 text-white">
|
||||||
|
Tune In
|
||||||
|
</button>
|
||||||
|
<span className="text-gray-300 text-xs p-1 bg-gray-100">`enter`</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ import NewChannelForm from '../compose/NewChannelForm';
|
|||||||
export default function MainPanel() {
|
export default function MainPanel() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
const { selectedLineId, showNewChannelForm } = useRealTimeRooms();
|
const { selectedLineId, showNewChannelForm, handleShowNewChannelForm } = useRealTimeRooms();
|
||||||
|
|
||||||
// already won't see stuff for overlay only mode as per parent configuration
|
// already won't see stuff for overlay only mode as per parent configuration
|
||||||
|
|
||||||
// TODO: if there is stuff in search, show that first
|
// TODO: if there is stuff in search, show that first
|
||||||
|
|
||||||
if (showNewChannelForm) return <NewChannelForm />;
|
if (showNewChannelForm)
|
||||||
|
return <NewChannelForm handleClose={() => handleShowNewChannelForm('hide')} />;
|
||||||
|
|
||||||
// if selected line, show line details
|
// if selected line, show line details
|
||||||
if (selectedLineId) return <LineDetails />;
|
if (selectedLineId) return <LineDetails />;
|
||||||
|
|||||||
Reference in New Issue
Block a user