Skip to content

Chat attachments: attachedFiles ordered by download completion (mergeMap), and received files lose their MIME type #1418

Description

@vgerasymenko

Summary

Two independent bugs make received chat attachments unusable in <Chat> / <ChatEntry>:

  1. attachedFiles is ordered by download-completion, not by send ordersetupChat 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).
  2. 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 mergeMapconcatMap change is one line.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions