refactor: paged memo list container

pull/3946/head
johnnyjoy 5 months ago
parent 41976cb894
commit 339c38750f

@ -0,0 +1,81 @@
import { Button } from "@mui/joy";
import { ArrowDownIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
import { useMemoList, useMemoStore } from "@/store/v1";
import { Memo } from "@/types/proto/api/v1/memo_service";
import { useTranslate } from "@/utils/i18n";
import Empty from "../Empty";
interface Props {
renderer: (memo: Memo) => JSX.Element;
listSort?: (list: Memo[]) => Memo[];
filter?: string;
pageSize?: number;
}
interface State {
isRequesting: boolean;
nextPageToken: string;
}
const PagedMemoList = (props: Props) => {
const t = useTranslate();
const memoStore = useMemoStore();
const memoList = useMemoList();
const [state, setState] = useState<State>({
isRequesting: false,
nextPageToken: "",
});
const sortedMemoList = props.listSort ? props.listSort(memoList.value) : memoList.value;
const setIsRequesting = (isRequesting: boolean) => {
setState((state) => ({ ...state, isRequesting }));
};
const fetchMoreMemos = async () => {
setIsRequesting(true);
const response = await memoStore.fetchMemos({
filter: props.filter || "",
pageSize: props.pageSize || DEFAULT_LIST_MEMOS_PAGE_SIZE,
pageToken: state.nextPageToken,
});
setState(() => ({
isRequesting: false,
nextPageToken: response.nextPageToken,
}));
};
useEffect(() => {
memoList.reset();
setState((state) => ({ ...state, nextPageToken: "" }));
fetchMoreMemos();
}, [props.filter, props.pageSize]);
return (
<>
{sortedMemoList.map((memo) => props.renderer(memo))}
{state.nextPageToken && (
<div className="w-full flex flex-row justify-center items-center my-4">
<Button
variant="plain"
color="neutral"
loading={state.isRequesting}
endDecorator={<ArrowDownIcon className="w-4 h-auto" />}
onClick={() => fetchMoreMemos()}
>
{t("memo.load-more")}
</Button>
</div>
)}
{!state.nextPageToken && sortedMemoList.length === 0 && (
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
<Empty />
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
</div>
)}
</>
);
};
export default PagedMemoList;

@ -0,0 +1,3 @@
import PagedMemoList from "./PagedMemoList";
export default PagedMemoList;

@ -1,17 +1,16 @@
import { Button, Tooltip } from "@mui/joy";
import { Tooltip } from "@mui/joy";
import dayjs from "dayjs";
import { ArchiveIcon, ArchiveRestoreIcon, ArrowDownIcon, TrashIcon } from "lucide-react";
import { ArchiveIcon, ArchiveRestoreIcon, TrashIcon } from "lucide-react";
import { ClientError } from "nice-grpc-web";
import { useEffect, useState } from "react";
import { useMemo } from "react";
import toast from "react-hot-toast";
import Empty from "@/components/Empty";
import MemoContent from "@/components/MemoContent";
import MemoFilters from "@/components/MemoFilters";
import MobileHeader from "@/components/MobileHeader";
import PagedMemoList from "@/components/PagedMemoList";
import SearchBar from "@/components/SearchBar";
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
import useCurrentUser from "@/hooks/useCurrentUser";
import { useMemoFilterStore, useMemoList, useMemoStore } from "@/store/v1";
import { useMemoFilterStore, useMemoStore } from "@/store/v1";
import { RowStatus } from "@/types/proto/api/v1/common";
import { Memo } from "@/types/proto/api/v1/memo_service";
import { useTranslate } from "@/utils/i18n";
@ -20,25 +19,9 @@ const Archived = () => {
const t = useTranslate();
const user = useCurrentUser();
const memoStore = useMemoStore();
const memoList = useMemoList();
const memoFilterStore = useMemoFilterStore();
const [isRequesting, setIsRequesting] = useState(true);
const [nextPageToken, setNextPageToken] = useState<string>("");
const sortedMemos = memoList.value
.filter((memo) => memo.rowStatus === RowStatus.ARCHIVED)
.sort((a, b) =>
memoFilterStore.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
);
useEffect(() => {
memoList.reset();
fetchMemos("");
}, [memoFilterStore.filters]);
const fetchMemos = async (nextPageToken: string) => {
setIsRequesting(true);
const memoListFilter = useMemo(() => {
const filters = [`creator == "${user.name}"`, `row_status == "ARCHIVED"`];
const contentSearch: string[] = [];
const tagSearch: string[] = [];
@ -58,14 +41,8 @@ const Archived = () => {
if (tagSearch.length > 0) {
filters.push(`tag_search == [${tagSearch.join(", ")}]`);
}
const response = await memoStore.fetchMemos({
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
filter: filters.join(" && "),
pageToken: nextPageToken,
});
setIsRequesting(false);
setNextPageToken(response.nextPageToken);
};
return filters.join(" && ");
}, [user, memoFilterStore.filters]);
const handleDeleteMemoClick = async (memo: Memo) => {
const confirmed = window.confirm(t("memo.delete-confirm"));
@ -105,52 +82,45 @@ const Archived = () => {
</div>
</div>
<MemoFilters />
{sortedMemos.map((memo) => (
<div
key={memo.name}
className="relative flex flex-col justify-start items-start w-full p-4 pt-3 mb-2 bg-white dark:bg-zinc-800 rounded-lg"
>
<div className="w-full mb-1 flex flex-row justify-between items-center">
<div className="w-full max-w-[calc(100%-20px)] flex flex-row justify-start items-center mr-1">
<div className="text-sm leading-6 text-gray-400 select-none">
<relative-time datetime={memo.displayTime?.toISOString()} tense="past"></relative-time>
<PagedMemoList
renderer={(memo: Memo) => (
<div
key={memo.name}
className="relative flex flex-col justify-start items-start w-full p-4 pt-3 mb-2 bg-white dark:bg-zinc-800 rounded-lg"
>
<div className="w-full mb-1 flex flex-row justify-between items-center">
<div className="w-full max-w-[calc(100%-20px)] flex flex-row justify-start items-center mr-1">
<div className="text-sm leading-6 text-gray-400 select-none">
<relative-time datetime={memo.displayTime?.toISOString()} tense="past"></relative-time>
</div>
</div>
<div className="flex flex-row justify-end items-center gap-x-2">
<Tooltip title={t("common.restore")} placement="top">
<button onClick={() => handleRestoreMemoClick(memo)}>
<ArchiveRestoreIcon className="w-4 h-auto cursor-pointer text-gray-500 dark:text-gray-400" />
</button>
</Tooltip>
<Tooltip title={t("common.delete")} placement="top">
<button onClick={() => handleDeleteMemoClick(memo)} className="text-gray-500 dark:text-gray-400">
<TrashIcon className="w-4 h-auto cursor-pointer" />
</button>
</Tooltip>
</div>
</div>
<div className="flex flex-row justify-end items-center gap-x-2">
<Tooltip title={t("common.restore")} placement="top">
<button onClick={() => handleRestoreMemoClick(memo)}>
<ArchiveRestoreIcon className="w-4 h-auto cursor-pointer text-gray-500 dark:text-gray-400" />
</button>
</Tooltip>
<Tooltip title={t("common.delete")} placement="top">
<button onClick={() => handleDeleteMemoClick(memo)} className="text-gray-500 dark:text-gray-400">
<TrashIcon className="w-4 h-auto cursor-pointer" />
</button>
</Tooltip>
</div>
<MemoContent key={`${memo.name}-${memo.displayTime}`} memoName={memo.name} nodes={memo.nodes} readonly={true} />
</div>
<MemoContent key={`${memo.name}-${memo.displayTime}`} memoName={memo.name} nodes={memo.nodes} readonly={true} />
</div>
))}
{nextPageToken && (
<div className="w-full flex flex-row justify-center items-center my-4">
<Button
variant="plain"
color="neutral"
loading={isRequesting}
endDecorator={<ArrowDownIcon className="w-4 h-auto" />}
onClick={() => fetchMemos(nextPageToken)}
>
{t("memo.load-more")}
</Button>
</div>
)}
{!nextPageToken && sortedMemos.length === 0 && (
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
<Empty />
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
</div>
)}
)}
listSort={(memos: Memo[]) =>
memos
.filter((memo) => memo.rowStatus === RowStatus.ARCHIVED)
.sort((a, b) =>
memoFilterStore.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
)
}
filter={memoListFilter}
/>
</div>
</div>
</section>

@ -1,41 +1,23 @@
import { Button } from "@mui/joy";
import clsx from "clsx";
import dayjs from "dayjs";
import { ArrowDownIcon } from "lucide-react";
import { useEffect, useState } from "react";
import Empty from "@/components/Empty";
import { useMemo } from "react";
import { ExploreSidebar, ExploreSidebarDrawer } from "@/components/ExploreSidebar";
import MemoFilters from "@/components/MemoFilters";
import MemoView from "@/components/MemoView";
import MobileHeader from "@/components/MobileHeader";
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
import PagedMemoList from "@/components/PagedMemoList";
import useCurrentUser from "@/hooks/useCurrentUser";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import { useMemoFilterStore, useMemoList, useMemoStore } from "@/store/v1";
import { useTranslate } from "@/utils/i18n";
import { useMemoFilterStore } from "@/store/v1";
import { RowStatus } from "@/types/proto/api/v1/common";
import { Memo } from "@/types/proto/api/v1/memo_service";
const Explore = () => {
const t = useTranslate();
const { md } = useResponsiveWidth();
const user = useCurrentUser();
const memoStore = useMemoStore();
const memoList = useMemoList();
const memoFilterStore = useMemoFilterStore();
const [isRequesting, setIsRequesting] = useState(true);
const [nextPageToken, setNextPageToken] = useState<string>("");
const sortedMemos = memoList.value.sort((a, b) =>
memoFilterStore.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
);
useEffect(() => {
memoList.reset();
fetchMemos("");
}, [memoFilterStore.filters]);
const fetchMemos = async (nextPageToken: string) => {
setIsRequesting(true);
const memoListFilter = useMemo(() => {
const filters = [`row_status == "NORMAL"`, `visibilities == [${user ? "'PUBLIC', 'PROTECTED'" : "'PUBLIC'"}]`];
const contentSearch: string[] = [];
const tagSearch: string[] = [];
@ -55,14 +37,8 @@ const Explore = () => {
if (tagSearch.length > 0) {
filters.push(`tag_search == [${tagSearch.join(", ")}]`);
}
const response = await memoStore.fetchMemos({
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
filter: filters.join(" && "),
pageToken: nextPageToken,
});
setIsRequesting(false);
setNextPageToken(response.nextPageToken);
};
return filters.join(" && ");
}, [user, memoFilterStore.filters, memoFilterStore.orderByTimeAsc]);
return (
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
@ -75,28 +51,19 @@ const Explore = () => {
<div className={clsx(md ? "w-[calc(100%-15rem)]" : "w-full")}>
<MemoFilters />
<div className="flex flex-col justify-start items-start w-full max-w-full">
{sortedMemos.map((memo) => (
<MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility showPinned compact />
))}
{nextPageToken && (
<div className="w-full flex flex-row justify-center items-center my-4">
<Button
variant="plain"
color="neutral"
loading={isRequesting}
endDecorator={<ArrowDownIcon className="w-4 h-auto" />}
onClick={() => fetchMemos(nextPageToken)}
>
{t("memo.load-more")}
</Button>
</div>
)}
{!nextPageToken && sortedMemos.length === 0 && (
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
<Empty />
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
</div>
)}
<PagedMemoList
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility compact />}
listSort={(memos: Memo[]) =>
memos
.filter((memo) => memo.rowStatus === RowStatus.ACTIVE)
.sort((a, b) =>
memoFilterStore.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
)
}
filter={memoListFilter}
/>
</div>
</div>
{md && (

@ -1,46 +1,24 @@
import { Button } from "@mui/joy";
import clsx from "clsx";
import dayjs from "dayjs";
import { ArrowDownIcon } from "lucide-react";
import { useEffect, useState } from "react";
import Empty from "@/components/Empty";
import { useMemo } from "react";
import { HomeSidebar, HomeSidebarDrawer } from "@/components/HomeSidebar";
import MemoEditor from "@/components/MemoEditor";
import MemoFilters from "@/components/MemoFilters";
import MemoView from "@/components/MemoView";
import MobileHeader from "@/components/MobileHeader";
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
import PagedMemoList from "@/components/PagedMemoList";
import useCurrentUser from "@/hooks/useCurrentUser";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
import { useMemoFilterStore, useMemoList, useMemoStore } from "@/store/v1";
import { useMemoFilterStore } from "@/store/v1";
import { RowStatus } from "@/types/proto/api/v1/common";
import { useTranslate } from "@/utils/i18n";
import { Memo } from "@/types/proto/api/v1/memo_service";
const Home = () => {
const t = useTranslate();
const { md } = useResponsiveWidth();
const user = useCurrentUser();
const memoStore = useMemoStore();
const memoList = useMemoList();
const memoFilterStore = useMemoFilterStore();
const [isRequesting, setIsRequesting] = useState(true);
const [nextPageToken, setNextPageToken] = useState<string>("");
const sortedMemos = memoList.value
.filter((memo) => memo.rowStatus === RowStatus.ACTIVE)
.sort((a, b) =>
memoFilterStore.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
)
.sort((a, b) => Number(b.pinned) - Number(a.pinned));
useEffect(() => {
memoList.reset();
fetchMemos("");
}, [memoFilterStore.filters]);
const fetchMemos = async (nextPageToken: string) => {
setIsRequesting(true);
const memoListFilter = useMemo(() => {
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`];
const contentSearch: string[] = [];
const tagSearch: string[] = [];
@ -70,14 +48,8 @@ const Home = () => {
if (tagSearch.length > 0) {
filters.push(`tag_search == [${tagSearch.join(", ")}]`);
}
const response = await memoStore.fetchMemos({
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
filter: filters.join(" && "),
pageToken: nextPageToken,
});
setIsRequesting(false);
setNextPageToken(response.nextPageToken);
};
return filters.join(" && ");
}, [user, memoFilterStore.filters, memoFilterStore.orderByTimeAsc]);
return (
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
@ -91,28 +63,20 @@ const Home = () => {
<MemoEditor className="mb-2" cacheKey="home-memo-editor" />
<MemoFilters />
<div className="flex flex-col justify-start items-start w-full max-w-full">
{sortedMemos.map((memo) => (
<MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showVisibility showPinned compact />
))}
{nextPageToken && (
<div className="w-full flex flex-row justify-center items-center my-4">
<Button
variant="plain"
color="neutral"
loading={isRequesting}
endDecorator={<ArrowDownIcon className="w-4 h-auto" />}
onClick={() => fetchMemos(nextPageToken)}
>
{t("memo.load-more")}
</Button>
</div>
)}
{!nextPageToken && sortedMemos.length === 0 && (
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
<Empty />
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
</div>
)}
<PagedMemoList
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />}
listSort={(memos: Memo[]) =>
memos
.filter((memo) => memo.rowStatus === RowStatus.ACTIVE)
.sort((a, b) =>
memoFilterStore.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
)
.sort((a, b) => Number(b.pinned) - Number(a.pinned))
}
filter={memoListFilter}
/>
</div>
</div>
{md && (

@ -1,18 +1,19 @@
import { Button } from "@mui/joy";
import copy from "copy-to-clipboard";
import dayjs from "dayjs";
import { ArrowDownIcon, ExternalLinkIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { ExternalLinkIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { useParams } from "react-router-dom";
import Empty from "@/components/Empty";
import MemoFilters from "@/components/MemoFilters";
import MemoView from "@/components/MemoView";
import MobileHeader from "@/components/MobileHeader";
import PagedMemoList from "@/components/PagedMemoList";
import UserAvatar from "@/components/UserAvatar";
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
import useLoading from "@/hooks/useLoading";
import { useMemoFilterStore, useMemoList, useMemoStore, useUserStore } from "@/store/v1";
import { useMemoFilterStore, useUserStore } from "@/store/v1";
import { RowStatus } from "@/types/proto/api/v1/common";
import { Memo } from "@/types/proto/api/v1/memo_service";
import { User } from "@/types/proto/api/v1/user_service";
import { useTranslate } from "@/utils/i18n";
@ -22,14 +23,7 @@ const UserProfile = () => {
const userStore = useUserStore();
const loadingState = useLoading();
const [user, setUser] = useState<User>();
const memoStore = useMemoStore();
const memoList = useMemoList();
const memoFilterStore = useMemoFilterStore();
const [isRequesting, setIsRequesting] = useState(true);
const [nextPageToken, setNextPageToken] = useState<string>("");
const sortedMemos = memoList.value
.sort((a, b) => dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix())
.sort((a, b) => Number(b.pinned) - Number(a.pinned));
useEffect(() => {
const username = params.username;
@ -53,21 +47,11 @@ const UserProfile = () => {
});
}, [params.username]);
useEffect(() => {
if (!user) {
return;
}
memoList.reset();
fetchMemos("");
}, [user, memoFilterStore.filters]);
const fetchMemos = async (nextPageToken: string) => {
const memoListFilter = useMemo(() => {
if (!user) {
return;
return "";
}
setIsRequesting(true);
const filters = [`creator == "${user.name}"`, `row_status == "NORMAL"`, `order_by_pinned == true`];
const contentSearch: string[] = [];
const tagSearch: string[] = [];
@ -84,14 +68,8 @@ const UserProfile = () => {
if (tagSearch.length > 0) {
filters.push(`tag_search == [${tagSearch.join(", ")}]`);
}
const response = await memoStore.fetchMemos({
pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE,
filter: filters.join(" && "),
pageToken: nextPageToken,
});
setIsRequesting(false);
setNextPageToken(response.nextPageToken);
};
return filters.join(" && ");
}, [user, memoFilterStore.filters]);
const handleCopyProfileLink = () => {
if (!user) {
@ -131,28 +109,22 @@ const UserProfile = () => {
</div>
</div>
<MemoFilters />
{sortedMemos.map((memo) => (
<MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />
))}
{nextPageToken && (
<div className="w-full flex flex-row justify-center items-center my-4">
<Button
variant="plain"
color="neutral"
loading={isRequesting}
endDecorator={<ArrowDownIcon className="w-4 h-auto" />}
onClick={() => fetchMemos(nextPageToken)}
>
{t("memo.load-more")}
</Button>
</div>
)}
{!nextPageToken && sortedMemos.length === 0 && (
<div className="w-full mt-12 mb-8 flex flex-col justify-center items-center italic">
<Empty />
<p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p>
</div>
)}
<PagedMemoList
renderer={(memo: Memo) => (
<MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact />
)}
listSort={(memos: Memo[]) =>
memos
.filter((memo) => memo.rowStatus === RowStatus.ACTIVE)
.sort((a, b) =>
memoFilterStore.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
)
.sort((a, b) => Number(b.pinned) - Number(a.pinned))
}
filter={memoListFilter}
/>
</>
) : (
<p>Not found</p>

Loading…
Cancel
Save