allow to create dm and blossom lists
give a couple templates for happy frens
This commit is contained in:
parent
57a252b983
commit
6534cad6eb
7 changed files with 729 additions and 204 deletions
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Relay Template - Nostr</title>
|
||||
<title>Comunitator - Nostr</title>
|
||||
<meta name="description" content="Create and share Nostr relay templates for your community" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
</head>
|
||||
|
|
@ -11,4 +11,4 @@
|
|||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
|||
12
src/App.css
12
src/App.css
|
|
@ -68,6 +68,7 @@ footer {
|
|||
/* Form Styles */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
|
|
@ -334,3 +335,14 @@ footer {
|
|||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Community Templates */
|
||||
.active-template {
|
||||
background: #f0ebff !important;
|
||||
border-color: #7c4dff !important;
|
||||
color: #7c4dff !important;
|
||||
}
|
||||
|
||||
.active-template:hover {
|
||||
background: #e8e0ff !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ function App() {
|
|||
<HashRouter>
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
<h1>⚡ Relay Template</h1>
|
||||
<h1>⚡ Communitator</h1>
|
||||
<nav>
|
||||
<Link to="/">🏠 Create Template</Link>
|
||||
{/* Remove the broken link - users will access /apply/:encoded via generated links */}
|
||||
|
|
@ -40,4 +40,4 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -3,15 +3,21 @@ import { Link, useParams } from 'react-router-dom';
|
|||
import { decodeTemplate, validateTemplate } from '../utils/templates';
|
||||
import useNostr from '../hooks/useNostr';
|
||||
|
||||
// Define BLAST_RELAYS here
|
||||
const BLAST_RELAYS = [
|
||||
'wss://relay.primal.net',
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol'
|
||||
];
|
||||
|
||||
const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
|
||||
const { encoded } = useParams();
|
||||
const [template, setTemplate] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [results, setResults] = useState(null);
|
||||
const [publishResults, setPublishResults] = useState(null);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const { publishKind10002, isConnected, connect, pubkey } = useNostr();
|
||||
const { publishKind10002, publishKind10050, publishKind10063, isConnected, connect, pubkey } = useNostr();
|
||||
|
||||
useEffect(() => {
|
||||
if (!encoded) {
|
||||
|
|
@ -20,7 +26,6 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
|
|||
}
|
||||
|
||||
try {
|
||||
// Decode and validate the template
|
||||
const decoded = decodeTemplate(encoded);
|
||||
validateTemplate(decoded);
|
||||
setTemplate(decoded);
|
||||
|
|
@ -31,7 +36,6 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
|
|||
}
|
||||
}, [encoded]);
|
||||
|
||||
// Update parent when connection changes
|
||||
useEffect(() => {
|
||||
if (pubkey && !connectedPubkey) {
|
||||
setConnectedPubkey(pubkey);
|
||||
|
|
@ -56,14 +60,12 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
|
|||
|
||||
setApplying(true);
|
||||
setError('');
|
||||
setPublishResults(null);
|
||||
setResults(null);
|
||||
|
||||
try {
|
||||
// Get pubkey from parent or hook
|
||||
let pubkeyToUse = connectedPubkey || pubkey;
|
||||
|
||||
if (!pubkeyToUse) {
|
||||
// Try to connect
|
||||
try {
|
||||
pubkeyToUse = await connect();
|
||||
setConnectedPubkey(pubkeyToUse);
|
||||
|
|
@ -76,35 +78,88 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
|
|||
throw new Error('No Nostr public key available. Please connect your extension.');
|
||||
}
|
||||
|
||||
// Publish the kind 10002 event
|
||||
const result = await publishKind10002(template.relays, pubkeyToUse);
|
||||
|
||||
setPublishResults({
|
||||
success: true,
|
||||
eventId: result.event.id,
|
||||
publishedTo: result.results.filter(r => r.success).length,
|
||||
totalRelays: result.results.length,
|
||||
details: result.results
|
||||
});
|
||||
const allResults = [];
|
||||
const publishConfigs = [];
|
||||
|
||||
// 1. Publish kind 10002 (relays)
|
||||
if (template.relays && template.relays.length > 0) {
|
||||
publishConfigs.push({
|
||||
name: 'Relays (kind 10002)',
|
||||
kind: '10002',
|
||||
relays: template.relays,
|
||||
publishFn: () => publishKind10002(template.relays, pubkeyToUse)
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Publish kind 10063 (blossom servers)
|
||||
if (template.blossomServers && template.blossomServers.length > 0) {
|
||||
publishConfigs.push({
|
||||
name: 'Blossom Servers (kind 10063)',
|
||||
kind: '10063',
|
||||
relays: template.blossomServers.map(s => ({ url: s.url, read: true, write: true })),
|
||||
publishFn: () => publishKind10063(template.blossomServers, pubkeyToUse)
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Publish kind 10050 (DM relays)
|
||||
if (template.dmRelays && template.dmRelays.length > 0) {
|
||||
publishConfigs.push({
|
||||
name: 'DM Relays (kind 10050)',
|
||||
kind: '10050',
|
||||
relays: template.dmRelays.map(r => ({ url: r.url, read: true, write: true })),
|
||||
publishFn: () => publishKind10050(template.dmRelays, pubkeyToUse)
|
||||
});
|
||||
}
|
||||
|
||||
if (publishConfigs.length === 0) {
|
||||
throw new Error('No events to publish');
|
||||
}
|
||||
|
||||
// Publish each kind
|
||||
for (const config of publishConfigs) {
|
||||
console.log(`Publishing ${config.name}...`);
|
||||
try {
|
||||
const result = await config.publishFn();
|
||||
|
||||
const successCount = result.results.filter(r => r.success).length;
|
||||
const blastSuccess = result.blastResults ? result.blastResults.filter(r => r.success).length : 0;
|
||||
const userSuccess = result.userResults ? result.userResults.filter(r => r.success).length : 0;
|
||||
|
||||
allResults.push({
|
||||
kind: config.kind,
|
||||
name: config.name,
|
||||
success: successCount > 0,
|
||||
eventId: result.event.id,
|
||||
publishedTo: successCount,
|
||||
totalRelays: result.results.length,
|
||||
blastPublished: blastSuccess,
|
||||
blastTotal: result.blastResults ? result.blastResults.length : 0,
|
||||
userPublished: userSuccess,
|
||||
userTotal: result.userResults ? result.userResults.length : 0,
|
||||
details: result.results
|
||||
});
|
||||
} catch (err) {
|
||||
allResults.push({
|
||||
kind: config.kind,
|
||||
name: config.name,
|
||||
success: false,
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setResults({
|
||||
success: true,
|
||||
eventId: result.event.id,
|
||||
published: result.event
|
||||
success: allResults.some(r => r.success),
|
||||
events: allResults
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
setError('Failed to apply template: ' + err.message);
|
||||
setPublishResults({
|
||||
success: false,
|
||||
error: err.message
|
||||
});
|
||||
} finally {
|
||||
setApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle case where template fails to decode
|
||||
if (error && !template) {
|
||||
return (
|
||||
<div className="template-applier">
|
||||
|
|
@ -112,9 +167,6 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
|
|||
<div className="error-box">
|
||||
<h3>❌ Error Loading Template</h3>
|
||||
<p>{error}</p>
|
||||
<p style={{ marginTop: '10px', fontSize: '14px', color: '#666' }}>
|
||||
The link you clicked may be malformed or expired. Please try generating a new template.
|
||||
</p>
|
||||
<Link to="/" className="btn-primary" style={{ display: 'inline-block', marginTop: '10px', width: 'auto' }}>
|
||||
← Go Back to Create
|
||||
</Link>
|
||||
|
|
@ -144,8 +196,9 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
|
|||
</p>
|
||||
)}
|
||||
|
||||
{/* Main Relays */}
|
||||
<div className="relay-preview">
|
||||
<h4>📡 Relays in this template ({template.relays.length}):</h4>
|
||||
<h4>📡 Relays ({template.relays.length}):</h4>
|
||||
<ul>
|
||||
{template.relays.map((relay, index) => (
|
||||
<li key={index}>
|
||||
|
|
@ -158,9 +211,40 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
|
|||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Blossom Servers */}
|
||||
{template.blossomServers && template.blossomServers.length > 0 && (
|
||||
<div className="relay-preview" style={{ marginTop: '16px' }}>
|
||||
<h4>🌺 Blossom Servers ({template.blossomServers.length}):</h4>
|
||||
<ul>
|
||||
{template.blossomServers.map((server, index) => (
|
||||
<li key={index}>
|
||||
<span className="relay-url">{server.url}</span>
|
||||
<span style={{ fontSize: '12px', color: '#999' }}>
|
||||
#{index + 1} {index === 0 ? '⭐ Most trusted' : ''}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* DM Relays */}
|
||||
{template.dmRelays && template.dmRelays.length > 0 && (
|
||||
<div className="relay-preview" style={{ marginTop: '16px' }}>
|
||||
<h4>💬 DM Relays ({template.dmRelays.length}):</h4>
|
||||
<ul>
|
||||
{template.dmRelays.map((relay, index) => (
|
||||
<li key={index}>
|
||||
<span className="relay-url">{relay.url}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Connection Status & Button */}
|
||||
{/* Connection Status */}
|
||||
<div className="connection-status" style={{ marginBottom: '16px' }}>
|
||||
{(connectedPubkey || pubkey) ? (
|
||||
<div className="info-box">
|
||||
|
|
@ -201,42 +285,77 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
|
|||
</button>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{error && !results && (
|
||||
<div className="error-box" style={{ marginTop: '16px' }}>
|
||||
<strong>Error:</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success Results */}
|
||||
{publishResults && publishResults.success && (
|
||||
{/* Results */}
|
||||
{results && results.events && (
|
||||
<div className="success-box" style={{ marginTop: '20px' }}>
|
||||
<h3>✅ Template Applied Successfully!</h3>
|
||||
<p style={{ marginTop: '8px' }}>
|
||||
Published to <strong>{publishResults.publishedTo}</strong> of {publishResults.totalRelays} relays
|
||||
</p>
|
||||
<p style={{ fontSize: '13px', color: '#555', marginTop: '4px' }}>
|
||||
Event ID: <code style={{ fontSize: '12px', wordBreak: 'break-all' }}>
|
||||
{publishResults.eventId}
|
||||
</code>
|
||||
</p>
|
||||
<h3>
|
||||
{results.success ? '✅ Template Applied Successfully!' : '⚠️ Some Events Failed'}
|
||||
</h3>
|
||||
|
||||
<details style={{ marginTop: '12px' }}>
|
||||
<summary style={{ cursor: 'pointer', color: '#2e7d32' }}>
|
||||
📊 Publication Details
|
||||
</summary>
|
||||
<ul style={{ marginTop: '8px', fontSize: '13px' }}>
|
||||
{publishResults.details.map((result, index) => (
|
||||
<li key={index} style={{
|
||||
padding: '4px 0',
|
||||
color: result.success ? '#2e7d32' : '#c62828'
|
||||
}}>
|
||||
{result.success ? '✅' : '❌'} {result.url}
|
||||
{result.error && ` - ${result.error}`}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
{results.events.map((event, index) => (
|
||||
<div key={index} style={{
|
||||
marginTop: '12px',
|
||||
padding: '16px',
|
||||
background: event.success ? 'rgba(76, 175, 80, 0.1)' : 'rgba(255, 82, 82, 0.1)',
|
||||
borderRadius: '6px',
|
||||
border: event.success ? '1px solid #4caf50' : '1px solid #ff5252'
|
||||
}}>
|
||||
<strong>{event.name}</strong>
|
||||
{event.success ? (
|
||||
<>
|
||||
<p style={{ marginTop: '4px' }}>
|
||||
✅ Published to <strong>{event.publishedTo}</strong> of {event.totalRelays} relays
|
||||
</p>
|
||||
|
||||
{event.blastTotal > 0 && (
|
||||
<p style={{ fontSize: '13px', color: '#555' }}>
|
||||
📡 Blast relays: <strong>{event.blastPublished}/{event.blastTotal}</strong>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{event.userTotal > 0 && (
|
||||
<p style={{ fontSize: '13px', color: '#555' }}>
|
||||
👤 Your relays: <strong>{event.userPublished}/{event.userTotal}</strong>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p style={{ fontSize: '12px', color: '#555', marginTop: '4px' }}>
|
||||
Event ID: <code style={{ fontSize: '11px', wordBreak: 'break-all' }}>
|
||||
{event.eventId}
|
||||
</code>
|
||||
</p>
|
||||
|
||||
<details style={{ marginTop: '8px' }}>
|
||||
<summary style={{ cursor: 'pointer', color: '#2e7d32', fontSize: '13px' }}>
|
||||
📊 Publication Details ({event.publishedTo} successful)
|
||||
</summary>
|
||||
<ul style={{ marginTop: '8px', fontSize: '12px', maxHeight: '200px', overflowY: 'auto' }}>
|
||||
{event.details.map((result, idx) => (
|
||||
<li key={idx} style={{
|
||||
padding: '2px 0',
|
||||
color: result.success ? '#2e7d32' : '#c62828'
|
||||
}}>
|
||||
{result.success ? '✅' : '❌'} {result.url}
|
||||
{result.error && ` - ${result.error}`}
|
||||
{BLAST_RELAYS.includes(result.url) && ' 🔥'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
</>
|
||||
) : (
|
||||
<p style={{ color: '#c62828', marginTop: '4px' }}>
|
||||
❌ Failed: {event.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div style={{ marginTop: '16px', display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
|
||||
<Link to="/" className="btn-secondary">
|
||||
|
|
@ -251,21 +370,6 @@ const TemplateApplier = ({ connectedPubkey, setConnectedPubkey }) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Results */}
|
||||
{publishResults && !publishResults.success && (
|
||||
<div className="error-box" style={{ marginTop: '20px' }}>
|
||||
<h3>❌ Failed to Publish</h3>
|
||||
<p>{publishResults.error}</p>
|
||||
<button
|
||||
onClick={() => setPublishResults(null)}
|
||||
className="btn-secondary"
|
||||
style={{ marginTop: '10px' }}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,29 +1,42 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { encodeTemplate, generateTemplateId, validateTemplate } from '../utils/templates';
|
||||
import { encodeTemplate, generateTemplateId, validateTemplate, getCommunityTemplates } from '../utils/templates';
|
||||
import RelayList from './RelayList';
|
||||
import NostrConnect from './NostrConnect';
|
||||
|
||||
// Configure your blast relays here (hidden from UI)
|
||||
// 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 navigate = useNavigate();
|
||||
const [templateName, setTemplateName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [relays, setRelays] = useState([
|
||||
{ url: 'wss://relay.dyne.org', read: false, write: true },
|
||||
{ url: 'wss://relay.dyne.org/inbox', read: true, write: false },
|
||||
{ url: 'wss://basspistol.org', read: false, write: true },
|
||||
{ url: 'wss://basspistol.org/inbox', read: true, write: false },
|
||||
{ url: 'wss://spatia-arcana.com', read: false, write: true },
|
||||
{ url: 'wss://spatia-arcana.com/inbox', read: true, write: false },
|
||||
{ url: 'wss://nestr.nedao.ch', read: false, write: true },
|
||||
{ url: 'wss://nestr.nedao.ch/inbox', read: true, write: false },
|
||||
{ url: 'wss://pyramid.fiatjaf.com', read: false, write: true },
|
||||
{ url: 'wss://pyramid.fiatjaf.com/inbox', read: true, write: false },
|
||||
{ url: 'wss://relay.ditto.pub', read: true, write: true },
|
||||
{ url: 'wss://relay.damus.io', read: true, write: true },
|
||||
{ url: 'wss://nos.lol', read: true, write: true }
|
||||
]);
|
||||
|
||||
// Blossom servers (kind 10063)
|
||||
const [blossomServers, setBlossomServers] = useState([
|
||||
{ url: 'https://cdn.satellite.earth' }
|
||||
]);
|
||||
const [showBlossom, setShowBlossom] = useState(true);
|
||||
|
||||
// DM relays (kind 10050)
|
||||
const [dmRelays, setDmRelays] = useState([
|
||||
{ url: 'wss://relay.private-msgs.com' }
|
||||
]);
|
||||
const [showDmRelays, setShowDmRelays] = useState(true);
|
||||
|
||||
const [error, setError] = useState('');
|
||||
const [shareUrl, setShareUrl] = useState('');
|
||||
const [selectedTemplate, setSelectedTemplate] = useState('');
|
||||
|
||||
const communityTemplates = getCommunityTemplates();
|
||||
|
||||
const handleCreateTemplate = () => {
|
||||
try {
|
||||
|
|
@ -32,27 +45,51 @@ const TemplateCreator = ({ setConnectedPubkey }) => {
|
|||
name: templateName,
|
||||
description: description,
|
||||
relays: relays,
|
||||
blossomServers: showBlossom ? blossomServers : [],
|
||||
dmRelays: showDmRelays ? dmRelays : [],
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
|
||||
validateTemplate(template);
|
||||
|
||||
|
||||
const encoded = encodeTemplate(template);
|
||||
// Use HashRouter format
|
||||
const url = `${window.location.origin}${window.location.pathname}#/apply/${encoded}`;
|
||||
setShareUrl(url);
|
||||
|
||||
// Copy to clipboard automatically
|
||||
navigator.clipboard?.writeText(url).catch(() => {
|
||||
// Fallback - just show the URL
|
||||
});
|
||||
|
||||
|
||||
navigator.clipboard?.writeText(url).catch(() => {});
|
||||
|
||||
setError('');
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
const addRelay = () => {
|
||||
setRelays([...relays, { url: '', read: true, write: true }]);
|
||||
};
|
||||
|
|
@ -67,19 +104,130 @@ const TemplateCreator = ({ setConnectedPubkey }) => {
|
|||
setRelays(updated);
|
||||
};
|
||||
|
||||
// Blossom server functions
|
||||
const addBlossomServer = () => {
|
||||
setBlossomServers([...blossomServers, { url: '' }]);
|
||||
};
|
||||
|
||||
const removeBlossomServer = (index) => {
|
||||
setBlossomServers(blossomServers.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const updateBlossomServer = (index, value) => {
|
||||
const updated = [...blossomServers];
|
||||
updated[index].url = value;
|
||||
setBlossomServers(updated);
|
||||
};
|
||||
|
||||
// DM relay functions
|
||||
const addDmRelay = () => {
|
||||
setDmRelays([...dmRelays, { url: '' }]);
|
||||
};
|
||||
|
||||
const removeDmRelay = (index) => {
|
||||
setDmRelays(dmRelays.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const updateDmRelay = (index, value) => {
|
||||
const updated = [...dmRelays];
|
||||
updated[index].url = value;
|
||||
setDmRelays(updated);
|
||||
};
|
||||
|
||||
const addCommonRelay = (url) => {
|
||||
if (!relays.some(r => r.url === url)) {
|
||||
setRelays([...relays, { url, read: true, write: true }]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="template-creator">
|
||||
<h2>Create Relay Template</h2>
|
||||
|
||||
<NostrConnect setConnectedPubkey={setConnectedPubkey} />
|
||||
|
||||
{/* Community Templates Section */}
|
||||
<div className="form-group" style={{
|
||||
background: '#f8f9fa',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e9ecef',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '18px' }}>📚</span>
|
||||
Community Templates
|
||||
</label>
|
||||
<p style={{ fontSize: '13px', color: '#666', marginBottom: '10px' }}>
|
||||
Load a pre-configured template from popular communities
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
{Object.entries(communityTemplates).map(([key, template]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => loadCommunityTemplate(key)}
|
||||
className={`btn-secondary ${selectedTemplate === key ? 'active-template' : ''}`}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
border: selectedTemplate === key ? '2px solid #7c4dff' : '1px solid #ddd',
|
||||
background: selectedTemplate === key ? '#f0ebff' : 'white',
|
||||
fontWeight: selectedTemplate === key ? '600' : 'normal',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
>
|
||||
{template.name}
|
||||
<span style={{ fontSize: '11px', color: '#999', marginLeft: '4px' }}>
|
||||
({template.relays.length} relays)
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedTemplate && (
|
||||
<div style={{
|
||||
marginTop: '10px',
|
||||
fontSize: '13px',
|
||||
color: '#666',
|
||||
padding: '8px 12px',
|
||||
background: 'white',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #e9ecef'
|
||||
}}>
|
||||
💡 <strong>{communityTemplates[selectedTemplate].name}:</strong> {communityTemplates[selectedTemplate].description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedTemplate && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTemplate('');
|
||||
setRelays([
|
||||
{ url: 'wss://relay.damus.io', read: true, write: true },
|
||||
{ url: 'wss://nos.lol', read: true, write: true }
|
||||
]);
|
||||
setTemplateName('');
|
||||
setDescription('');
|
||||
setBlossomServers([{ url: 'https://cdn.satellite.earth' }]);
|
||||
setDmRelays([{ url: 'wss://relay.private-msgs.com' }]);
|
||||
setShowBlossom(true);
|
||||
setShowDmRelays(true);
|
||||
}}
|
||||
className="btn-secondary"
|
||||
style={{ marginTop: '8px', fontSize: '12px' }}
|
||||
>
|
||||
✕ Clear Template
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Template Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateName}
|
||||
onChange={(e) => setTemplateName(e.target.value)}
|
||||
placeholder="e.g., Nostr Plebs Default Relays"
|
||||
placeholder="e.g., My Community Relays"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -93,6 +241,7 @@ const TemplateCreator = ({ setConnectedPubkey }) => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Relays Section */}
|
||||
<div className="form-group">
|
||||
<label>Relays *</label>
|
||||
<RelayList
|
||||
|
|
@ -100,9 +249,128 @@ const TemplateCreator = ({ setConnectedPubkey }) => {
|
|||
onUpdate={updateRelay}
|
||||
onRemove={removeRelay}
|
||||
/>
|
||||
<button onClick={addRelay} className="btn-secondary">
|
||||
+ Add Relay
|
||||
</button>
|
||||
|
||||
<div style={{ marginTop: '10px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<button onClick={addRelay} className="btn-secondary">
|
||||
+ Add Custom Relay
|
||||
</button>
|
||||
<button
|
||||
onClick={() => addCommonRelay('wss://relay.primal.net')}
|
||||
className="btn-secondary"
|
||||
style={{ fontSize: '12px' }}
|
||||
>
|
||||
+ Primal
|
||||
</button>
|
||||
<button
|
||||
onClick={() => addCommonRelay('wss://relay.damus.io')}
|
||||
className="btn-secondary"
|
||||
style={{ fontSize: '12px' }}
|
||||
>
|
||||
+ Damus
|
||||
</button>
|
||||
<button
|
||||
onClick={() => addCommonRelay('wss://nos.lol')}
|
||||
className="btn-secondary"
|
||||
style={{ fontSize: '12px' }}
|
||||
>
|
||||
+ Nos.lol
|
||||
</button>
|
||||
<button
|
||||
onClick={() => addCommonRelay('wss://relay.snort.social')}
|
||||
className="btn-secondary"
|
||||
style={{ fontSize: '12px' }}
|
||||
>
|
||||
+ Snort
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Blossom Servers Section */}
|
||||
<div className="form-group">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '8px' }}>
|
||||
<label style={{ marginBottom: 0 }}>🌺 Blossom Servers (kind 10063)</label>
|
||||
<button
|
||||
onClick={() => setShowBlossom(!showBlossom)}
|
||||
className="btn-secondary"
|
||||
style={{ fontSize: '12px', padding: '4px 10px' }}
|
||||
>
|
||||
{showBlossom ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showBlossom && (
|
||||
<>
|
||||
<p style={{ fontSize: '13px', color: '#666', marginBottom: '10px' }}>
|
||||
Servers used to host your blobs/images. Order determines trust/reliability.
|
||||
</p>
|
||||
{blossomServers.map((server, index) => (
|
||||
<div key={index} className="relay-item">
|
||||
<input
|
||||
type="text"
|
||||
value={server.url}
|
||||
onChange={(e) => updateBlossomServer(index, e.target.value)}
|
||||
placeholder="https://cdn.example.com"
|
||||
className="relay-input"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeBlossomServer(index)}
|
||||
className="btn-danger"
|
||||
disabled={blossomServers.length === 1}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addBlossomServer} className="btn-secondary" style={{ marginTop: '4px' }}>
|
||||
+ Add Blossom Server
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* DM Relays Section */}
|
||||
<div className="form-group">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '8px' }}>
|
||||
<label style={{ marginBottom: 0 }}>💬 DM Relays (kind 10050)</label>
|
||||
<button
|
||||
onClick={() => setShowDmRelays(!showDmRelays)}
|
||||
className="btn-secondary"
|
||||
style={{ fontSize: '12px', padding: '4px 10px' }}
|
||||
>
|
||||
{showDmRelays ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showDmRelays && (
|
||||
<>
|
||||
<p style={{ fontSize: '13px', color: '#666', marginBottom: '10px' }}>
|
||||
Relays used for private messages.
|
||||
</p>
|
||||
{dmRelays.map((relay, index) => (
|
||||
<div key={index} className="relay-item">
|
||||
<input
|
||||
type="text"
|
||||
value={relay.url}
|
||||
onChange={(e) => updateDmRelay(index, e.target.value)}
|
||||
placeholder="wss://relay.private-msgs.com"
|
||||
className="relay-input"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeDmRelay(index)}
|
||||
className="btn-danger"
|
||||
disabled={dmRelays.length === 1}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addDmRelay} className="btn-secondary" style={{ marginTop: '4px' }}>
|
||||
+ Add DM Relay
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
|
@ -123,10 +391,13 @@ const TemplateCreator = ({ setConnectedPubkey }) => {
|
|||
<button onClick={() => navigator.clipboard?.writeText(shareUrl)}>
|
||||
Copy Link
|
||||
</button>
|
||||
<p style={{ fontSize: '12px', color: '#666', marginTop: '8px' }}>
|
||||
🔒 Template will be published to blast relays (configured in code)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateCreator;
|
||||
export default TemplateCreator;
|
||||
|
|
@ -1,5 +1,16 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
// Blast relays - configure these once and they'll be used for publishing
|
||||
// These are hidden from the UI and used to blast events to reliable relays
|
||||
const BLAST_RELAYS = [
|
||||
'wss://relay.primal.net',
|
||||
'wss://relay.damus.io',
|
||||
'wss://sendit.nosflare.com',
|
||||
'wss://nostr.mom',
|
||||
'wss://relay.ditto.pub',
|
||||
'wss://nos.lol'
|
||||
];
|
||||
|
||||
export const useNostr = () => {
|
||||
const [pubkey, setPubkey] = useState(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
|
@ -51,10 +62,6 @@ export const useNostr = () => {
|
|||
return await extension.signEvent(event);
|
||||
}, [extension]);
|
||||
|
||||
/**
|
||||
* Publish to a single relay using native WebSocket
|
||||
* Relays are passed in from the template - nothing hardcoded
|
||||
*/
|
||||
const publishToSingleRelay = useCallback((url, event) => {
|
||||
return new Promise((resolve) => {
|
||||
let ws = null;
|
||||
|
|
@ -62,14 +69,11 @@ export const useNostr = () => {
|
|||
let timeoutIds = [];
|
||||
|
||||
try {
|
||||
console.log(`Connecting to ${url}...`);
|
||||
ws = new WebSocket(url);
|
||||
|
||||
// Connection timeout
|
||||
const connectTimeout = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.log(`⏰ Connection timeout for ${url}`);
|
||||
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
||||
try { ws.close(); } catch (e) {}
|
||||
}
|
||||
|
|
@ -79,13 +83,9 @@ export const useNostr = () => {
|
|||
timeoutIds.push(connectTimeout);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log(`Connected to ${url}, sending event...`);
|
||||
|
||||
// Send the event as a JSON-RPC message
|
||||
const message = JSON.stringify(['EVENT', event]);
|
||||
ws.send(message);
|
||||
|
||||
// Publish timeout
|
||||
const publishTimeout = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
|
|
@ -96,40 +96,24 @@ export const useNostr = () => {
|
|||
}, 15000);
|
||||
timeoutIds.push(publishTimeout);
|
||||
|
||||
// Listen for responses
|
||||
ws.onmessage = (msg) => {
|
||||
try {
|
||||
const data = JSON.parse(msg.data);
|
||||
// Check for OK response for our event
|
||||
if (Array.isArray(data) && data[0] === 'OK' && data[1] === event.id) {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.log(`✅ Published to ${url}`);
|
||||
// Clear all timeouts
|
||||
timeoutIds.forEach(id => clearTimeout(id));
|
||||
try { ws.close(); } catch (e) {}
|
||||
resolve({ url, success: true });
|
||||
}
|
||||
}
|
||||
// Check for error response
|
||||
if (Array.isArray(data) && data[0] === 'OK' && data[1] !== event.id && data[2] === false) {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.log(`❌ Relay rejected event for ${url}:`, data[3]);
|
||||
timeoutIds.forEach(id => clearTimeout(id));
|
||||
try { ws.close(); } catch (e) {}
|
||||
resolve({ url, success: false, error: data[3] || 'Event rejected' });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parse errors for non-OK messages
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
ws.onerror = () => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.log(`❌ WebSocket error for ${url}:`, error);
|
||||
timeoutIds.forEach(id => clearTimeout(id));
|
||||
try { ws.close(); } catch (e) {}
|
||||
resolve({ url, success: false, error: 'WebSocket error' });
|
||||
|
|
@ -139,17 +123,15 @@ export const useNostr = () => {
|
|||
ws.onclose = () => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.log(`⚠️ Connection closed for ${url}`);
|
||||
timeoutIds.forEach(id => clearTimeout(id));
|
||||
resolve({ url, success: false, error: 'Connection closed' });
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
ws.onerror = () => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.log(`❌ Connection error for ${url}:`, error);
|
||||
timeoutIds.forEach(id => clearTimeout(id));
|
||||
resolve({ url, success: false, error: 'Connection error' });
|
||||
}
|
||||
|
|
@ -158,7 +140,6 @@ export const useNostr = () => {
|
|||
} catch (err) {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.log(`❌ Error with ${url}:`, err.message);
|
||||
timeoutIds.forEach(id => clearTimeout(id));
|
||||
if (ws) {
|
||||
try { ws.close(); } catch (e) {}
|
||||
|
|
@ -170,12 +151,11 @@ export const useNostr = () => {
|
|||
}, []);
|
||||
|
||||
/**
|
||||
* Publish to multiple relays (all from the user's template)
|
||||
* Publish to multiple relays with detailed results
|
||||
*/
|
||||
const publishToRelays = useCallback(async (event, relayUrls) => {
|
||||
console.log(`Publishing to ${relayUrls.length} relays...`);
|
||||
const publishToRelays = useCallback(async (event, relayUrls, label = '') => {
|
||||
console.log(`Publishing ${label} to ${relayUrls.length} relays...`);
|
||||
|
||||
// Filter out invalid URLs
|
||||
const validUrls = relayUrls.filter(url => {
|
||||
try {
|
||||
new URL(url);
|
||||
|
|
@ -189,26 +169,65 @@ export const useNostr = () => {
|
|||
return [{ url: 'none', success: false, error: 'No valid relay URLs' }];
|
||||
}
|
||||
|
||||
// Publish to each relay in parallel
|
||||
const publishPromises = validUrls.map(url => publishToSingleRelay(url, event));
|
||||
const results = await Promise.all(publishPromises);
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
console.log(`Published to ${successCount}/${results.length} relays`);
|
||||
console.log(`Published ${label} to ${successCount}/${results.length} relays`);
|
||||
|
||||
return results;
|
||||
}, [publishToSingleRelay]);
|
||||
|
||||
const publishKind10002 = useCallback(async (relays, pubkey) => {
|
||||
/**
|
||||
* Generic publish function that publishes to BOTH blast relays AND user relays
|
||||
*/
|
||||
const publishKind = useCallback(async (kind, data, pubkey, userRelayUrls = []) => {
|
||||
if (!extension) {
|
||||
throw new Error('No Nostr extension connected');
|
||||
}
|
||||
|
||||
// relays comes from the user's template - NOT hardcoded
|
||||
const event = {
|
||||
kind: kind,
|
||||
pubkey: pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: data.tags || [],
|
||||
content: data.content || ''
|
||||
};
|
||||
|
||||
console.log(`Signing kind ${kind} event...`);
|
||||
const signedEvent = await extension.signEvent(event);
|
||||
console.log(`Kind ${kind} event signed successfully`);
|
||||
|
||||
// Combine blast relays and user relays (deduplicated)
|
||||
const allRelays = [...new Set([...BLAST_RELAYS, ...userRelayUrls])];
|
||||
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}`);
|
||||
|
||||
// Separate results for better reporting
|
||||
const blastResults = results.filter(r => BLAST_RELAYS.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 {
|
||||
event: signedEvent,
|
||||
results: results,
|
||||
blastResults: blastResults,
|
||||
userResults: userResults
|
||||
};
|
||||
}, [extension, publishToRelays]);
|
||||
|
||||
// Kind 10002 - Relays
|
||||
const publishKind10002 = useCallback(async (relays, pubkey) => {
|
||||
const validRelays = relays.filter(r => r.url && r.url.trim() !== '');
|
||||
|
||||
if (validRelays.length === 0) {
|
||||
throw new Error('No valid relays to publish to');
|
||||
throw new Error('No valid relays to publish');
|
||||
}
|
||||
|
||||
const tags = validRelays.map(relay => {
|
||||
|
|
@ -218,30 +237,41 @@ export const useNostr = () => {
|
|||
return ['r', relay.url, ...params];
|
||||
});
|
||||
|
||||
const event = {
|
||||
kind: 10002,
|
||||
pubkey: pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: tags,
|
||||
content: ''
|
||||
};
|
||||
|
||||
console.log('Signing kind 10002 event...');
|
||||
const signedEvent = await extension.signEvent(event);
|
||||
console.log('Event signed successfully');
|
||||
|
||||
// Extract URLs from the user's relay list
|
||||
const relayUrls = [...new Set(validRelays.map(r => r.url))];
|
||||
console.log('Publishing to relay URLs:', relayUrls);
|
||||
// Extract user relay URLs for combined publishing
|
||||
const userRelayUrls = validRelays.map(r => r.url);
|
||||
|
||||
const results = await publishToRelays(signedEvent, relayUrls);
|
||||
console.log('Final results:', results);
|
||||
return await publishKind(10002, { tags }, pubkey, userRelayUrls);
|
||||
}, [publishKind]);
|
||||
|
||||
// Kind 10063 - Blossom Servers
|
||||
const publishKind10063 = useCallback(async (servers, pubkey) => {
|
||||
const validServers = servers.filter(s => s.url && s.url.trim() !== '');
|
||||
|
||||
return {
|
||||
event: signedEvent,
|
||||
results: results
|
||||
};
|
||||
}, [extension, publishToRelays]);
|
||||
if (validServers.length === 0) {
|
||||
throw new Error('No valid blossom servers');
|
||||
}
|
||||
|
||||
const tags = validServers.map(server => ['server', server.url]);
|
||||
|
||||
// For blossom servers, we want to publish to blast relays
|
||||
// The user's main relays are used for kind 10002, not kind 10063
|
||||
return await publishKind(10063, { tags }, pubkey, []);
|
||||
}, [publishKind]);
|
||||
|
||||
// Kind 10050 - DM Relays
|
||||
const publishKind10050 = useCallback(async (relays, pubkey) => {
|
||||
const validRelays = relays.filter(r => r.url && r.url.trim() !== '');
|
||||
|
||||
if (validRelays.length === 0) {
|
||||
throw new Error('No valid DM relays');
|
||||
}
|
||||
|
||||
const tags = validRelays.map(relay => ['relay', relay.url]);
|
||||
|
||||
// For DM relays, we want to publish to blast relays
|
||||
// The user's main relays are used for kind 10002, not kind 10050
|
||||
return await publishKind(10050, { tags }, pubkey, []);
|
||||
}, [publishKind]);
|
||||
|
||||
return {
|
||||
pubkey,
|
||||
|
|
@ -251,8 +281,11 @@ export const useNostr = () => {
|
|||
connect,
|
||||
signEvent,
|
||||
publishToRelays,
|
||||
publishKind10002
|
||||
publishKind,
|
||||
publishKind10002,
|
||||
publishKind10063,
|
||||
publishKind10050
|
||||
};
|
||||
};
|
||||
|
||||
export default useNostr;
|
||||
export default useNostr;
|
||||
|
|
|
|||
|
|
@ -6,15 +6,11 @@
|
|||
|
||||
/**
|
||||
* Encode a template object into a URL-safe base64 string
|
||||
* Using btoa with proper UTF-8 handling
|
||||
*/
|
||||
export const encodeTemplate = (template) => {
|
||||
try {
|
||||
const json = JSON.stringify(template);
|
||||
// Proper UTF-8 encoding for btoa
|
||||
const utf8Encoded = encodeURIComponent(json);
|
||||
const base64 = btoa(utf8Encoded);
|
||||
return base64;
|
||||
return btoa(encodeURIComponent(json));
|
||||
} catch (error) {
|
||||
throw new Error('Failed to encode template: ' + error.message);
|
||||
}
|
||||
|
|
@ -25,11 +21,8 @@ export const encodeTemplate = (template) => {
|
|||
*/
|
||||
export const decodeTemplate = (encoded) => {
|
||||
try {
|
||||
// First, make sure the string is clean
|
||||
const clean = encoded.trim();
|
||||
// Decode from base64
|
||||
const decoded = atob(clean);
|
||||
// Decode the URI component
|
||||
const json = decodeURIComponent(decoded);
|
||||
return JSON.parse(json);
|
||||
} catch (error) {
|
||||
|
|
@ -65,6 +58,7 @@ export const validateTemplate = (template) => {
|
|||
throw new Error('Template must have a name');
|
||||
}
|
||||
|
||||
// Validate main relays
|
||||
if (!Array.isArray(template.relays) || template.relays.length === 0) {
|
||||
throw new Error('Template must have at least one relay');
|
||||
}
|
||||
|
|
@ -74,7 +68,6 @@ export const validateTemplate = (template) => {
|
|||
throw new Error(`Relay ${index + 1} must have a URL`);
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
const url = new URL(relay.url);
|
||||
if (url.protocol !== 'wss:' && url.protocol !== 'ws:') {
|
||||
|
|
@ -93,6 +86,40 @@ export const validateTemplate = (template) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Validate blossom servers (optional)
|
||||
if (template.blossomServers && Array.isArray(template.blossomServers)) {
|
||||
template.blossomServers.forEach((server, index) => {
|
||||
if (!server.url || typeof server.url !== 'string') {
|
||||
throw new Error(`Blossom server ${index + 1} must have a URL`);
|
||||
}
|
||||
try {
|
||||
const url = new URL(server.url);
|
||||
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
||||
throw new Error(`Blossom server ${index + 1} must use https:// or http:// protocol`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Blossom server ${index + 1} has invalid URL: ${server.url}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Validate DM relays (optional)
|
||||
if (template.dmRelays && Array.isArray(template.dmRelays)) {
|
||||
template.dmRelays.forEach((relay, index) => {
|
||||
if (!relay.url || typeof relay.url !== 'string') {
|
||||
throw new Error(`DM relay ${index + 1} must have a URL`);
|
||||
}
|
||||
try {
|
||||
const url = new URL(relay.url);
|
||||
if (url.protocol !== 'wss:' && url.protocol !== 'ws:') {
|
||||
throw new Error(`DM relay ${index + 1} must use wss:// or ws:// protocol`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`DM relay ${index + 1} has invalid URL: ${relay.url}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
@ -123,6 +150,12 @@ export const getDefaultTemplate = () => {
|
|||
{ url: 'wss://nos.lol', read: true, write: true },
|
||||
{ url: 'wss://relay.snort.social', read: true, write: true }
|
||||
],
|
||||
blossomServers: [
|
||||
{ url: 'https://cdn.satellite.earth' }
|
||||
],
|
||||
dmRelays: [
|
||||
{ url: 'wss://relay.private-msgs.com' }
|
||||
],
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
};
|
||||
|
|
@ -132,35 +165,108 @@ export const getDefaultTemplate = () => {
|
|||
*/
|
||||
export const getCommunityTemplates = () => {
|
||||
return {
|
||||
'nostr-punks': {
|
||||
id: 'nostr-punks',
|
||||
name: 'Nostr Punks',
|
||||
description: 'Relay set for the Nostr Punks community',
|
||||
'planet-dyne': {
|
||||
id: 'planet-dyne',
|
||||
name: 'Planet Dyne',
|
||||
description: 'Relay set for dynes like you',
|
||||
relays: [
|
||||
{ url: 'wss://relay.nostr-punks.com', read: true, write: true },
|
||||
{ url: 'wss://relay.damus.io', read: true, write: true },
|
||||
{ url: 'wss://nostr-pub.wellorder.net', read: true, write: true }
|
||||
{ url: 'wss://relay.dyne.org', read: false, write: true },
|
||||
{ url: 'wss://relay.dyne.org/inbox', read: true, write: false },
|
||||
{ url: 'wss://relay.ditto.pub', read: true, write: true }
|
||||
],
|
||||
blossomServers: [
|
||||
{ url: 'https://relay.dyne.org' },
|
||||
{ url: 'https://blossom.primal.net' }
|
||||
],
|
||||
dmRelays: [
|
||||
{ url: 'wss://relay.dyne.org/inbox' }
|
||||
]
|
||||
},
|
||||
'plebchain': {
|
||||
id: 'plebchain',
|
||||
name: 'PlebChain',
|
||||
description: 'Relay set for PlebChain community',
|
||||
'basspistol': {
|
||||
id: 'basspistol',
|
||||
name: 'Basspistol',
|
||||
description: 'Relay set for Basspistol outernational music syndicate',
|
||||
relays: [
|
||||
{ url: 'wss://basspistol.org', read: false, write: true },
|
||||
{ url: 'wss://basspistol.org/inbox', read: true, write: false },
|
||||
{ url: 'wss://relay.ditto.pub', read: true, write: true }
|
||||
],
|
||||
blossomServers: [
|
||||
{ url: 'https://basspistol.org' },
|
||||
{ url: 'https://blossom.primal.net' }
|
||||
],
|
||||
dmRelays: [
|
||||
{ url: 'wss://basspistol.org/inbox' }
|
||||
]
|
||||
},
|
||||
'spatia-arcana': {
|
||||
id: 'spatia-arcana',
|
||||
name: 'Spatia Arcana',
|
||||
description: 'Relay set for Spatia Arcana',
|
||||
relays: [
|
||||
{ url: 'wss://spatia-arcana.com', read: false, write: true },
|
||||
{ url: 'wss://spatia-arcana.com/inbox', read: true, write: false },
|
||||
{ url: 'wss://relay.ditto.pub', read: true, write: true }
|
||||
],
|
||||
blossomServers: [
|
||||
{ url: 'https://spatia-arcana.com' },
|
||||
{ url: 'https://blossom.primal.net' }
|
||||
],
|
||||
dmRelays: [
|
||||
{ url: 'wss://spatia-arcana.com/inbox' }
|
||||
]
|
||||
},
|
||||
'pyramid-fiatjaf': {
|
||||
id: 'pyramid-fiatjaf',
|
||||
name: 'Fiatjaf Pyramid',
|
||||
description: 'Relay set for Fiatjaf Pyramid',
|
||||
relays: [
|
||||
{ url: 'wss://pyramid.fiatjaf.com', read: false, write: true },
|
||||
{ url: 'wss://pyramid.fiatjaf.com/inbox', read: true, write: false },
|
||||
{ url: 'wss://relay.ditto.pub', read: true, write: true }
|
||||
],
|
||||
blossomServers: [
|
||||
{ url: 'https://pyramid.fiatjaf.com' },
|
||||
{ url: 'https://blossom.primal.net' }
|
||||
],
|
||||
dmRelays: [
|
||||
{ url: 'wss://pyramid.fiatjaf.com/inbox' }
|
||||
]
|
||||
},
|
||||
'neuch-blockchain': {
|
||||
id: 'neuch-blockchain',
|
||||
name: 'Neuchatel Blockchain',
|
||||
description: 'Relay set for Neuchatel Blockchain community',
|
||||
relays: [
|
||||
{ url: 'wss://nestr.nedao.ch', read: false, write: true },
|
||||
{ url: 'wss://nestr.nedao.ch/inbox', read: true, write: false },
|
||||
{ url: 'wss://relay.ditto.pub', read: true, write: true }
|
||||
],
|
||||
blossomServers: [
|
||||
{ url: 'https://nestr.nedao.ch' },
|
||||
{ url: 'https://blossom.primal.net' }
|
||||
],
|
||||
dmRelays: [
|
||||
{ url: 'wss://nestr.nedao.ch/inbox' }
|
||||
]
|
||||
},
|
||||
'anon': {
|
||||
id: 'anon',
|
||||
name: 'Anon Relays',
|
||||
description: 'Relay set for anons',
|
||||
relays: [
|
||||
{ url: 'wss://relay.damus.io', read: true, write: true },
|
||||
{ url: 'wss://nos.lol', read: true, write: true },
|
||||
{ url: 'wss://relay.snort.social', read: true, write: true },
|
||||
{ url: 'wss://offchain.pub', read: true, write: true }
|
||||
{ url: 'wss://nostr.mom', read: true, write: true },
|
||||
{ url: 'wss://relay.ditto.pub', read: true, write: true }
|
||||
],
|
||||
blossomServers: [
|
||||
{ url: 'https://spatia-arcana.com' },
|
||||
{ url: 'https://blossom.primal.net' }
|
||||
],
|
||||
dmRelays: [
|
||||
{ url: 'wss://spatia-arcana.com/inbox' }
|
||||
]
|
||||
},
|
||||
'minimal': {
|
||||
id: 'minimal',
|
||||
name: 'Minimal Set',
|
||||
description: 'Just the essentials to get started',
|
||||
relays: [
|
||||
{ url: 'wss://relay.damus.io', read: true, write: true }
|
||||
]
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -178,7 +284,6 @@ export const addRelayToTemplate = (template, relay) => {
|
|||
throw new Error('Relay must have a URL');
|
||||
}
|
||||
|
||||
// Check if relay already exists
|
||||
if (template.relays.some(r => r.url === relay.url)) {
|
||||
throw new Error('Relay already exists in template');
|
||||
}
|
||||
|
|
@ -264,6 +369,8 @@ export const kind10002TagsToTemplate = (tags, name = 'Imported Relays') => {
|
|||
name: name,
|
||||
description: `Imported from existing relay set`,
|
||||
relays: relays,
|
||||
blossomServers: [],
|
||||
dmRelays: [],
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
};
|
||||
|
|
@ -283,10 +390,8 @@ export const saveTemplateToStorage = (template) => {
|
|||
...template,
|
||||
saved_at: Date.now()
|
||||
};
|
||||
// Remove duplicates
|
||||
const filtered = history.filter(h => h.id !== template.id);
|
||||
filtered.unshift(entry);
|
||||
// Keep last 50 templates
|
||||
localStorage.setItem('templateHistory', JSON.stringify(filtered.slice(0, 50)));
|
||||
return entry;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue