feat(web): voice messages (record, send, playback)
Click-to-record (README §3.3's explicit 取消/发送 buttons), not the old
app's press-and-hold-with-swipe-to-cancel gesture -- the new design replaces
that touch gesture with plain buttons, a reasonable desktop-first
adaptation; the underlying MediaRecorder mechanics (audio/webm or
audio/mp4 depending on browser support, 24kbps, 2s minimum/10min maximum
duration) are ported as-is from the old app's audio-recorder.jsx.
- VoiceRecorder.tsx: recording UI + MediaRecorder lifecycle. Silently
discards (no send) recordings under MIN_DURATION, matching the old app.
Deliberately skips the `webm-duration-fix` package the old app pulled in
for Chrome's well-known missing-webm-duration-metadata bug -- this app's
own displayed duration comes from its own recording timer, not the file's
embedded metadata, so only a browser's native <audio> scrubber would be
affected, not anything this app actually shows.
- attachments.ts: sendVoice() -- inline base64 or upload-then-reference,
same size-based branch as images/files, Drafty.insertAudio's confirmed
{mime,val/ref,duration,size} shape.
- AttachmentPreview.tsx: AudioAttachment play/pause button + static
waveform (CSS repeating-gradient, not real waveform analysis) + duration.
Verified the failure path live (this sandbox blocks getUserMedia): the
recorder's error state shows a clear message and recovers cleanly back to
the normal composer with no crash. The actual record -> encode -> send ->
playback round trip could not be exercised in this environment and needs
a manual check with real microphone access.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
aa3e464b35
commit
03b8098b3c
@@ -62,3 +62,22 @@ export async function sendFile(topic: Topic, file: File): Promise<unknown> {
|
||||
msg.head = Object.assign(msg.head || {}, { 'x-im-type': 'file' });
|
||||
return topic.publishDraft(msg, Promise.resolve());
|
||||
}
|
||||
|
||||
/**
|
||||
* `durationMs` is measured by this app's own recording timer, independent of whatever duration
|
||||
* metadata (if any) ends up embedded in the audio file itself -- Chrome's MediaRecorder webm output
|
||||
* is well known to omit duration metadata entirely (the old app worked around this with the
|
||||
* `webm-duration-fix` package; skipped here as a deliberate scope cut since the Drafty `duration`
|
||||
* field this app actually displays from doesn't depend on it, only a browser's native <audio>
|
||||
* scrubber would).
|
||||
*/
|
||||
export async function sendVoice(topic: Topic, blob: Blob, durationMs: number): Promise<unknown> {
|
||||
const file = new File([blob], `voice.${blob.type.includes('mp4') ? 'm4a' : 'webm'}`, { type: blob.type });
|
||||
const content =
|
||||
file.size <= MAX_INBAND_ATTACHMENT_SIZE
|
||||
? Drafty.insertAudio(undefined, 0, { mime: file.type, bits: await fileToBase64(file), duration: durationMs, size: file.size })
|
||||
: Drafty.insertAudio(undefined, 0, { mime: file.type, refurl: (await uploadFile(file, topic.name)).url, duration: durationMs, size: file.size });
|
||||
const msg = topic.createMessage(content, false);
|
||||
msg.head = Object.assign(msg.head || {}, { 'x-im-type': 'voice' });
|
||||
return topic.publishDraft(msg, Promise.resolve());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user