mirror of
https://github.com/hoornet/vega.git
synced 2026-06-08 14:11:55 -07:00
Bump to v0.12.7 — fix NIP-96 upload endpoints, block SVG uploads
This commit is contained in:
@@ -69,13 +69,11 @@ jobs:
|
||||
|
||||
> **Windows note:** The installer is not yet code-signed. Windows SmartScreen will show an "Unknown publisher" warning — click "More info → Run anyway" to install.
|
||||
|
||||
### v0.12.6 — DM Polish & Image Fixes
|
||||
### v0.12.7 — Upload Fixes
|
||||
|
||||
- **Clickable links in DMs** — URLs, nostr entities, and images now render properly inside direct messages; images show inline, links open in browser
|
||||
- **Multi-image upload fix** — selecting multiple images in the article editor now inserts all of them (previously only the last one was kept)
|
||||
- **Article image lightbox** — clicking a thumbnail in the editor strip opens a full-size lightbox overlay
|
||||
- **Blossom image rendering** — images served from content-addressed storage (Blossom/NIP-96) with non-standard extensions (`.jp` etc.) now render inline instead of as plain links
|
||||
- **nostr: entity parsing** — case-insensitive matching for `nostr:` prefix; broader extension support for image URLs
|
||||
- **Image uploads fixed** — updated to current NIP-96 endpoints for nostr.build and files.sovbit.host; removed dead services (void.cat, nostrcheck.me)
|
||||
- **SVG upload blocked** — SVG files are now rejected with a clear error message; they were silently uploading but rendering as broken images on all Nostr clients
|
||||
- **NIP-98 payload hash** — upload auth headers now include the required SHA-256 body hash for strict NIP-96 servers
|
||||
|
||||
### v0.12.5 — UI Polish & Consistency
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## v0.12.7 — Upload Fixes (2026-04-13)
|
||||
|
||||
### Fixed
|
||||
- Image uploads now work again — nostr.build and files.sovbit.host endpoints updated to their current NIP-96 URLs; removed void.cat (dead) and nostrcheck.me (returned broken URLs without file extensions)
|
||||
- NIP-98 HTTP Auth header now includes the required SHA-256 payload hash, fixing rejections from strict NIP-96 servers
|
||||
- SVG files are now rejected with a clear error message before upload in profile picture, banner, compose box, and inline reply — SVGs were silently uploading but rendering as broken images on all Nostr clients
|
||||
|
||||
## v0.12.6 — Rich Text Everywhere (2026-04-10)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: hoornet <hoornet@users.noreply.github.com>
|
||||
pkgname=vega-nostr
|
||||
pkgver=0.12.6
|
||||
pkgver=0.12.7
|
||||
pkgrel=1
|
||||
pkgdesc="Cross-platform Nostr desktop client with Lightning integration"
|
||||
arch=('x86_64')
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vega",
|
||||
"private": true,
|
||||
"version": "0.12.6",
|
||||
"version": "0.12.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "vega"
|
||||
version = "0.12.6"
|
||||
version = "0.12.7"
|
||||
description = "Cross-platform Nostr desktop client with Lightning integration"
|
||||
authors = ["hoornet"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Vega",
|
||||
"version": "0.12.6",
|
||||
"version": "0.12.7",
|
||||
"identifier": "com.hoornet.vega",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -97,9 +97,14 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () =
|
||||
const bytes = await readFile(filePath);
|
||||
const fileName = filePath.split(/[\\/]/).pop() || "file";
|
||||
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
||||
if (ext === "svg") {
|
||||
setError("SVG files are not supported — please use PNG or JPG.");
|
||||
setUploading(false);
|
||||
return;
|
||||
}
|
||||
const mimeMap: Record<string, string> = {
|
||||
jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif",
|
||||
webp: "image/webp", svg: "image/svg+xml", mp4: "video/mp4", webm: "video/webm",
|
||||
webp: "image/webp", mp4: "video/mp4", webm: "video/webm",
|
||||
mov: "video/quicktime", ogg: "video/ogg", m4v: "video/mp4",
|
||||
};
|
||||
const mimeType = mimeMap[ext] || "application/octet-stream";
|
||||
@@ -160,7 +165,7 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () =
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{ name: "Media", extensions: ["jpg", "jpeg", "png", "gif", "webp", "svg", "mp4", "webm", "mov", "ogg", "m4v"] },
|
||||
{ name: "Media", extensions: ["jpg", "jpeg", "png", "gif", "webp", "mp4", "webm", "mov", "ogg", "m4v"] },
|
||||
],
|
||||
});
|
||||
if (!selected) return;
|
||||
|
||||
@@ -48,6 +48,10 @@ export function InlineReplyBox({ event, name, rootEvent }: InlineReplyBoxProps)
|
||||
};
|
||||
|
||||
const handleImageUpload = async (file: File) => {
|
||||
if (file.type === "image/svg+xml") {
|
||||
setUploadError("SVG files are not supported — please use PNG or JPG.");
|
||||
return;
|
||||
}
|
||||
setUploading(true);
|
||||
setUploadError(null);
|
||||
try {
|
||||
|
||||
@@ -9,6 +9,11 @@ export function ImageField({ label, value, onChange }: { label: string; value: s
|
||||
const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
if (file.type === "image/svg+xml") {
|
||||
setUploadError("SVG files are not supported — please use PNG or JPG.");
|
||||
if (fileRef.current) fileRef.current.value = "";
|
||||
return;
|
||||
}
|
||||
setUploading(true);
|
||||
setUploadError(null);
|
||||
try {
|
||||
|
||||
+32
-18
@@ -2,17 +2,22 @@ import { fetch } from "@tauri-apps/plugin-http";
|
||||
import { getNDK } from "./nostr";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
|
||||
const UPLOAD_SERVICES = [
|
||||
"https://nostr.build/api/v2/upload/files",
|
||||
"https://void.cat/upload",
|
||||
"https://nostrimg.com/api/upload",
|
||||
interface UploadService {
|
||||
url: string;
|
||||
field: string; // multipart field name expected by the service
|
||||
}
|
||||
|
||||
const UPLOAD_SERVICES: UploadService[] = [
|
||||
{ url: "https://nostr.build/api/v2/nip96/upload", field: "file" },
|
||||
{ url: "https://files.sovbit.host/api/v2/media", field: "file" },
|
||||
{ url: "https://nostrimg.com/api/upload", field: "file" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a NIP-98 HTTP Auth event (kind 27235) for a given URL and method.
|
||||
* Returns a base64-encoded signed event for the Authorization header.
|
||||
*/
|
||||
async function createNip98AuthHeader(url: string, method: string): Promise<string> {
|
||||
async function createNip98AuthHeader(url: string, method: string, body?: Uint8Array): Promise<string> {
|
||||
const ndk = getNDK();
|
||||
if (!ndk.signer) throw new Error("Not logged in — cannot sign NIP-98 auth");
|
||||
|
||||
@@ -24,6 +29,15 @@ async function createNip98AuthHeader(url: string, method: string): Promise<strin
|
||||
["u", url],
|
||||
["method", method.toUpperCase()],
|
||||
];
|
||||
|
||||
if (body) {
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", body);
|
||||
const hashHex = Array.from(new Uint8Array(hashBuffer))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
event.tags.push(["payload", hashHex]);
|
||||
}
|
||||
|
||||
await event.sign();
|
||||
const encoded = btoa(JSON.stringify(event.rawEvent()));
|
||||
return `Nostr ${encoded}`;
|
||||
@@ -70,47 +84,47 @@ function buildMultipart(fieldName: string, data: Uint8Array, fileName: string, m
|
||||
* Upload raw bytes with NIP-98 auth. Tries nostr.build first, then fallbacks.
|
||||
*/
|
||||
export async function uploadBytes(bytes: Uint8Array, fileName: string, mimeType: string): Promise<string> {
|
||||
const { body, contentType } = buildMultipart("file", bytes, fileName, mimeType);
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const serviceUrl of UPLOAD_SERVICES) {
|
||||
for (const service of UPLOAD_SERVICES) {
|
||||
const { body, contentType } = buildMultipart(service.field, bytes, fileName, mimeType);
|
||||
try {
|
||||
const headers: Record<string, string> = { "Content-Type": contentType };
|
||||
try {
|
||||
headers["Authorization"] = await createNip98AuthHeader(serviceUrl, "POST");
|
||||
headers["Authorization"] = await createNip98AuthHeader(service.url, "POST", body);
|
||||
} catch {
|
||||
// If not logged in, try without auth (some services allow anonymous)
|
||||
}
|
||||
|
||||
const resp = await fetch(serviceUrl, {
|
||||
const resp = await fetch(service.url, {
|
||||
method: "POST",
|
||||
body,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
errors.push(`${serviceUrl}: HTTP ${resp.status}`);
|
||||
errors.push(`${service.url}: HTTP ${resp.status}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
// nostr.build response format
|
||||
// NIP-96 standard response format
|
||||
if (data.nip94_event?.tags) {
|
||||
const urlTag = data.nip94_event.tags.find((t: string[]) => t[0] === "url");
|
||||
if (urlTag?.[1]) return urlTag[1] as string;
|
||||
}
|
||||
// nostr.build legacy / plain url field
|
||||
if (data.status === "success" && data.data?.[0]?.url) {
|
||||
return data.data[0].url as string;
|
||||
}
|
||||
// void.cat response format
|
||||
if (data.file?.url) {
|
||||
return data.file.url as string;
|
||||
}
|
||||
// nostrimg.com response format
|
||||
if (data.url) {
|
||||
return data.url as string;
|
||||
}
|
||||
|
||||
errors.push(`${serviceUrl}: no URL in response`);
|
||||
errors.push(`${service.url}: no URL in response`);
|
||||
} catch (err) {
|
||||
errors.push(`${serviceUrl}: ${err}`);
|
||||
errors.push(`${service.url}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user