cleaner flow

This commit is contained in:
sakrecoer 2026-08-18 21:15:51 +02:00
parent 3dd1cf29da
commit 980aaa501d
5 changed files with 346 additions and 286 deletions

View file

@ -8,9 +8,9 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"nostr-tools": "^2.24.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"nostr-tools": "^2.0.0",
"react-router-dom": "^6.20.0" "react-router-dom": "^6.20.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -4,44 +4,83 @@ const NostrConnect = ({ setConnectedPubkey }) => {
const [pubkey, setPubkey] = useState(null); const [pubkey, setPubkey] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// Check if already connected
useEffect(() => {
if (window.nostr?.getPublicKey) {
window.nostr.getPublicKey()
.then(pk => {
setPubkey(pk);
setConnectedPubkey(pk);
})
.catch(() => {});
}
}, []);
const connect = async () => { const connect = async () => {
setLoading(true); setLoading(true);
try { try {
if (!window.nostr) { if (!window.nostr) {
throw new Error('Please install a Nostr extension (Alby, Nos2x, etc.)'); throw new Error('Please install a Nostr extension (Alby, Nos2x, etc.)');
} }
const pk = await window.nostr.getPublicKey();
const pubkey = await window.nostr.getPublicKey(); setPubkey(pk);
setPubkey(pubkey); setConnectedPubkey(pk);
setConnectedPubkey(pubkey); setLoading(false);
} catch (error) { } catch (error) {
alert('Failed to connect: ' + error.message); alert('Failed to connect: ' + error.message);
} finally {
setLoading(false); setLoading(false);
} }
}; };
useEffect(() => { const disconnect = () => {
// Check if already connected setPubkey(null);
if (window.nostr) { setConnectedPubkey(null);
window.nostr.getPublicKey().then(pk => { };
setPubkey(pk);
setConnectedPubkey(pk);
}).catch(() => {});
}
}, []);
if (pubkey) { if (pubkey) {
return ( return (
<div className="nostr-connected"> <div className="nostr-connected" style={{
Connected: {pubkey.slice(0, 8)}...{pubkey.slice(-8)} display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
background: '#e8f5e9',
borderRadius: '8px',
border: '1px solid #4caf50',
flexWrap: 'wrap',
gap: '8px'
}}>
<span>
Connected: <code>{pubkey.slice(0, 8)}...{pubkey.slice(-8)}</code>
</span>
<button
onClick={disconnect}
className="btn-secondary"
style={{ fontSize: '12px', padding: '4px 12px' }}
>
Disconnect
</button>
</div> </div>
); );
} }
return ( return (
<button onClick={connect} disabled={loading} className="btn-nostr"> <button
{loading ? 'Connecting...' : '🔑 Connect Nostr'} onClick={connect}
disabled={loading}
className="btn-nostr"
style={{
padding: '10px 20px',
background: loading ? '#ccc' : '#ff6b00',
color: 'white',
border: 'none',
borderRadius: '6px',
fontWeight: '600',
cursor: loading ? 'not-allowed' : 'pointer',
transition: 'background 0.2s'
}}
>
{loading ? '⏳ Connecting...' : '🔑 Connect with Extension'}
</button> </button>
); );
}; };

View file

@ -2,23 +2,52 @@ import React, { useState, useEffect } from 'react';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { decodeTemplate, validateTemplate } from '../utils/templates'; import { decodeTemplate, validateTemplate } from '../utils/templates';
import useNostr from '../hooks/useNostr'; import useNostr from '../hooks/useNostr';
import NostrConnect from './NostrConnect';
// Define BLAST_RELAYS here const TemplateApplier = ({ setConnectedPubkey }) => {
const BLAST_RELAYS = [
'wss://relay.primal.net',
'wss://relay.damus.io',
'wss://nos.lol'
];
const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
const { encoded } = useParams(); const { encoded } = useParams();
const nostr = useNostr();
const [template, setTemplate] = useState(null); const [template, setTemplate] = useState(null);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [applying, setApplying] = useState(false); const [applying, setApplying] = useState(false);
const [results, setResults] = useState(null); const [results, setResults] = useState(null);
const [isConnecting, setIsConnecting] = useState(false);
const { publishKind10002, publishKind10050, publishKind10063, isConnected, connect, pubkey } = useNostr();
// ============================================================
// CONNECTION CHECK
// ============================================================
useEffect(() => {
const checkConnection = async () => {
if (nostr.pubkey) {
setConnectedPubkey(nostr.pubkey);
return;
}
if (window.nostr?.getPublicKey) {
try {
const pk = await window.nostr.getPublicKey();
if (pk) {
await nostr.connect();
setConnectedPubkey(pk);
}
} catch (e) {
console.log('Extension not authorized');
}
}
};
checkConnection();
}, []);
useEffect(() => {
if (nostr.pubkey) {
setConnectedPubkey(nostr.pubkey);
}
}, [nostr.pubkey, setConnectedPubkey]);
// ============================================================
// LOAD TEMPLATE
// ============================================================
useEffect(() => { useEffect(() => {
if (!encoded) { if (!encoded) {
setError('No template provided'); setError('No template provided');
@ -36,25 +65,9 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
} }
}, [encoded]); }, [encoded]);
useEffect(() => { // ============================================================
if (pubkey && !connectedPubkey) { // PUBLISH FUNCTIONS
setConnectedPubkey(pubkey); // ============================================================
}
}, [pubkey, connectedPubkey, setConnectedPubkey]);
const handleConnect = async () => {
setIsConnecting(true);
try {
const pk = await connect();
setConnectedPubkey(pk);
setError('');
} catch (err) {
setError('Failed to connect: ' + err.message);
} finally {
setIsConnecting(false);
}
};
const handleApplyTemplate = async () => { const handleApplyTemplate = async () => {
if (!template) return; if (!template) return;
@ -63,20 +76,12 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
setResults(null); setResults(null);
try { try {
let pubkeyToUse = connectedPubkey || pubkey; if (!nostr.pubkey) {
throw new Error('Please connect your Nostr signer first');
if (!pubkeyToUse) {
try {
pubkeyToUse = await connect();
setConnectedPubkey(pubkeyToUse);
} catch (err) {
throw new Error('Please connect your Nostr extension first');
}
} }
if (!pubkeyToUse) { // Collect ALL user relay URLs from the main relays
throw new Error('No Nostr public key available. Please connect your extension.'); const allUserRelays = template.relays.map(r => r.url);
}
const allResults = []; const allResults = [];
const publishConfigs = []; const publishConfigs = [];
@ -86,28 +91,25 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
publishConfigs.push({ publishConfigs.push({
name: 'Relays (kind 10002)', name: 'Relays (kind 10002)',
kind: '10002', kind: '10002',
relays: template.relays, publishFn: () => nostr.publishKind10002(template.relays, nostr.pubkey)
publishFn: () => publishKind10002(template.relays, pubkeyToUse)
}); });
} }
// 2. Publish kind 10063 (blossom servers) // 2. Publish kind 10063 (blossom servers) - Pass all user relays
if (template.blossomServers && template.blossomServers.length > 0) { if (template.blossomServers && template.blossomServers.length > 0) {
publishConfigs.push({ publishConfigs.push({
name: 'Blossom Servers (kind 10063)', name: 'Blossom Servers (kind 10063)',
kind: '10063', kind: '10063',
relays: template.blossomServers.map(s => ({ url: s.url, read: true, write: true })), publishFn: () => nostr.publishKind10063(template.blossomServers, nostr.pubkey, allUserRelays)
publishFn: () => publishKind10063(template.blossomServers, pubkeyToUse)
}); });
} }
// 3. Publish kind 10050 (DM relays) // 3. Publish kind 10050 (DM relays) - Pass all user relays
if (template.dmRelays && template.dmRelays.length > 0) { if (template.dmRelays && template.dmRelays.length > 0) {
publishConfigs.push({ publishConfigs.push({
name: 'DM Relays (kind 10050)', name: 'DM Relays (kind 10050)',
kind: '10050', kind: '10050',
relays: template.dmRelays.map(r => ({ url: r.url, read: true, write: true })), publishFn: () => nostr.publishKind10050(template.dmRelays, nostr.pubkey, allUserRelays)
publishFn: () => publishKind10050(template.dmRelays, pubkeyToUse)
}); });
} }
@ -160,6 +162,10 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
} }
}; };
// ============================================================
// RENDER
// ============================================================
if (error && !template) { if (error && !template) {
return ( return (
<div className="template-applier"> <div className="template-applier">
@ -183,6 +189,8 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
); );
} }
const isConnected = nostr.isConnected && nostr.pubkey;
return ( return (
<div className="template-applier"> <div className="template-applier">
<h2>Apply Relay Template</h2> <h2>Apply Relay Template</h2>
@ -204,8 +212,9 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
<li key={index}> <li key={index}>
<span className="relay-url">{relay.url}</span> <span className="relay-url">{relay.url}</span>
<span className="relay-permissions"> <span className="relay-permissions">
{relay.read ? '📖 Read' : ''} {relay.read && relay.write ? '📖✍️' :
{relay.write ? ' ✍️ Write' : ''} relay.read ? '📖' :
relay.write ? '✍️' : ''}
</span> </span>
</li> </li>
))} ))}
@ -221,7 +230,7 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
<li key={index}> <li key={index}>
<span className="relay-url">{server.url}</span> <span className="relay-url">{server.url}</span>
<span style={{ fontSize: '12px', color: '#999' }}> <span style={{ fontSize: '12px', color: '#999' }}>
#{index + 1} {index === 0 ? '⭐ Perferred' : ''} #{index + 1} {index === 0 ? '⭐ Preferred' : ''}
</span> </span>
</li> </li>
))} ))}
@ -244,29 +253,56 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
)} )}
</div> </div>
{/* Connection Status */} {/* AUTH SECTION */}
<div className="connection-status" style={{ marginBottom: '16px' }}> {!isConnected && (
{(connectedPubkey || pubkey) ? ( <div className="auth-section" style={{
<div className="info-box"> padding: '20px',
Connected as: <code>{(connectedPubkey || pubkey).slice(0, 8)}...{(connectedPubkey || pubkey).slice(-8)}</code> background: '#f8f9fa',
</div> borderRadius: '8px',
) : ( border: '1px solid #e9ecef',
<div className="warning" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '10px' }}> marginBottom: '16px'
<span> Not connected to Nostr. Please connect your extension to publish.</span> }}>
<button <h4 style={{ marginBottom: '12px' }}>🔑 Connect Your Nostr Signer</h4>
onClick={handleConnect} <p style={{ fontSize: '14px', color: '#666', marginBottom: '12px' }}>
disabled={isConnecting} You need to connect a signer to publish this template.
className="btn-nostr" </p>
style={{ width: 'auto' }} <NostrConnect setConnectedPubkey={setConnectedPubkey} />
</div>
)}
{/* CONNECTED STATUS */}
{isConnected && (
<div className="connection-status" style={{ marginBottom: '16px' }}>
<div className="info-box" style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
background: '#e8f5e9',
borderRadius: '6px',
border: '1px solid #4caf50',
flexWrap: 'wrap',
gap: '8px'
}}>
<span>
Connected: <code>{nostr.pubkey.slice(0, 8)}...{nostr.pubkey.slice(-8)}</code>
</span>
<button
onClick={() => {
nostr.disconnect();
setConnectedPubkey(null);
}}
className="btn-secondary"
style={{ fontSize: '12px', padding: '4px 12px' }}
> >
{isConnecting ? '⏳ Connecting...' : '🔑 Connect Nostr'} Disconnect
</button> </button>
</div> </div>
)} </div>
</div> )}
{/* Publish Button */} {/* PUBLISH BUTTON */}
{!results && (connectedPubkey || pubkey) && ( {!results && isConnected && (
<button <button
onClick={handleApplyTemplate} onClick={handleApplyTemplate}
className="btn-primary" className="btn-primary"
@ -277,11 +313,7 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
marginTop: '10px' marginTop: '10px'
}} }}
> >
{applying ? ( {applying ? '⏳ Publishing...' : '✅ Apply Template & Publish to Relays'}
'⏳ Publishing...'
) : (
'✅ Apply Template & Publish to Relays'
)}
</button> </button>
)} )}
@ -291,7 +323,7 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
</div> </div>
)} )}
{/* Results */} {/* RESULTS */}
{results && results.events && ( {results && results.events && (
<div className="success-box" style={{ marginTop: '20px' }}> <div className="success-box" style={{ marginTop: '20px' }}>
<h3> <h3>
@ -343,7 +375,6 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
}}> }}>
{result.success ? '✅' : '❌'} {result.url} {result.success ? '✅' : '❌'} {result.url}
{result.error && ` - ${result.error}`} {result.error && ` - ${result.error}`}
{BLAST_RELAYS.includes(result.url) && ' 🔥'}
</li> </li>
))} ))}
</ul> </ul>
@ -374,4 +405,4 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
); );
}; };
export default TemplateApplier; export default TemplateApplier;

View file

@ -1,34 +1,21 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { encodeTemplate, generateTemplateId, validateTemplate, getCommunityTemplates } from '../utils/templates'; import { encodeTemplate, generateTemplateId, validateTemplate, getCommunityTemplates } from '../utils/templates';
import RelayList from './RelayList'; import RelayList from './RelayList';
import NostrConnect from './NostrConnect';
// Configure your blast relays here (hidden from UI) const TemplateCreator = () => {
// These will be used to publish the events, but won't appear in the template
const BLAST_RELAYS = [
'wss://relay.primal.net',
'wss://relay.damus.io',
'wss://nos.lol'
// Add your preferred blast relays here
];
const TemplateCreator = ({ setConnectedPubkey }) => {
const [templateName, setTemplateName] = useState(''); const [templateName, setTemplateName] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [relays, setRelays] = useState([ const [relays, setRelays] = useState([
{ url: 'wss://relay.damus.io', read: true, write: true }, { url: 'wss://your-cool.relay', read: true, write: true }
{ url: 'wss://nos.lol', read: true, write: true }
]); ]);
// Blossom servers (kind 10063)
const [blossomServers, setBlossomServers] = useState([ const [blossomServers, setBlossomServers] = useState([
{ url: 'https://cdn.satellite.earth' } { url: 'https://your-blossom.community' }
]); ]);
const [showBlossom, setShowBlossom] = useState(true); const [showBlossom, setShowBlossom] = useState(true);
// DM relays (kind 10050)
const [dmRelays, setDmRelays] = useState([ const [dmRelays, setDmRelays] = useState([
{ url: 'wss://relay.private-msgs.com' } { url: 'wss://your-dm.relay' }
]); ]);
const [showDmRelays, setShowDmRelays] = useState(true); const [showDmRelays, setShowDmRelays] = useState(true);
@ -64,31 +51,6 @@ const TemplateCreator = ({ setConnectedPubkey }) => {
} }
}; };
const loadCommunityTemplate = (templateKey) => {
const template = communityTemplates[templateKey];
if (template) {
setRelays(template.relays);
setTemplateName(template.name);
setDescription(template.description);
if (template.blossomServers && template.blossomServers.length > 0) {
setBlossomServers(template.blossomServers);
setShowBlossom(true);
} else {
setBlossomServers([{ url: '' }]);
setShowBlossom(false);
}
if (template.dmRelays && template.dmRelays.length > 0) {
setDmRelays(template.dmRelays);
setShowDmRelays(true);
} else {
setDmRelays([{ url: '' }]);
setShowDmRelays(false);
}
setSelectedTemplate(templateKey);
setError('');
}
};
// Relay functions // Relay functions
const addRelay = () => { const addRelay = () => {
setRelays([...relays, { url: '', read: true, write: true }]); setRelays([...relays, { url: '', read: true, write: true }]);
@ -104,6 +66,12 @@ const TemplateCreator = ({ setConnectedPubkey }) => {
setRelays(updated); setRelays(updated);
}; };
const addCommonRelay = (url) => {
if (!relays.some(r => r.url === url)) {
setRelays([...relays, { url, read: true, write: true }]);
}
};
// Blossom server functions // Blossom server functions
const addBlossomServer = () => { const addBlossomServer = () => {
setBlossomServers([...blossomServers, { url: '' }]); setBlossomServers([...blossomServers, { url: '' }]);
@ -134,17 +102,34 @@ const TemplateCreator = ({ setConnectedPubkey }) => {
setDmRelays(updated); setDmRelays(updated);
}; };
const addCommonRelay = (url) => { const loadCommunityTemplate = (templateKey) => {
if (!relays.some(r => r.url === url)) { const template = communityTemplates[templateKey];
setRelays([...relays, { url, read: true, write: true }]); if (template) {
setRelays(template.relays);
setTemplateName(template.name);
setDescription(template.description);
if (template.blossomServers && template.blossomServers.length > 0) {
setBlossomServers(template.blossomServers);
setShowBlossom(true);
} else {
setBlossomServers([{ url: '' }]);
setShowBlossom(false);
}
if (template.dmRelays && template.dmRelays.length > 0) {
setDmRelays(template.dmRelays);
setShowDmRelays(true);
} else {
setDmRelays([{ url: '' }]);
setShowDmRelays(false);
}
setSelectedTemplate(templateKey);
setError('');
} }
}; };
return ( return (
<div className="template-creator"> <div className="template-creator">
<h2>Create Relay Template</h2> <h2>Create Relay Template</h2>
<NostrConnect setConnectedPubkey={setConnectedPubkey} />
{/* Community Templates Section */} {/* Community Templates Section */}
<div className="form-group" style={{ <div className="form-group" style={{
@ -243,7 +228,7 @@ const TemplateCreator = ({ setConnectedPubkey }) => {
{/* Main Relays Section */} {/* Main Relays Section */}
<div className="form-group"> <div className="form-group">
<label>Relays *</label> <label>📡 Relays *</label>
<RelayList <RelayList
relays={relays} relays={relays}
onUpdate={updateRelay} onUpdate={updateRelay}
@ -400,4 +385,4 @@ const TemplateCreator = ({ setConnectedPubkey }) => {
); );
}; };
export default TemplateCreator; export default TemplateCreator;

View file

@ -1,13 +1,13 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
// Blast relays - configure these once and they'll be used for publishing // Blast relays - configured once, used for publishing
// These are hidden from the UI and used to blast events to reliable relays
const BLAST_RELAYS = [ const BLAST_RELAYS = [
'wss://relay.primal.net', 'wss://relay.primal.net',
'wss://relay.damus.io', 'wss://relay.damus.io',
'wss://relay.ditto.pub',
'wss://offchain.pub',
'wss://sendit.nosflare.com', 'wss://sendit.nosflare.com',
'wss://nostr.mom', 'wss://nostr.mom',
'wss://relay.ditto.pub',
'wss://nos.lol' 'wss://nos.lol'
]; ];
@ -16,143 +16,144 @@ export const useNostr = () => {
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [extension, setExtension] = useState(null); const [extension, setExtension] = useState(null);
const [isConnecting, setIsConnecting] = useState(false);
// ============================================================
// INIT - Check for existing extension
// ============================================================
useEffect(() => { useEffect(() => {
const checkExtension = async () => { const checkConnection = async () => {
if (isConnected && pubkey) return;
try { try {
if (window.nostr) { if (window.nostr?.getPublicKey) {
setExtension(window.nostr);
try { try {
const pk = await window.nostr.getPublicKey(); const pk = await window.nostr.getPublicKey();
setExtension(window.nostr);
setPubkey(pk); setPubkey(pk);
setIsConnected(true); setIsConnected(true);
} catch (err) { console.log('✅ Connected via extension:', pk.slice(0, 8) + '...');
return;
} catch (e) {
console.log('Extension available but not authorized'); console.log('Extension available but not authorized');
} }
} }
} catch (err) { console.log(' No extension found');
setError('No Nostr extension found'); } catch (e) {
console.error('Init error:', e);
} }
}; };
checkExtension();
}, []);
checkConnection();
const interval = setInterval(() => {
if (window.nostr?.getPublicKey && !isConnected) {
window.nostr.getPublicKey().then(pk => {
setExtension(window.nostr);
setPubkey(pk);
setIsConnected(true);
console.log('✅ Extension connected:', pk.slice(0, 8) + '...');
}).catch(() => {});
}
}, 2000);
return () => clearInterval(interval);
}, [isConnected, pubkey]);
// ============================================================
// CONNECT
// ============================================================
const connect = useCallback(async () => { const connect = useCallback(async () => {
try { try {
setIsConnecting(true);
if (!window.nostr) { if (!window.nostr) {
throw new Error('Please install a Nostr extension (Alby, Nos2x, etc.)'); throw new Error('Please install a Nostr extension (Alby, Nos2x, etc.)');
} }
const pk = await window.nostr.getPublicKey(); const pk = await window.nostr.getPublicKey();
setExtension(window.nostr);
setPubkey(pk); setPubkey(pk);
setIsConnected(true); setIsConnected(true);
setExtension(window.nostr);
setError(null); setError(null);
setIsConnecting(false);
return pk; return pk;
} catch (err) { } catch (e) {
setError(err.message); setError(e.message);
setIsConnected(false); setIsConnecting(false);
throw err; throw e;
} }
}, []); }, []);
// ============================================================
// SIGN EVENT
// ============================================================
const signEvent = useCallback(async (event) => { const signEvent = useCallback(async (event) => {
if (!extension) { if (!extension) {
throw new Error('No Nostr extension connected'); throw new Error('No Nostr extension connected');
} }
return await extension.signEvent(event); try {
console.log('📱 Signing event...');
const signed = await extension.signEvent(event);
console.log('✅ Event signed');
return signed;
} catch (e) {
console.error('Signing failed:', e);
throw e;
}
}, [extension]); }, [extension]);
// ============================================================
// PUBLISH HELPERS
// ============================================================
const publishToSingleRelay = useCallback((url, event) => { const publishToSingleRelay = useCallback((url, event) => {
return new Promise((resolve) => { return new Promise((resolve) => {
let ws = null; const ws = new WebSocket(url);
let resolved = false; let resolved = false;
let timeoutIds = [];
const timeout = setTimeout(() => {
try {
ws = new WebSocket(url);
const connectTimeout = setTimeout(() => {
if (!resolved) {
resolved = true;
if (ws && ws.readyState !== WebSocket.CLOSED) {
try { ws.close(); } catch (e) {}
}
resolve({ url, success: false, error: 'Connection timeout' });
}
}, 10000);
timeoutIds.push(connectTimeout);
ws.onopen = () => {
const message = JSON.stringify(['EVENT', event]);
ws.send(message);
const publishTimeout = setTimeout(() => {
if (!resolved) {
resolved = true;
console.log(`⏰ Publish timeout for ${url}`);
try { ws.close(); } catch (e) {}
resolve({ url, success: false, error: 'Publish timeout' });
}
}, 15000);
timeoutIds.push(publishTimeout);
ws.onmessage = (msg) => {
try {
const data = JSON.parse(msg.data);
if (Array.isArray(data) && data[0] === 'OK' && data[1] === event.id) {
if (!resolved) {
resolved = true;
console.log(`✅ Published to ${url}`);
timeoutIds.forEach(id => clearTimeout(id));
try { ws.close(); } catch (e) {}
resolve({ url, success: true });
}
}
} catch (e) {}
};
ws.onerror = () => {
if (!resolved) {
resolved = true;
timeoutIds.forEach(id => clearTimeout(id));
try { ws.close(); } catch (e) {}
resolve({ url, success: false, error: 'WebSocket error' });
}
};
ws.onclose = () => {
if (!resolved) {
resolved = true;
timeoutIds.forEach(id => clearTimeout(id));
resolve({ url, success: false, error: 'Connection closed' });
}
};
};
ws.onerror = () => {
if (!resolved) {
resolved = true;
timeoutIds.forEach(id => clearTimeout(id));
resolve({ url, success: false, error: 'Connection error' });
}
};
} catch (err) {
if (!resolved) { if (!resolved) {
resolved = true; resolved = true;
timeoutIds.forEach(id => clearTimeout(id)); try { ws.close(); } catch (e) {}
if (ws) { resolve({ url, success: false, error: 'Timeout' });
try { ws.close(); } catch (e) {}
}
resolve({ url, success: false, error: err.message });
} }
} }, 10000);
ws.onopen = () => {
ws.send(JSON.stringify(['EVENT', event]));
const pubTimeout = setTimeout(() => {
if (!resolved) {
resolved = true;
try { ws.close(); } catch (e) {}
resolve({ url, success: false, error: 'Publish timeout' });
}
}, 15000);
ws.onmessage = (msg) => {
try {
const data = JSON.parse(msg.data);
if (data[0] === 'OK' && data[1] === event.id) {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
clearTimeout(pubTimeout);
try { ws.close(); } catch (e) {}
resolve({ url, success: true });
}
}
} catch (e) {}
};
};
ws.onerror = () => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
try { ws.close(); } catch (e) {}
resolve({ url, success: false, error: 'WebSocket error' });
}
};
}); });
}, []); }, []);
/**
* Publish to multiple relays with detailed results
*/
const publishToRelays = useCallback(async (event, relayUrls, label = '') => { const publishToRelays = useCallback(async (event, relayUrls, label = '') => {
console.log(`Publishing ${label} to ${relayUrls.length} relays...`); console.log(`Publishing ${label} to ${relayUrls.length} relays...`);
@ -169,8 +170,9 @@ export const useNostr = () => {
return [{ url: 'none', success: false, error: 'No valid relay URLs' }]; return [{ url: 'none', success: false, error: 'No valid relay URLs' }];
} }
const publishPromises = validUrls.map(url => publishToSingleRelay(url, event)); const results = await Promise.all(
const results = await Promise.all(publishPromises); validUrls.map(url => publishToSingleRelay(url, event))
);
const successCount = results.filter(r => r.success).length; const successCount = results.filter(r => r.success).length;
console.log(`Published ${label} to ${successCount}/${results.length} relays`); console.log(`Published ${label} to ${successCount}/${results.length} relays`);
@ -178,8 +180,16 @@ export const useNostr = () => {
return results; return results;
}, [publishToSingleRelay]); }, [publishToSingleRelay]);
// ============================================================
// PUBLISH KIND FUNCTIONS
// ============================================================
/** /**
* Generic publish function that publishes to BOTH blast relays AND user relays * Generic publish function that publishes to ALL relays
* @param {number} kind - Event kind
* @param {object} data - Event data with tags and content
* @param {string} pubkey - User's pubkey
* @param {string[]} userRelayUrls - User's relay URLs (all of them, not just for this kind)
*/ */
const publishKind = useCallback(async (kind, data, pubkey, userRelayUrls = []) => { const publishKind = useCallback(async (kind, data, pubkey, userRelayUrls = []) => {
if (!extension) { if (!extension) {
@ -187,105 +197,100 @@ export const useNostr = () => {
} }
const event = { const event = {
kind: kind, kind,
pubkey: pubkey, pubkey,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: data.tags || [], tags: data.tags || [],
content: data.content || '' content: data.content || ''
}; };
console.log(`Signing kind ${kind} event...`); console.log(`Signing kind ${kind} event...`);
const signedEvent = await extension.signEvent(event); const signedEvent = await signEvent(event);
console.log(`Kind ${kind} event signed successfully`); console.log(`Kind ${kind} event signed successfully`);
// Combine blast relays and user relays (deduplicated) // ✅ Combine ALL relays: blast + ALL user relays
const allRelays = [...new Set([...BLAST_RELAYS, ...userRelayUrls])]; const allRelays = [...new Set([...BLAST_RELAYS, ...userRelayUrls])];
console.log(`Publishing kind ${kind} to ${allRelays.length} relays (${BLAST_RELAYS.length} blast + ${userRelayUrls.length} user)`); console.log(`Publishing kind ${kind} to ${allRelays.length} relays (${BLAST_RELAYS.length} blast + ${userRelayUrls.length} user)`);
// Publish to all relays
const results = await publishToRelays(signedEvent, allRelays, `kind ${kind}`); const results = await publishToRelays(signedEvent, allRelays, `kind ${kind}`);
// Separate results for better reporting
const blastResults = results.filter(r => BLAST_RELAYS.includes(r.url)); const blastResults = results.filter(r => BLAST_RELAYS.includes(r.url));
const userResults = results.filter(r => userRelayUrls.includes(r.url)); const userResults = results.filter(r => userRelayUrls.includes(r.url));
console.log(`Kind ${kind} results:`, {
blast: `${blastResults.filter(r => r.success).length}/${blastResults.length}`,
user: `${userResults.filter(r => r.success).length}/${userResults.length}`
});
return { return {
event: signedEvent, event: signedEvent,
results: results, results,
blastResults: blastResults, blastResults,
userResults: userResults userResults
}; };
}, [extension, publishToRelays]); }, [extension, signEvent, publishToRelays]);
// Kind 10002 - Relays // Kind 10002 - Relays
const publishKind10002 = useCallback(async (relays, pubkey) => { const publishKind10002 = useCallback(async (relays, pubkey) => {
const validRelays = relays.filter(r => r.url && r.url.trim() !== ''); const validRelays = relays.filter(r => r.url && r.url.trim() !== '');
if (validRelays.length === 0) { if (validRelays.length === 0) {
throw new Error('No valid relays to publish'); throw new Error('No valid relays to publish');
} }
const tags = validRelays.map(relay => { const tags = validRelays.map(relay => {
const params = []; const params = [];
if (!relay.read) params.push('read'); if (!relay.read) params.push('read');
if (!relay.write) params.push('write'); if (!relay.write) params.push('write');
return ['r', relay.url, ...params]; return ['r', relay.url, ...params];
}); });
// ✅ Pass ALL user relay URLs (they're the same as the relays being published)
// Extract user relay URLs for combined publishing
const userRelayUrls = validRelays.map(r => r.url); const userRelayUrls = validRelays.map(r => r.url);
return await publishKind(10002, { tags }, pubkey, userRelayUrls); return await publishKind(10002, { tags }, pubkey, userRelayUrls);
}, [publishKind]); }, [publishKind]);
// Kind 10063 - Blossom Servers // Kind 10063 - Blossom Servers
const publishKind10063 = useCallback(async (servers, pubkey) => { const publishKind10063 = useCallback(async (servers, pubkey, allUserRelays = []) => {
const validServers = servers.filter(s => s.url && s.url.trim() !== ''); const validServers = servers.filter(s => s.url && s.url.trim() !== '');
if (validServers.length === 0) { if (validServers.length === 0) {
throw new Error('No valid blossom servers'); throw new Error('No valid blossom servers');
} }
const tags = validServers.map(server => ['server', server.url]); const tags = validServers.map(server => ['server', server.url]);
// ✅ Pass ALL user relays (main relays, not just blossom servers)
// For blossom servers, we want to publish to blast relays return await publishKind(10063, { tags }, pubkey, allUserRelays);
// The user's main relays are used for kind 10002, not kind 10063
return await publishKind(10063, { tags }, pubkey, []);
}, [publishKind]); }, [publishKind]);
// Kind 10050 - DM Relays // Kind 10050 - DM Relays
const publishKind10050 = useCallback(async (relays, pubkey) => { const publishKind10050 = useCallback(async (relays, pubkey, allUserRelays = []) => {
const validRelays = relays.filter(r => r.url && r.url.trim() !== ''); const validRelays = relays.filter(r => r.url && r.url.trim() !== '');
if (validRelays.length === 0) { if (validRelays.length === 0) {
throw new Error('No valid DM relays'); throw new Error('No valid DM relays');
} }
const tags = validRelays.map(relay => ['relay', relay.url]); const tags = validRelays.map(relay => ['relay', relay.url]);
// ✅ Pass ALL user relays (main relays, not just DM relays)
// For DM relays, we want to publish to blast relays return await publishKind(10050, { tags }, pubkey, allUserRelays);
// The user's main relays are used for kind 10002, not kind 10050
return await publishKind(10050, { tags }, pubkey, []);
}, [publishKind]); }, [publishKind]);
// ============================================================
// DISCONNECT
// ============================================================
const disconnect = useCallback(() => {
setPubkey(null);
setIsConnected(false);
setExtension(null);
console.log('👋 Disconnected');
}, []);
// ============================================================
// RETURN
// ============================================================
return { return {
pubkey, pubkey,
isConnected, isConnected,
error, error,
extension, isConnecting,
connect, connect,
signEvent, signEvent,
disconnect,
publishToRelays, publishToRelays,
publishKind, publishKind,
publishKind10002, publishKind10002,
publishKind10063, publishKind10063,
publishKind10050 publishKind10050,
}; };
}; };
export default useNostr; export default useNostr;