Repository navigation
Description
Problem Statement
On Slack, thread.post({ markdown, files }) produces two messages. The adapter's postMessage first calls uploadFiles, which runs files.completeUploadExternal with channel_id and thread_ts, so Slack posts the files as their own thread message; it then posts the text with chat.postMessage. In a thread the reader sees two bubbles per post, the text below the images it belongs to.
Slack also posts the file message asynchronously: completeUploadExternal returns before the file message is visible. When an app posts two messages in a row, each with text and images, the thread can end up ordered text, text, image, image.
Proposed Solution
When a Slack post carries image files, upload them privately and reference them from the text message's blocks, so one chat.postMessage carries both:
files.completeUploadExternalwith nochannel_id. The files stay private to the bot and Slack posts nothing for them.chat.postMessagewithblocks: amarkdownblock holding the text, followed by oneimageblock per file usingslack_file: { id }.
The post is synchronous, so thread order follows call order, and the markdown rendering of the text is kept. SentMessage.id becomes the real ts of the one message, so callers no longer get the synthetic file-{timestamp} id for a files-only post.
One Slack behaviour the adapter would need to absorb: for a short window after completeUploadExternal returns (about 500 ms in our runs), chat.postMessage rejects the reference with invalid_blocks and an error entry naming slack_file. A short bounded retry on that specific error addresses this.
The markdown block has the same 12,000-character cap as markdown_text, cumulative across the markdown blocks of one payload, so the existing fallback for long text still applies. Non-image files cannot go in an image block, so a post that carries them would keep the current path, or the behaviour could be opt-in through a post option.
Alternatives Considered
Calling the Slack Web API directly from our app: this is what we do today for this one path, which means bypassing the adapter's postMessage and reimplementing its token handling for one message type.
Use Case
import { Chat } from "chat";
// Today this is two Slack messages (files, then text); the request is
// for one message with the text above the images.
await bot.thread(threadId).post({
markdown: noteText,
files: [
{ data: screenshotA, filename: "before.png", mimeType: "image/png" },
{ data: screenshotB, filename: "after.png", mimeType: "image/png" },
],
});Priority
Nice to have
Contribution
- I am willing to help implement this feature
Additional Context
No response
Activity