mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-10 22:01:16 +00:00
fix: bot send confirmation, preview picker dialog nesting, upload button
- Bot: wait for updateMessageSendSucceeded/Failed before marking send complete (was returning on temp message, actual send was async) - Preview picker: move ArchivePreviewPicker outside parent Dialog to fix Radix nested dialog focus trap conflict - Upload: add explicit "Upload Preview" button always visible in the action bar alongside "Pick Preview" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,11 @@ export async function closeBotClient(): Promise<void> {
|
|||||||
*
|
*
|
||||||
* The fromChatId is the Telegram chat ID from the DB (e.g. -1003767441152).
|
* The fromChatId is the Telegram chat ID from the DB (e.g. -1003767441152).
|
||||||
* The messageId is the TDLib message ID stored in the DB.
|
* The messageId is the TDLib message ID stored in the DB.
|
||||||
|
*
|
||||||
|
* IMPORTANT: forwardMessages with send_copy returns a *temporary* message
|
||||||
|
* synchronously. The actual file copy/send is asynchronous inside TDLib.
|
||||||
|
* We must listen for updateMessageSendSucceeded / updateMessageSendFailed
|
||||||
|
* to know whether the message actually reached the user.
|
||||||
*/
|
*/
|
||||||
export async function copyMessageToUser(
|
export async function copyMessageToUser(
|
||||||
fromChatId: bigint,
|
fromChatId: bigint,
|
||||||
@@ -94,25 +99,114 @@ export async function copyMessageToUser(
|
|||||||
log.warn({ err, chatId: fromChatId.toString() }, "getChat failed for source channel");
|
log.warn({ err, chatId: fromChatId.toString() }, "getChat failed for source channel");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await withFloodWait(
|
// Wait for the actual send to complete, not just the temporary message.
|
||||||
() =>
|
// Pattern mirrors worker/src/upload/channel.ts sendAndWaitForUpload.
|
||||||
c.invoke({
|
await new Promise<void>((resolve, reject) => {
|
||||||
_: "forwardMessages",
|
let settled = false;
|
||||||
chat_id: Number(toUserId),
|
let tempMsgId: number | null = null;
|
||||||
from_chat_id: Number(fromChatId),
|
|
||||||
message_ids: [Number(messageId)],
|
|
||||||
send_copy: true,
|
|
||||||
remove_caption: false,
|
|
||||||
}),
|
|
||||||
"copyMessageToUser"
|
|
||||||
);
|
|
||||||
|
|
||||||
// forwardMessages returns immediately with temp messages — check result
|
// Timeout: 5 minutes for the copy to complete
|
||||||
const messages = (result as { messages?: unknown[] })?.messages;
|
const TIMEOUT_MS = 5 * 60_000;
|
||||||
log.info(
|
const timer = setTimeout(() => {
|
||||||
{ messageCount: messages?.length ?? 0, result: JSON.stringify(result).slice(0, 500) },
|
if (!settled) {
|
||||||
"forwardMessages result"
|
settled = true;
|
||||||
);
|
cleanup();
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`copyMessageToUser timed out after ${TIMEOUT_MS / 60_000}min ` +
|
||||||
|
`(from=${fromChatId}, msg=${messageId}, to=${toUserId})`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, TIMEOUT_MS);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const handleUpdate = (update: any) => {
|
||||||
|
if (update?._ === "updateMessageSendSucceeded") {
|
||||||
|
const oldMsgId = update.old_message_id;
|
||||||
|
if (tempMsgId !== null && oldMsgId === tempMsgId) {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
const finalId = update.message?.id;
|
||||||
|
log.info(
|
||||||
|
{ tempMsgId, finalMsgId: finalId, toUserId: toUserId.toString() },
|
||||||
|
"Message copy confirmed by Telegram"
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update?._ === "updateMessageSendFailed") {
|
||||||
|
const oldMsgId = update.old_message_id;
|
||||||
|
if (tempMsgId !== null && oldMsgId === tempMsgId) {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
const errorMsg = update.error?.message ?? "Unknown send error";
|
||||||
|
const errorCode = update.error?.code ?? 0;
|
||||||
|
log.error(
|
||||||
|
{ tempMsgId, errorCode, errorMsg, toUserId: toUserId.toString() },
|
||||||
|
"Message copy failed"
|
||||||
|
);
|
||||||
|
reject(new Error(`copyMessageToUser failed: [${errorCode}] ${errorMsg}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
c.off("update", handleUpdate);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach listener BEFORE sending to avoid missing fast completions
|
||||||
|
c.on("update", handleUpdate);
|
||||||
|
|
||||||
|
// Send the copy — returns a temporary message immediately
|
||||||
|
withFloodWait(
|
||||||
|
() =>
|
||||||
|
c.invoke({
|
||||||
|
_: "forwardMessages",
|
||||||
|
chat_id: Number(toUserId),
|
||||||
|
from_chat_id: Number(fromChatId),
|
||||||
|
message_ids: [Number(messageId)],
|
||||||
|
send_copy: true,
|
||||||
|
remove_caption: false,
|
||||||
|
}),
|
||||||
|
"copyMessageToUser"
|
||||||
|
)
|
||||||
|
.then((result) => {
|
||||||
|
// forwardMessages returns { messages: [tempMsg, ...] }
|
||||||
|
const messages = (result as { messages?: Array<{ id: number }> })?.messages;
|
||||||
|
if (messages && messages.length > 0 && messages[0]) {
|
||||||
|
tempMsgId = messages[0].id;
|
||||||
|
log.debug(
|
||||||
|
{ tempMsgId, toUserId: toUserId.toString() },
|
||||||
|
"forwardMessages returned temp message, waiting for send confirmation"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// No temp message returned — likely an error in the API call itself
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`forwardMessages returned no messages (result: ${JSON.stringify(result).slice(0, 300)})`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ export function PackageFilesDrawer({ pkg, open, onOpenChange }: PackageFilesDraw
|
|||||||
}, [filtered]);
|
}, [filtered]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-2xl max-h-[80vh] flex flex-col gap-0 p-0">
|
<DialogContent className="sm:max-w-2xl max-h-[80vh] flex flex-col gap-0 p-0">
|
||||||
<DialogHeader className="px-6 pt-6 pb-4 border-b border-border space-y-3">
|
<DialogHeader className="px-6 pt-6 pb-4 border-b border-border space-y-3">
|
||||||
@@ -408,6 +409,20 @@ export function PackageFilesDrawer({ pkg, open, onOpenChange }: PackageFilesDraw
|
|||||||
Pick Preview
|
Pick Preview
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 gap-1.5 text-xs"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={uploading}
|
||||||
|
>
|
||||||
|
{uploading ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Upload className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
Upload Preview
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -512,19 +527,21 @@ export function PackageFilesDrawer({ pkg, open, onOpenChange }: PackageFilesDraw
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
{/* Archive preview picker modal */}
|
|
||||||
{pkg && pkg.archiveType !== "DOCUMENT" && !pkg.isMultipart && (
|
|
||||||
<ArchivePreviewPicker
|
|
||||||
packageId={pkg.id}
|
|
||||||
packageName={pkg.fileName}
|
|
||||||
open={showPreviewPicker}
|
|
||||||
onOpenChange={setShowPreviewPicker}
|
|
||||||
onPreviewSet={() => {
|
|
||||||
// Refresh the preview by setting a cache-busting URL
|
|
||||||
setLocalPreviewUrl(`/api/zips/${pkg.id}/preview?t=${Date.now()}`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Archive preview picker modal — rendered as sibling to avoid nested Dialog issues */}
|
||||||
|
{pkg && pkg.archiveType !== "DOCUMENT" && !pkg.isMultipart && (
|
||||||
|
<ArchivePreviewPicker
|
||||||
|
packageId={pkg.id}
|
||||||
|
packageName={pkg.fileName}
|
||||||
|
open={showPreviewPicker}
|
||||||
|
onOpenChange={setShowPreviewPicker}
|
||||||
|
onPreviewSet={() => {
|
||||||
|
// Refresh the preview by setting a cache-busting URL
|
||||||
|
setLocalPreviewUrl(`/api/zips/${pkg.id}/preview?t=${Date.now()}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user