Chat Audience Check

Before a sensitive message goes to a group chat, flags members who don't seem to belong, so it doesn't reach the wrong person.

StopsSecret message reached a stranger in group

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/chat-audience-check.json

The files land in your project. They're yours to edit.

Usage

chat-audience-check-usage.tsx
import { ChatAudienceCheck, type ChatMember } from "@/components/ui/chat-audience-check"

<ChatAudienceCheck
  conversation="Launch core team"
  organization="Acme"
  members={members}
  now={NOW}
  lastPostedAt="2026-10-11T12:00:00Z"
  defaultValue="Confidential: the launch date moves to 3 Nov. Please don't share outside this group."
  onRemove={(m) => new Promise<void>((resolve) => setTimeout(() => (setMembers((list) => list.filter((x) => x.id !== m.id)), resolve()), 400))}
  onViewProfile={(m) => setLog(`Opened ${m.name}'s profile.`)}
  onSend={(text) => new Promise<void>((resolve) => setTimeout(() => (setLog(`Sent: “${text}”`), resolve()), 500))}
/>

What it catches

  • Members whose display name disagrees with what you saved them as
  • Names nobody can identify (initials, blanks)
  • People who joined after your last message
  • People outside your organisation, or marked external by the platform
  • Messages with sensitive terms, which raise the check

Props

ChatAudienceCheck props
PropTypeDefault
value

Controlled message text.

string—
defaultValue

Initial message text when uncontrolled.

string""
onValueChange

Called with the text on every change.

(text: string) => void—
members* (required)

Everyone in the chat, you included (mark yourself with `isYou`).

ChatMember[]—
organization* (required)

Your organisation's name as members' `org` spells it.

string—
conversation* (required)

The chat's name, e.g. "#q4-deals" or "Launch core team".

string—
lastPostedAt

When you last posted here; people who joined later are called out.

Date | string | number—
sensitive

Forces the careful path (a confidential channel, a file marked internal). Otherwise the text decides.

boolean—
sensitiveTerms

Words that make a message worth a second look. Replaces the default list.

string[]DEFAULT_CHAT_SENSITIVE_TERMS
now

Pass for deterministic "joined 2 days ago" text.

Date—
locale

BCP 47 locale for dates.

string"en-IN"
onSend* (required)

Sends the message. Return a promise for a pending state; the text clears when it resolves.

(text: string) => void | Promise<void>—
onRemove

Removes someone from the chat. Without it there's no Remove button.

(member: ChatMember) => void | Promise<void>—
onViewProfile

Opens someone's profile so you can see who they really are.

(member: ChatMember) => void—
sendOnEnter

Enter sends and Shift+Enter adds a line. Off: only Ctrl/⌘+Enter sends.

booleantrue
messages

Override any visible string.

Partial<ChatAudienceCheckMessages>—

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Visible <label>; the textarea is described by the flagged-member list
  • Escape cancels the second-press state; Enter sends only when not composing (IME-safe)
  • Focus moves to the first action in the flagged list
  • Remove buttons set aria-busy; errors use role="alert" and status a role="status" region

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Membership and access to the conversation must be enforced by the chat service; this is a pre-send review.

  • Only as good as the member, contact and organisation data you pass
  • Sensitive-term matching is a word list (replaceable)
  • Sending is still possible after a second press

What it stops

Secret message reached a stranger in group

Also known as

group chat · members · signal · whatsapp group · slack channel · audience · external member · send confirmation

Field type

Dependencies

Pairs well with