diff --git a/packages/web/components/FullPageExperiences/CreateConversation.tsx.tsx b/packages/web/components/FullPageExperiences/CreateConversation.tsx.tsx index a4e16b4..ccc79a4 100644 --- a/packages/web/components/FullPageExperiences/CreateConversation.tsx.tsx +++ b/packages/web/components/FullPageExperiences/CreateConversation.tsx.tsx @@ -1,11 +1,21 @@ import { User, UserStatus } from "@nirvana/common/models/user"; -import { Select, Tag } from "antd"; -import { useEffect, useRef, useState } from "react"; +import { Divider, Select, Tag } from "antd"; +import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react"; +import { FaPlus, FaRegTimesCircle, FaSearch } from "react-icons/fa"; +import searchClient, { 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"; + +const searchDebounceMsTime = 3000; export default function CreateConversation() { const input = useRef(); const [selectedUsers, setSelectedUsers] = useState([] as User[]); + const [isLoading, setIsLoading] = useState(false); + const [userSearchInput, setUserSearchInput] = useState(""); // recommendations list: algolio results + cached "relevant users" from all cached conversations currently // options for the select @@ -14,10 +24,7 @@ export default function CreateConversation() { [] as User[] ); - const options = recommendedUsers.map((recUser) => { - value: recUser.firstName + " " + recUser.lastName; - }); - + // on loading this page always focus the select field useEffect(() => { input.current?.focus(); }, []); @@ -26,9 +33,61 @@ export default function CreateConversation() { // if it's an email of a non-nirvana user, then hit invite button to email them with a voice clip along with it // if it's an existing user, then show a nice chip of that user's profile picture and their name + 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 = (user) => { + setSelectedUsers((prevUsers) => [user, ...prevUsers]); + }; + return (
- { + setIsLoading(true); + debounceHandler(e); + }} /> + + {isLoading && } - + {/* show search results nicely like tailwind website */} - {/* if it's a one on one, and i already have a conversation with them, then block creating a new one? */} + {recommendedUsers && recommendedUsers.length > 0 ? ( + <> + + showing {recommendedUsers.length} results + + {recommendedUsers.map((recUser) => { + return ( + selectUser(recUser)} + className="p-2 rounded-full hover:cursor-pointer + text-teal-500 hover:bg-slate-200 ml-auto" + > + + + } + /> + ); + })} + + ) : ( + <> + )} + + {selectedUsers?.length ? ( + <> + + + + + Selected + + + + + + {/* list of selected people who are nirvana users */} + + {selectedUsers.map((selectUser) => ( + + + + } + /> + ))} + + + + + Name + + + Name this conversation: engineering, hangout, lunch time, or leave + it as is. + + + + + + + + ) : ( + <> + )} + + {/* show conversations already with these people */}
); } - -function tagRender(props) { - const { label, value, closable, onClose } = props; - const onPreventMouseDown = (event) => { - event.preventDefault(); - event.stopPropagation(); - }; - return ( - - {label} - - ); -} diff --git a/packages/web/components/Loading/SimpleLoadingDot.tsx b/packages/web/components/Loading/SimpleLoadingDot.tsx new file mode 100644 index 0000000..9ffea69 --- /dev/null +++ b/packages/web/components/Loading/SimpleLoadingDot.tsx @@ -0,0 +1,5 @@ +import { FaRegDotCircle } from "react-icons/fa"; + +export default function SimpleLoadingDot() { + return ; +} diff --git a/packages/web/components/UserDetails/SimpleUserDetailsRow.tsx b/packages/web/components/UserDetails/SimpleUserDetailsRow.tsx index f158003..5085f65 100644 --- a/packages/web/components/UserDetails/SimpleUserDetailsRow.tsx +++ b/packages/web/components/UserDetails/SimpleUserDetailsRow.tsx @@ -1,9 +1,11 @@ -import { Avatar } from "antd"; -import { FaPaperPlane, FaRegTimesCircle } from "react-icons/fa"; import { User } from "@nirvana/common/models/user"; import UserAvatar from "./UserAvatar"; +import { ReactElement } from "react"; -export default function SelectedUserRow(props: { user: User }) { +export default function SelectedUserRow(props: { + user: User; + actionButton: ReactElement; +}) { return ( {"joe@microsoft.com"} - + {props.actionButton} ); } diff --git a/packages/web/package.json b/packages/web/package.json index 9c27744..7b055e4 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -10,11 +10,14 @@ }, "dependencies": { "@nirvana/common": "*", + "@types/lodash": "^4.14.178", "agora-rtc-sdk-ng": "^4.8.1", + "algoliasearch": "^4.12.1", "antd": "^4.18.3", "firebase": "^9.6.2", "firebase-admin": "^10.0.1", "js-cookie": "^3.0.1", + "lodash": "^4.17.21", "mic-recorder-to-mp3": "^2.2.2", "moment": "^2.29.1", "next": "12.0.7", diff --git a/packages/web/pages/s/index.tsx b/packages/web/pages/s/index.tsx index 178ec33..b66c968 100644 --- a/packages/web/pages/s/index.tsx +++ b/packages/web/pages/s/index.tsx @@ -18,8 +18,6 @@ export default function Me() { const router = useRouter(); const currPage = router.query.page; - console.log(currPage); - function getCurrentContent() { switch (currPage) { case QueryRoutes.convos: diff --git a/packages/web/services/algoliaSearchService.ts b/packages/web/services/algoliaSearchService.ts new file mode 100644 index 0000000..47e98c8 --- /dev/null +++ b/packages/web/services/algoliaSearchService.ts @@ -0,0 +1,23 @@ +import algoliaSearch, { SearchClient } from "algoliasearch"; + +var searchClient: SearchClient = null; + +if ( + process.env.NEXT_PUBLIC_ALGOLIA_APP_ID && + process.env.NEXT_PUBLIC_ALGOLIA_API_KEY +) { + searchClient = algoliaSearch( + process.env.NEXT_PUBLIC_ALGOLIA_APP_ID, + process.env.NEXT_PUBLIC_ALGOLIA_API_KEY + ); + + console.log("set up algolia search successfully"); +} else { + console.error("unable to set up aloglia"); +} + +export const usersIndex = searchClient.initIndex( + process.env.NEXT_PUBLIC_ALGOLIA_USERS_INDEX_NAME || "" +); + +export default searchClient; diff --git a/yarn.lock b/yarn.lock index 517a3ef..46f2122 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1123,7 +1123,7 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/lodash@^4.14.104": +"@types/lodash@^4.14.104", "@types/lodash@^4.14.178": version "4.14.178" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8" integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==