322 lines
9.6 KiB
TypeScript
322 lines
9.6 KiB
TypeScript
import { User } from "@nirvana/common/models/user";
|
|
import { RefObject, useEffect, useMemo, useRef, useState } from "react";
|
|
import { FaPlus, FaRegTimesCircle, FaSearch } from "react-icons/fa";
|
|
import { usersIndex } from "../../services/algoliaSearchService";
|
|
import SimpleLoadingDot from "../Loading/SimpleLoadingDot";
|
|
import SimpleUserDetailsRow from "../UserDetails/SimpleUserDetailsRow";
|
|
import { debounce } from "lodash";
|
|
import toast from "react-hot-toast";
|
|
import { SearchResponse } from "@algolia/client-search";
|
|
import Conversation, {
|
|
ConversationMember,
|
|
ConversationMemberRole,
|
|
ConversationMemberState,
|
|
} from "@nirvana/common/models/conversation";
|
|
import { conversationService } from "@nirvana/common/services";
|
|
import { QueryRoutes, Routes } from "@nirvana/common/helpers/routes";
|
|
import { useRouter } from "next/router";
|
|
import { useRecoilState } from "recoil";
|
|
import { useAuth } from "../../contexts/authContext";
|
|
|
|
const searchDebounceMsTime = 3000;
|
|
|
|
export default function CreateConversation() {
|
|
const { currUser } = useAuth();
|
|
const router = useRouter();
|
|
|
|
const input = useRef<HTMLInputElement>();
|
|
|
|
const [selectedUsers, setSelectedUsers] = useState<User[]>([] as User[]);
|
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
const [userSearchInput, setUserSearchInput] = useState<string>("");
|
|
const [conversationName, setConversationName] = useState<string>("");
|
|
|
|
// recommendations list: algolio results + cached "relevant users" from all cached conversations currently
|
|
// options for the select
|
|
// have a list of 10 max?
|
|
const [recommendedUsers, setRecommendedUsers] = useState<User[]>(
|
|
[] as User[]
|
|
);
|
|
|
|
// on loading this page always focus the select field
|
|
useEffect(() => {
|
|
input.current?.focus();
|
|
}, []);
|
|
|
|
const handleChangeUserSearchInput = async (event) => {
|
|
const newInput = event?.target?.value;
|
|
console.log("searching for : " + newInput);
|
|
setUserSearchInput(newInput);
|
|
|
|
// no search if empty input
|
|
if (!newInput) {
|
|
setIsLoading(false);
|
|
console.warn("no valid input to search...no point searching");
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// do the search on the search value
|
|
const searchedUsers: SearchResponse = await usersIndex.search(newInput);
|
|
console.log(searchedUsers.hits);
|
|
|
|
if (searchedUsers.hits) {
|
|
const userHits = searchedUsers.hits.map((returnedUser) => {
|
|
const recUser = returnedUser as unknown as User;
|
|
recUser.id = returnedUser.objectID;
|
|
return recUser;
|
|
});
|
|
|
|
setRecommendedUsers(userHits);
|
|
}
|
|
} catch (error) {
|
|
console.log(error);
|
|
toast.error("Problem in searching");
|
|
}
|
|
|
|
// stop showing loading to say that we got results
|
|
setIsLoading(false);
|
|
};
|
|
|
|
const debounceHandler = useMemo(
|
|
() => debounce(handleChangeUserSearchInput, searchDebounceMsTime),
|
|
[]
|
|
);
|
|
|
|
// Stop the invocation of the debounced function
|
|
// after unmounting
|
|
useEffect(() => {
|
|
return () => {
|
|
debounceHandler.cancel();
|
|
};
|
|
}, []);
|
|
|
|
const selectUser = (addUser: User) => {
|
|
if (addUser.id == currUser!.uid) {
|
|
toast.error("You cannot add yourself, silly!");
|
|
return;
|
|
}
|
|
if (selectedUsers.some((currUser) => currUser.id == addUser.id)) {
|
|
toast("Already added member!");
|
|
return;
|
|
}
|
|
|
|
// also remove from the search set
|
|
// for now unless they re-search, they will see it again but for now not an issue
|
|
setRecommendedUsers((prevRecUsers) =>
|
|
prevRecUsers.filter((pU) => pU.id != addUser.id)
|
|
);
|
|
setSelectedUsers((prevUsers) => [addUser, ...prevUsers]);
|
|
};
|
|
|
|
const removeUser = (removeUser: User) => {
|
|
setSelectedUsers((prevUsers) =>
|
|
prevUsers.filter((pUser) => pUser.id != removeUser.id)
|
|
);
|
|
};
|
|
|
|
const clearSelected = () => {
|
|
setSelectedUsers([] as User[]);
|
|
};
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
|
|
const createConversation = async (e) => {
|
|
e.preventDefault();
|
|
setIsSubmitting(true);
|
|
|
|
if (!conversationName) {
|
|
toast.error("Please put in a conversation name");
|
|
setIsSubmitting(false);
|
|
return;
|
|
}
|
|
|
|
if (!selectedUsers?.length) {
|
|
toast.error("You must select members!");
|
|
setIsSubmitting(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const arrActiveMembers: string[] = [] as string[];
|
|
|
|
// create each conversation member based on the selected people
|
|
const members = selectedUsers.map((selUser) => {
|
|
arrActiveMembers.push(selUser.id);
|
|
|
|
return new ConversationMember(
|
|
selUser.id,
|
|
ConversationMemberState.default,
|
|
ConversationMemberRole.member
|
|
);
|
|
});
|
|
|
|
// add myself to this collection of members
|
|
members.push(
|
|
new ConversationMember(
|
|
currUser!.uid,
|
|
ConversationMemberState.default,
|
|
ConversationMemberRole.admin
|
|
)
|
|
);
|
|
|
|
const newConversation = new Conversation(
|
|
currUser!.uid,
|
|
conversationName,
|
|
arrActiveMembers
|
|
);
|
|
|
|
console.log(members);
|
|
console.log(newConversation);
|
|
|
|
// create main conversation object and all of the members associated
|
|
await conversationService.createConversation(newConversation, members);
|
|
|
|
toast.success("Created Conversation");
|
|
|
|
router.push({
|
|
pathname: Routes.home,
|
|
query: { page: QueryRoutes.convos },
|
|
});
|
|
} catch (error) {
|
|
toast.error("Problem in creating conversation");
|
|
}
|
|
|
|
setIsSubmitting(false);
|
|
};
|
|
|
|
return (
|
|
<div className="mx-auto my-auto w-96 flex flex-col">
|
|
{/* <Select
|
|
placeholder={"Look up emails, names, etc."}
|
|
tagRender={tagRender}
|
|
style={{ width: "100%" }}
|
|
options={options}
|
|
size="large"
|
|
mode="multiple"
|
|
allowClear
|
|
// defaultValue={['a10', 'c12']}
|
|
ref={input}
|
|
/> */}
|
|
|
|
{/* search bar that debounces and has a loading button while debouncing */}
|
|
|
|
<span className="flex flex-row items-center space-x-2 mb-5">
|
|
<FaSearch className="text-slate-300 text-lg" />
|
|
|
|
<input
|
|
className="p-2 flex-1 rounded bg-slate-50 border placeholder-slate-300"
|
|
placeholder="look up an email address or name"
|
|
ref={input}
|
|
onChange={(e) => {
|
|
setIsLoading(true);
|
|
debounceHandler(e);
|
|
}}
|
|
/>
|
|
|
|
{isLoading && <SimpleLoadingDot />}
|
|
</span>
|
|
|
|
{/* show search results nicely like tailwind website */}
|
|
<span className="text-slate-400 text-xs text-center">
|
|
showing {recommendedUsers.length} results
|
|
</span>
|
|
<span className="text-slate-400 text-xs text-center">
|
|
Click{" "}
|
|
<a href="mailto:?subject=Join my conversation on Nirvana!">here</a> to
|
|
invite a user to Nirvana.
|
|
</span>
|
|
|
|
{recommendedUsers?.length > 0 ? (
|
|
<div className="flex flex-col items-stretch">
|
|
{recommendedUsers.map((recUser) => {
|
|
return (
|
|
<SimpleUserDetailsRow
|
|
key={recUser.id}
|
|
user={recUser}
|
|
actionButton={
|
|
<span
|
|
onClick={() => selectUser(recUser)}
|
|
className="p-2 rounded-full hover:cursor-pointer
|
|
text-teal-500 hover:bg-slate-200 ml-auto"
|
|
>
|
|
<FaPlus className="text-lg" />
|
|
</span>
|
|
}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<></>
|
|
)}
|
|
|
|
{selectedUsers?.length ? (
|
|
<>
|
|
<span className="flex flex-row items-center mb-2">
|
|
<span className="uppercase tracking-widest text-slate-400 font-semibold">
|
|
Selected
|
|
</span>
|
|
|
|
<button
|
|
onClick={clearSelected}
|
|
className="ml-auto text-slate-300 text-xs"
|
|
>
|
|
clear
|
|
</button>
|
|
</span>
|
|
|
|
{/* list of selected people who are nirvana users */}
|
|
<span className="flex flex-col">
|
|
{selectedUsers.map((selectUser) => (
|
|
<SimpleUserDetailsRow
|
|
key={selectUser.id}
|
|
user={selectUser}
|
|
actionButton={
|
|
<button
|
|
onClick={() => removeUser(selectUser)}
|
|
className="p-2 rounded-full hover:cursor-pointer text-orange-500 ml-auto"
|
|
>
|
|
<FaRegTimesCircle className="ml-auto text-lg" />
|
|
</button>
|
|
}
|
|
/>
|
|
))}
|
|
</span>
|
|
|
|
<span className="flex flex-col mt-10">
|
|
<span className="uppercase tracking-widest text-slate-400 font-semibold">
|
|
Name<span className="text-orange-500">*</span>
|
|
</span>
|
|
<span className="text-slate-300 text-xs">
|
|
It can be casual or like an email subject line.
|
|
</span>
|
|
|
|
<input
|
|
className="p-3 rounded mt-2 bg-slate-50 border placeholder-slate-300"
|
|
placeholder="ex: code 'n chill, sales, birthdays, sprint 7..."
|
|
value={conversationName}
|
|
onChange={(e) => setConversationName(e.target.value)}
|
|
/>
|
|
</span>
|
|
|
|
{!isSubmitting && (
|
|
<button
|
|
onClick={createConversation}
|
|
className="rounded p-2 border flex flex-row items-center space-x-2
|
|
text-white font-semibold text-xs ml-auto mt-5 bg-teal-600 hover:scale-110 transition-all"
|
|
disabled={isSubmitting}
|
|
>
|
|
<span>Create</span>
|
|
</button>
|
|
)}
|
|
</>
|
|
) : (
|
|
<></>
|
|
)}
|
|
|
|
{/* show conversations already with these people */}
|
|
</div>
|
|
);
|
|
}
|