diff --git a/web/src/components/PagedMemoList/PagedMemoList.tsx b/web/src/components/PagedMemoList/PagedMemoList.tsx
index 518635094..f1dc9a336 100644
--- a/web/src/components/PagedMemoList/PagedMemoList.tsx
+++ b/web/src/components/PagedMemoList/PagedMemoList.tsx
@@ -1,5 +1,5 @@
import { useQueryClient } from "@tanstack/react-query";
-import { ArrowUpIcon } from "lucide-react";
+import { ArrowUpIcon, ChevronDownIcon, ChevronRightIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { matchPath } from "react-router-dom";
import { Button } from "@/components/ui/button";
@@ -28,6 +28,7 @@ interface Props {
pageSize?: number;
showCreator?: boolean;
enabled?: boolean;
+ collapsiblePinned?: boolean;
}
function useAutoFetchWhenNotScrollable({
@@ -104,6 +105,29 @@ const PagedMemoList = (props: Props) => {
// Apply custom sorting if provided, otherwise use memos directly
const sortedMemoList = useMemo(() => (props.listSort ? props.listSort(memos) : memos), [memos, props.listSort]);
+ const enablePinnedSection = props.collapsiblePinned === true;
+ const pinnedStorageKey = "memos.ui.pinsCollapsed";
+
+ const [isPinnedCollapsed, setIsPinnedCollapsed] = useState(() => {
+ if (!enablePinnedSection) return false;
+ if (typeof window === "undefined") return false;
+ return window.localStorage.getItem(pinnedStorageKey) === "true";
+ });
+
+ const pinnedMemos = useMemo(() => {
+ if (!enablePinnedSection) return [];
+ return sortedMemoList.filter((memo) => memo.pinned);
+ }, [enablePinnedSection, sortedMemoList]);
+
+ const unpinnedMemos = useMemo(() => {
+ if (!enablePinnedSection) return sortedMemoList;
+ return sortedMemoList.filter((memo) => !memo.pinned);
+ }, [enablePinnedSection, sortedMemoList]);
+
+ useEffect(() => {
+ if (!enablePinnedSection || typeof window === "undefined") return;
+ window.localStorage.setItem(pinnedStorageKey, String(isPinnedCollapsed));
+ }, [enablePinnedSection, isPinnedCollapsed]);
// Prefetch creators when new data arrives to improve performance
useEffect(() => {
@@ -155,19 +179,71 @@ const PagedMemoList = (props: Props) => {
) : (
<>
- {
+ const hasPinned = pinnedMemos.length > 0;
+ const pinnedToggle = enablePinnedSection && hasPinned && (
+
+
+
+
+ );
+
+ const prefixElement = (
<>
{showMemoEditor ? (
) : undefined}
+ {pinnedToggle}
>
+ );
+
+ if (!enablePinnedSection) {
+ return ;
}
- listMode={layout === "LIST"}
- />
+
+ if (layout === "LIST") {
+ const listMemoList = isPinnedCollapsed ? unpinnedMemos : sortedMemoList;
+ const lastPinnedName = !isPinnedCollapsed && hasPinned ? pinnedMemos[pinnedMemos.length - 1]?.name : undefined;
+ const listRenderer = lastPinnedName
+ ? (memo: Memo, context?: MemoRenderContext) => (
+ <>
+ {props.renderer(memo, context)}
+ {memo.name === lastPinnedName && (
+
+ )}
+ >
+ )
+ : props.renderer;
+
+ return ;
+ }
+ return (
+ <>
+
+ {hasPinned && !isPinnedCollapsed && }
+
+
+
+ >
+ );
+ })()}
{/* Loading indicator for pagination */}
{isFetchingNextPage && }
diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx
index 417a0cd10..c055b6dd4 100644
--- a/web/src/pages/Home.tsx
+++ b/web/src/pages/Home.tsx
@@ -28,6 +28,7 @@ const Home = () => {
listSort={listSort}
orderBy={orderBy}
filter={memoFilter}
+ collapsiblePinned
enabled={isInitialized && !!user} // Wait for contexts to stabilize before fetching
/>