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" ? (