# Presence avatars

> Who else has a record open (a channel, a comment thread's record or a document), on the record's presence topic.

- Block: [messaging](https://bettersupabase.com/blocks/messaging.md)
- Kind: component
- Hook: `usePresence` from `better-supabase/react`
- Page: https://bettersupabase.com/recipes/presence-avatars

## Install the block

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

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add avatar
```

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

```tsx
"use client";

import { usePresence } from "better-supabase/react";
import {
  subjectPresenceTopic,
  subjectPresenceValues,
} from "better-supabase/realtime";
import { useExtracted } from "next-intl";
import * as v from "valibot";

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarGroupCount,
} from "@/components/ui/avatar";
import { useSession } from "@/lib/hooks";

const SHOWN = 4;

/** States that fail the schema never reach `members`. */
const viewers = subjectPresenceTopic(
  v.object({ userId: v.string(), name: v.string() }),
);

/**
 * Who else has a record open: a channel, a comment thread's record or a
 * document. The subjects module lets anyone who reads the record join its
 * presence topic. States come from the clients, so they only label avatars.
 */
export function PresenceAvatars({
  subject,
}: {
  subject: { readonly type: string; readonly id: string };
}) {
  const t = useExtracted("chat");
  const session = useSession();
  const me = session.kind === "user" ? session.user : null;

  const { members } = usePresence(
    viewers,
    subjectPresenceValues(me ? subject : null),
    { state: me ? { userId: me.id, name: me.email ?? t("Someone") } : null },
  );

  const others = new Map<string, string>();

  for (const { state } of members) {
    if (state.userId !== me?.id) {
      others.set(state.userId, state.name);
    }
  }

  if (others.size === 0) {
    return null;
  }

  const people = [...others];

  return (
    <AvatarGroup
      aria-label={t(
        "{count, plural, one {# other viewer} other {# other viewers}}",
        { count: people.length },
      )}
    >
      {people.slice(0, SHOWN).map(([userId, name]) => (
        <Avatar key={userId} size="sm" title={name}>
          <AvatarFallback>{name.charAt(0).toUpperCase()}</AvatarFallback>
        </Avatar>
      ))}
      {people.length > SHOWN ? (
        <AvatarGroupCount>+{people.length - SHOWN}</AvatarGroupCount>
      ) : null}
    </AvatarGroup>
  );
}
```

## Prompt

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

### Web

```text
Build "Presence avatars" in a React web app that uses shadcn/ui components. Who else has a record open (a channel, a comment thread's record or a document), on the record's presence topic.

It uses the better-supabase `messaging` block (Messaging). It calls the hook `usePresence` from `better-supabase/react`.

Steps:

1. If the app doesn't have the block yet, add the SQL with `npx better-supabase add messaging` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add avatar`.
3. The recipe needs no glue: the hook calls the block from the client.
4. Write the component. Call `usePresence` from `better-supabase/react`, starting from the recipe's snippet (`apps/examples/nextjs/src/features/chat/components/presence-avatars.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/presence-avatars.md
- Block docs: https://bettersupabase.com/docs/blocks/messaging
```
