@linktr.ee/messaging-react 4.1.5 → 4.2.0-rc-1787083791
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Card-DDv7af2K.js → Card-B4lMv-0o.js} +2 -2
- package/dist/{Card-DDv7af2K.js.map → Card-B4lMv-0o.js.map} +1 -1
- package/dist/{Card-BD16XAaT.cjs → Card-DWL5I62S.cjs} +2 -2
- package/dist/{Card-BD16XAaT.cjs.map → Card-DWL5I62S.cjs.map} +1 -1
- package/dist/assets/index.css +1 -1
- package/dist/index-B-9LkWep.cjs +3 -0
- package/dist/index-B-9LkWep.cjs.map +1 -0
- package/dist/{index-qKUd4Fz2.js → index-C93LTGLc.js} +1495 -1356
- package/dist/index-C93LTGLc.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +58 -43
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/components/CustomMessage/StreamAttachmentMessage.test.tsx +33 -0
- package/src/components/CustomMessage/StreamAttachmentMessage.tsx +6 -0
- package/src/components/MessageAttachment/Audio/AudioAttachment.stories.tsx +165 -58
- package/src/components/MessageAttachment/Audio/WaveformAudioRow.tsx +335 -0
- package/src/components/MessageAttachment/Audio/index.tsx +71 -77
- package/src/components/MessageAttachment/MessageAttachment.test.tsx +176 -74
- package/src/components/MessageAttachment/index.tsx +25 -9
- package/src/components/MessageAttachment/types.ts +19 -9
- package/src/styles.css +120 -0
- package/dist/index-9elRVMIF.cjs +0 -3
- package/dist/index-9elRVMIF.cjs.map +0 -1
- package/dist/index-qKUd4Fz2.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-B-9LkWep.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageBubble=e.MessageBubble;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessaging=e.useMessaging;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -36,37 +36,24 @@ export declare type AttachmentSourceType = 'image' | 'audio' | 'video' | 'docume
|
|
|
36
36
|
declare interface AudioAttachmentSharedProps extends MessageAttachmentBaseProps {
|
|
37
37
|
/** Audio source URL (`mp3`, `aac`, `wav`, …). */
|
|
38
38
|
src?: string;
|
|
39
|
-
/** MIME type hint —
|
|
39
|
+
/** MIME type hint — gates playability via the player's `canPlayType` probe. */
|
|
40
40
|
mimeType?: string;
|
|
41
41
|
/**
|
|
42
|
-
* Filename —
|
|
43
|
-
*
|
|
44
|
-
* expose a built-in title slot, so we don't render the filename
|
|
45
|
-
* inside the bubble itself.
|
|
42
|
+
* Filename — the download default name for the trailing action, and
|
|
43
|
+
* part of each control's accessible name.
|
|
46
44
|
*/
|
|
47
45
|
filename?: string;
|
|
48
46
|
/**
|
|
49
47
|
* Stacked audio. Takes precedence over `src` when set. Each item
|
|
50
|
-
* renders its own
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* attachment at a time.
|
|
48
|
+
* renders its own waveform row, vertically stacked inside the same
|
|
49
|
+
* bubble with an 8px gap. Sent + Received only — the composer surface
|
|
50
|
+
* accepts a single attachment at a time.
|
|
54
51
|
*/
|
|
55
52
|
items?: MessageAttachmentAudioItem[];
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* - Single audio (no `items`, or `items.length === 1`) →
|
|
61
|
-
* `'metadata'` so the native player surfaces duration
|
|
62
|
-
* immediately.
|
|
63
|
-
* - Stacked audio (`items.length > 1`) → `'none'` so a thread
|
|
64
|
-
* of voice memos doesn't fan out N parallel metadata requests
|
|
65
|
-
* on first paint.
|
|
66
|
-
*
|
|
67
|
-
* Per-track overrides live on `AudioItem.preload`.
|
|
68
|
-
*/
|
|
69
|
-
preload?: MediaPreloadMode;
|
|
53
|
+
/** See `AudioItem.durationSeconds`. Applies to the single-`src` shape. */
|
|
54
|
+
durationSeconds?: number;
|
|
55
|
+
/** See `AudioItem.waveformData`. Applies to the single-`src` shape. */
|
|
56
|
+
waveformData?: number[];
|
|
70
57
|
}
|
|
71
58
|
|
|
72
59
|
/**
|
|
@@ -994,7 +981,7 @@ declare type MediaPreloadMode = 'none' | 'metadata' | 'auto';
|
|
|
994
981
|
* - Pdf / Audio / File render their compact rows / players in a
|
|
995
982
|
* vertical stack with an 8px gap between rows. Each row keeps
|
|
996
983
|
* its own activation target — Pdf opens the carousel-aware
|
|
997
|
-
* `PdfViewer` at that index, Audio gets its own
|
|
984
|
+
* `PdfViewer` at that index, Audio gets its own waveform player,
|
|
998
985
|
* and File downloads the asset for that row.
|
|
999
986
|
*
|
|
1000
987
|
* The Composer surface accepts only a single attachment at a time,
|
|
@@ -1012,12 +999,29 @@ declare type MediaPreloadMode = 'none' | 'metadata' | 'auto';
|
|
|
1012
999
|
* download button to the chrome; the Video viewer relies on the
|
|
1013
1000
|
* native `<video controls>` download menu, and the Pdf viewer
|
|
1014
1001
|
* inherits the browser's built-in PDF download. Sibling-row Download
|
|
1015
|
-
* buttons on Pdf / File and the
|
|
1002
|
+
* buttons on Pdf / File and the Audio card's trailing action expose
|
|
1016
1003
|
* downloads outside the viewer too. The lightbox chrome is styled
|
|
1017
1004
|
* with plain CSS (`.mes-media-viewer*`) so the package's shipped
|
|
1018
1005
|
* stylesheet is enough to render the viewer correctly without a
|
|
1019
1006
|
* Tailwind dependency in the consumer.
|
|
1020
1007
|
*
|
|
1008
|
+
* # Audio requires a `<Channel>` ancestor
|
|
1009
|
+
*
|
|
1010
|
+
* `MessageAttachment.Audio.*` renders the waveform player card —
|
|
1011
|
+
* a 32px play toggle, a 35-bar waveform with a progress fill and a
|
|
1012
|
+
* native seek control, the duration, and a 32px trailing action
|
|
1013
|
+
* (Remove in the composer, Download once sent). Playback is
|
|
1014
|
+
* `stream-chat-react`'s pooled `AudioPlayer`, which is what supplies
|
|
1015
|
+
* single-player-at-a-time across a thread, pause-on-navigation and
|
|
1016
|
+
* saved-position restore across virtualized remounts.
|
|
1017
|
+
*
|
|
1018
|
+
* That pool is provided by `<Channel>`. Rendered outside one the card
|
|
1019
|
+
* still draws in full, but its play and seek controls are disabled —
|
|
1020
|
+
* so a story or a new consumer surface must mount `<Channel>` (or
|
|
1021
|
+
* `stream-chat-react`'s `WithAudioPlayback`) for the card to be
|
|
1022
|
+
* playable. The waveform bars themselves are styled with plain CSS
|
|
1023
|
+
* (`.mes-audio-wave*`), so the shipped stylesheet is enough.
|
|
1024
|
+
*
|
|
1021
1025
|
* # Lazy-loading defaults
|
|
1022
1026
|
*
|
|
1023
1027
|
* Media attachments bake in network-friendly defaults for long chat
|
|
@@ -1038,13 +1042,12 @@ declare type MediaPreloadMode = 'none' | 'metadata' | 'auto';
|
|
|
1038
1042
|
* `VideoItem.preload` per item). Once the viewer is open the
|
|
1039
1043
|
* active item bumps to `preload="metadata"` so duration / the
|
|
1040
1044
|
* first frame appear immediately.
|
|
1041
|
-
* - **Audio** —
|
|
1042
|
-
* `
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1046
|
-
*
|
|
1047
|
-
* (or `AudioItem.preload` per track).
|
|
1045
|
+
* - **Audio** — no audio bytes are fetched until the user presses
|
|
1046
|
+
* play: the pooled `AudioPlayer` acquires the shared `<audio>`
|
|
1047
|
+
* element lazily. A card whose `durationSeconds` is unknown makes
|
|
1048
|
+
* one `preload="metadata"` header request to fill the duration
|
|
1049
|
+
* slot; supplying `durationSeconds` (which `StreamAttachmentMessage`
|
|
1050
|
+
* maps from the attachment's persisted `duration`) avoids even that.
|
|
1048
1051
|
* - **Pdf** / **File** — no inline media element to throttle; the
|
|
1049
1052
|
* bubble renders compact rows that download / open lazily on
|
|
1050
1053
|
* activation. No `loading` / `preload` props on these surfaces.
|
|
@@ -1089,28 +1092,40 @@ export declare interface MessageAttachmentAudioComposerProps extends ComposerExt
|
|
|
1089
1092
|
src?: string;
|
|
1090
1093
|
mimeType?: string;
|
|
1091
1094
|
filename?: string;
|
|
1092
|
-
/** See `
|
|
1093
|
-
|
|
1095
|
+
/** See `AudioItem.durationSeconds`. */
|
|
1096
|
+
durationSeconds?: number;
|
|
1097
|
+
/** See `AudioItem.waveformData`. */
|
|
1098
|
+
waveformData?: number[];
|
|
1094
1099
|
}
|
|
1095
1100
|
|
|
1096
1101
|
/** A single audio file inside an Audio attachment (single or stacked). */
|
|
1097
1102
|
export declare interface MessageAttachmentAudioItem {
|
|
1098
1103
|
src: string;
|
|
1099
|
-
/** MIME type hint —
|
|
1104
|
+
/** MIME type hint — gates playability via the player's `canPlayType` probe. */
|
|
1100
1105
|
mimeType?: string;
|
|
1101
1106
|
/**
|
|
1102
|
-
* Filename —
|
|
1103
|
-
*
|
|
1107
|
+
* Filename — the download default name for the card's trailing
|
|
1108
|
+
* action, and part of the card's accessible name.
|
|
1104
1109
|
*/
|
|
1105
1110
|
filename?: string;
|
|
1106
1111
|
/**
|
|
1107
|
-
*
|
|
1108
|
-
* the
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
+
* Track length in seconds. Rendered as `MM:SS` before playback
|
|
1113
|
+
* starts, so the duration is correct on first paint with no network
|
|
1114
|
+
* request. Sourced from the Stream attachment's `duration` field,
|
|
1115
|
+
* which the composer writes at send time. When omitted the card
|
|
1116
|
+
* shows `--:--` until a one-shot `loadedmetadata` probe resolves.
|
|
1112
1117
|
*/
|
|
1113
|
-
|
|
1118
|
+
durationSeconds?: number;
|
|
1119
|
+
/**
|
|
1120
|
+
* Waveform amplitudes, each `0..1`, in track order. Resampled to the
|
|
1121
|
+
* rendered bar count, so any length is accepted. Sourced from the
|
|
1122
|
+
* Stream attachment's `waveform_data`, which the composer computes
|
|
1123
|
+
* from the local file and writes at send time.
|
|
1124
|
+
*
|
|
1125
|
+
* When omitted, the card renders a flat placeholder row: every bar at
|
|
1126
|
+
* the minimum height, with the progress fill still tracking playback.
|
|
1127
|
+
*/
|
|
1128
|
+
waveformData?: number[];
|
|
1114
1129
|
}
|
|
1115
1130
|
|
|
1116
1131
|
export declare type MessageAttachmentAudioReceivedProps = AudioAttachmentSharedProps;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, b as t, C as i, c as n, d as o, e as m, F as r, f as l, L as g, h as c, M, i as h, j as L, k as u, l as d, R as p, m as C, n as v, p as A, q as k, s as F, t as b, u as P, v as f, w as x, o as y, x as z, y as S, z as q, B, D, E, G as R } from "./index-
|
|
1
|
+
import { a as e, b as t, C as i, c as n, d as o, e as m, F as r, f as l, L as g, h as c, M, i as h, j as L, k as u, l as d, R as p, m as C, n as v, p as A, q as k, s as F, t as b, u as P, v as f, w as x, o as y, x as z, y as S, z as q, B, D, E, G as R } from "./index-C93LTGLc.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ActionButton,
|
|
4
4
|
t as Avatar,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0-rc-1787083791",
|
|
4
4
|
"description": "React messaging components built on messaging-core for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"chromatic:local": "yarn storybook:build && chromatic"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@linktr.ee/messaging-core": "
|
|
53
|
+
"@linktr.ee/messaging-core": "2.4.0-rc-1787083791",
|
|
54
54
|
"@linktr.ee/messaging-taxonomy": "^0.10.0",
|
|
55
55
|
"@phosphor-icons/react": "^2.1.10"
|
|
56
56
|
},
|
|
@@ -257,4 +257,37 @@ describe('StreamAttachmentMessage', () => {
|
|
|
257
257
|
)
|
|
258
258
|
).toHaveTextContent('from the trip')
|
|
259
259
|
})
|
|
260
|
+
|
|
261
|
+
it('forwards a persisted duration and waveform to the audio card', () => {
|
|
262
|
+
// Audio is sent as `type: "file"` with an `audio/*` mime type (for
|
|
263
|
+
// mobile compatibility), and the composer persists `duration` +
|
|
264
|
+
// `waveform_data` on the outgoing attachment. Without this
|
|
265
|
+
// passthrough every sent and received card would fall back to
|
|
266
|
+
// `--:--` and a flat placeholder even though the data is there.
|
|
267
|
+
renderWithProviders(
|
|
268
|
+
<StreamAttachmentMessage
|
|
269
|
+
groupPosition="single"
|
|
270
|
+
isMyMessage
|
|
271
|
+
message={message([
|
|
272
|
+
{
|
|
273
|
+
type: 'file',
|
|
274
|
+
asset_url: 'https://cdn.example.com/take-1.mp3',
|
|
275
|
+
mime_type: 'audio/mpeg',
|
|
276
|
+
title: 'take-1.mp3',
|
|
277
|
+
duration: 12,
|
|
278
|
+
waveform_data: [0.2, 0.9, 0.5, 0.7],
|
|
279
|
+
},
|
|
280
|
+
])}
|
|
281
|
+
/>
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
expect(screen.getByText('00:12')).toBeInTheDocument()
|
|
285
|
+
// The flat placeholder renders every bar at 0%; a real waveform is
|
|
286
|
+
// observable as bars drawn above that floor.
|
|
287
|
+
const barHeights = screen
|
|
288
|
+
.getAllByTestId('amplitude-bar')
|
|
289
|
+
.map((bar) => bar.style.getPropertyValue('--mes-audio-wave-bar'))
|
|
290
|
+
expect(barHeights).toHaveLength(35)
|
|
291
|
+
expect(barHeights.some((height) => height !== '0%')).toBe(true)
|
|
292
|
+
})
|
|
260
293
|
})
|
|
@@ -221,6 +221,12 @@ function mediaItemForKind(
|
|
|
221
221
|
src: trimToUndefined(attachment.asset_url) ?? '',
|
|
222
222
|
mimeType: trimToUndefined(attachment.mime_type) ?? 'audio/mpeg',
|
|
223
223
|
filename: trimToUndefined((attachment as { title?: string }).title),
|
|
224
|
+
// Both are first-class `stream-chat` Attachment fields. The
|
|
225
|
+
// composer writes them at send time so the waveform card paints
|
|
226
|
+
// the real amplitudes and the correct duration with no remote
|
|
227
|
+
// decode and no CDN CORS exposure.
|
|
228
|
+
durationSeconds: attachment.duration,
|
|
229
|
+
waveformData: attachment.waveform_data,
|
|
224
230
|
}
|
|
225
231
|
case 'pdf':
|
|
226
232
|
return {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { Meta, StoryFn } from '@storybook/react'
|
|
1
|
+
import type { Decorator, Meta, StoryFn } from '@storybook/react'
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import { Chat, WithAudioPlayback } from 'stream-chat-react'
|
|
3
4
|
|
|
5
|
+
import { createMockStreamChatClient } from '../../../testing/createMockStreamChatClient'
|
|
4
6
|
import {
|
|
5
7
|
StoryGrid,
|
|
6
8
|
StoryHeading,
|
|
@@ -14,27 +16,63 @@ import AudioAttachment from '.'
|
|
|
14
16
|
const AUDIO_SRC =
|
|
15
17
|
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
|
|
16
18
|
|
|
19
|
+
/** Resolves nothing, so the metadata probe never fills the duration slot. */
|
|
20
|
+
const UNREACHABLE_SRC = 'https://cdn.example.invalid/unreachable.mp3'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The exact amplitudes Figma draws (`2754:17710` → `sound-wave`
|
|
24
|
+
* `…;2754:8065`), as bar heights on the 30px track normalised to `0..1`.
|
|
25
|
+
* Rendering the design's own values is what makes these stories a usable
|
|
26
|
+
* pixel reference.
|
|
27
|
+
*/
|
|
28
|
+
const FIGMA_WAVEFORM = [
|
|
29
|
+
4, 4, 4, 4, 24, 20, 16, 21, 8, 9, 13, 15, 16, 21, 24, 18, 14, 11, 11, 11, 11,
|
|
30
|
+
11, 18, 24, 24, 30, 25, 21, 19, 16, 11, 12, 8, 4, 4,
|
|
31
|
+
].map((height) => height / 30)
|
|
32
|
+
|
|
17
33
|
const STACK_AUDIO: AudioItem[] = [
|
|
18
34
|
{
|
|
19
35
|
src: AUDIO_SRC,
|
|
20
36
|
mimeType: 'audio/mpeg',
|
|
21
37
|
filename: 'Take 1 — full mix.mp3',
|
|
38
|
+
durationSeconds: 12,
|
|
39
|
+
waveformData: FIGMA_WAVEFORM,
|
|
22
40
|
},
|
|
23
41
|
{
|
|
24
42
|
src: AUDIO_SRC,
|
|
25
43
|
mimeType: 'audio/mpeg',
|
|
26
44
|
filename: 'Take 2 — vocals isolated.mp3',
|
|
45
|
+
durationSeconds: 95,
|
|
46
|
+
waveformData: [...FIGMA_WAVEFORM].reverse(),
|
|
27
47
|
},
|
|
28
48
|
{
|
|
29
49
|
src: AUDIO_SRC,
|
|
30
50
|
mimeType: 'audio/mpeg',
|
|
31
51
|
filename: 'Take 3 — instrumental only.mp3',
|
|
52
|
+
durationSeconds: 3812,
|
|
32
53
|
},
|
|
33
54
|
]
|
|
34
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The card's playback comes from `stream-chat-react`'s pooled
|
|
58
|
+
* `AudioPlayer`, which `<Channel>` mounts in production. Mounting the
|
|
59
|
+
* pool directly gives the stories the same contract — real playback,
|
|
60
|
+
* one player at a time across the page — without needing a channel that
|
|
61
|
+
* can `watch()` against a backend. Without it every play / seek control
|
|
62
|
+
* renders disabled.
|
|
63
|
+
*/
|
|
64
|
+
const withAudioPlayback: Decorator = (Story) => (
|
|
65
|
+
<Chat client={createMockStreamChatClient({ id: 'story-user' })}>
|
|
66
|
+
<WithAudioPlayback>
|
|
67
|
+
<Story />
|
|
68
|
+
</WithAudioPlayback>
|
|
69
|
+
</Chat>
|
|
70
|
+
)
|
|
71
|
+
|
|
35
72
|
const meta: Meta = {
|
|
36
73
|
title: 'MessageAttachment/Audio',
|
|
37
74
|
parameters: { layout: 'fullscreen' },
|
|
75
|
+
decorators: [withAudioPlayback],
|
|
38
76
|
}
|
|
39
77
|
export default meta
|
|
40
78
|
|
|
@@ -43,16 +81,16 @@ const handleDismiss = () => {
|
|
|
43
81
|
}
|
|
44
82
|
|
|
45
83
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
84
|
+
* The design's waveform player card: a 299×72 bubble holding a 32px
|
|
85
|
+
* play toggle, a 35-bar waveform with a progress fill and an invisible
|
|
86
|
+
* native seek control over it, the duration, and a 32px trailing action
|
|
87
|
+
* — Remove in the composer, Download once sent.
|
|
50
88
|
*/
|
|
51
89
|
export const Single: StoryFn = () => (
|
|
52
90
|
<StoryPage>
|
|
53
91
|
<StoryHeading
|
|
54
92
|
title="Audio attachment"
|
|
55
|
-
description="
|
|
93
|
+
description="Play to watch the fill advance bar by bar and the duration count up; click or drag the waveform to seek, or Tab to it and use the arrow keys. Starting a second card pauses the first — one player at a time across the whole thread."
|
|
56
94
|
/>
|
|
57
95
|
<StoryGrid>
|
|
58
96
|
<StoryRow
|
|
@@ -62,6 +100,8 @@ export const Single: StoryFn = () => (
|
|
|
62
100
|
src={AUDIO_SRC}
|
|
63
101
|
mimeType="audio/mpeg"
|
|
64
102
|
filename="Morning meditation.mp3"
|
|
103
|
+
durationSeconds={12}
|
|
104
|
+
waveformData={FIGMA_WAVEFORM}
|
|
65
105
|
onDismiss={handleDismiss}
|
|
66
106
|
/>
|
|
67
107
|
}
|
|
@@ -70,6 +110,8 @@ export const Single: StoryFn = () => (
|
|
|
70
110
|
src={AUDIO_SRC}
|
|
71
111
|
mimeType="audio/mpeg"
|
|
72
112
|
filename="Morning meditation.mp3"
|
|
113
|
+
durationSeconds={12}
|
|
114
|
+
waveformData={FIGMA_WAVEFORM}
|
|
73
115
|
/>
|
|
74
116
|
}
|
|
75
117
|
received={
|
|
@@ -77,6 +119,8 @@ export const Single: StoryFn = () => (
|
|
|
77
119
|
src={AUDIO_SRC}
|
|
78
120
|
mimeType="audio/mpeg"
|
|
79
121
|
filename="Morning meditation.mp3"
|
|
122
|
+
durationSeconds={12}
|
|
123
|
+
waveformData={FIGMA_WAVEFORM}
|
|
80
124
|
/>
|
|
81
125
|
}
|
|
82
126
|
/>
|
|
@@ -84,20 +128,28 @@ export const Single: StoryFn = () => (
|
|
|
84
128
|
</StoryPage>
|
|
85
129
|
)
|
|
86
130
|
|
|
87
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Every case where no amplitudes can be produced — a decode failure, an
|
|
133
|
+
* unsupported codec, a file over the composer's decode ceiling, a clip
|
|
134
|
+
* re-staged from *Recently shared* (whose `File` is a zero-byte
|
|
135
|
+
* placeholder), or anything sent before `waveform_data` was persisted
|
|
136
|
+
* (legacy web, the mobile app, a visitor).
|
|
137
|
+
*/
|
|
138
|
+
export const NoWaveform: StoryFn = () => (
|
|
88
139
|
<StoryPage>
|
|
89
140
|
<StoryHeading
|
|
90
|
-
title="
|
|
91
|
-
description="
|
|
141
|
+
title="Degraded states"
|
|
142
|
+
description="The waveform falls back to every bar at the design's 4px minimum, so the card keeps exactly the same silhouette and the progress fill still tracks playback. An unknown duration holds its slot as `--:--` until a metadata probe resolves it."
|
|
92
143
|
/>
|
|
93
144
|
<StoryGrid>
|
|
94
145
|
<StoryRow
|
|
95
|
-
label="
|
|
146
|
+
label="No waveform"
|
|
96
147
|
composer={
|
|
97
148
|
<AudioAttachment.Composer
|
|
98
149
|
src={AUDIO_SRC}
|
|
99
150
|
mimeType="audio/mpeg"
|
|
100
|
-
filename="
|
|
151
|
+
filename="Voice note.m4a"
|
|
152
|
+
durationSeconds={12}
|
|
101
153
|
onDismiss={handleDismiss}
|
|
102
154
|
/>
|
|
103
155
|
}
|
|
@@ -105,87 +157,100 @@ export const SingleWithText: StoryFn = () => (
|
|
|
105
157
|
<AudioAttachment.Sent
|
|
106
158
|
src={AUDIO_SRC}
|
|
107
159
|
mimeType="audio/mpeg"
|
|
108
|
-
filename="
|
|
109
|
-
|
|
160
|
+
filename="Voice note.m4a"
|
|
161
|
+
durationSeconds={12}
|
|
110
162
|
/>
|
|
111
163
|
}
|
|
112
164
|
received={
|
|
113
165
|
<AudioAttachment.Received
|
|
114
166
|
src={AUDIO_SRC}
|
|
115
167
|
mimeType="audio/mpeg"
|
|
116
|
-
filename="
|
|
117
|
-
|
|
168
|
+
filename="Voice note.m4a"
|
|
169
|
+
durationSeconds={12}
|
|
118
170
|
/>
|
|
119
171
|
}
|
|
120
172
|
/>
|
|
121
|
-
</StoryGrid>
|
|
122
|
-
</StoryPage>
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Stacked audio attachments — multiple clips inside one bubble. Each
|
|
127
|
-
* clip renders its own native `<audio controls>` player, vertically
|
|
128
|
-
* stacked with a hairline separator.
|
|
129
|
-
*
|
|
130
|
-
* Sent + Received only — the composer surface accepts a single
|
|
131
|
-
* attachment at a time.
|
|
132
|
-
*/
|
|
133
|
-
export const Stacked: StoryFn = () => (
|
|
134
|
-
<StoryPage>
|
|
135
|
-
<StoryHeading
|
|
136
|
-
title="Stacked audio"
|
|
137
|
-
description="Each clip plays independently with its own native controls. Composer column is intentionally blank — the composer accepts a single attachment at a time."
|
|
138
|
-
/>
|
|
139
|
-
<StoryGrid>
|
|
140
173
|
<StoryRow
|
|
141
|
-
label="
|
|
174
|
+
label="No waveform, no duration"
|
|
142
175
|
composer={
|
|
143
176
|
<AudioAttachment.Composer
|
|
144
|
-
src={
|
|
145
|
-
|
|
146
|
-
filename="Morning meditation.mp3"
|
|
177
|
+
src={UNREACHABLE_SRC}
|
|
178
|
+
filename="Unreachable.mp3"
|
|
147
179
|
onDismiss={handleDismiss}
|
|
148
180
|
/>
|
|
149
181
|
}
|
|
150
|
-
sent={
|
|
151
|
-
|
|
182
|
+
sent={
|
|
183
|
+
<AudioAttachment.Sent
|
|
184
|
+
src={UNREACHABLE_SRC}
|
|
185
|
+
filename="Unreachable.mp3"
|
|
186
|
+
/>
|
|
187
|
+
}
|
|
188
|
+
received={
|
|
189
|
+
<AudioAttachment.Received
|
|
190
|
+
src={UNREACHABLE_SRC}
|
|
191
|
+
filename="Unreachable.mp3"
|
|
192
|
+
/>
|
|
193
|
+
}
|
|
152
194
|
/>
|
|
153
195
|
<StoryRow
|
|
154
|
-
label="
|
|
196
|
+
label="Unplayable codec"
|
|
155
197
|
composer={
|
|
156
198
|
<AudioAttachment.Composer
|
|
157
199
|
src={AUDIO_SRC}
|
|
158
|
-
mimeType="audio/
|
|
159
|
-
filename="
|
|
200
|
+
mimeType="audio/x-made-up"
|
|
201
|
+
filename="Master.aiff"
|
|
202
|
+
durationSeconds={12}
|
|
203
|
+
waveformData={FIGMA_WAVEFORM}
|
|
160
204
|
onDismiss={handleDismiss}
|
|
161
205
|
/>
|
|
162
206
|
}
|
|
163
|
-
sent={
|
|
164
|
-
|
|
207
|
+
sent={
|
|
208
|
+
<AudioAttachment.Sent
|
|
209
|
+
src={AUDIO_SRC}
|
|
210
|
+
mimeType="audio/x-made-up"
|
|
211
|
+
filename="Master.aiff"
|
|
212
|
+
durationSeconds={12}
|
|
213
|
+
waveformData={FIGMA_WAVEFORM}
|
|
214
|
+
/>
|
|
215
|
+
}
|
|
216
|
+
received={
|
|
217
|
+
<AudioAttachment.Received
|
|
218
|
+
src={AUDIO_SRC}
|
|
219
|
+
mimeType="audio/x-made-up"
|
|
220
|
+
filename="Master.aiff"
|
|
221
|
+
durationSeconds={12}
|
|
222
|
+
waveformData={FIGMA_WAVEFORM}
|
|
223
|
+
/>
|
|
224
|
+
}
|
|
165
225
|
/>
|
|
166
226
|
</StoryGrid>
|
|
167
227
|
</StoryPage>
|
|
168
228
|
)
|
|
169
229
|
|
|
170
|
-
/**
|
|
171
|
-
*
|
|
172
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Stacked audio — multiple clips clustered into one bubble, as
|
|
232
|
+
* `StreamAttachmentMessage` does for consecutive same-kind attachments.
|
|
233
|
+
* Sent + Received only: the composer accepts one attachment at a time.
|
|
234
|
+
*/
|
|
235
|
+
export const Stacked: StoryFn = () => (
|
|
173
236
|
<StoryPage>
|
|
174
237
|
<StoryHeading
|
|
175
|
-
title="Stacked audio
|
|
176
|
-
description="
|
|
238
|
+
title="Stacked audio"
|
|
239
|
+
description="Each clip is its own row and its own player, stacked with an 8px gap. Only one can play at a time. The third row has no waveform, so it shows the flat fallback beside two real ones."
|
|
177
240
|
/>
|
|
178
241
|
<StoryGrid>
|
|
242
|
+
<StoryRow
|
|
243
|
+
label="2 clips"
|
|
244
|
+
sent={<AudioAttachment.Sent items={STACK_AUDIO.slice(0, 2)} />}
|
|
245
|
+
received={<AudioAttachment.Received items={STACK_AUDIO.slice(0, 2)} />}
|
|
246
|
+
/>
|
|
247
|
+
<StoryRow
|
|
248
|
+
label="3 clips"
|
|
249
|
+
sent={<AudioAttachment.Sent items={STACK_AUDIO} />}
|
|
250
|
+
received={<AudioAttachment.Received items={STACK_AUDIO} />}
|
|
251
|
+
/>
|
|
179
252
|
<StoryRow
|
|
180
253
|
label="3 clips + caption"
|
|
181
|
-
composer={
|
|
182
|
-
<AudioAttachment.Composer
|
|
183
|
-
src={AUDIO_SRC}
|
|
184
|
-
mimeType="audio/mpeg"
|
|
185
|
-
filename="Morning meditation.mp3"
|
|
186
|
-
onDismiss={handleDismiss}
|
|
187
|
-
/>
|
|
188
|
-
}
|
|
189
254
|
sent={
|
|
190
255
|
<AudioAttachment.Sent
|
|
191
256
|
items={STACK_AUDIO}
|
|
@@ -202,3 +267,45 @@ export const StackedWithText: StoryFn = () => (
|
|
|
202
267
|
</StoryGrid>
|
|
203
268
|
</StoryPage>
|
|
204
269
|
)
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The card keeps a uniform 20px radius in every grouping position — a
|
|
273
|
+
* 32px control tucked into a 4px flattened corner would collide with it
|
|
274
|
+
* — so a run of audio bubbles reads as a stack of identical cards.
|
|
275
|
+
*/
|
|
276
|
+
export const Grouped: StoryFn = () => (
|
|
277
|
+
<StoryPage>
|
|
278
|
+
<StoryHeading
|
|
279
|
+
title="Same-author run"
|
|
280
|
+
description="`groupPosition` still serialises onto the bubble, but the audio card opts out of the corner flattening the text bubbles apply."
|
|
281
|
+
/>
|
|
282
|
+
<StoryGrid>
|
|
283
|
+
{(['first', 'middle', 'end'] as const).map((groupPosition) => (
|
|
284
|
+
<StoryRow
|
|
285
|
+
key={groupPosition}
|
|
286
|
+
label={groupPosition}
|
|
287
|
+
sent={
|
|
288
|
+
<AudioAttachment.Sent
|
|
289
|
+
src={AUDIO_SRC}
|
|
290
|
+
mimeType="audio/mpeg"
|
|
291
|
+
filename="Morning meditation.mp3"
|
|
292
|
+
durationSeconds={12}
|
|
293
|
+
waveformData={FIGMA_WAVEFORM}
|
|
294
|
+
groupPosition={groupPosition}
|
|
295
|
+
/>
|
|
296
|
+
}
|
|
297
|
+
received={
|
|
298
|
+
<AudioAttachment.Received
|
|
299
|
+
src={AUDIO_SRC}
|
|
300
|
+
mimeType="audio/mpeg"
|
|
301
|
+
filename="Morning meditation.mp3"
|
|
302
|
+
durationSeconds={12}
|
|
303
|
+
waveformData={FIGMA_WAVEFORM}
|
|
304
|
+
groupPosition={groupPosition}
|
|
305
|
+
/>
|
|
306
|
+
}
|
|
307
|
+
/>
|
|
308
|
+
))}
|
|
309
|
+
</StoryGrid>
|
|
310
|
+
</StoryPage>
|
|
311
|
+
)
|