Stale Reply

Warns before replying to an old message in a thread that has moved on, and shows what was said since.

StopsReplied to a thread that moved on

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/stale-reply.json

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

Usage

stale-reply-usage.tsx
import { StaleReply, type ThreadMessage } from "@/components/ui/stale-reply"

<StaleReply
  replyingTo={original}
  newer={newer}
  now={NOW}
  onReplyToLatest={setTarget}
  onQuote={(message) => {
    setDraft(`> ${message.author}: ${message.text}\n\n${draft}`)
    setDismissed(true)
  }}
  onDismiss={() => setDismissed(true)}
/>

What it catches

  • Replies to messages older than staleAfter (default 7 days) with at least minNewer newer messages (default 10)
  • Newer related messages: direct replies, same author or shared keywords
  • Messages that look like they already resolved it
  • One-tap reply-to-latest or quote-original

Props

StaleReply props
PropTypeDefault
replyingTo* (required)

The message you're replying to.

ThreadMessage—
newer* (required)

Every message posted after it in the thread or channel.

ThreadMessage[]—
now

Current time in ms. Defaults to the clock, read after mount.

number—
staleAfter

Warn when the message is older than this (ms)…

number7 * DAY
minNewer

…or when at least this many messages followed it.

number10
show

How many newer messages to show.

number3
onReplyToLatest

Called with the newest relevant message to reply to it instead.

(message: ThreadMessage) => void—
onQuote

Reply here, quoting the original so readers have context.

(original: ThreadMessage) => void—
onDismiss

Called when the notice is dismissed.

() => void—
message

Templates: `{age}`, `{count}`, `{author}`, `{text}`, `{when}` are filled in.

string"This was {age} and {count} followed."
resolvedText

Marks a message that seems to have settled it.

string"{author} may have settled it {when}: “{text}”"
latestLabel

Label of the reply-to-latest button.

string"Reply to latest"
quoteLabel

Label of the quote-original button.

string"Quote & reply"
newestLabel

Label of the newer-messages toggle.

string"Newest related"
dismissLabel

Accessible label of the dismiss button.

string"Reply here anyway"
regionLabel

Accessible name of the banner.

string"Old message"

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

Accessibility

  • Notice is a region with an aria-label; summary in a polite live region
  • Newer messages expand with aria-expanded/aria-controls; collapsed content is inert
  • Dismiss has an aria-label

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

Limitations

  • Relatedness is keyword-based and can miss or over-include
  • "Resolved" detection is a guess from wording

What it stops

Replied to a thread that moved on

Also known as

reply · thread · old message · forum · github issue · comments · slack · quote

Field type

Dependencies

Pairs well with