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; incomingdefault, outgoingprimary. - 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 fromid/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: 1pxat ≤ 720 px; the fix is a separate acrm MR. - Row
.vm-rowunder 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-trinside the bubble, no frame: meta line.vm-tr-meta(language icon, "Přepis · čeština", copy.vm-copy), then sentences.vm-seg(hover--fg6 %, spoken sentence.onaccent 20 %; dark 8 % / 25 %). Withoutsegmentsplaintext. - 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-unplayableunder the player —labels.unplayablein--critical-ink+ a download link. - Touch target on phone: play, Přepis and speed get an invisible
::beforeextension 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 inonEndedviaref.current.play()(VoiceMessageHandle:play,pause,seek,openTranscript). - Keep the transcript collapsed by default.
- Pass time and
fromLabelalready 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()invoice-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.DisplayNamesinlocale(defaultcs). - Durations
m:ss; speed1×,1,5×,2×(Czech decimal comma).
AD decisions
- 10 Oct 2026 (FA261/FA262): round play 32 × 32
--radius-pillis a deliberate exception from the rounded-square button rule, for media players only; 32 px next to--control-h30 is part of it. - 10 Oct 2026: bubble 358 px instead of 360 px is correct (
.cht-msgmax-width of the thread), not a deviation. - 10 Oct 2026: accent tints 35 / 20 / 10 % (dark 35 / 25 / 14 %) and
--fg6 / 8 % stay localcolor-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-3padding); 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-tintoutgoing 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.
Related
- Video note, Video message — same player and transcript.
- Chat thread — the bubble
.cht-msgand the thread. - Button — the play button (
default/primary, icon).