diff --git a/web/package.json b/web/package.json index 5fc12473f..0b7aef756 100644 --- a/web/package.json +++ b/web/package.json @@ -55,7 +55,6 @@ "react-leaflet": "^4.2.1", "react-markdown": "^10.1.0", "react-router-dom": "^7.9.6", - "react-simple-pull-to-refresh": "^1.3.3", "react-use": "^17.6.0", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", diff --git a/web/src/components/MobileHeader.tsx b/web/src/components/MobileHeader.tsx index 6cbd7b9c5..8b5923a2e 100644 --- a/web/src/components/MobileHeader.tsx +++ b/web/src/components/MobileHeader.tsx @@ -1,5 +1,5 @@ import useWindowScroll from "react-use/lib/useWindowScroll"; -import useResponsiveWidth from "@/hooks/useResponsiveWidth"; +import useMediaQuery from "@/hooks/useMediaQuery"; import { cn } from "@/lib/utils"; import NavigationDrawer from "./NavigationDrawer"; @@ -10,19 +10,22 @@ interface Props { const MobileHeader = (props: Props) => { const { className, children } = props; - const { sm } = useResponsiveWidth(); const { y: offsetTop } = useWindowScroll(); + const md = useMediaQuery("md"); + const sm = useMediaQuery("sm"); + + if (md) return null; return (
0 && "shadow-md", className, )} > -
{!sm && }
-
{children}
+ {!sm && } +
{children}
); }; diff --git a/web/src/components/PagedMemoList/PagedMemoList.tsx b/web/src/components/PagedMemoList/PagedMemoList.tsx index 1c81588cc..a102ae42d 100644 --- a/web/src/components/PagedMemoList/PagedMemoList.tsx +++ b/web/src/components/PagedMemoList/PagedMemoList.tsx @@ -1,13 +1,11 @@ import { ArrowUpIcon, LoaderIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { matchPath } from "react-router-dom"; -import PullToRefresh from "react-simple-pull-to-refresh"; import { Button } from "@/components/ui/button"; import { userServiceClient } from "@/connect"; import { useView } from "@/contexts/ViewContext"; import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; import { useInfiniteMemos } from "@/hooks/useMemoQueries"; -import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import { Routes } from "@/router"; import { State } from "@/types/proto/api/v1/common_pb"; import type { Memo } from "@/types/proto/api/v1/memo_service_pb"; @@ -82,14 +80,13 @@ function useAutoFetchWhenNotScrollable({ const PagedMemoList = (props: Props) => { const t = useTranslate(); - const { md } = useResponsiveWidth(); const { layout } = useView(); // Show memo editor only on the root route const showMemoEditor = Boolean(matchPath(Routes.ROOT, window.location.pathname)); // Use React Query's infinite query for pagination - const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, refetch } = useInfiniteMemos({ + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteMemos({ state: props.state || State.NORMAL, orderBy: props.orderBy || "display_time desc", filter: props.filter, @@ -192,29 +189,7 @@ const PagedMemoList = (props: Props) => { ); - if (md) { - return children; - } - - return ( - { - await refetch(); - }} - pullingContent={ -
- -
- } - refreshingContent={ -
- -
- } - > - {children} -
- ); + return children; }; const BackToTop = () => { diff --git a/web/src/hooks/index.ts b/web/src/hooks/index.ts index 4856dc193..c0551077e 100644 --- a/web/src/hooks/index.ts +++ b/web/src/hooks/index.ts @@ -3,9 +3,9 @@ export * from "./useCurrentUser"; export * from "./useDateFilterNavigation"; export * from "./useFilteredMemoStats"; export * from "./useLoading"; +export * from "./useMediaQuery"; export * from "./useMemoFilters"; export * from "./useMemoSorting"; export * from "./useNavigateTo"; -export * from "./useResponsiveWidth"; export * from "./useUserLocale"; export * from "./useUserTheme"; diff --git a/web/src/hooks/useMediaQuery.ts b/web/src/hooks/useMediaQuery.ts new file mode 100644 index 000000000..af8df11a1 --- /dev/null +++ b/web/src/hooks/useMediaQuery.ts @@ -0,0 +1,35 @@ +import { useEffect, useState } from "react"; + +type Breakpoint = "sm" | "md" | "lg"; + +const BREAKPOINTS: Record = { + sm: 640, + md: 768, + lg: 1024, +}; + +const useMediaQuery = (breakpoint: Breakpoint): boolean => { + const [matches, setMatches] = useState(() => { + if (typeof window === "undefined") return false; + return window.matchMedia(`(min-width: ${BREAKPOINTS[breakpoint]}px)`).matches; + }); + + useEffect(() => { + const mediaQuery = window.matchMedia(`(min-width: ${BREAKPOINTS[breakpoint]}px)`); + + const handleChange = (e: MediaQueryListEvent) => { + setMatches(e.matches); + }; + + mediaQuery.addEventListener("change", handleChange); + + return () => { + mediaQuery.removeEventListener("change", handleChange); + }; + }, [breakpoint]); + + return matches; +}; + +export default useMediaQuery; + diff --git a/web/src/hooks/useResponsiveWidth.ts b/web/src/hooks/useResponsiveWidth.ts deleted file mode 100644 index e73804bf6..000000000 --- a/web/src/hooks/useResponsiveWidth.ts +++ /dev/null @@ -1,18 +0,0 @@ -import useWindowSize from "react-use/lib/useWindowSize"; - -enum TailwindResponsiveWidth { - sm = 640, - md = 768, - lg = 1024, -} - -const useResponsiveWidth = () => { - const { width } = useWindowSize(); - return { - sm: width >= TailwindResponsiveWidth.sm, - md: width >= TailwindResponsiveWidth.md, - lg: width >= TailwindResponsiveWidth.lg, - }; -}; - -export default useResponsiveWidth; diff --git a/web/src/layouts/MainLayout.tsx b/web/src/layouts/MainLayout.tsx index 51a8d9073..55570641e 100644 --- a/web/src/layouts/MainLayout.tsx +++ b/web/src/layouts/MainLayout.tsx @@ -6,12 +6,13 @@ import MobileHeader from "@/components/MobileHeader"; import { userServiceClient } from "@/connect"; import useCurrentUser from "@/hooks/useCurrentUser"; import { useFilteredMemoStats } from "@/hooks/useFilteredMemoStats"; -import useResponsiveWidth from "@/hooks/useResponsiveWidth"; +import useMediaQuery from "@/hooks/useMediaQuery"; import { cn } from "@/lib/utils"; import { Routes } from "@/router"; const MainLayout = () => { - const { md, lg } = useResponsiveWidth(); + const md = useMediaQuery("md"); + const lg = useMediaQuery("lg"); const location = useLocation(); const currentUser = useCurrentUser(); const [profileUserName, setProfileUserName] = useState(); diff --git a/web/src/layouts/RootLayout.tsx b/web/src/layouts/RootLayout.tsx index 6c5d1008a..6568172e7 100644 --- a/web/src/layouts/RootLayout.tsx +++ b/web/src/layouts/RootLayout.tsx @@ -5,7 +5,7 @@ import Navigation from "@/components/Navigation"; import { useInstance } from "@/contexts/InstanceContext"; import { useMemoFilterContext } from "@/contexts/MemoFilterContext"; import useCurrentUser from "@/hooks/useCurrentUser"; -import useResponsiveWidth from "@/hooks/useResponsiveWidth"; +import useMediaQuery from "@/hooks/useMediaQuery"; import { cn } from "@/lib/utils"; import Loading from "@/pages/Loading"; import { Routes } from "@/router"; @@ -13,7 +13,7 @@ import { Routes } from "@/router"; const RootLayout = () => { const location = useLocation(); const [searchParams] = useSearchParams(); - const { sm } = useResponsiveWidth(); + const sm = useMediaQuery("sm"); const currentUser = useCurrentUser(); const { memoRelatedSetting } = useInstance(); const { removeFilter } = useMemoFilterContext(); diff --git a/web/src/pages/Attachments.tsx b/web/src/pages/Attachments.tsx index fd12df78b..8e26c8d5c 100644 --- a/web/src/pages/Attachments.tsx +++ b/web/src/pages/Attachments.tsx @@ -15,7 +15,7 @@ import { attachmentServiceClient } from "@/connect"; import { useDeleteAttachment } from "@/hooks/useAttachmentQueries"; import useDialog from "@/hooks/useDialog"; import useLoading from "@/hooks/useLoading"; -import useResponsiveWidth from "@/hooks/useResponsiveWidth"; +import useMediaQuery from "@/hooks/useMediaQuery"; import i18n from "@/i18n"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import { useTranslate } from "@/utils/i18n"; @@ -69,7 +69,7 @@ const AttachmentItem = ({ attachment }: AttachmentItemProps) => ( const Attachments = () => { const t = useTranslate(); - const { md } = useResponsiveWidth(); + const md = useMediaQuery("md"); const loadingState = useLoading(); const deleteUnusedAttachmentsDialog = useDialog(); const { mutateAsync: deleteAttachment } = useDeleteAttachment(); diff --git a/web/src/pages/Inboxes.tsx b/web/src/pages/Inboxes.tsx index 79ea57d2d..1e0c0a97f 100644 --- a/web/src/pages/Inboxes.tsx +++ b/web/src/pages/Inboxes.tsx @@ -5,7 +5,7 @@ import { useState } from "react"; import Empty from "@/components/Empty"; import MemoCommentMessage from "@/components/Inbox/MemoCommentMessage"; import MobileHeader from "@/components/MobileHeader"; -import useResponsiveWidth from "@/hooks/useResponsiveWidth"; +import useMediaQuery from "@/hooks/useMediaQuery"; import { useNotifications } from "@/hooks/useUserQueries"; import { cn } from "@/lib/utils"; import { UserNotification, UserNotification_Status, UserNotification_Type } from "@/types/proto/api/v1/user_service_pb"; @@ -13,7 +13,7 @@ import { useTranslate } from "@/utils/i18n"; const Inboxes = () => { const t = useTranslate(); - const { md } = useResponsiveWidth(); + const md = useMediaQuery("md"); const [filter, setFilter] = useState<"all" | "unread" | "archived">("all"); // Fetch notifications with React Query diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index acc724e20..37e0cc4ef 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -10,15 +10,15 @@ import MobileHeader from "@/components/MobileHeader"; import { Button } from "@/components/ui/button"; import { memoNamePrefix } from "@/helpers/resource-names"; import useCurrentUser from "@/hooks/useCurrentUser"; +import useMediaQuery from "@/hooks/useMediaQuery"; import { useMemo, useMemoComments } from "@/hooks/useMemoQueries"; import useNavigateTo from "@/hooks/useNavigateTo"; -import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import { cn } from "@/lib/utils"; import { useTranslate } from "@/utils/i18n"; const MemoDetail = () => { const t = useTranslate(); - const { md } = useResponsiveWidth(); + const md = useMediaQuery("md"); const params = useParams(); const navigateTo = useNavigateTo(); const { state: locationState } = useLocation(); @@ -70,7 +70,7 @@ const MemoDetail = () => { )}
-
+
{parentMemo && (
= { const Setting = () => { const t = useTranslate(); - const { md } = useResponsiveWidth(); + const sm = useMediaQuery("sm"); const location = useLocation(); const user = useCurrentUser(); const { profile, fetchSetting } = useInstance(); @@ -85,57 +85,61 @@ const Setting = () => { return (
- {!md && } + {!sm && }
-
- {t("common.basic")} -
- {BASIC_SECTIONS.map((item) => ( - handleSectionSelectorItemClick(item)} - /> - ))} + {sm && ( +
+ {t("common.basic")} +
+ {BASIC_SECTIONS.map((item) => ( + handleSectionSelectorItemClick(item)} + /> + ))} +
+ {isHost ? ( + <> + {t("common.admin")} +
+ {ADMIN_SECTIONS.map((item) => ( + handleSectionSelectorItemClick(item)} + /> + ))} + + {t("setting.version")}: v{profile.version} + +
+ + ) : null}
- {isHost ? ( - <> - {t("common.admin")} -
- {ADMIN_SECTIONS.map((item) => ( - handleSectionSelectorItemClick(item)} - /> - ))} - - {t("setting.version")}: v{profile.version} - -
- - ) : null} -
+ )}
-
- -
+ {!sm && ( +
+ +
+ )} {state.selectedSection === "my-account" ? ( ) : state.selectedSection === "preference" ? (