Voice message

A voice note inside a chat bubble. React <VoiceMessage> (packages/ui/src/components/VoiceMessage.tsx, shared parts in voice-message-parts.tsx) renders .cht-msg.vm-msg with the .vm player (apps/web/src/ds/voice-message.css) over a real <audio preload="metadata">. It fetches nothing: the host passes src, durationSec, waveform, transcript and unplayed. Consumer: apps/web/src/pages/komunikace/ChatMessageAttachments.tsx.

Siblings in the family: Video note (round video) and Video message (16:9 video) share the player hook, the Přepis link and the transcript block.

Visual contract: prototype proto/chat-hlasove-zpravy@6ed969dc9, approved by the owner 8 Oct 2026 (acrm !4918, !4935, tap target !4956).

When to use

  • A voice note in a chat thread (ChatThread / .cht-msg), incoming or outgoing.
  • Wherever an audio recording with an automatic transcript is shown as a message.

When not to use

  • A meeting recording with a long transcript — use the meeting player (TranscriptPlayer, Meeting analysis).
  • An audio file as a plain attachment to download — use Attachment.
  • A video — use Video note (round) or Video message (16:9).

Anatomy

  • Bubble .cht-msg.vm-msg, width 360 px, capped by the thread (358 px in the narrow Komunikace panel, 88 % of the thread on phone ≤ 720 px). Incoming --line-100, outgoing --accent-tint.
  • Grid .vm: column 1 the round play button, column 2 the track; column-gap: var(--sp-3), row-gap: var(--sp-0).
  • Play .acrm-btn.icon.vm-play: 32 × 32, --radius-pill, centred on the track; incoming default, outgoing primary.
  • Track .vm-wave: height --sp-6, 64 bars <i> (min 2 px, pill), unplayed --ink-300 (outgoing: accent 35 %), played .p --accent. waveform = 0–255 samples (≤ 128); without it a deterministic wave from id/src. On a narrow bubble fewer bars are drawn, never thinner ones.
  • Phone (owner 11 Oct 2026): the bars have no gap, so all 64 fit on the ~237 px track. Main still has gap: 1px at ≤ 720 px; the fix is a separate acrm MR.
  • Row .vm-row under the track (height --control-h-sm): duration .vm-time (--small, --fg-muted); after the first play current time and the speed button .vm-speed (1× / 1,5× / 2×); unplayed dot .vm-dot (--sp-2, --accent); on the right the Přepis link .vm-trlink.
  • Transcript block .vm-tr inside the bubble, no frame: meta line .vm-tr-meta (language icon, "Přepis · čeština", copy .vm-copy), then sentences .vm-seg (hover --fg 6 %, spoken sentence .on accent 20 %; dark 8 % / 25 %). Without segments plain text.
  • Transcript states (transcript.status): none / skipped → nothing; done → Přepis ⇄ Skrýt přepis; queued / processing → "Přepisuji…" with spinner; empty → "Bez řeči"; error → "Přepis se nepodařil ·" + "Zkusit znovu" (onRetry).
  • Unplayable media (browser cannot decode it): .vm-trst.err.vm-unplayable under the player — labels.unplayable in --critical-ink + a download link.
  • Touch target on phone: play, Přepis and speed get an invisible ::before extension to --tap-min (44 px) without changing the look (RULES §19).

Best practices

Do

  • Let the host own state: unplayed (e.g. localStorage per user and message), the retry call, an open transcript that must survive re-renders (transcriptOpen / onTranscriptOpenChange).
  • Clear "unplayed" in onPlay; chain the next unplayed incoming message in onEnded via ref.current.play() (VoiceMessageHandle: play, pause, seek, openTranscript).
  • Keep the transcript collapsed by default.
  • Pass time and fromLabel already formatted.

Don't

  • Draw your own player or a "Transcript" box — extend this component.
  • Store the transcript in chat_messages.text; it is a derived value shown only here.
  • Add hex colours or new global tokens for the accent tints — they stay local color-mix() in voice-message.css (AD 10 Oct 2026).
  • Play two messages at once — the component pauses the previous one (module singleton, pauseActiveVoiceMessage).

Content

  • Labels default to Czech ("Přehrát", "Pozastavit", "Přepis", "Skrýt přepis", "Přepisuji…", "Bez řeči", "Přepis se nepodařil", "Zkusit znovu") and are overridable through labels.
  • Language codes are shown as names via Intl.DisplayNames in locale (default cs).
  • Durations m:ss; speed 1×, 1,5×, 2× (Czech decimal comma).

AD decisions

  • 10 Oct 2026 (FA261/FA262): round play 32 × 32 --radius-pill is a deliberate exception from the rounded-square button rule, for media players only; 32 px next to --control-h 30 is part of it.
  • 10 Oct 2026: bubble 358 px instead of 360 px is correct (.cht-msg max-width of the thread), not a deviation.
  • 10 Oct 2026: accent tints 35 / 20 / 10 % (dark 35 / 25 / 14 %) and --fg 6 / 8 % stay local color-mix().
  • 11 Oct 2026 (owner, R43 a): Voice message is its own family with three sub-components (Voice message, Video note, Video message); it replaces the single entry under Discussion. On phone the track bars have no gap.
  • 17 Oct 2026 (proto/bublina-hlas-ajda@1740bc21d): outgoing voice, video and round video are solid --accent (round video in an accent bubble, --sp-3 padding); incoming --line-100. On accent the player is --fg-on-accent (play = transparent centre, white 1 px outline; waveform unplayed 75 %; highlighted sentence --accent-press); on grey time --ink-600, unplayed waveform --ink-500. Transcript state sits on its own row under the time row. Supersedes the --accent-tint outgoing tone.

Published state (IM-3b, 13 Oct 2026)

A media bubble is a chat message like any other: pass published and metaExtra exactly as on ChatBubble. The owner decided on 13 Oct 2026 that publishing does NOT change the bubble shape — no side stripe, no squared corners (the stripe from acrm !4949/!4989 was never in the approved preview chaty.html). .pub is a hook class only; the published state is carried by the meta text next to the time (who · when / "Private"), rendered on the last media bubble without a text bubble after it, the same rule as time.

  • Video note, Video message — same player and transcript.
  • Chat thread — the bubble .cht-msg and the thread.
  • Button — the play button (default / primary, icon).

Source: docs/voice-message/voice-message.usage.md · GitLab