diff --git a/web/src/components/MemoDisplaySettingMenu.tsx b/web/src/components/MemoDisplaySettingMenu.tsx index cb4e1e04..9327475d 100644 --- a/web/src/components/MemoDisplaySettingMenu.tsx +++ b/web/src/components/MemoDisplaySettingMenu.tsx @@ -1,7 +1,7 @@ -import { Option, Select, Switch } from "@mui/joy"; +import { Option, Select } from "@mui/joy"; import { Settings2Icon } from "lucide-react"; import { observer } from "mobx-react-lite"; -import { useMemoFilterStore } from "@/store/v1"; +import { viewStore } from "@/store/v2"; import { cn } from "@/utils"; import { useTranslate } from "@/utils/i18n"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/Popover"; @@ -12,34 +12,44 @@ interface Props { const MemoDisplaySettingMenu = observer(({ className }: Props) => { const t = useTranslate(); - const memoFilterStore = useMemoFilterStore(); - const isApplying = Boolean(memoFilterStore.orderByTimeAsc) !== false || memoFilterStore.masonry; + const isApplying = viewStore.state.orderByTimeAsc !== false || viewStore.state.layout !== "LIST"; return (
-
- {t("memo.order-by")} - -
{t("memo.direction")} - + viewStore.state.setPartial({ + orderByTimeAsc: Boolean(value), + }) + } + >
{t("memo.masonry-view")} - memoFilterStore.setMasonry(event.target.checked)} /> +
diff --git a/web/src/components/MemoFilters.tsx b/web/src/components/MemoFilters.tsx index 681fba03..8f90b7a8 100644 --- a/web/src/components/MemoFilters.tsx +++ b/web/src/components/MemoFilters.tsx @@ -1,62 +1,33 @@ import { isEqual } from "lodash-es"; import { CalendarIcon, CheckCircleIcon, CodeIcon, EyeIcon, HashIcon, LinkIcon, SearchIcon, XIcon } from "lucide-react"; -import { useEffect, useRef } from "react"; +import { useEffect } from "react"; import { useSearchParams } from "react-router-dom"; -import { FilterFactor, getMemoFilterKey, MemoFilter, parseFilterQuery, stringifyFilters, useMemoFilterStore } from "@/store/v1"; +import { FilterFactor, getMemoFilterKey, MemoFilter, stringifyFilters, useMemoFilterStore } from "@/store/v1"; const MemoFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); const memoFilterStore = useMemoFilterStore(); const filters = memoFilterStore.filters; - const orderByTimeAsc = memoFilterStore.orderByTimeAsc; - const lastUpdateRef = useRef<"url" | "store">("url"); - - // set lastUpdateRef to store when filters or orderByTimeAsc changes - useEffect(() => { - lastUpdateRef.current = "store"; - }, [filters, orderByTimeAsc]); - - // set lastUpdateRef to url when searchParams changes - useEffect(() => { - lastUpdateRef.current = "url"; - }, [searchParams]); const checkAndSync = () => { const filtersInURL = searchParams.get("filter") || ""; - const orderByTimeAscInURL = searchParams.get("orderBy") === "asc"; - const storeMatchesURL = filtersInURL === stringifyFilters(filters) && orderByTimeAscInURL === orderByTimeAsc; + const storeMatchesURL = filtersInURL === stringifyFilters(filters); if (!storeMatchesURL) { - if (lastUpdateRef.current === "url") { - // Sync URL -> Store - memoFilterStore.setState({ - filters: parseFilterQuery(filtersInURL), - orderByTimeAsc: orderByTimeAscInURL, - masonry: memoFilterStore.masonry, - }); - } else if (lastUpdateRef.current === "store") { - // Sync Store -> URL - const newSearchParams = new URLSearchParams(searchParams); - - if (orderByTimeAsc) { - newSearchParams.set("orderBy", "asc"); - } else { - newSearchParams.delete("orderBy"); - } + // Sync Store -> URL + const newSearchParams = new URLSearchParams(searchParams); - if (filters.length > 0) { - newSearchParams.set("filter", stringifyFilters(filters)); - } else { - newSearchParams.delete("filter"); - } - - setSearchParams(newSearchParams); + if (filters.length > 0) { + newSearchParams.set("filter", stringifyFilters(filters)); + } else { + newSearchParams.delete("filter"); } + + setSearchParams(newSearchParams); } }; - // Watch both URL and store changes - useEffect(checkAndSync, [searchParams, filters, orderByTimeAsc]); + useEffect(checkAndSync, [searchParams, filters]); const getFilterDisplayText = (filter: MemoFilter) => { if (filter.value) { diff --git a/web/src/components/PagedMemoList/PagedMemoList.tsx b/web/src/components/PagedMemoList/PagedMemoList.tsx index f079728f..2902d937 100644 --- a/web/src/components/PagedMemoList/PagedMemoList.tsx +++ b/web/src/components/PagedMemoList/PagedMemoList.tsx @@ -7,7 +7,8 @@ import PullToRefresh from "react-simple-pull-to-refresh"; import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import { Routes } from "@/router"; -import { useMemoFilterStore, useMemoList, useMemoStore } from "@/store/v1"; +import { useMemoList, useMemoStore } from "@/store/v1"; +import { viewStore } from "@/store/v2"; import { Direction, State } from "@/types/proto/api/v1/common"; import { Memo } from "@/types/proto/api/v1/memo_service"; import { useTranslate } from "@/utils/i18n"; @@ -36,7 +37,6 @@ const PagedMemoList = observer((props: Props) => { const { md } = useResponsiveWidth(); const memoStore = useMemoStore(); const memoList = useMemoList(); - const memoFilterStore = useMemoFilterStore(); const [state, setState] = useState({ isRequesting: true, // Initial request nextPageToken: "", @@ -77,7 +77,7 @@ const PagedMemoList = observer((props: Props) => { memoList={sortedMemoList} renderer={props.renderer} prefixElement={showMemoEditor ? : undefined} - listMode={!memoFilterStore.masonry} + listMode={viewStore.state.layout === "LIST"} /> {state.isRequesting && (
@@ -92,7 +92,7 @@ const PagedMemoList = observer((props: Props) => {

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

) : ( -
+
{state.nextPageToken && (
); -}; +}); export default UserProfile; diff --git a/web/src/store/v1/memoFilter.ts b/web/src/store/v1/memoFilter.ts index 6c904f43..644c8062 100644 --- a/web/src/store/v1/memoFilter.ts +++ b/web/src/store/v1/memoFilter.ts @@ -40,19 +40,14 @@ export const stringifyFilters = (filters: MemoFilter[]): string => { interface State { filters: MemoFilter[]; - orderByTimeAsc: boolean; // The id of selected shortcut. shortcut?: string; - // TODO: Remove this when the masonry view is implemented. - masonry: boolean; } const getInitialState = (): State => { const searchParams = new URLSearchParams(window.location.search); return { filters: parseFilterQuery(searchParams.get("filter")), - orderByTimeAsc: searchParams.get("orderBy") === "asc", - masonry: false, }; }; @@ -63,8 +58,6 @@ export const useMemoFilterStore = create( getFiltersByFactor: (factor: FilterFactor) => get().filters.filter((f) => f.factor === factor), addFilter: (filter: MemoFilter) => set((state) => ({ filters: uniqBy([...state.filters, filter], getMemoFilterKey) })), removeFilter: (filterFn: (f: MemoFilter) => boolean) => set((state) => ({ filters: state.filters.filter((f) => !filterFn(f)) })), - setOrderByTimeAsc: (orderByTimeAsc: boolean) => set({ orderByTimeAsc }), setShortcut: (shortcut?: string) => set({ shortcut }), - setMasonry: (masonry: boolean) => set({ masonry }), })), ); diff --git a/web/src/store/v2/index.ts b/web/src/store/v2/index.ts index 67fd7ede..94307e44 100644 --- a/web/src/store/v2/index.ts +++ b/web/src/store/v2/index.ts @@ -1,4 +1,5 @@ import userStore from "./user"; +import viewStore from "./view"; import workspaceStore from "./workspace"; -export { workspaceStore, userStore }; +export { workspaceStore, userStore, viewStore }; diff --git a/web/src/store/v2/view.ts b/web/src/store/v2/view.ts new file mode 100644 index 00000000..a8535ad1 --- /dev/null +++ b/web/src/store/v2/view.ts @@ -0,0 +1,49 @@ +import { makeAutoObservable } from "mobx"; + +const LOCAL_STORAGE_KEY = "memos-view-setting"; + +class LocalState { + orderByTimeAsc: boolean = false; + layout: "LIST" | "MASONRY" = "LIST"; + + constructor() { + makeAutoObservable(this); + } + + setPartial(partial: Partial) { + Object.assign(this, partial); + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this)); + } +} + +const viewStore = (() => { + const state = new LocalState(); + + return { + state, + }; +})(); + +// Initial state from localStorage. +(async () => { + const localCache = localStorage.getItem(LOCAL_STORAGE_KEY); + if (!localCache) { + return; + } + + try { + const cache = JSON.parse(localCache); + if (Object.hasOwn(cache, "orderByTimeAsc")) { + viewStore.state.setPartial({ orderByTimeAsc: Boolean(cache.orderByTimeAsc) }); + } + if (Object.hasOwn(cache, "layout")) { + if (["LIST", "MASONRY"].includes(cache.layout)) { + viewStore.state.setPartial({ layout: cache.layout }); + } + } + } catch { + // Do nothing + } +})(); + +export default viewStore;