terry-staging #4

Merged
terry merged 2 commits from terry-staging into main 2026-05-27 05:18:47 +00:00
2 changed files with 20 additions and 76 deletions

View File

@@ -27,20 +27,29 @@ export function MessageBubble({ post }: { post: Post }) {
const { lang } = useI18n(); const { lang } = useI18n();
const Bubble = pickBubble(post); const Bubble = pickBubble(post);
const isTextOnly = post.attachments.length === 0; const isTextOnly = post.attachments.length === 0;
const isVisual = post.attachments.some(
(a) => a.kind === "image" || a.kind === "video",
);
return ( return (
<article <div
id={`post-${post.id}`} id={`post-${post.id}`}
className={`relative mx-auto w-full max-w-[380px] rounded-2xl bg-ark-panel text-left shadow-sm md:max-w-[680px] lg:max-w-[900px] xl:max-w-[1120px] ${isTextOnly ? "px-3 py-2" : "p-2"}`} className="mx-auto w-full max-w-[380px] md:max-w-[680px] lg:max-w-[900px] xl:max-w-[1120px]"
> >
<Bubble post={post} /> <article
<time className={`relative rounded-2xl bg-ark-panel text-left shadow-sm ${
dateTime={post.publishedAt} isVisual ? "w-full" : "w-fit max-w-full"
className="ml-2 mt-1 inline-block float-right text-[10.5px] leading-none text-neutral-500" } ${isTextOnly ? "px-3 py-2" : "p-2"}`}
> >
{formatDateTime(post.publishedAt, lang)} <Bubble post={post} />
</time> <time
<span className="block clear-both" /> dateTime={post.publishedAt}
</article> className="ml-2 mt-1 inline-block float-right text-[10.5px] leading-none text-neutral-500"
>
{formatDateTime(post.publishedAt, lang)}
</time>
<span className="block clear-both" />
</article>
</div>
); );
} }

View File

@@ -1,27 +1,5 @@
import { assetUrl } from "../../../api"; import { assetUrl } from "../../../api";
type SaveFilePicker = (options?: {
suggestedName?: string;
types?: Array<{
description?: string;
accept: Record<string, string[]>;
}>;
}) => Promise<{
createWritable: () => Promise<{
write: (data: Blob) => Promise<void>;
close: () => Promise<void>;
}>;
}>;
type NavigatorWithFileShare = Navigator & {
canShare?: (data: { files?: File[] }) => boolean;
share?: (data: { files?: File[]; title?: string }) => Promise<void>;
};
type WindowWithSavePicker = Window & {
showSaveFilePicker?: SaveFilePicker;
};
export function attachmentDownloadUrl(postId: string, attachmentId: string) { export function attachmentDownloadUrl(postId: string, attachmentId: string) {
return assetUrl( return assetUrl(
`/api/posts/${encodeURIComponent(postId)}/attachments/${encodeURIComponent( `/api/posts/${encodeURIComponent(postId)}/attachments/${encodeURIComponent(
@@ -39,45 +17,7 @@ export async function downloadAttachment(
} }
export async function downloadFile(url: string, filename: string) { export async function downloadFile(url: string, filename: string) {
const res = await fetch(url, { credentials: "include" }); triggerDownload(url, filename || "download");
if (!res.ok) throw new Error(await res.text());
const blob = await res.blob();
const safeName = filename || "download";
if (window.isSecureContext) {
const picker = (window as WindowWithSavePicker).showSaveFilePicker;
if (picker) {
const handle = await picker({
suggestedName: safeName,
types: blob.type
? [
{
description: "File",
accept: { [blob.type]: [extensionFromName(safeName)] },
},
]
: undefined,
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return;
}
}
const file = new File([blob], safeName, {
type: blob.type || "application/octet-stream",
});
const nav = navigator as NavigatorWithFileShare;
if (nav.canShare?.({ files: [file] }) && nav.share) {
await nav.share({ files: [file], title: safeName });
return;
}
const objectUrl = URL.createObjectURL(blob);
triggerDownload(objectUrl, safeName);
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
} }
function triggerDownload(url: string, filename: string) { function triggerDownload(url: string, filename: string) {
@@ -89,8 +29,3 @@ function triggerDownload(url: string, filename: string) {
a.click(); a.click();
a.remove(); a.remove();
} }
function extensionFromName(filename: string) {
const match = /\.[^.]+$/.exec(filename);
return match?.[0] || ".bin";
}