refactor: nav and search.

feat: use recoil to replace redux
feat: use react-native

THIS IS NOT FINISHED. DONT USE THIS
This commit is contained in:
Wentao Lyu 2023-03-28 20:36:21 +08:00
parent d8ccc5b870
commit af3d74b104
33 changed files with 1142 additions and 473 deletions

View file

@ -2,50 +2,31 @@ import React from 'react';
import TrashIcon from '../svg/TrashIcon';
import { useSWRConfig } from 'swr';
import manualSWR from '~/utils/fetchers';
import { useDispatch } from 'react-redux';
import { setNewConvo, removeAll } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmission } from '~/store/submitSlice';
import { Dialog, DialogTrigger } from '../ui/Dialog.tsx';
import DialogTemplate from '../ui/DialogTemplate';
import store from '~/store';
export default function ClearConvos() {
const dispatch = useDispatch();
const { newConversation } = store.useConversation();
const { refreshConversations } = store.useConversations();
const { mutate } = useSWRConfig();
const { trigger } = manualSWR(`/api/convos/clear`, 'post', () => {
dispatch(setMessages([]));
dispatch(setNewConvo());
dispatch(setSubmission({}));
mutate(`/api/convos`);
newConversation();
refreshConversations();
});
const clickHandler = () => {
console.log('Clearing conversations...');
dispatch(removeAll());
trigger({});
};
return (
<Dialog>
<DialogTrigger asChild>
<a
className="flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
// onClick={clickHandler}
onClick={clickHandler}
>
<TrashIcon />
Clear conversations
</a>
</DialogTrigger>
<DialogTemplate
title="Clear conversations"
description="Are you sure you want to clear all conversations? This is irreversible."
selection={{
selectHandler: clickHandler,
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectText: 'Clear',
}}
/>
</Dialog>
);
}

View file

@ -1,13 +1,13 @@
import React, { useState, useContext } from 'react';
import { useSelector } from 'react-redux';
import React from 'react';
import LogOutIcon from '../svg/LogOutIcon';
import { useRecoilValue } from 'recoil';
import store from '~/store';
export default function Logout() {
const { user } = useSelector((state) => state.user);
const user = useRecoilValue(store.user);
const clickHandler = () => {
window.location.href = "/auth/logout";
window.location.href = '/auth/logout';
};
return (

View file

@ -1,33 +1,19 @@
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { setNewConvo } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmission } from '~/store/submitSlice';
import { setText } from '~/store/textSlice';
import { useRecoilValue } from 'recoil';
import store from '~/store';
export default function MobileNav({ setNavVisible }) {
const dispatch = useDispatch();
const { conversationId, convos, title } = useSelector((state) => state.convo);
const toggleNavVisible = () => {
setNavVisible((prev) => {
return !prev
})
}
const newConvo = () => {
dispatch(setText(''));
dispatch(setMessages([]));
dispatch(setNewConvo());
dispatch(setSubmission({}));
}
const conversation = useRecoilValue(store.conversation);
const { newConversation } = store.useConversation();
const { title = 'New Chat' } = conversation || {};
return (
<div className="fixed top-0 left-0 right-0 z-10 flex items-center border-b border-white/20 bg-gray-800 pl-1 pt-1 text-gray-200 sm:pl-3 md:hidden">
<button
type="button"
className="-ml-0.5 -mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white dark:hover:text-white"
onClick={toggleNavVisible}
onClick={() => setNavVisible(prev => !prev)}
>
<span className="sr-only">Open sidebar</span>
<svg
@ -66,7 +52,7 @@ export default function MobileNav({ setNavVisible }) {
<button
type="button"
className="px-3"
onClick={newConvo}
onClick={() => newConversation()}
>
<svg
stroke="currentColor"

View file

@ -3,13 +3,17 @@ import SearchBar from './SearchBar';
import ClearConvos from './ClearConvos';
import DarkMode from './DarkMode';
import Logout from './Logout';
import { useSelector } from 'react-redux';
export default function NavLinks({ fetch, onSearchSuccess, clearSearch }) {
const { searchEnabled } = useSelector((state) => state.search);
export default function NavLinks({ fetch, onSearchSuccess, clearSearch, isSearchEnabled }) {
return (
<>
{ !!searchEnabled && <SearchBar fetch={fetch} onSuccess={onSearchSuccess} clearSearch={clearSearch}/>}
{!!isSearchEnabled && (
<SearchBar
fetch={fetch}
onSuccess={onSearchSuccess}
clearSearch={clearSearch}
/>
)}
<DarkMode />
<ClearConvos />
<Logout />

View file

@ -1,23 +1,13 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { setNewConvo, refreshConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmission, setDisabled } from '~/store/submitSlice';
import { setText } from '~/store/textSlice';
import { setInputValue, setQuery } from '~/store/searchSlice';
import store from '~/store';
export default function NewChat() {
const dispatch = useDispatch();
const { newConversation } = store.useConversation();
const clickHandler = () => {
dispatch(setText(''));
dispatch(setMessages([]));
dispatch(setNewConvo());
dispatch(refreshConversation());
dispatch(setSubmission({}));
dispatch(setDisabled(false));
dispatch(setInputValue(''));
dispatch(setQuery(''));
// dispatch(setInputValue(''));
// dispatch(setQuery(''));
newConversation();
};
return (

View file

@ -1,41 +1,44 @@
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { debounce } from 'lodash';
import { useSelector, useDispatch } from 'react-redux';
import { Search } from 'lucide-react';
import { setInputValue, setQuery } from '~/store/searchSlice';
import { useSetRecoilState } from 'recoil';
import store from '~/store';
export default function SearchBar({ fetch, clearSearch }) {
const dispatch = useDispatch();
const { inputValue } = useSelector((state) => state.search);
// const dispatch = useDispatch();
const [inputValue, setInputValue] = useState('');
const setSearchQuery = useSetRecoilState(store.searchQuery);
// const [inputValue, setInputValue] = useState('');
const debouncedChangeHandler = useCallback(
debounce((q) => {
dispatch(setQuery(q));
debounce(q => {
setSearchQuery(q);
if (q.length > 0) {
fetch(q, 1);
}
}, 750),
[dispatch]
[setSearchQuery]
);
const handleKeyUp = (e) => {
const handleKeyUp = e => {
const { value } = e.target;
if (e.keyCode === 8 && value === '') {
if (e.keyCode === 8 && value === '') {
// Value after clearing input: ""
console.log(`Value after clearing input: "${value}"`);
dispatch(setQuery(''));
setSearchQuery('');
clearSearch();
}
}
};
const changeHandler = (e) => {
const changeHandler = e => {
let q = e.target.value;
dispatch(setInputValue(q));
setInputValue(q);
q = q.trim();
if (q === '') {
dispatch(setQuery(''));
setSearchQuery('');
clearSearch();
} else {
debouncedChangeHandler(q);

View file

@ -6,67 +6,121 @@ import Pages from '../Conversations/Pages';
import Conversations from '../Conversations';
import NavLinks from './NavLinks';
import { searchFetcher, swr } from '~/utils/fetchers';
import { useDispatch, useSelector } from 'react-redux';
import { setConvos, setNewConvo, refreshConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setDisabled } from '~/store/submitSlice';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import store from '~/store';
export default function Nav({ navVisible, setNavVisible }) {
const dispatch = useDispatch();
const [isHovering, setIsHovering] = useState(false);
const [isFetching, setIsFetching] = useState(false);
const containerRef = useRef(null);
const scrollPositionRef = useRef(null);
// const dispatch = useDispatch();
const [conversations, setConversations] = useState([]);
// current page
const [pageNumber, setPageNumber] = useState(1);
// total pages
const [pages, setPages] = useState(1);
const [pageNumber, setPage] = useState(1);
const { search, query } = useSelector((state) => state.search);
const { conversationId, convos, refreshConvoHint } = useSelector((state) => state.convo);
// search
const searchQuery = useRecoilValue(store.searchQuery);
const isSearchEnabled = useRecoilValue(store.isSearchEnabled);
const isSearching = useRecoilValue(store.isSearching);
const { newConversation } = store.useConversation();
// current conversation
const conversation = useRecoilValue(store.conversation);
const { conversationId } = conversation || {};
const setMessages = useSetRecoilState(store.messages);
// refreshConversationsHint is used for other components to ask refresh of Nav
const refreshConversationsHint = useRecoilValue(store.refreshConversationsHint);
const { refreshConversations } = store.useConversations();
const [isFetching, setIsFetching] = useState(false);
const onSuccess = (data, searchFetch = false) => {
if (search) {
if (isSearching) {
return;
}
const { conversations, pages } = data;
let { conversations, pages } = data;
if (pageNumber > pages) {
setPage(pages);
setPageNumber(pages);
} else {
dispatch(setConvos({ convos: conversations, searchFetch }));
if (!searchFetch)
conversations = conversations.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
setConversations(conversations);
setPages(pages);
}
};
const onSearchSuccess = (data, expectedPage) => {
const res = data;
dispatch(setConvos({ convos: res.conversations, searchFetch: true }));
setConversations(res.conversations);
if (expectedPage) {
setPage(expectedPage);
setPageNumber(expectedPage);
}
setPage(res.pageNumber);
setPageNumber(res.pageNumber);
setPages(res.pages);
setIsFetching(false);
if (res.messages?.length > 0) {
dispatch(setMessages(res.messages));
dispatch(setDisabled(true));
setMessages(res.messages);
// dispatch(setDisabled(true));
}
};
const fetch = useCallback(_.partialRight(searchFetcher.bind(null, () => setIsFetching(true)), onSearchSuccess), [dispatch]);
// TODO: dont need this
const fetch = useCallback(
_.partialRight(
searchFetcher.bind(null, () => setIsFetching(true)),
onSearchSuccess
),
[setIsFetching]
);
const clearSearch = () => {
setPage(1);
dispatch(refreshConversation());
if (!conversationId) {
dispatch(setNewConvo());
dispatch(setMessages([]));
setPageNumber(1);
refreshConversations();
if (conversationId == 'search') {
newConversation();
}
dispatch(setDisabled(false));
// dispatch(setDisabled(false));
};
const { data, isLoading, mutate } = swr(`/api/convos?pageNumber=${pageNumber}`, onSuccess, {
revalidateOnMount: false,
revalidateOnMount: false
});
const containerRef = useRef(null);
const scrollPositionRef = useRef(null);
const nextPage = async () => {
moveToTop();
if (!isSearching) {
setPageNumber(prev => prev + 1);
await mutate();
} else {
await fetch(searchQuery, +pageNumber + 1);
}
};
const previousPage = async () => {
moveToTop();
if (!isSearching) {
setPageNumber(prev => prev - 1);
await mutate();
} else {
await fetch(searchQuery, +pageNumber - 1);
}
};
useEffect(() => {
if (!isSearching) {
mutate();
}
}, [pageNumber, conversationId, refreshConversationsHint]);
const moveToTop = () => {
const container = containerRef.current;
@ -75,35 +129,7 @@ export default function Nav({ navVisible, setNavVisible }) {
}
};
const nextPage = async () => {
moveToTop();
if (!search) {
setPage((prev) => prev + 1);
await mutate();
} else {
await fetch(query, +pageNumber + 1);
}
};
const previousPage = async () => {
moveToTop();
if (!search) {
setPage((prev) => prev - 1);
await mutate();
} else {
await fetch(query, +pageNumber - 1);
}
};
useEffect(() => {
if (!search) {
mutate();
}
}, [pageNumber, conversationId, refreshConvoHint]);
useEffect(() => {
const moveTo = () => {
const container = containerRef.current;
if (container && scrollPositionRef.current !== null) {
@ -112,18 +138,20 @@ export default function Nav({ navVisible, setNavVisible }) {
container.scrollTop = Math.min(maxScrollTop, scrollPositionRef.current);
}
};
const toggleNavVisible = () => {
setNavVisible(prev => !prev);
};
useEffect(() => {
moveTo();
}, [data]);
useEffect(() => {
setNavVisible(false);
}, [conversationId]);
const toggleNavVisible = () => {
setNavVisible((prev) => {
return !prev;
});
};
const containerClasses =
isLoading && pageNumber === 1
? 'flex flex-col gap-2 text-gray-100 text-sm h-full justify-center items-center'
@ -151,11 +179,11 @@ export default function Nav({ navVisible, setNavVisible }) {
>
<div className={containerClasses}>
{/* {(isLoading && pageNumber === 1) ? ( */}
{(isLoading && pageNumber === 1) || (isFetching) ? (
{(isLoading && pageNumber === 1) || isFetching ? (
<Spinner />
) : (
<Conversations
conversations={convos}
conversations={conversations}
conversationId={conversationId}
moveToTop={moveToTop}
/>
@ -172,6 +200,7 @@ export default function Nav({ navVisible, setNavVisible }) {
fetch={fetch}
onSearchSuccess={onSearchSuccess}
clearSearch={clearSearch}
isSearchEnabled={isSearchEnabled}
/>
</nav>
</div>