Skip to content
Product block

Message Bubble Thread

Bubbles with the parts that get skipped: per-message delivery state, grouping by author and time, day separators, and a retry on the failed bubble itself.

325 lineslucide-reactAdded 13 Sept 2026
  • chat
  • messages
  • bubbles
  • thread
  • messaging

What's included

  • components/message-bubble-thread.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add message-bubble-thread

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Priya Raman

Northwind logistics · online

Thursday 11 September

Priya Raman, 16:41

The Rotterdam bay is booked out until Tuesday. Do you want me to split the delivery?

Priya Raman, 16:41

Half now, half when the bay frees up. No extra charge either way.

You, 17:02

Split it. The chairs are the urgent half.

Read

Friday 12 September

Priya Raman, 09:14

You: Split it. The chairs are the urgent half.

Chairs are on the 07:00 run Monday. I moved the desks to Wednesday.

You, 09:20

Perfect. Can you put the reference on the paperwork this time?

Delivered

You, 09:21

ORD-88410, in case it helps.

Not sent

Could not send.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/message-bubble-thread.tsx
'use client'

/**
 * <MessageBubbleThread> — the bubbles, done properly once.
 *
 * `chat-thread-panel` in this catalog is an *assistant* thread: one human, one
 * model, no delivery state, no identity to establish. This is the human-to-
 * human version, and the differences are not cosmetic.
 *
 * WHAT A HUMAN THREAD NEEDS THAT AN ASSISTANT THREAD DOES NOT
 *
 *  - **Delivery state per message.** Sending, sent, delivered, read, failed.
 *    A message that silently failed is the worst outcome in any messaging
 *    product, and the failed state needs a retry on the bubble itself rather
 *    than a toast that scrolls away.
 *  - **Grouping by author and time.** Consecutive messages from one person
 *    within a few minutes share an avatar and a timestamp. Repeating them per
 *    bubble is what makes a hand-built thread look wrong in a way people
 *    cannot name.
 *  - **Day separators.** Absolute dates between days, relative times within
 *    them. A thread of bare "09:14" stamps is unreadable after a week.
 *  - **A reply quote.** Threads fork; the quoted parent is how a reply to a
 *    message from four screens ago makes sense at all.
 *
 * THE CORNER RADII CARRY THE DIRECTION
 *
 * `rounded-ee-sm` on your own bubbles and `rounded-es-sm` on theirs — logical
 * corners, so the tail sits on the correct side in Arabic and Hebrew without a
 * second rule. A thread built with `rounded-br-sm` has both tails on the same
 * side in RTL, which reads as everyone talking to themselves.
 *
 * ACCESSIBILITY: the thread is a `role="log"` with `aria-live="polite"`, so a
 * new message is announced without dragging focus out of the composer; each
 * bubble is an `<article>` with a visually hidden author-and-time line, since
 * an avatar and an alignment are not information to a screen reader; delivery
 * state is a word, not a tick glyph alone.
 */

import * as React from 'react'
import { Check, CheckCheck, Clock, RotateCcw, Send, TriangleAlert } from 'lucide-react'

export type Delivery = 'sending' | 'sent' | 'delivered' | 'read' | 'failed'

export interface ThreadMessage {
  id: string
  author: string
  initials: string
  mine?: boolean
  body: string
  at: string
  /** Absolute day label. A change from the previous message inserts a rule. */
  day: string
  delivery?: Delivery
  /** Quoted parent, for a reply that would otherwise make no sense. */
  replyTo?: { author: string; body: string }
}

export interface MessageBubbleThreadProps {
  messages?: ThreadMessage[]
  className?: string
}

const DEFAULT_MESSAGES: ThreadMessage[] = [
  {
    id: '1',
    author: 'Priya Raman',
    initials: 'PR',
    day: 'Thursday 11 September',
    at: '16:41',
    body: 'The Rotterdam bay is booked out until Tuesday. Do you want me to split the delivery?',
  },
  {
    id: '2',
    author: 'Priya Raman',
    initials: 'PR',
    day: 'Thursday 11 September',
    at: '16:41',
    body: 'Half now, half when the bay frees up. No extra charge either way.',
  },
  {
    id: '3',
    author: 'You',
    initials: 'YU',
    mine: true,
    day: 'Thursday 11 September',
    at: '17:02',
    body: 'Split it. The chairs are the urgent half.',
    delivery: 'read',
  },
  {
    id: '4',
    author: 'Priya Raman',
    initials: 'PR',
    day: 'Friday 12 September',
    at: '09:14',
    body: 'Chairs are on the 07:00 run Monday. I moved the desks to Wednesday.',
    replyTo: { author: 'You', body: 'Split it. The chairs are the urgent half.' },
  },
  {
    id: '5',
    author: 'You',
    initials: 'YU',
    mine: true,
    day: 'Friday 12 September',
    at: '09:20',
    body: 'Perfect. Can you put the reference on the paperwork this time?',
    delivery: 'delivered',
  },
  {
    id: '6',
    author: 'You',
    initials: 'YU',
    mine: true,
    day: 'Friday 12 September',
    at: '09:21',
    body: 'ORD-88410, in case it helps.',
    delivery: 'failed',
  },
]

const DELIVERY_LABEL: Record<Delivery, string> = {
  sending: 'Sending',
  sent: 'Sent',
  delivered: 'Delivered',
  read: 'Read',
  failed: 'Not sent',
}

function DeliveryMark({ state }: { state: Delivery }) {
  const Icon =
    state === 'sending'
      ? Clock
      : state === 'failed'
        ? TriangleAlert
        : state === 'sent'
          ? Check
          : CheckCheck
  return (
    <span
      className={`inline-flex items-center gap-1 text-[10px] ${
        state === 'failed'
          ? 'text-destructive'
          : state === 'read'
            ? 'text-primary-foreground/90'
            : 'text-primary-foreground/70'
      }`}
    >
      <Icon aria-hidden className="h-3 w-3" />
      {/* A word, not a tick glyph alone. */}
      {DELIVERY_LABEL[state]}
    </span>
  )
}

export function MessageBubbleThread({
  messages = DEFAULT_MESSAGES,
  className = '',
}: MessageBubbleThreadProps) {
  const uid = React.useId()
  const [thread, setThread] = React.useState(messages)
  const [draft, setDraft] = React.useState('')

  function retry(id: string) {
    setThread((t) =>
      t.map((m) => (m.id === id ? { ...m, delivery: 'sending' as Delivery } : m)),
    )
    window.setTimeout(() => {
      setThread((t) => t.map((m) => (m.id === id ? { ...m, delivery: 'sent' as Delivery } : m)))
    }, 800)
  }

  function send(event: React.FormEvent) {
    event.preventDefault()
    if (!draft.trim()) return
    setThread((t) => [
      ...t,
      {
        id: `n${t.length + 1}`,
        author: 'You',
        initials: 'YU',
        mine: true,
        day: 'Friday 12 September',
        at: '09:24',
        body: draft.trim(),
        delivery: 'sent',
      },
    ])
    setDraft('')
  }

  return (
    <section className={`bg-background px-4 py-10 sm:px-6 ${className}`}>
      <div className="mx-auto flex h-[34rem] max-w-2xl flex-col overflow-hidden rounded-2xl border border-border bg-card">
        <header className="flex items-center gap-3 border-b border-border p-4">
          <span
            aria-hidden
            className="grid h-9 w-9 place-items-center rounded-full bg-primary/10 text-xs font-bold text-primary"
          >
            PR
          </span>
          <div className="min-w-0">
            <h2 className="truncate text-sm font-semibold">Priya Raman</h2>
            <p className="text-xs text-muted-foreground">Northwind logistics · online</p>
          </div>
        </header>

        <div
          role="log"
          aria-live="polite"
          aria-label="Messages"
          className="min-h-0 flex-1 space-y-1 overflow-y-auto p-4"
        >
          {thread.map((message, index) => {
            const previous = thread[index - 1]
            const newDay = previous?.day !== message.day
            // Grouped by author within a day — the detail that makes a
            // hand-built thread look wrong when it is missing.
            const grouped = !newDay && previous?.author === message.author
            return (
              <React.Fragment key={message.id}>
                {newDay ? (
                  <div className="flex items-center gap-3 py-3">
                    <span aria-hidden className="h-px flex-1 bg-border" />
                    <span className="text-[11px] font-medium text-muted-foreground">
                      {message.day}
                    </span>
                    <span aria-hidden className="h-px flex-1 bg-border" />
                  </div>
                ) : null}

                <article
                  className={`flex gap-2 ${message.mine ? 'justify-end' : 'justify-start'} ${
                    grouped ? '' : 'pt-2'
                  }`}
                >
                  {!message.mine ? (
                    <span
                      aria-hidden
                      className={`mt-auto grid h-7 w-7 shrink-0 place-items-center rounded-full text-[10px] font-bold ${
                        grouped ? 'invisible' : 'bg-muted text-muted-foreground'
                      }`}
                    >
                      {message.initials}
                    </span>
                  ) : null}

                  <div className={`max-w-[78%] ${message.mine ? 'items-end' : ''}`}>
                    {/* An avatar and an alignment are not information. */}
                    <p className="sr-only">
                      {message.author}, {message.at}
                    </p>

                    {message.replyTo ? (
                      <blockquote className="mb-1 rounded-lg border-s-2 border-primary/50 bg-muted/60 px-2 py-1 text-[11px] text-muted-foreground">
                        <span className="font-semibold">{message.replyTo.author}:</span>{' '}
                        {message.replyTo.body}
                      </blockquote>
                    ) : null}

                    <div
                      className={`rounded-2xl px-3 py-2 text-sm ${
                        message.mine
                          ? 'rounded-ee-sm bg-primary text-primary-foreground'
                          : 'rounded-es-sm bg-muted text-foreground'
                      }`}
                    >
                      <p>{message.body}</p>
                      <p
                        className={`mt-1 flex items-center gap-2 text-[10px] ${
                          message.mine
                            ? 'justify-end text-primary-foreground/70'
                            : 'text-muted-foreground'
                        }`}
                      >
                        <span aria-hidden>{message.at}</span>
                        {message.delivery ? <DeliveryMark state={message.delivery} /> : null}
                      </p>
                    </div>

                    {/* Retry on the bubble, not in a toast that scrolls away. */}
                    {message.delivery === 'failed' ? (
                      <p className="mt-1 flex items-center justify-end gap-1.5 text-[11px] text-destructive">
                        Could not send.
                        <button
                          type="button"
                          onClick={() => retry(message.id)}
                          className="inline-flex items-center gap-1 rounded font-semibold underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                        >
                          <RotateCcw aria-hidden className="h-3 w-3" />
                          Retry
                        </button>
                      </p>
                    ) : null}
                  </div>
                </article>
              </React.Fragment>
            )
          })}
        </div>

        <form onSubmit={send} className="flex items-center gap-2 border-t border-border p-3">
          <label htmlFor={`${uid}-draft`} className="sr-only">
            Write a message
          </label>
          <input
            id={`${uid}-draft`}
            value={draft}
            onChange={(e) => setDraft(e.target.value)}
            placeholder="Message Priya"
            className="min-w-0 flex-1 rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none transition-colors focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-ring"
          />
          <button
            type="submit"
            disabled={!draft.trim()}
            aria-label="Send message"
            className="rounded-lg bg-primary p-2 text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
          >
            <Send aria-hidden className="h-4 w-4 rtl:rotate-180" />
          </button>
        </form>
      </div>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/message-bubble-thread.tsx and import it where you need the section:

import { MessageBubbleThread } from '@/components/message-bubble-thread'

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
messagesThreadMessage[]DEFAULT_MESSAGES
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

message-bubble-thread.html
<!--
  Message Bubble Thread — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section class="bg-background px-4 py-10 sm:px-6 ">
  <div class="mx-auto flex h-[34rem] max-w-2xl flex-col overflow-hidden rounded-2xl border border-border bg-card">
    <header class="flex items-center gap-3 border-b border-border p-4">
      <span aria-hidden="true" class="grid h-9 w-9 place-items-center rounded-full bg-primary/10 text-xs font-bold text-primary">PR</span>
      <div class="min-w-0">
        <h2 class="truncate text-sm font-semibold">Priya Raman</h2>
        <p class="text-xs text-muted-foreground">Northwind logistics · online</p>
      </div>
    </header>
    <div role="log" aria-live="polite" aria-label="Messages" class="min-h-0 flex-1 space-y-1 overflow-y-auto p-4">
      <div class="flex items-center gap-3 py-3">
        <span aria-hidden="true" class="h-px flex-1 bg-border"></span>
        <span class="text-[11px] font-medium text-muted-foreground">Thursday 11 September</span>
        <span aria-hidden="true" class="h-px flex-1 bg-border"></span>
      </div>
      <article class="flex gap-2 justify-start pt-2">
        <span aria-hidden="true" class="mt-auto grid h-7 w-7 shrink-0 place-items-center rounded-full text-[10px] font-bold bg-muted text-muted-foreground">PR</span>
        <div class="max-w-[78%] ">
          <p class="sr-only">Priya Raman, 16:41</p>
          <div class="rounded-2xl px-3 py-2 text-sm rounded-es-sm bg-muted text-foreground">
            <p>The Rotterdam bay is booked out until Tuesday. Do you want me to split the delivery?</p>
            <p class="mt-1 flex items-center gap-2 text-[10px] text-muted-foreground">
              <span aria-hidden="true">16:41</span>
            </p>
          </div>
        </div>
      </article>
      <article class="flex gap-2 justify-start ">
        <span aria-hidden="true" class="mt-auto grid h-7 w-7 shrink-0 place-items-center rounded-full text-[10px] font-bold invisible">PR</span>
        <div class="max-w-[78%] ">
          <p class="sr-only">Priya Raman, 16:41</p>
          <div class="rounded-2xl px-3 py-2 text-sm rounded-es-sm bg-muted text-foreground">
            <p>Half now, half when the bay frees up. No extra charge either way.</p>
            <p class="mt-1 flex items-center gap-2 text-[10px] text-muted-foreground">
              <span aria-hidden="true">16:41</span>
            </p>
          </div>
        </div>
      </article>
      <article class="flex gap-2 justify-end pt-2">
        <div class="max-w-[78%] items-end">
          <p class="sr-only">You, 17:02</p>
          <div class="rounded-2xl px-3 py-2 text-sm rounded-ee-sm bg-primary text-primary-foreground">
            <p>Split it. The chairs are the urgent half.</p>
            <p class="mt-1 flex items-center gap-2 text-[10px] justify-end text-primary-foreground/70">
              <span aria-hidden="true">17:02</span>
              <span class="inline-flex items-center gap-1 text-[10px] text-primary-foreground/90"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check-check h-3 w-3" aria-hidden="true"><path d="M18 6 7 17l-5-5"></path><path d="m22 10-7.5 7.5L13 16"></path></svg>Read</span>
            </p>
          </div>
        </div>
      </article>
      <div class="flex items-center gap-3 py-3">
        <span aria-hidden="true" class="h-px flex-1 bg-border"></span>
        <span class="text-[11px] font-medium text-muted-foreground">Friday 12 September</span>
        <span aria-hidden="true" class="h-px flex-1 bg-border"></span>
      </div>
      <article class="flex gap-2 justify-start pt-2">
        <span aria-hidden="true" class="mt-auto grid h-7 w-7 shrink-0 place-items-center rounded-full text-[10px] font-bold bg-muted text-muted-foreground">PR</span>
        <div class="max-w-[78%] ">
          <p class="sr-only">Priya Raman, 09:14</p>
          <blockquote class="mb-1 rounded-lg border-s-2 border-primary/50 bg-muted/60 px-2 py-1 text-[11px] text-muted-foreground"><span class="font-semibold">You:</span> Split it. The chairs are the urgent half.</blockquote>
          <div class="rounded-2xl px-3 py-2 text-sm rounded-es-sm bg-muted text-foreground">
            <p>Chairs are on the 07:00 run Monday. I moved the desks to Wednesday.</p>
            <p class="mt-1 flex items-center gap-2 text-[10px] text-muted-foreground">
              <span aria-hidden="true">09:14</span>
            </p>
          </div>
        </div>
      </article>
      <article class="flex gap-2 justify-end pt-2">
        <div class="max-w-[78%] items-end">
          <p class="sr-only">You, 09:20</p>
          <div class="rounded-2xl px-3 py-2 text-sm rounded-ee-sm bg-primary text-primary-foreground">
            <p>Perfect. Can you put the reference on the paperwork this time?</p>
            <p class="mt-1 flex items-center gap-2 text-[10px] justify-end text-primary-foreground/70">
              <span aria-hidden="true">09:20</span>
              <span class="inline-flex items-center gap-1 text-[10px] text-primary-foreground/70"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check-check h-3 w-3" aria-hidden="true"><path d="M18 6 7 17l-5-5"></path><path d="m22 10-7.5 7.5L13 16"></path></svg>Delivered</span>
            </p>
          </div>
        </div>
      </article>
      <article class="flex gap-2 justify-end ">
        <div class="max-w-[78%] items-end">
          <p class="sr-only">You, 09:21</p>
          <div class="rounded-2xl px-3 py-2 text-sm rounded-ee-sm bg-primary text-primary-foreground">
            <p>ORD-88410, in case it helps.</p>
            <p class="mt-1 flex items-center gap-2 text-[10px] justify-end text-primary-foreground/70">
              <span aria-hidden="true">09:21</span>
              <span class="inline-flex items-center gap-1 text-[10px] text-destructive"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert h-3 w-3" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>Not sent</span>
            </p>
          </div>
          <p class="mt-1 flex items-center justify-end gap-1.5 text-[11px] text-destructive">Could not send.<button type="button" class="inline-flex items-center gap-1 rounded font-semibold underline underline-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw h-3 w-3" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path></svg>Retry</button></p>
        </div>
      </article>
    </div>
    <form class="flex items-center gap-2 border-t border-border p-3">
      <label for="_R_0_-draft" class="sr-only">Write a message</label>
      <input id="_R_0_-draft" placeholder="Message Priya" class="min-w-0 flex-1 rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none transition-colors focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-ring" value="" />
      <button type="submit" disabled="" aria-label="Send message" class="rounded-lg bg-primary p-2 text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send h-4 w-4 rtl:rotate-180" aria-hidden="true">
          <path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"></path>
          <path d="m21.854 2.147-10.94 10.939"></path>
        </svg>
      </button>
    </form>
  </div>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

8 more blocks in Communication

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Communication

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Communication blocks

View category
Open the full page for this block

Support Chat Widget

The corner launcher, with real availability instead of a stock reassurance, article deflection before the form, no self-opening, and a way to make it go away.

Communication307 lines1 dep

Attachment Bubbles

Five kinds of thing that is not text - image, document, link, location and an upload still in flight - each answering the question its own kind raises.

Communication305 lines1 dep
Open the full page for this block

Inbox Thread List

The list half of a two-pane inbox, with unread and selected as visibly different states and bulk selection as a mode rather than a hover checkbox.

Communication333 lines1 dep
Open the full page for this block

Inbox Reading Pane

The reading pane with the composer always present, quoted history collapsed behind a count, and an internal-note mode whose send button names the recipient.

Communication311 lines1 dep
Open the full page for this block

Voice Message Player

A voice note you can navigate: a waveform that is a real seek control, a speed button, a transcript, and a played state that survives finishing.

Communication271 lines1 dep
Open the full page for this block

Video Call Layout

The call grid with the state that makes it usable - active speaker, per-tile connection quality, hide self-view, and the you-are-muted nudge.

Communication286 lines1 dep
Open the full page for this block

Call Device Settings

The green room, where every picker has a test beside it - an input meter, a test tone, a camera preview - and blocked permission is a state, not an error.

Communication315 lines1 dep
Open the full page for this block

Support Ticket Thread

The customer side of a ticket, where a named owner and a deadline replace Status: Open - including the waiting-on-you state that stops the argument about response times.

Communication340 lines1 dep