allow to create dm and blossom lists

give a couple templates for happy frens
This commit is contained in:
sakrecoer 2026-08-18 16:18:23 +02:00
parent 57a252b983
commit 6534cad6eb
7 changed files with 729 additions and 204 deletions

View file

@ -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>
);
};

View file

@ -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;