diff --git a/web/src/components/MasonryView/MasonryView.tsx b/web/src/components/MasonryView/MasonryView.tsx new file mode 100644 index 00000000..df5a382d --- /dev/null +++ b/web/src/components/MasonryView/MasonryView.tsx @@ -0,0 +1,66 @@ +import { useEffect, useRef, useState } from "react"; +import { Memo } from "@/types/proto/api/v1/memo_service"; +import { cn } from "@/utils"; + +interface Props { + memoList: Memo[]; + renderer: (memo: Memo) => JSX.Element; + prefixElement?: JSX.Element; + listMode?: boolean; +} + +interface LocalState { + columns: number; +} + +const MINIMUM_MEMO_VIEWPORT_WIDTH = 512; + +const MasonryView = (props: Props) => { + const [state, setState] = useState({ + columns: 1, + }); + const containerRef = useRef(null); + + useEffect(() => { + const handleResize = () => { + if (!containerRef.current) { + return; + } + if (props.listMode) { + setState({ + columns: 1, + }); + return; + } + + const containerWidth = containerRef.current.offsetWidth; + const scale = containerWidth / MINIMUM_MEMO_VIEWPORT_WIDTH; + setState({ + columns: scale > 2 ? Math.floor(scale) : 1, + }); + }; + + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [props.listMode]); + + return ( +
+ {Array.from({ length: state.columns }).map((_, columnIndex) => ( +
+ {props.prefixElement && columnIndex === 0 &&
{props.prefixElement}
} + {props.memoList.filter((_, index) => index % state.columns === columnIndex).map((memo) => props.renderer(memo))} +
+ ))} +
+ ); +}; + +export default MasonryView; diff --git a/web/src/components/MasonryView/index.ts b/web/src/components/MasonryView/index.ts new file mode 100644 index 00000000..11425016 --- /dev/null +++ b/web/src/components/MasonryView/index.ts @@ -0,0 +1,3 @@ +import MasonryView from "./MasonryView"; + +export default MasonryView; diff --git a/web/src/components/MemoDisplaySettingMenu.tsx b/web/src/components/MemoDisplaySettingMenu.tsx index e437e571..ee855d99 100644 --- a/web/src/components/MemoDisplaySettingMenu.tsx +++ b/web/src/components/MemoDisplaySettingMenu.tsx @@ -1,5 +1,6 @@ -import { Option, Select } from "@mui/joy"; +import { Option, Select, Switch } from "@mui/joy"; import { Settings2Icon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { useMemoFilterStore } from "@/store/v1"; import { cn } from "@/utils"; import { useTranslate } from "@/utils/i18n"; @@ -9,10 +10,10 @@ interface Props { className?: string; } -const MemoDisplaySettingMenu = ({ className }: Props) => { +const MemoDisplaySettingMenu = observer(({ className }: Props) => { const t = useTranslate(); const memoFilterStore = useMemoFilterStore(); - const isApplying = Boolean(memoFilterStore.orderByTimeAsc) !== false; + const isApplying = Boolean(memoFilterStore.orderByTimeAsc) !== false || memoFilterStore.masonry; return ( @@ -36,10 +37,14 @@ const MemoDisplaySettingMenu = ({ className }: Props) => { +
+ Masonry View + memoFilterStore.setMasonry(event.target.checked)} /> +
); -}; +}); export default MemoDisplaySettingMenu; diff --git a/web/src/components/PagedMemoList/PagedMemoList.tsx b/web/src/components/PagedMemoList/PagedMemoList.tsx index 517f284e..f079728f 100644 --- a/web/src/components/PagedMemoList/PagedMemoList.tsx +++ b/web/src/components/PagedMemoList/PagedMemoList.tsx @@ -1,14 +1,19 @@ import { Button } from "@usememos/mui"; -import { ArrowDownIcon, ArrowUpIcon, LoaderIcon, SlashIcon } from "lucide-react"; +import { ArrowDownIcon, ArrowUpIcon, LoaderIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; import { useEffect, useState } from "react"; +import { matchPath } from "react-router-dom"; import PullToRefresh from "react-simple-pull-to-refresh"; import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; import useResponsiveWidth from "@/hooks/useResponsiveWidth"; -import { useMemoList, useMemoStore } from "@/store/v1"; +import { Routes } from "@/router"; +import { useMemoFilterStore, useMemoList, useMemoStore } from "@/store/v1"; import { Direction, 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 from "../MasonryView"; +import MemoEditor from "../MemoEditor"; interface Props { renderer: (memo: Memo) => JSX.Element; @@ -26,16 +31,18 @@ interface LocalState { nextPageToken: string; } -const PagedMemoList = (props: Props) => { +const PagedMemoList = observer((props: Props) => { const t = useTranslate(); const { md } = useResponsiveWidth(); const memoStore = useMemoStore(); const memoList = useMemoList(); + const memoFilterStore = useMemoFilterStore(); const [state, setState] = useState({ isRequesting: true, // Initial request nextPageToken: "", }); const sortedMemoList = props.listSort ? props.listSort(memoList.value) : memoList.value; + const showMemoEditor = Boolean(matchPath(Routes.ROOT, window.location.pathname)); const fetchMoreMemos = async (nextPageToken: string) => { setState((state) => ({ ...state, isRequesting: true })); @@ -66,7 +73,12 @@ const PagedMemoList = (props: Props) => { const children = (
- {sortedMemoList.map((memo) => props.renderer(memo))} + : undefined} + listMode={!memoFilterStore.masonry} + /> {state.isRequesting && (
@@ -82,13 +94,10 @@ const PagedMemoList = (props: Props) => { ) : (
{state.nextPageToken && ( - <> - - - + )}
@@ -120,7 +129,7 @@ const PagedMemoList = (props: Props) => { {children} ); -}; +}); const BackToTop = () => { const t = useTranslate(); diff --git a/web/src/components/SearchBar.tsx b/web/src/components/SearchBar.tsx index 504f8bb0..10e589fb 100644 --- a/web/src/components/SearchBar.tsx +++ b/web/src/components/SearchBar.tsx @@ -39,7 +39,7 @@ const SearchBar = () => { onChange={onTextChange} onKeyDown={onKeyDown} /> - +
); }; diff --git a/web/src/layouts/HomeLayout.tsx b/web/src/layouts/HomeLayout.tsx index 423082fc..f9931f26 100644 --- a/web/src/layouts/HomeLayout.tsx +++ b/web/src/layouts/HomeLayout.tsx @@ -27,7 +27,7 @@ const HomeLayout = observer(() => {
)}
-
+
diff --git a/web/src/pages/Archived.tsx b/web/src/pages/Archived.tsx index 5aa55b56..cf184a31 100644 --- a/web/src/pages/Archived.tsx +++ b/web/src/pages/Archived.tsx @@ -1,19 +1,13 @@ import dayjs from "dayjs"; -import { ArchiveIcon } from "lucide-react"; import { useMemo } from "react"; -import MemoFilters from "@/components/MemoFilters"; import MemoView from "@/components/MemoView"; -import MobileHeader from "@/components/MobileHeader"; import PagedMemoList from "@/components/PagedMemoList"; -import SearchBar from "@/components/SearchBar"; import useCurrentUser from "@/hooks/useCurrentUser"; import { useMemoFilterStore } from "@/store/v1"; import { Direction, State } from "@/types/proto/api/v1/common"; import { Memo } from "@/types/proto/api/v1/memo_service"; -import { useTranslate } from "@/utils/i18n"; const Archived = () => { - const t = useTranslate(); const user = useCurrentUser(); const memoFilterStore = useMemoFilterStore(); @@ -38,39 +32,22 @@ const Archived = () => { }, [user, memoFilterStore.filters]); return ( -
- -
-
-
-
- - {t("common.archived")} -
-
- -
-
- - } - listSort={(memos: Memo[]) => - memos - .filter((memo) => memo.state === State.ARCHIVED) - .sort((a, b) => - memoFilterStore.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} - oldFilter={memoListFilter} - /> -
-
-
+ } + listSort={(memos: Memo[]) => + memos + .filter((memo) => memo.state === State.ARCHIVED) + .sort((a, b) => + memoFilterStore.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} + oldFilter={memoListFilter} + /> ); }; diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index d2344dfc..4f4d6766 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -44,24 +44,20 @@ const Explore = () => { }, [user, memoFilterStore.filters, memoFilterStore.orderByTimeAsc]); return ( - <> -
- } - listSort={(memos: Memo[]) => - memos - .filter((memo) => memo.state === State.NORMAL) - .sort((a, b) => - memoFilterStore.orderByTimeAsc - ? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix() - : dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(), - ) - } - direction={memoFilterStore.orderByTimeAsc ? Direction.ASC : Direction.DESC} - oldFilter={memoListFilter} - /> -
- + } + listSort={(memos: Memo[]) => + memos + .filter((memo) => memo.state === State.NORMAL) + .sort((a, b) => + memoFilterStore.orderByTimeAsc + ? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix() + : dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(), + ) + } + direction={memoFilterStore.orderByTimeAsc ? Direction.ASC : Direction.DESC} + oldFilter={memoListFilter} + /> ); }; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 41f9bdd9..f2032e2b 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,7 +1,6 @@ import dayjs from "dayjs"; import { observer } from "mobx-react-lite"; import { useMemo } from "react"; -import MemoEditor from "@/components/MemoEditor"; import MemoView from "@/components/MemoView"; import PagedMemoList from "@/components/PagedMemoList"; import useCurrentUser from "@/hooks/useCurrentUser"; @@ -48,28 +47,23 @@ const Home = observer(() => { }, [user, memoFilterStore.filters, memoFilterStore.orderByTimeAsc]); return ( - <> - -
- } - listSort={(memos: Memo[]) => - memos - .filter((memo) => memo.state === State.NORMAL) - .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)) - } - owner={user.name} - direction={memoFilterStore.orderByTimeAsc ? Direction.ASC : Direction.DESC} - filter={selectedShortcut?.filter || ""} - oldFilter={memoListFilter} - /> -
- + } + listSort={(memos: Memo[]) => + memos + .filter((memo) => memo.state === State.NORMAL) + .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)) + } + owner={user.name} + direction={memoFilterStore.orderByTimeAsc ? Direction.ASC : Direction.DESC} + filter={selectedShortcut?.filter || ""} + oldFilter={memoListFilter} + /> ); }); diff --git a/web/src/pages/UserProfile.tsx b/web/src/pages/UserProfile.tsx index 15db930c..c3d3fb07 100644 --- a/web/src/pages/UserProfile.tsx +++ b/web/src/pages/UserProfile.tsx @@ -76,7 +76,7 @@ const UserProfile = () => { }; return ( -
+
{!loadingState.isLoading && (user ? ( diff --git a/web/src/store/v1/memoFilter.ts b/web/src/store/v1/memoFilter.ts index d13ef33c..6c904f43 100644 --- a/web/src/store/v1/memoFilter.ts +++ b/web/src/store/v1/memoFilter.ts @@ -43,6 +43,8 @@ interface State { orderByTimeAsc: boolean; // The id of selected shortcut. shortcut?: string; + // TODO: Remove this when the masonry view is implemented. + masonry: boolean; } const getInitialState = (): State => { @@ -50,6 +52,7 @@ const getInitialState = (): State => { return { filters: parseFilterQuery(searchParams.get("filter")), orderByTimeAsc: searchParams.get("orderBy") === "asc", + masonry: false, }; }; @@ -62,5 +65,6 @@ export const useMemoFilterStore = create( 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 }), })), );