Summary
Two independent bugs make received chat attachments unusable in <Chat> / <ChatEntry>:
attachedFiles is ordered by download-completion, not by send order — setupChat combines the per-attachment futures with mergeMap, which emits as each promise settles. Send three files and they render in whatever order they finished downloading (i.e. smallest first).
- A received attachment has no MIME type, because it is reconstructed as
new File(buffer, fileName) with no type argument — yet ChatEntry renders attachments only when file.type.startsWith('image/'). That condition is therefore never true for a received file, so <ChatEntry> renders nothing for an incoming image.
Both are in current main. (2) makes (1) invisible with the stock components, but any app with its own entry renderer hits the ordering bug immediately — which is how we found it.
1. Ordering — mergeMap should be concatMap
https://github.com/livekit/components-js/blob/main/packages/core/src/components/chat.ts#L122-L136
return from(attachments.values()).pipe(
mergeMap((attachment) => from(attachment.promise)), // <-- emits in COMPLETION order
scan(
(acc, attachment) => [...acc, new File(attachment.buffer, attachment.fileName)],
[] as Array<File>,
),
map((attachedFiles) => ({ chunk, attachedFiles })),
);
attachments is a Map built from reader.info.attachedStreamIds, so it is already in the sender's order. mergeMap subscribes to all the promises concurrently and emits each as it resolves, so the scan accumulates them in resolution order. Replacing it with concatMap preserves the map's order at no cost (all the promises are already in flight; concatMap only sequences the emissions).
Observed (LiveKit Cloud, livekit-client 2.21.0, @livekit/components-react 2.9.23):
| Sent |
Rendered |
first.png (large), second.png, third.png |
second, third, first |
image1.png (large), doc.pdf (largest), image2.png (small) |
image2, image1, doc.pdf |
The pattern is exactly ascending file size. A single-file message is of course unaffected, which is probably why this hasn't been reported.
Repro
// sender
await room.localParticipant.sendText('three files', {
topic: 'lk.chat',
attachments: [bigFile, smallFile, tinyFile], // ~5 MB, ~50 KB, ~5 KB
});
// receiver
const { chatMessages } = useChat();
// chatMessages.at(-1).attachedFiles.map(f => f.name)
// expected: [big, small, tiny]
// actual: [tiny, small, big]
2. Received files lose their MIME type
https://github.com/livekit/components-js/blob/main/packages/core/src/components/chat.ts#L130
new File(attachment.buffer, attachment.fileName)
// ^ no { type } — File.type === ''
The byte stream's reader.info.mimeType is available at the point the future is resolved (the byte-stream handler already reads reader.info.name from it), so it could be threaded through:
// byte-stream handler
attachment.resolve?.({ fileName: reader.info.name, mimeType: reader.info.mimeType, buffer: bufferList });
// text-stream handler
new File(attachment.buffer, attachment.fileName, { type: attachment.mimeType })
Consequence in ChatEntry:
https://github.com/livekit/components-js/blob/main/packages/react/src/components/ChatEntry.tsx#L75-L77
{entry.attachedFiles?.map(
(file) =>
file.type.startsWith('image/') && ( // always false for a received file
So a received image renders as nothing, and non-image attachments are dropped entirely (there is no branch for them). The sender's own local echo does carry a real type (it is the File from the OS picker), so this looks like it works when you test by sending to yourself in one tab — the asymmetry is easy to miss.
Environment
@livekit/components-react 2.9.23, @livekit/components-core 0.12.14, livekit-client 2.21.0
- LiveKit Cloud (server 1.13.4), Chrome, multi-participant room (visitor + agent + human operator)
- Verified against
main as of today, not just the published build.
Workaround (for anyone hitting this before a fix)
- Ordering: have the sender put the intended order in a
sendText attributes entry (e.g. newline-joined filenames) and sort attachedFiles by it on receive. Forward-compatible — once concatMap lands, the sort is a no-op.
- MIME type: infer from the filename extension when
file.type is empty, and render non-images as a download link.
Happy to open a PR for either or both if that is useful — the mergeMap → concatMap change is one line.
Summary
Two independent bugs make received chat attachments unusable in
<Chat>/<ChatEntry>:attachedFilesis ordered by download-completion, not by send order —setupChatcombines the per-attachment futures withmergeMap, which emits as each promise settles. Send three files and they render in whatever order they finished downloading (i.e. smallest first).new File(buffer, fileName)with notypeargument — yetChatEntryrenders attachments only whenfile.type.startsWith('image/'). That condition is therefore never true for a received file, so<ChatEntry>renders nothing for an incoming image.Both are in current
main. (2) makes (1) invisible with the stock components, but any app with its own entry renderer hits the ordering bug immediately — which is how we found it.1. Ordering —
mergeMapshould beconcatMaphttps://github.com/livekit/components-js/blob/main/packages/core/src/components/chat.ts#L122-L136
attachmentsis aMapbuilt fromreader.info.attachedStreamIds, so it is already in the sender's order.mergeMapsubscribes to all the promises concurrently and emits each as it resolves, so thescanaccumulates them in resolution order. Replacing it withconcatMappreserves the map's order at no cost (all the promises are already in flight;concatMaponly sequences the emissions).Observed (LiveKit Cloud,
livekit-client2.21.0,@livekit/components-react2.9.23):first.png(large),second.png,third.pngimage1.png(large),doc.pdf(largest),image2.png(small)The pattern is exactly ascending file size. A single-file message is of course unaffected, which is probably why this hasn't been reported.
Repro
2. Received files lose their MIME type
https://github.com/livekit/components-js/blob/main/packages/core/src/components/chat.ts#L130
The byte stream's
reader.info.mimeTypeis available at the point the future is resolved (the byte-stream handler already readsreader.info.namefrom it), so it could be threaded through:Consequence in
ChatEntry:https://github.com/livekit/components-js/blob/main/packages/react/src/components/ChatEntry.tsx#L75-L77
So a received image renders as nothing, and non-image attachments are dropped entirely (there is no branch for them). The sender's own local echo does carry a real
type(it is theFilefrom the OS picker), so this looks like it works when you test by sending to yourself in one tab — the asymmetry is easy to miss.Environment
@livekit/components-react2.9.23,@livekit/components-core0.12.14,livekit-client2.21.0mainas of today, not just the published build.Workaround (for anyone hitting this before a fix)
sendTextattributesentry (e.g. newline-joined filenames) and sortattachedFilesby it on receive. Forward-compatible — onceconcatMaplands, the sort is a no-op.file.typeis empty, and render non-images as a download link.Happy to open a PR for either or both if that is useful — the
mergeMap→concatMapchange is one line.