From b55362032d795d6e47fb026dd2872e8579e6946b Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 31 Mar 2026 18:53:27 -0700 Subject: [PATCH] Switch IPFS from standalone kubo to rSpace collab-server endpoints - Update IPFS_API_URL to ipfs-api.rspace.online (was kubo:5001) - Update IPFS_GATEWAY_URL to ipfs.rspace.online (was ipfs.jeffemmett.com) - Enhance image button: file upload via /api/uploads with IPFS encryption instead of manual URL prompt, with fallback on failure Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 6 ++--- src/components/NoteEditor.tsx | 46 ++++++++++++++++++++++++++++++----- src/lib/ipfs.ts | 4 +-- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 65c2a20..87d17de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,10 +12,10 @@ services: - INFISICAL_ENV=prod - INFISICAL_URL=http://infisical:8080 - DATABASE_URL=postgresql://rnotes:${DB_PASSWORD}@rnotes-postgres:5432/rnotes - # IPFS integration (encrypted file storage) + # IPFS integration (encrypted file storage via rSpace collab-server kubo) - IPFS_ENABLED=true - - IPFS_API_URL=http://kubo:5001 - - IPFS_GATEWAY_URL=https://ipfs.jeffemmett.com + - IPFS_API_URL=https://ipfs-api.rspace.online + - IPFS_GATEWAY_URL=https://ipfs.rspace.online volumes: - uploads_data:/app/uploads labels: diff --git a/src/components/NoteEditor.tsx b/src/components/NoteEditor.tsx index 7b765ce..1018f8c 100644 --- a/src/components/NoteEditor.tsx +++ b/src/components/NoteEditor.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { useEditor, EditorContent } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import Link from '@tiptap/extension-link'; @@ -45,6 +45,9 @@ function ToolbarButton({ } function RichEditor({ value, onChange, valueJson, placeholder: placeholderText }: Omit) { + const fileInputRef = useRef(null); + const [uploading, setUploading] = useState(false); + const editor = useEditor({ extensions: [ StarterKit.configure({ @@ -83,9 +86,33 @@ function RichEditor({ value, onChange, valueJson, placeholder: placeholderText } const addImage = useCallback(() => { if (!editor) return; - const url = window.prompt('Image URL'); - if (url) { - editor.chain().focus().setImage({ src: url }).run(); + fileInputRef.current?.click(); + }, [editor]); + + const handleFileUpload = useCallback(async (e: React.ChangeEvent) => { + if (!editor || !e.target.files?.length) return; + const file = e.target.files[0]; + e.target.value = ''; // reset input + + setUploading(true); + try { + const formData = new FormData(); + formData.append('file', file); + const res = await fetch('/api/uploads', { method: 'POST', body: formData }); + if (!res.ok) throw new Error(`Upload failed: ${res.status}`); + const data = await res.json(); + // Prefer IPFS proxy URL (decrypts on demand), fall back to local + const src = data.ipfs + ? `/api/ipfs/${data.ipfs.cid}?key=${encodeURIComponent(data.ipfs.encKey)}` + : data.url; + editor.chain().focus().setImage({ src, alt: file.name }).run(); + } catch (err) { + console.error('Image upload failed:', err); + // Fall back to URL prompt + const url = window.prompt('Upload failed. Enter image URL manually:'); + if (url) editor.chain().focus().setImage({ src: url }).run(); + } finally { + setUploading(false); } }, [editor]); @@ -200,10 +227,17 @@ function RichEditor({ value, onChange, valueJson, placeholder: placeholderText } - Img + {uploading ? '...' : 'Img'} +
diff --git a/src/lib/ipfs.ts b/src/lib/ipfs.ts index 528a332..a34851b 100644 --- a/src/lib/ipfs.ts +++ b/src/lib/ipfs.ts @@ -68,8 +68,8 @@ export interface FileMetadata { // ─── IPFS Client ─── -const IPFS_API_URL = process.env.IPFS_API_URL || 'http://kubo:5001' -const IPFS_GATEWAY_URL = process.env.IPFS_GATEWAY_URL || 'https://ipfs.jeffemmett.com' +const IPFS_API_URL = process.env.IPFS_API_URL || 'https://ipfs-api.rspace.online' +const IPFS_GATEWAY_URL = process.env.IPFS_GATEWAY_URL || 'https://ipfs.rspace.online' export function isIPFSEnabled(): boolean { return process.env.IPFS_ENABLED === 'true'