feat: add skeleton loader

This commit is contained in:
2026-09-18 07:10:38 +08:00
parent d9e627cc5f
commit 8aa10d38b1
3 changed files with 55 additions and 5 deletions
+17 -1
View File
@@ -1,4 +1,5 @@
import Avatar from "@/components/ui/avatar";
import { Skeleton } from "@/components/ui/skeleton";
import { useChats } from "@/hooks/useChats";
import { useContacts } from "@/hooks/useContact";
import { getFormattedName } from "@/lib/utils";
@@ -23,7 +24,7 @@ export default function ChatsSection({ selected, onSelect }: Props) {
const [query, setQuery] = useState("");
const contacts = useContacts();
const { data: chats } = useChats({
const { data: chats, isLoading } = useChats({
refetchInterval: 10_000,
});
@@ -77,6 +78,21 @@ export default function ChatsSection({ selected, onSelect }: Props) {
</div> */}
<div className="flex-1 overflow-y-auto px-2 pb-3">
{chats == null || isLoading
? [...Array(20)].map((_, i) => (
<div key={i} className="w-full flex items-center gap-3 px-3 mb-4">
<Skeleton key={i} className="size-12 rounded-full bg-accent" />
<div className="flex-1">
<Skeleton
key={i}
className="flex-1 h-4 bg-accent mb-2 w-1/2"
/>
<Skeleton key={i} className="flex-1 h-11 bg-accent" />
</div>
</div>
))
: null}
{filteredChats?.map((chat) => (
<button
key={chat.id}
+25 -4
View File
@@ -24,6 +24,7 @@ import Avatar from "@/components/ui/avatar";
import { useIntersection } from "@/hooks/useIntersection";
import Paragraph from "@/components/paragraph";
import { nanoid } from "nanoid";
import { Skeleton } from "@/components/ui/skeleton";
dayjs.extend(relativeTime);
@@ -280,15 +281,35 @@ export default function MessagesSection({ chatId, maxCount = 50 }: Props) {
</div>
))}
{messages == null || isLoading || isFetchingNextPage
? [...Array(10)].map((_, i) => (
<div
key={i}
className={cn(
"mb-4 shrink-0 w-full max-w-[44%]",
i % 2 === 0 ? "self-start" : "self-end",
)}
>
<Skeleton key={i} className="w-20 mb-3 h-6 bg-card" />
<Skeleton
key={i}
className={cn(
"w-full h-20 bg-card rounded-2xl",
["h-20", "h-32", "h-24", "h-40"][i % 4],
)}
/>
</div>
))
: null}
<div ref={endScrollRef} className="h-1" />
<div className="mb-6 flex items-center justify-center gap-2 text-[11px] text-muted-foreground">
<span className="h-px flex-1 bg-border" />
<div className="flex items-center gap-2">
{isLoading ||
(isFetchingNextPage && (
<Loader2Icon className="animate-spin size-4" />
))}
{isLoading || isFetchingNextPage ? (
<Loader2Icon className="animate-spin size-4" />
) : null}
<span>
{isLoading || isFetchingNextPage
? "Loading..."
+13
View File
@@ -0,0 +1,13 @@
import { cn } from "cn";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("animate-pulse rounded-md bg-background/30", className)}
{...props}
/>
);
}
export { Skeleton };