# Message list

> A conversation's messages over Realtime, with older pages on demand, who is typing, reply counts and the composer.

- Blocks: [messaging](https://bettersupabase.com/blocks/messaging.md), [channels](https://bettersupabase.com/blocks/channels.md)
- Kind: component
- Hook: `useMessages` from `better-supabase/blocks/messaging/react`
- Page: https://bettersupabase.com/recipes/message-list

## Install the blocks

```bash
npx better-supabase add messaging channels
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add alert button card empty scroll-area skeleton
```

From `apps/examples/nextjs/src/features/chat/components/message-list.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/chat/components/message-list.tsx):

```tsx
"use client";

import type { Message } from "better-supabase/blocks/messaging";

import { useMessages } from "better-supabase/blocks/messaging/react";
import { MessagesSquareIcon, RotateCwIcon } from "lucide-react";
import { useExtracted } from "next-intl";
import { useRef } from "react";

import { Alert, AlertAction, AlertTitle } from "@/components/reui/alert";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Skeleton } from "@/components/ui/skeleton";
import { useErrorMessage } from "@/lib/use-error-message";

import { MessageComposer } from "./message-composer";
import { MessageItem } from "./message-item";
import { PresenceAvatars } from "./presence-avatars";

/**
 * A channel's top-level messages, live over Realtime: older pages on demand,
 * who is typing, and the composer. Replies open in a thread.
 */
export function MessageList({
  conversationId,
  name,
  topic,
  onOpenThread,
}: {
  conversationId: string;
  name: string;
  topic?: string | undefined;
  onOpenThread: (root: Message) => void;
}) {
  const t = useExtracted("chat");
  const errorMessage = useErrorMessage();
  const input = useRef<HTMLTextAreaElement>(null);

  const {
    messages,
    hasOlder,
    typing,
    error,
    send,
    setTyping,
    loadOlder,
    refresh,
  } = useMessages({ conversationId });

  return (
    <Card size="sm" className="min-h-0">
      <CardHeader>
        <CardTitle>#{name}</CardTitle>
        {topic ? <CardDescription>{topic}</CardDescription> : null}
        <CardAction>
          <PresenceAvatars
            subject={{ type: "conversation", id: conversationId }}
          />
        </CardAction>
      </CardHeader>
      <CardContent className="flex min-h-0 flex-1 flex-col gap-3">
        {error ? (
          <Alert variant="destructive" aria-live="polite">
            <AlertTitle>{errorMessage(error)}</AlertTitle>
            <AlertAction>
              <Button
                type="button"
                size="sm"
                variant="outline"
                onClick={() => void refresh()}
              >
                <RotateCwIcon aria-hidden />
                {t("Retry")}
              </Button>
            </AlertAction>
          </Alert>
        ) : null}
        {messages === undefined ? (
          <div
            className="flex flex-col gap-4"
            aria-busy="true"
            aria-live="polite"
          >
            <span className="sr-only">{t("Loading messages")}</span>
            <Skeleton className="h-12" />
            <Skeleton className="h-12" />
            <Skeleton className="h-12" />
          </div>
        ) : messages.length === 0 ? (
          <Empty className="flex-1">
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <MessagesSquareIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No messages yet")}</EmptyTitle>
              <EmptyDescription>
                {t("Say hello to start the conversation.")}
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <Button
                type="button"
                size="sm"
                onClick={() => input.current?.focus()}
              >
                {t("Write a message")}
              </Button>
            </EmptyContent>
          </Empty>
        ) : (
          <ScrollArea className="min-h-0 flex-1">
            <div className="flex flex-col gap-4 pr-3">
              {hasOlder ? (
                <Button
                  type="button"
                  size="xs"
                  variant="ghost"
                  className="self-center"
                  onClick={() => void loadOlder()}
                >
                  {t("Load older messages")}
                </Button>
              ) : null}
              <ol className="flex flex-col gap-4" data-testid="messages">
                {messages.map((message) => (
                  <MessageItem
                    key={message.id}
                    message={message}
                    onOpenThread={onOpenThread}
                  />
                ))}
              </ol>
            </div>
          </ScrollArea>
        )}
      </CardContent>
      <CardFooter className="flex-col items-stretch gap-1">
        <p className="text-xs text-muted-foreground" aria-live="polite">
          {typing.length > 0
            ? t(
                "{count, plural, one {Someone is typing} other {# people are typing}}",
                { count: typing.length },
              )
            : null}
        </p>
        <MessageComposer
          conversationId={conversationId}
          inputRef={input}
          placeholder={t("Message #{name}", { name })}
          onSend={send}
          onTyping={setTyping}
        />
      </CardFooter>
    </Card>
  );
}
```

## Prompt

Paste this into a coding agent to build the recipe in your app.

### Web

```text
Build "Message list" in a React web app that uses shadcn/ui components. A conversation's messages over Realtime, with older pages on demand, who is typing, reply counts and the composer.

It uses the better-supabase `messaging` block (Messaging) with `channels`. It calls the hook `useMessages` from `better-supabase/blocks/messaging/react`.

Steps:

1. If the app doesn't have the blocks yet, add the SQL with `npx better-supabase add messaging channels` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add alert button card empty scroll-area skeleton`.
3. The recipe needs no glue: the hook calls the block from the client.
4. Write the component. Call `useMessages` from `better-supabase/blocks/messaging/react`, starting from the recipe's snippet (`apps/examples/nextjs/src/features/chat/components/message-list.tsx`). Adapt it to the app's existing components, styling and data loading instead of copying it as is.
5. Check the result against the recipe and the block's docs: it typechecks, it calls the same hook, and it handles the loading and empty states the snippet handles.

Read the full code first:

- Recipe: https://bettersupabase.com/recipes/message-list.md
- Block docs: https://bettersupabase.com/docs/blocks/messaging
```
