Add compose preview with image attachments (NIP-92 imeta)
This commit is contained in:
parent
17e8ace5ab
commit
3e3467b006
11 changed files with 710 additions and 57 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { app, BrowserWindow, clipboard, ipcMain, protocol } from 'electron';
|
||||
import { app, BrowserWindow, clipboard, dialog, ipcMain, protocol } from 'electron';
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createInterface } from 'node:readline';
|
||||
|
|
@ -113,6 +113,68 @@ async function backendRequest(method: string, params: Record<string, unknown>):
|
|||
return response;
|
||||
}
|
||||
|
||||
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif'];
|
||||
|
||||
/** A best-effort MIME type derived from the file name. */
|
||||
function mimeForPath(filePath: string): string {
|
||||
switch (path.extname(filePath).toLowerCase()) {
|
||||
case '.png':
|
||||
return 'image/png';
|
||||
case '.jpg':
|
||||
case '.jpeg':
|
||||
return 'image/jpeg';
|
||||
case '.gif':
|
||||
return 'image/gif';
|
||||
case '.webp':
|
||||
return 'image/webp';
|
||||
case '.avif':
|
||||
return 'image/avif';
|
||||
default:
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
/** Show an open dialog and return the chosen image files (empty when cancelled). */
|
||||
async function pickImage(): Promise<{ path: string; name: string; mime: string }[]> {
|
||||
const result = await dialog.showOpenDialog({
|
||||
title: 'Attach image(s)',
|
||||
filters: [{ name: 'Images', extensions: IMAGE_EXTENSIONS }],
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
});
|
||||
if (result.canceled) {
|
||||
return [];
|
||||
}
|
||||
return result.filePaths.map((filePath) => ({
|
||||
path: filePath,
|
||||
name: path.basename(filePath),
|
||||
mime: mimeForPath(filePath),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Upload one image file to nostr.build and return the public URL + MIME type. */
|
||||
async function uploadImage(filePath: string): Promise<{ url: string; mime: string }> {
|
||||
const data = readFileSync(filePath);
|
||||
const mime = mimeForPath(filePath);
|
||||
const form = new FormData();
|
||||
form.append('fileToUpload', new Blob([data], { type: mime }), path.basename(filePath) || 'image');
|
||||
const response = await fetch('https://nostr.build/api/v2/upload/files', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
const payload = (await response.json().catch(() => ({}))) as {
|
||||
status?: string;
|
||||
message?: string;
|
||||
data?: { url?: string; mime?: string }[];
|
||||
};
|
||||
const uploaded = payload.data?.[0];
|
||||
if (!response.ok || payload.status !== 'success' || !uploaded?.url) {
|
||||
throw new Error(
|
||||
payload.message || `The image host rejected the upload (HTTP ${response.status}).`,
|
||||
);
|
||||
}
|
||||
return { url: uploaded.url, mime: uploaded.mime ?? mime };
|
||||
}
|
||||
|
||||
function createWindow(): void {
|
||||
const window = new BrowserWindow({
|
||||
width: 1160,
|
||||
|
|
@ -160,8 +222,34 @@ app.whenReady().then(() => {
|
|||
|
||||
ipcMain.handle(
|
||||
'backend:request',
|
||||
(_event, payload: { method: string; params?: Record<string, unknown> }) => {
|
||||
async (_event, payload: { method: string; params?: Record<string, unknown> }) => {
|
||||
const params = payload.params ?? {};
|
||||
// Media tasks are handled here (Electron) rather than the Rust backend:
|
||||
// they need a native file dialog and the hosting upload.
|
||||
if (payload.method === 'pick_image') {
|
||||
return { status: 'ok', data: await pickImage() };
|
||||
}
|
||||
if (payload.method === 'upload_image') {
|
||||
const filePath = String(params.path ?? '');
|
||||
if (!filePath) {
|
||||
return {
|
||||
status: 'error',
|
||||
code: 'bad_request',
|
||||
message: 'No image file given.',
|
||||
details: null,
|
||||
};
|
||||
}
|
||||
try {
|
||||
return { status: 'ok', data: await uploadImage(filePath) };
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
code: 'upload_failed',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
details: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
return backendRequest(payload.method, params);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue