diff --git a/web/src/components/MemoSkeleton.tsx b/web/src/components/MemoSkeleton.tsx new file mode 100644 index 000000000..59435a6c1 --- /dev/null +++ b/web/src/components/MemoSkeleton.tsx @@ -0,0 +1,56 @@ +import { cn } from "@/lib/utils"; + +interface Props { + showCreator?: boolean; + count?: number; +} + +const MemoSkeleton = ({ showCreator = false, count = 6 }: Props) => { + return ( + <> + {Array.from({ length: count }).map((_, index) => ( +
+ {/* Header section */} +
+
+ {showCreator ? ( +
+ {/* Avatar skeleton */} +
+
+ {/* Creator name skeleton */} +
+ {/* Timestamp skeleton */} +
+
+
+ ) : ( +
+ )} +
+ {/* Action buttons skeleton */} +
+
+
+
+
+ + {/* Content section */} +
+ {/* Text content skeleton - varied heights for realism */} +
+
+
+ {index % 2 === 0 &&
} +
+
+
+ ))} + + ); +}; + +export default MemoSkeleton; diff --git a/web/src/components/PagedMemoList/PagedMemoList.tsx b/web/src/components/PagedMemoList/PagedMemoList.tsx index bc04fcc3a..5eb716616 100644 --- a/web/src/components/PagedMemoList/PagedMemoList.tsx +++ b/web/src/components/PagedMemoList/PagedMemoList.tsx @@ -7,13 +7,14 @@ import { Button } from "@/components/ui/button"; import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import { Routes } from "@/router"; -import { memoStore, viewStore } from "@/store"; +import { memoStore, userStore, viewStore } from "@/store"; import { State } from "@/types/proto/api/v1/common"; import { Memo } from "@/types/proto/api/v1/memo_service"; import { useTranslate } from "@/utils/i18n"; import Empty from "../Empty"; import MasonryView, { MemoRenderContext } from "../MasonryView"; import MemoEditor from "../MemoEditor"; +import MemoSkeleton from "../MemoSkeleton"; interface Props { renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element; @@ -22,6 +23,7 @@ interface Props { orderBy?: string; filter?: string; pageSize?: number; + showCreator?: boolean; } const PagedMemoList = observer((props: Props) => { @@ -55,6 +57,13 @@ const PagedMemoList = observer((props: Props) => { }); setNextPageToken(response?.nextPageToken || ""); + + // Batch-fetch creators in parallel to avoid individual fetches in MemoView + // This significantly improves perceived performance by pre-populating the cache + if (response?.memos && props.showCreator) { + const uniqueCreators = Array.from(new Set(response.memos.map((memo) => memo.creator))); + await Promise.allSettled(uniqueCreators.map((creator) => userStore.getOrFetchUserByName(creator))); + } } finally { setIsRequesting(false); } @@ -134,33 +143,43 @@ const PagedMemoList = observer((props: Props) => { const children = (
- : undefined} - listMode={viewStore.state.layout === "LIST"} - /> - - {/* Loading indicator */} - {isRequesting && ( -
- -
- )} - - {/* Empty state or back-to-top button */} - {!isRequesting && ( + {/* Show skeleton loader during initial load */} + {isRequesting && sortedMemoList.length === 0 ? ( <> - {!nextPageToken && sortedMemoList.length === 0 ? ( -
- -

{t("message.no-data")}

-
- ) : ( -
- + {showMemoEditor && } + + + ) : ( + <> + : undefined} + listMode={viewStore.state.layout === "LIST"} + /> + + {/* Loading indicator for pagination */} + {isRequesting && ( +
+
)} + + {/* Empty state or back-to-top button */} + {!isRequesting && ( + <> + {!nextPageToken && sortedMemoList.length === 0 ? ( +
+ +

{t("message.no-data")}

+
+ ) : ( +
+ +
+ )} + + )} )}
diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index bcc41f6ec..bf551024b 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -30,6 +30,7 @@ const Explore = observer(() => { ) } orderBy={viewStore.state.orderByTimeAsc ? "display_time asc" : "display_time desc"} + showCreator />
diff --git a/web/src/store/user.ts b/web/src/store/user.ts index 518bd62cd..1e656fbaa 100644 --- a/web/src/store/user.ts +++ b/web/src/store/user.ts @@ -194,8 +194,11 @@ const userStore = (() => { return; } - const { settings } = await userServiceClient.listUserSettings({ parent: state.currentUser }); - const { shortcuts } = await shortcutServiceClient.listShortcuts({ parent: state.currentUser }); + // Fetch settings and shortcuts in parallel for better performance + const [{ settings }, { shortcuts }] = await Promise.all([ + userServiceClient.listUserSettings({ parent: state.currentUser }), + shortcutServiceClient.listShortcuts({ parent: state.currentUser }), + ]); // Extract and store each setting type const generalSetting = settings.find((s) => s.generalSetting)?.generalSetting;