feat: add infinite scroll & fix some ui

This commit is contained in:
2026-09-18 01:10:35 +08:00
parent 65a2e534d7
commit 6e852ab224
9 changed files with 443 additions and 91 deletions
+22
View File
@@ -0,0 +1,22 @@
import { useWAClient } from "@/app/context";
import { getId } from "@/lib/utils";
import type { WhatsAppClient } from "@client/whatsapp";
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
async function getChats(client: WhatsAppClient) {
return client
.listChats({ limit: 500 })
.then((res) => res.map((i) => ({ ...i, id: getId(i.id) })));
}
type Response = Awaited<ReturnType<typeof getChats>>;
export function useChats(opt?: Partial<UseQueryOptions>) {
const { client, isReady } = useWAClient();
return useQuery<Response, Error>({
queryKey: ["chats"],
queryFn: () => getChats(client),
enabled: isReady,
...((opt || {}) as any),
});
}
+111
View File
@@ -0,0 +1,111 @@
import { useCallback, useEffect, useState } from "react";
type HashLocation = {
path: string;
query: URLSearchParams;
search: string;
};
type HashRouter = HashLocation & {
navigate: (path: string, options?: NavigateOptions) => void;
push: (path: string) => void;
reset: (path?: string) => void;
canGoback: () => boolean;
goBack: () => void;
};
type NavigateOptions = {
replace?: boolean;
};
function getHashLocation(): HashLocation {
const hash = window.location.hash.slice(1);
const [pathname, search = ""] = hash.split("?");
const path = pathname
? pathname.startsWith("/")
? pathname
: `/${pathname}`
: "/";
return {
path,
search: search ? `?${search}` : "",
query: new URLSearchParams(search),
};
}
function normalizePath(path: string) {
return path.startsWith("/") ? path : `/${path}`;
}
function setHashPath(path: string, replace = false) {
const normalized = normalizePath(path);
const hash = `#${normalized}`;
if (replace) {
window.history.replaceState(null, "", hash);
// replaceState doesn't emit hashchange
window.dispatchEvent(new HashChangeEvent("hashchange"));
} else {
window.location.hash = normalized;
}
}
export function useHashRouter(): HashRouter {
const [location, setLocation] = useState<HashLocation>(getHashLocation);
useEffect(() => {
const handleChange = () => {
setLocation(getHashLocation());
};
window.addEventListener("hashchange", handleChange);
return () => {
window.removeEventListener("hashchange", handleChange);
};
}, []);
const navigate = useCallback(
(path: string, options: NavigateOptions = {}) => {
setHashPath(path, options.replace);
},
[],
);
const push = useCallback(
(path: string) => {
navigate(path);
},
[navigate],
);
const reset = useCallback(
(path = "/") => {
navigate(path, { replace: true });
},
[navigate],
);
const canGoback = useCallback(() => window.history.length > 1, []);
const goBack = useCallback(() => {
if (canGoback()) {
window.history.back();
} else {
reset();
}
}, [canGoback, reset]);
return {
...location,
navigate,
push,
reset,
canGoback,
goBack,
};
}
+25
View File
@@ -0,0 +1,25 @@
import { useEffect, useRef } from "react";
export function useIntersection(
callback: (entry: IntersectionObserverEntry) => void,
options: any = {},
) {
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
callback(entry);
}
}, options);
observer.observe(element);
return () => observer.disconnect();
}, [callback, options]);
return ref;
}