mirror of
				https://github.com/usememos/memos.git
				synced 2025-10-31 08:46:39 +08:00 
			
		
		
		
	refactor: view store
This commit is contained in:
		
							parent
							
								
									2e0467e3d1
								
							
						
					
					
						commit
						56ad8ab3bd
					
				
					 10 changed files with 107 additions and 80 deletions
				
			
		|  | @ -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 ( | ||||
|     <Popover> | ||||
|       <PopoverTrigger | ||||
|         className={cn(className, isApplying ? "text-teal-600 bg-teal-50 dark:text-teal-500 dark:bg-teal-900 rounded-sm" : "opacity-40")} | ||||
|         className={cn(className, isApplying ? "text-teal-600 bg-teal-100 dark:text-teal-500 dark:bg-teal-900 rounded" : "opacity-40")} | ||||
|       > | ||||
|         <Settings2Icon className="w-4 h-auto shrink-0" /> | ||||
|       </PopoverTrigger> | ||||
|       <PopoverContent align="end" alignOffset={-12} sideOffset={14}> | ||||
|         <div className="flex flex-col gap-2"> | ||||
|           <div className="w-full flex flex-row justify-between items-center"> | ||||
|             <span className="text-sm shrink-0 mr-3">{t("memo.order-by")}</span> | ||||
|             <Select value="displayTime"> | ||||
|               <Option value={"displayTime"}>{t("memo.display-time")}</Option> | ||||
|             </Select> | ||||
|           </div> | ||||
|           <div className="w-full flex flex-row justify-between items-center"> | ||||
|             <span className="text-sm shrink-0 mr-3">{t("memo.direction")}</span> | ||||
|             <Select value={memoFilterStore.orderByTimeAsc} onChange={(_, value) => memoFilterStore.setOrderByTimeAsc(Boolean(value))}> | ||||
|             <Select | ||||
|               value={viewStore.state.orderByTimeAsc} | ||||
|               onChange={(_, value) => | ||||
|                 viewStore.state.setPartial({ | ||||
|                   orderByTimeAsc: Boolean(value), | ||||
|                 }) | ||||
|               } | ||||
|             > | ||||
|               <Option value={false}>{t("memo.direction-desc")}</Option> | ||||
|               <Option value={true}>{t("memo.direction-asc")}</Option> | ||||
|             </Select> | ||||
|           </div> | ||||
|           <div className="w-full flex flex-row justify-between items-center"> | ||||
|             <span className="text-sm shrink-0 mr-3">{t("memo.masonry-view")}</span> | ||||
|             <Switch checked={memoFilterStore.masonry} onChange={(event) => memoFilterStore.setMasonry(event.target.checked)} /> | ||||
|             <Select | ||||
|               value={viewStore.state.layout} | ||||
|               onChange={(_, value) => | ||||
|                 viewStore.state.setPartial({ | ||||
|                   layout: value as "LIST" | "MASONRY", | ||||
|                 }) | ||||
|               } | ||||
|             > | ||||
|               <Option value={"LIST"}>{"List"}</Option> | ||||
|               <Option value={"MASONRY"}>{"Masonry"}</Option> | ||||
|             </Select> | ||||
|           </div> | ||||
|         </div> | ||||
|       </PopoverContent> | ||||
|  |  | |||
|  | @ -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); | ||||
|       // Sync Store -> URL
 | ||||
|       const newSearchParams = new URLSearchParams(searchParams); | ||||
| 
 | ||||
|         if (orderByTimeAsc) { | ||||
|           newSearchParams.set("orderBy", "asc"); | ||||
|         } else { | ||||
|           newSearchParams.delete("orderBy"); | ||||
|         } | ||||
| 
 | ||||
|         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) { | ||||
|  |  | |||
|  | @ -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<LocalState>({ | ||||
|     isRequesting: true, // Initial request
 | ||||
|     nextPageToken: "", | ||||
|  | @ -77,7 +77,7 @@ const PagedMemoList = observer((props: Props) => { | |||
|         memoList={sortedMemoList} | ||||
|         renderer={props.renderer} | ||||
|         prefixElement={showMemoEditor ? <MemoEditor className="mb-2" cacheKey="home-memo-editor" /> : undefined} | ||||
|         listMode={!memoFilterStore.masonry} | ||||
|         listMode={viewStore.state.layout === "LIST"} | ||||
|       /> | ||||
|       {state.isRequesting && ( | ||||
|         <div className="w-full flex flex-row justify-center items-center my-4"> | ||||
|  | @ -92,7 +92,7 @@ const PagedMemoList = observer((props: Props) => { | |||
|               <p className="mt-2 text-gray-600 dark:text-gray-400">{t("message.no-data")}</p> | ||||
|             </div> | ||||
|           ) : ( | ||||
|             <div className="w-full flex flex-row justify-center items-center my-4"> | ||||
|             <div className="w-full opacity-70 flex flex-row justify-center items-center my-4"> | ||||
|               {state.nextPageToken && ( | ||||
|                 <Button variant="plain" onClick={() => fetchMoreMemos(state.nextPageToken)}> | ||||
|                   {t("memo.load-more")} | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import MemoView from "@/components/MemoView"; | |||
| import PagedMemoList from "@/components/PagedMemoList"; | ||||
| import useCurrentUser from "@/hooks/useCurrentUser"; | ||||
| import { useMemoFilterStore } 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"; | ||||
| 
 | ||||
|  | @ -38,14 +39,14 @@ const Archived = () => { | |||
|         memos | ||||
|           .filter((memo) => memo.state === State.ARCHIVED) | ||||
|           .sort((a, b) => | ||||
|             memoFilterStore.orderByTimeAsc | ||||
|             viewStore.state.orderByTimeAsc | ||||
|               ? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix() | ||||
|               : dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(), | ||||
|           ) | ||||
|       } | ||||
|       owner={user.name} | ||||
|       state={State.ARCHIVED} | ||||
|       direction={memoFilterStore.orderByTimeAsc ? Direction.ASC : Direction.DESC} | ||||
|       direction={viewStore.state.orderByTimeAsc ? Direction.ASC : Direction.DESC} | ||||
|       oldFilter={memoListFilter} | ||||
|     /> | ||||
|   ); | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import MemoView from "@/components/MemoView"; | |||
| import PagedMemoList from "@/components/PagedMemoList"; | ||||
| import useCurrentUser from "@/hooks/useCurrentUser"; | ||||
| import { useMemoFilterStore } 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"; | ||||
| 
 | ||||
|  | @ -41,7 +42,7 @@ const Explore = () => { | |||
|       conditions.push(`tag_search == [${tagSearch.join(", ")}]`); | ||||
|     } | ||||
|     return conditions.join(" && "); | ||||
|   }, [user, memoFilterStore.filters, memoFilterStore.orderByTimeAsc]); | ||||
|   }, [user, memoFilterStore.filters, viewStore.state.orderByTimeAsc]); | ||||
| 
 | ||||
|   return ( | ||||
|     <PagedMemoList | ||||
|  | @ -50,12 +51,12 @@ const Explore = () => { | |||
|         memos | ||||
|           .filter((memo) => memo.state === State.NORMAL) | ||||
|           .sort((a, b) => | ||||
|             memoFilterStore.orderByTimeAsc | ||||
|             viewStore.state.orderByTimeAsc | ||||
|               ? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix() | ||||
|               : dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(), | ||||
|           ) | ||||
|       } | ||||
|       direction={memoFilterStore.orderByTimeAsc ? Direction.ASC : Direction.DESC} | ||||
|       direction={viewStore.state.orderByTimeAsc ? Direction.ASC : Direction.DESC} | ||||
|       oldFilter={memoListFilter} | ||||
|     /> | ||||
|   ); | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import MemoView from "@/components/MemoView"; | |||
| import PagedMemoList from "@/components/PagedMemoList"; | ||||
| import useCurrentUser from "@/hooks/useCurrentUser"; | ||||
| import { useMemoFilterStore } from "@/store/v1"; | ||||
| import { userStore } from "@/store/v2"; | ||||
| import { viewStore, userStore } from "@/store/v2"; | ||||
| import { Direction, State } from "@/types/proto/api/v1/common"; | ||||
| import { Memo } from "@/types/proto/api/v1/memo_service"; | ||||
| 
 | ||||
|  | @ -44,7 +44,7 @@ const Home = observer(() => { | |||
|       conditions.push(`tag_search == [${tagSearch.join(", ")}]`); | ||||
|     } | ||||
|     return conditions.join(" && "); | ||||
|   }, [user, memoFilterStore.filters, memoFilterStore.orderByTimeAsc]); | ||||
|   }, [user, memoFilterStore.filters, viewStore.state.orderByTimeAsc]); | ||||
| 
 | ||||
|   return ( | ||||
|     <PagedMemoList | ||||
|  | @ -53,14 +53,14 @@ const Home = observer(() => { | |||
|         memos | ||||
|           .filter((memo) => memo.state === State.NORMAL) | ||||
|           .sort((a, b) => | ||||
|             memoFilterStore.orderByTimeAsc | ||||
|             viewStore.state.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)) | ||||
|       } | ||||
|       owner={user.name} | ||||
|       direction={memoFilterStore.orderByTimeAsc ? Direction.ASC : Direction.DESC} | ||||
|       direction={viewStore.state.orderByTimeAsc ? Direction.ASC : Direction.DESC} | ||||
|       filter={selectedShortcut?.filter || ""} | ||||
|       oldFilter={memoListFilter} | ||||
|     /> | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { Button } from "@usememos/mui"; | |||
| import copy from "copy-to-clipboard"; | ||||
| import dayjs from "dayjs"; | ||||
| import { ExternalLinkIcon } from "lucide-react"; | ||||
| import { observer } from "mobx-react-lite"; | ||||
| import { useEffect, useMemo, useState } from "react"; | ||||
| import { toast } from "react-hot-toast"; | ||||
| import { useParams } from "react-router-dom"; | ||||
|  | @ -11,13 +12,13 @@ import PagedMemoList from "@/components/PagedMemoList"; | |||
| import UserAvatar from "@/components/UserAvatar"; | ||||
| import useLoading from "@/hooks/useLoading"; | ||||
| import { useMemoFilterStore } from "@/store/v1"; | ||||
| import { userStore } from "@/store/v2"; | ||||
| import { viewStore, userStore } from "@/store/v2"; | ||||
| import { Direction, State } 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"; | ||||
| 
 | ||||
| const UserProfile = () => { | ||||
| const UserProfile = observer(() => { | ||||
|   const t = useTranslate(); | ||||
|   const params = useParams(); | ||||
|   const loadingState = useLoading(); | ||||
|  | @ -107,14 +108,14 @@ const UserProfile = () => { | |||
|                   memos | ||||
|                     .filter((memo) => memo.state === State.NORMAL) | ||||
|                     .sort((a, b) => | ||||
|                       memoFilterStore.orderByTimeAsc | ||||
|                       viewStore.state.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)) | ||||
|                 } | ||||
|                 owner={user.name} | ||||
|                 direction={memoFilterStore.orderByTimeAsc ? Direction.ASC : Direction.DESC} | ||||
|                 direction={viewStore.state.orderByTimeAsc ? Direction.ASC : Direction.DESC} | ||||
|                 oldFilter={memoListFilter} | ||||
|               /> | ||||
|             </> | ||||
|  | @ -124,6 +125,6 @@ const UserProfile = () => { | |||
|       </div> | ||||
|     </section> | ||||
|   ); | ||||
| }; | ||||
| }); | ||||
| 
 | ||||
| export default UserProfile; | ||||
|  |  | |||
|  | @ -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 }), | ||||
|   })), | ||||
| ); | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| import userStore from "./user"; | ||||
| import viewStore from "./view"; | ||||
| import workspaceStore from "./workspace"; | ||||
| 
 | ||||
| export { workspaceStore, userStore }; | ||||
| export { workspaceStore, userStore, viewStore }; | ||||
|  |  | |||
							
								
								
									
										49
									
								
								web/src/store/v2/view.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								web/src/store/v2/view.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<LocalState>) { | ||||
|     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; | ||||
		Loading…
	
	Add table
		
		Reference in a new issue