You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

559 lines
27 KiB

{{define "content"}}
<article class="contact-page">
<h1>Contact Us</h1>
<div class="contact-links">
<h2>Find us elsewhere:</h2>
<ul>
<li>
<a href="https://aitherboard.imwald.eu/repos/naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqq9xw6t5vd5hgctyv4kqde47kt?tab=about" target="_blank" rel="noopener noreferrer">
<span class="icon-inline">{{icon "package"}}</span> Aitherboard Repository
</a>
</li>
<li>
<a href="https://github.com/ShadowySupercode" target="_blank" rel="noopener noreferrer">
<span class="icon-inline">{{icon "github"}}</span> GitHub: ShadowySupercode
</a>
</li>
<li>
<a href="https://alexandria.gitcitadel.eu/events?id=npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz" target="_blank" rel="noopener noreferrer">
<span class="icon-inline">{{icon "user"}}</span> GitCitadel on Alexandria
</a>
</li>
</ul>
</div>
{{if .Profile}}
<div class="nostr-profile">
<h2>Nostr Profile</h2>
<div class="nostr-profile-content">
{{if .Profile.Picture}}
<img src="{{.Profile.Picture}}" alt="Profile picture" class="nostr-profile-picture">
{{end}}
<div class="nostr-profile-info">
<h3>
{{if .Profile.DisplayName}}{{.Profile.DisplayName}}{{else if .Profile.Name}}{{.Profile.Name}}{{else}}Anonymous{{end}}
</h3>
{{if .Profile.Name}}
<p class="nostr-profile-name">@{{.Profile.Name}}</p>
{{end}}
{{if .Profile.About}}
<p class="nostr-profile-about">{{.Profile.About}}</p>
{{end}}
{{if .Profile.Website}}
<p class="nostr-profile-website">
<a href="{{.Profile.Website}}" target="_blank" rel="noopener noreferrer">
<span class="icon-inline">{{icon "globe"}}</span> Website
</a>
</p>
{{end}}
{{if .Profile.NIP05}}
<p class="nostr-profile-nip05">
<span class="icon-inline">{{icon "check-circle"}}</span> NIP-05: {{.Profile.NIP05}}
</p>
{{end}}
</div>
</div>
</div>
{{end}}
<p>Have a question, suggestion, or want to report an issue? Fill out the form below and we'll get back to you.</p>
{{if .Success}}
<div class="alert alert-success" role="alert">
<strong><span class="icon-inline">{{icon "check-circle"}}</span> Success!</strong> Your message has been submitted. Thank you for contacting us!
{{if .EventID}}
<br><small><span class="icon-inline">{{icon "hash"}}</span> Issue ID: {{.EventID}}</small>
{{end}}
</div>
{{end}}
{{if .Error}}
{{template "alert-error" .Error}}
{{end}}
<form id="contact-form" method="POST" action="/contact" class="contact-form">
<div id="nostr-status" class="alert" style="display: none;"></div>
<div class="form-group">
<label for="subject">Subject <span class="required">*</span></label>
<input type="text" id="subject" name="subject" required
value="{{.FormData.Subject}}"
placeholder="Brief description of your message"
aria-required="true">
</div>
<div class="form-group">
<label for="content">Message <span class="required">*</span></label>
<textarea id="content" name="content" rows="10" required
placeholder="Please provide details about your question, suggestion, or issue..."
aria-required="true">{{.FormData.Content}}</textarea>
<small class="form-help">You can use Markdown formatting in your message.</small>
</div>
<div class="form-group">
<label for="labels">Labels (optional)</label>
<input type="text" id="labels" name="labels"
value="{{.FormData.Labels}}"
placeholder="bug, feature-request, question (comma-separated)">
<small class="form-help">Add labels to categorize your issue (comma-separated).</small>
</div>
<div class="form-actions">
<button type="submit" id="submit-btn" class="btn btn-primary"><span class="icon-inline">{{icon "send"}}</span> Submit</button>
<button type="reset" class="btn btn-secondary"><span class="icon-inline">{{icon "x"}}</span> Clear</button>
</div>
</form>
<!-- Contact Submission Modal -->
<div id="contact-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h2>Choose Submission Method</h2>
<button type="button" class="modal-close" id="modal-close-btn" aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<p>How would you like to submit your message?</p>
<div class="modal-options">
<button type="button" id="login-btn" class="btn btn-primary">
<span class="icon-inline">{{icon "key"}}</span> Login with Browser Extension
</button>
<button type="button" id="anonymous-btn" class="btn btn-secondary">
<span class="icon-inline">{{icon "user-x"}}</span> Submit Anonymously
</button>
</div>
</div>
</div>
</div>
<!-- Success Modal -->
<div id="success-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h2><span class="icon-inline">{{icon "check-circle"}}</span> Success!</h2>
<button type="button" class="modal-close" id="success-modal-close-btn" aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<p>Your message has been successfully submitted to the Nostr relays.</p>
<div class="modal-options">
<a href="https://aitherboard.imwald.eu/repos/naddr1qvzqqqrhnypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqq9xw6t5vd5hgctyv4kqde47kt?tab=issues" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
<span class="icon-inline">{{icon "external-link"}}</span> View Issue Board
</a>
<button type="button" id="success-modal-close-btn2" class="btn btn-secondary">
<span class="icon-inline">{{icon "x"}}</span> Close
</button>
</div>
</div>
</div>
</div>
<!-- Failure Modal -->
<div id="failure-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h2><span class="icon-inline">{{icon "alert-circle"}}</span> Submission Failed</h2>
<button type="button" class="modal-close" id="failure-modal-close-btn" aria-label="Close">&times;</button>
</div>
<div class="modal-body">
<p id="failure-message">Failed to publish your message to any of the configured relays. Please try again later.</p>
<div class="modal-options">
<button type="button" id="failure-modal-close-btn2" class="btn btn-primary">
<span class="icon-inline">{{icon "x"}}</span> Close
</button>
</div>
</div>
</div>
</div>
{{if .RepoAnnouncement}}
<script type="application/json" id="repo-announcement-data">{{json .RepoAnnouncement}}</script>
{{end}}
<script type="application/json" id="contact-relays-data">{{json .ContactRelays}}</script>
<script src="/static/js/nostr.bundle.js"></script>
<script>
(function() {
const form = document.getElementById('contact-form');
const submitBtn = document.getElementById('submit-btn');
const statusDiv = document.getElementById('nostr-status');
const modal = document.getElementById('contact-modal');
const modalCloseBtn = document.getElementById('modal-close-btn');
const loginBtn = document.getElementById('login-btn');
const anonymousBtn = document.getElementById('anonymous-btn');
const successModal = document.getElementById('success-modal');
const successModalCloseBtn = document.getElementById('success-modal-close-btn');
const successModalCloseBtn2 = document.getElementById('success-modal-close-btn2');
const failureModal = document.getElementById('failure-modal');
const failureModalCloseBtn = document.getElementById('failure-modal-close-btn');
const failureModalCloseBtn2 = document.getElementById('failure-modal-close-btn2');
const failureMessage = document.getElementById('failure-message');
// Get contact relays from JSON script tag
let contactRelays = [];
const contactRelaysEl = document.getElementById('contact-relays-data');
if (contactRelaysEl) {
try {
contactRelays = JSON.parse(contactRelaysEl.textContent);
} catch (e) {
console.error('Failed to parse contact relays data:', e);
// Fallback to empty array
contactRelays = [];
}
}
// Get repo announcement data from JSON script tag
let repoAnnouncement = null;
const repoDataEl = document.getElementById('repo-announcement-data');
if (repoDataEl) {
try {
// Trim whitespace before parsing
let jsonText = repoDataEl.textContent.trim();
let parsed = JSON.parse(jsonText);
// Handle double-encoded JSON (if the result is still a string, parse again)
if (typeof parsed === 'string') {
parsed = JSON.parse(parsed);
}
// Ensure all required fields are present and valid
const pubkey = parsed?.Pubkey;
const dTag = parsed?.DTag;
const hasPubkey = pubkey && typeof pubkey === 'string' && pubkey.trim() !== '';
const hasDTag = dTag && typeof dTag === 'string' && dTag.trim() !== '';
if (hasPubkey && hasDTag) {
repoAnnouncement = parsed;
console.log('Repo announcement loaded successfully:', {
dTag: dTag,
pubkey: pubkey.substring(0, 16) + '...',
maintainersCount: parsed.Maintainers ? parsed.Maintainers.length : 0,
relaysCount: parsed.Relays ? parsed.Relays.length : 0
});
} else {
console.error('Repo announcement data incomplete:', {
hasPubkey,
hasDTag,
pubkeyType: typeof pubkey,
pubkeyValue: pubkey,
dTagType: typeof dTag,
dTagValue: dTag,
fullObject: parsed
});
}
} catch (e) {
console.error('Failed to parse repo announcement data:', e, 'Raw content:', repoDataEl.textContent);
}
} else {
console.warn('Repo announcement data element not found');
}
// Store form data for submission
let pendingFormData = null;
function showStatus(message, isError) {
statusDiv.textContent = message;
statusDiv.className = 'alert ' + (isError ? 'alert-error' : 'alert-success');
statusDiv.style.display = 'block';
}
function hideStatus() {
statusDiv.style.display = 'none';
}
function showModal() {
modal.style.display = 'flex';
}
function hideModal() {
modal.style.display = 'none';
}
function showSuccessModal() {
successModal.style.display = 'flex';
}
function hideSuccessModal() {
successModal.style.display = 'none';
// Reset form
form.reset();
submitBtn.disabled = false;
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg></span> Submit';
}
function showFailureModal(errorMessage) {
if (errorMessage) {
failureMessage.textContent = errorMessage;
}
failureModal.style.display = 'flex';
}
function hideFailureModal() {
failureModal.style.display = 'none';
submitBtn.disabled = false;
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg></span> Submit';
}
// Close modal handlers
modalCloseBtn.addEventListener('click', hideModal);
modal.addEventListener('click', function(e) {
if (e.target === modal) {
hideModal();
}
});
successModalCloseBtn.addEventListener('click', hideSuccessModal);
successModalCloseBtn2.addEventListener('click', hideSuccessModal);
successModal.addEventListener('click', function(e) {
if (e.target === successModal) {
hideSuccessModal();
}
});
failureModalCloseBtn.addEventListener('click', hideFailureModal);
failureModalCloseBtn2.addEventListener('click', hideFailureModal);
failureModal.addEventListener('click', function(e) {
if (e.target === failureModal) {
hideFailureModal();
}
});
// Generate key pair for anonymous submission
async function generateKeyPair() {
const secretKey = NostrTools.generateSecretKey();
const pubkey = NostrTools.getPublicKey(secretKey);
return { privateKey: secretKey, pubkey: pubkey };
}
// Sign event with private key
function signEvent(event, privateKey) {
return NostrTools.finalizeEvent(event, privateKey);
}
// Extract outbox (write) relays from user's relay list (kind 10002)
function extractOutboxRelays(relayListEvent) {
const outboxRelays = [];
if (relayListEvent && relayListEvent.tags) {
for (const tag of relayListEvent.tags) {
// Format: ["r", "<relay-url>", "write"] for outbox relays
if (tag[0] === 'r' && tag.length >= 3 && tag[2] === 'write') {
const relayUrl = tag[1];
if (relayUrl && !outboxRelays.includes(relayUrl)) {
outboxRelays.push(relayUrl);
}
}
}
}
return outboxRelays;
}
// Submit event
async function submitEvent(signedEvent, additionalRelays = []) {
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg></span> Publishing...';
// Include additional relays (outbox relays from user's relay list)
const eventWithRelays = {
...signedEvent,
additionalRelays: additionalRelays
};
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ event: eventWithRelays })
});
const result = await response.json();
if (response.ok && result.success) {
// Show success modal
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg></span> Submit';
showSuccessModal();
} else {
// Show failure modal
const errorMsg = 'Failed to publish your message to any of the configured relays. ' + (result.error || 'Please try again later.');
showFailureModal(errorMsg);
}
}
// Process submission with pubkey
async function processSubmission(useExtension) {
hideModal();
submitBtn.disabled = true;
hideStatus();
const { subject, content, labelsStr } = pendingFormData;
try {
let pubkey;
let signFunction;
let userOutboxRelays = [];
if (useExtension) {
// Login with browser extension
if (!window.nostr) {
showStatus('Nostr extension not found. Please install a Nostr browser extension (e.g., nos2x, Alby).', true);
submitBtn.disabled = false;
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg></span> Submit';
return;
}
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg></span> Signing...';
pubkey = await window.nostr.getPublicKey();
signFunction = (event) => window.nostr.signEvent(event);
// Fetch user's relay list (kind 10002) to get outbox relays
if (window.nostr.getRelays) {
try {
const relayListEvent = await window.nostr.getRelays();
if (relayListEvent) {
userOutboxRelays = extractOutboxRelays(relayListEvent);
}
} catch (e) {
console.warn('Failed to fetch user relay list:', e);
// Continue without user relay list - not critical
}
}
} else {
// Anonymous submission - generate key pair
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg></span> Generating key...';
const keyPair = await generateKeyPair();
pubkey = keyPair.pubkey;
signFunction = (event) => {
return signEvent(event, keyPair.privateKey);
};
}
// Parse labels
const labels = labelsStr ? labelsStr.split(',').map(l => l.trim()).filter(l => l) : [];
// Build event tags
const tags = [];
// Add 'a' tag for repository announcement if available (required for NIP-34 issues)
// Format: ["a", "30617:<pubkey>:<d-tag>"]
// Note: Field names are capitalized (Pubkey, DTag) as they come from Go
const repoPubkey = repoAnnouncement?.Pubkey;
const repoDTag = repoAnnouncement?.DTag;
if (repoAnnouncement && repoPubkey && typeof repoPubkey === 'string' && repoPubkey.trim() !== '' &&
repoDTag && typeof repoDTag === 'string' && repoDTag.trim() !== '') {
tags.push(['a', `30617:${repoPubkey.trim()}:${repoDTag.trim()}`]);
// Collect unique pubkeys for 'p' tags (owner + maintainers, deduplicated)
const uniquePubkeys = new Set();
// Add repository owner (required for NIP-34 issues)
uniquePubkeys.add(repoPubkey.trim());
// Add maintainers (deduplicated - owner won't be added twice)
if (repoAnnouncement.Maintainers && Array.isArray(repoAnnouncement.Maintainers)) {
repoAnnouncement.Maintainers.forEach(maintainer => {
if (maintainer && typeof maintainer === 'string' && maintainer.trim() !== '') {
uniquePubkeys.add(maintainer.trim());
}
});
}
// Add all unique pubkeys as 'p' tags
uniquePubkeys.forEach(pk => {
tags.push(['p', pk]);
});
} else {
// This should not happen if validation passed, but log for debugging
console.error('Repo announcement data missing or incomplete in tag building:', JSON.stringify(repoAnnouncement));
showFailureModal('Repository configuration is invalid. Please refresh the page and try again.');
return;
}
// Add subject tag (required for NIP-34 issues)
if (subject) {
tags.push(['subject', subject]);
}
// Add label tags (t tags for issue labels per NIP-34)
labels.forEach(label => {
if (label) {
tags.push(['t', label]);
}
});
// Add client tag
tags.push(['client', 'GitCitadel.com']);
// Create unsigned event (kind 1621 for issues per NIP-34)
const unsignedEvent = {
kind: 1621,
pubkey: pubkey,
created_at: Math.floor(Date.now() / 1000),
tags: tags,
content: content // Just the content, subject is in tags
};
// Sign the event
submitBtn.innerHTML = '<span class="icon-inline"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg></span> Signing...';
const signedEvent = await signFunction(unsignedEvent);
// Submit event with user's outbox relays
await submitEvent(signedEvent, userOutboxRelays);
} catch (error) {
console.error('Error:', error);
showFailureModal('Error: ' + error.message);
}
}
// Login button handler
loginBtn.addEventListener('click', () => processSubmission(true));
// Anonymous button handler
anonymousBtn.addEventListener('click', () => processSubmission(false));
// Form submit handler
form.addEventListener('submit', async function(e) {
e.preventDefault();
// Validate repo announcement data
if (!repoAnnouncement) {
showStatus('Repository configuration not available. Please try again later.', true);
console.error('Repo announcement is null or undefined');
return;
}
// Additional validation (should already be validated during parsing, but double-check)
if (!repoAnnouncement.Pubkey || typeof repoAnnouncement.Pubkey !== 'string' || repoAnnouncement.Pubkey.trim() === '') {
showStatus('Repository configuration incomplete: missing pubkey. Please try again later.', true);
console.error('Repo announcement missing Pubkey:', JSON.stringify(repoAnnouncement));
return;
}
if (!repoAnnouncement.DTag || typeof repoAnnouncement.DTag !== 'string' || repoAnnouncement.DTag.trim() === '') {
showStatus('Repository configuration incomplete: missing d-tag. Please try again later.', true);
console.error('Repo announcement missing DTag:', JSON.stringify(repoAnnouncement));
return;
}
const subject = document.getElementById('subject').value.trim();
const content = document.getElementById('content').value.trim();
const labelsStr = document.getElementById('labels').value.trim();
if (!subject || !content) {
showStatus('Subject and message are required.', true);
return;
}
// Store form data
pendingFormData = { subject, content, labelsStr };
// Show modal
showModal();
});
})();
</script>
</article>
{{end}}
{{/* Feed is defined in components.html */}}