Skip to content

Commit aa48484

Browse files
committed
fix image upload for apps
1 parent 25a2ac4 commit aa48484

5 files changed

Lines changed: 102 additions & 0 deletions

File tree

capacitor.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const config: CapacitorConfig = {
1313
androidScheme: 'https'
1414
},
1515
plugins: {
16+
CapacitorHttp: {
17+
enabled: true
18+
},
1619
StatusBar: {
1720
overlaysWebView: false
1821
},

electron/main.cjs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,63 @@ function registerSecureSessionHandlers() {
195195
ipcMain.handle('secure-session:clear', () => clearSecureSession());
196196
}
197197

198+
function registerUploadHandlers() {
199+
ipcMain.handle('upload:nostr-build', async (_event, payload) => {
200+
try {
201+
return await uploadToNostrBuild(payload);
202+
} catch (error) {
203+
return {
204+
ok: false,
205+
status: 0,
206+
data: { message: error?.message || 'Could not reach nostr.build for upload.' },
207+
error: error?.message || 'Could not reach nostr.build for upload.'
208+
};
209+
}
210+
});
211+
}
212+
213+
async function uploadToNostrBuild(payload) {
214+
const authorization = typeof payload?.authorization === 'string' ? payload.authorization : '';
215+
const mediaType = ['avatar', 'banner', 'media'].includes(payload?.mediaType) ? payload.mediaType : 'media';
216+
const name = safeUploadName(typeof payload?.name === 'string' && payload.name ? payload.name : 'upload');
217+
const type = typeof payload?.type === 'string' && payload.type ? payload.type : 'application/octet-stream';
218+
const size = Number.isFinite(payload?.size) ? String(payload.size) : '';
219+
const bytes = Buffer.isBuffer(payload?.bytes) ? payload.bytes : Buffer.from(payload?.bytes ?? []);
220+
if (!authorization.startsWith('Nostr ') || !bytes.length) return { ok: false, status: 400, data: { message: 'Missing upload data.' } };
221+
222+
const form = new FormData();
223+
form.set('file', new Blob([bytes], { type }), name);
224+
form.set('media_type', mediaType);
225+
form.set('content_type', type);
226+
form.set('size', size || String(bytes.length));
227+
228+
const response = await fetch('https://nostr.build/api/v2/upload/files', {
229+
method: 'POST',
230+
headers: { Authorization: authorization },
231+
body: form
232+
});
233+
const text = await response.text().catch(() => '');
234+
return {
235+
ok: response.ok,
236+
status: response.status,
237+
location: response.headers.get('location') || '',
238+
data: parseJsonOrText(text)
239+
};
240+
}
241+
242+
function safeUploadName(name) {
243+
return name.replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_').slice(0, 180) || 'upload';
244+
}
245+
246+
function parseJsonOrText(text) {
247+
if (!text) return '';
248+
try {
249+
return JSON.parse(text);
250+
} catch {
251+
return text;
252+
}
253+
}
254+
198255
registerNostrProtocol();
199256
openNostrUrl(findNostrUrl());
200257

@@ -213,6 +270,7 @@ if (!gotSingleInstanceLock) {
213270

214271
app.whenReady().then(() => {
215272
registerSecureSessionHandlers();
273+
registerUploadHandlers();
216274
return createWindow();
217275
});
218276
}

electron/preload.cjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ contextBridge.exposeInMainWorld('nostrDesktopSecureStorage', {
66
write: (value) => ipcRenderer.invoke('secure-session:write', value),
77
clear: () => ipcRenderer.invoke('secure-session:clear')
88
});
9+
10+
contextBridge.exposeInMainWorld('nostrDesktopUpload', {
11+
uploadToNostrBuild: (payload) => ipcRenderer.invoke('upload:nostr-build', payload)
12+
});

src/app.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ declare global {
99
write(value: string): Promise<boolean>;
1010
clear(): Promise<void>;
1111
};
12+
nostrDesktopUpload?: {
13+
uploadToNostrBuild(payload: {
14+
name: string;
15+
type: string;
16+
size: number;
17+
mediaType: 'avatar' | 'banner' | 'media';
18+
authorization: string;
19+
bytes: ArrayBuffer;
20+
}): Promise<{ ok: boolean; status: number; data?: unknown; location?: string; error?: string }>;
21+
};
1222
nostr?: {
1323
getPublicKey(): Promise<string>;
1424
signEvent(event: Record<string, unknown>): Promise<Record<string, unknown>>;

src/lib/nostr/upload.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export async function uploadToNostrBuild(session: Session, file: File, mediaType
1313
form.set('size', String(file.size));
1414

1515
const authorization = await getNip98AuthorizationHeader(session, nostrBuildUploadUrl, 'POST');
16+
const desktopUploadUrl = await uploadWithDesktopBridge(file, mediaType, authorization);
17+
if (desktopUploadUrl) return desktopUploadUrl;
18+
1619
const response = await fetch(nostrBuildUploadUrl, {
1720
method: 'POST',
1821
headers: { Authorization: authorization },
@@ -28,6 +31,30 @@ export async function uploadToNostrBuild(session: Session, file: File, mediaType
2831
return url;
2932
}
3033

34+
async function uploadWithDesktopBridge(file: File, mediaType: NostrBuildMediaType, authorization: string) {
35+
if (typeof window === 'undefined') return '';
36+
const bridge = window.nostrDesktopUpload;
37+
if (!bridge?.uploadToNostrBuild) return '';
38+
39+
const result = await bridge
40+
.uploadToNostrBuild({
41+
name: file.name,
42+
type: file.type,
43+
size: file.size,
44+
mediaType,
45+
authorization,
46+
bytes: await file.arrayBuffer()
47+
})
48+
.catch((err) => {
49+
throw new Error(uploadNetworkErrorMessage(err));
50+
});
51+
52+
if (!result.ok) throw new Error(result.error || uploadErrorMessage(result.data) || `Upload failed with ${result.status}.`);
53+
const url = uploadResponseUrl(result.data) || result.location || '';
54+
if (!url) throw new Error('Upload finished but no media URL was returned.');
55+
return url;
56+
}
57+
3158
export function uploadResponseUrl(data: unknown): string {
3259
const tags = uploadResponseTags(data);
3360
const urlTag = tags.find((tag) => tag[0] === 'url' && tag[1]);

0 commit comments

Comments
 (0)