diff --git a/src/main.js b/src/main.js index 01a0b11..d7551da 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,4 @@ -import {generatePrivateKey, getPublicKey, nip19, signEvent} from 'nostr-tools'; +import {nip19, signEvent} from 'nostr-tools'; import {zeroLeadingBitsCount} from './utils/crypto'; import {elem, elemCanvas, elemShrink, parseTextContent, updateElemHeight} from './utils/dom'; import {bounce, dateTime, formatTime} from './utils/time'; @@ -8,7 +8,7 @@ import {sub24hFeed, subNote, subProfile} from './subscriptions' import {publish} from './relays'; import {getReplyTo, hasEventTag, isMention, sortByCreatedAt, sortEventCreatedAt, validatePow} from './events'; import {clearView, getViewContent, getViewElem, setViewElem, view} from './view'; -import {config} from './settings'; +import {closeSettingsView, config, toggleSettingsView} from './settings'; // curl -H 'accept: application/nostr+json' https://relay.nostr.ch/ function onEvent(evt, relay) { @@ -608,7 +608,6 @@ window.addEventListener('popstate', (event) => { route(location.pathname); }); -const settingsView = document.querySelector('#settings'); const publishView = document.querySelector('#newNote'); document.body.addEventListener('click', (e) => { @@ -618,9 +617,7 @@ document.body.addEventListener('click', (e) => { if (a) { if ('nav' in a.dataset) { e.preventDefault(); - if (!settingsView.hidden) { - settingsView.hidden = true; - } + closeSettingsView(); if (!publishView.hidden) { publishView.hidden = true; } @@ -646,7 +643,7 @@ document.body.addEventListener('click', (e) => { upvote(id, pubkey); break; case 'settings': - settingsView.hidden = !settingsView.hidden; + toggleSettingsView(); break; case 'new-note': if (publishView.hidden) { @@ -682,129 +679,3 @@ document.body.addEventListener('click', (e) => { // hideNewMessage(true); // } // }); - -// settings -const settingsForm = document.querySelector('form[name="settings"]'); -const privateKeyInput = settingsForm.querySelector('#privatekey'); -const pubKeyInput = settingsForm.querySelector('#pubkey'); -const statusMessage = settingsForm.querySelector('#keystatus'); -const generateBtn = settingsForm.querySelector('button[name="generate"]'); -const importBtn = settingsForm.querySelector('button[name="import"]'); -const privateTgl = settingsForm.querySelector('button[name="privatekey-toggle"]'); - -generateBtn.addEventListener('click', () => { - const privatekey = generatePrivateKey(); - const pubkey = getPublicKey(privatekey); - if (validKeys(privatekey, pubkey)) { - privateKeyInput.value = privatekey; - pubKeyInput.value = pubkey; - statusMessage.textContent = 'private-key created!'; - statusMessage.hidden = false; - } -}); - -importBtn.addEventListener('click', () => { - const privatekey = privateKeyInput.value; - const pubkeyInput = pubKeyInput.value; - if (validKeys(privatekey, pubkeyInput)) { - localStorage.setItem('private_key', privatekey); - localStorage.setItem('pub_key', pubkeyInput); - statusMessage.textContent = 'stored private and public key locally!'; - statusMessage.hidden = false; - config.pubkey = pubkeyInput; - } -}); - -settingsForm.addEventListener('input', () => validKeys(privateKeyInput.value, pubKeyInput.value)); -privateKeyInput.addEventListener('paste', (event) => { - if (pubKeyInput.value || !event.clipboardData) { - return; - } - if (privateKeyInput.value === '' || ( // either privatekey field is empty - privateKeyInput.selectionStart === 0 // or the whole text is selected and replaced with the clipboard - && privateKeyInput.selectionEnd === privateKeyInput.value.length - )) { // only generate the pubkey if no data other than the text from clipboard will be used - try { - pubKeyInput.value = getPublicKey(event.clipboardData.getData('text')); - } catch(err) {} // settings form will call validKeys on input and display the error - } -}); - -function validKeys(privatekey, pubkey) { - try { - if (getPublicKey(privatekey) === pubkey) { - statusMessage.hidden = true; - statusMessage.textContent = 'public-key corresponds to private-key'; - importBtn.removeAttribute('disabled'); - return true; - } else { - statusMessage.textContent = 'private-key does not correspond to public-key!' - } - } catch (e) { - statusMessage.textContent = `not a valid private-key: ${e.message || e}`; - } - statusMessage.hidden = false; - importBtn.setAttribute('disabled', true); - return false; -} - -privateTgl.addEventListener('click', () => { - privateKeyInput.type = privateKeyInput.type === 'text' ? 'password' : 'text'; -}); - -privateKeyInput.value = localStorage.getItem('private_key'); -pubKeyInput.value = localStorage.getItem('pub_key'); - -// profile -const profileForm = document.querySelector('form[name="profile"]'); -const profileSubmit = profileForm.querySelector('button[type="submit"]'); -const profileStatus = document.querySelector('#profilestatus'); -const onProfileError = err => { - profileStatus.hidden = false; - profileStatus.textContent = err.message -}; -profileForm.addEventListener('input', (e) => { - if (e.target.nodeName === 'TEXTAREA') { - updateElemHeight(e.target); - } - const form = new FormData(profileForm); - const name = form.get('name'); - const about = form.get('about'); - const picture = form.get('picture'); - profileSubmit.disabled = !(name || about || picture); -}); - -profileForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const form = new FormData(profileForm); - const newProfile = await powEvent({ - kind: 0, - pubkey: config.pubkey, - content: JSON.stringify(Object.fromEntries(form)), - tags: [], - created_at: Math.floor(Date.now() * 0.001), - }, { - difficulty: config.difficulty, - statusElem: profileStatus, - timeout: config.timeout, - }).catch(console.warn); - if (!newProfile) { - profileStatus.textContent = 'publishing profile data canceled'; - profileStatus.hidden = false; - return; - } - const privatekey = localStorage.getItem('private_key'); - const sig = signEvent(newProfile, privatekey); - // TODO: validateEvent - if (sig) { - publish({...newProfile, sig}, (relay, error) => { - if (error) { - return console.error(error, relay); - } - console.info(`publish request sent to ${relay}`); - profileStatus.textContent = 'profile metadata successfully published'; - profileStatus.hidden = false; - profileSubmit.disabled = true; - }); - } -}); diff --git a/src/settings.ts b/src/settings.ts index 407bdba..589e092 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,4 +1,13 @@ -import {generatePrivateKey, getPublicKey} from 'nostr-tools'; +import {generatePrivateKey, getPublicKey, signEvent} from 'nostr-tools'; +import {updateElemHeight} from './utils/dom'; +import {powEvent} from './system'; +import {publish} from './relays'; + +const settingsView = document.querySelector('#settings') as HTMLElement; + +export const closeSettingsView = () => settingsView.hidden = true; + +export const toggleSettingsView = () => settingsView.hidden = !settingsView.hidden; let pubkey: string = ''; @@ -90,3 +99,141 @@ miningTimeoutInput.addEventListener('input', (e) => { }); timeout = getNumberFromStorage('mining_timeout', 5); miningTimeoutInput.valueAsNumber = timeout; + + +// settings +const settingsForm = document.querySelector('form[name="settings"]') as HTMLFormElement; +const privateKeyInput = settingsForm.querySelector('#privatekey') as HTMLInputElement; +const pubKeyInput = settingsForm.querySelector('#pubkey') as HTMLInputElement; +const statusMessage = settingsForm.querySelector('#keystatus') as HTMLElement; +const generateBtn = settingsForm.querySelector('button[name="generate"]') as HTMLButtonElement; +const importBtn = settingsForm.querySelector('button[name="import"]') as HTMLButtonElement; +const privateTgl = settingsForm.querySelector('button[name="privatekey-toggle"]') as HTMLButtonElement; + +const validKeys = ( + privatekey: string, + pubkey: string, +) => { + try { + if (getPublicKey(privatekey) === pubkey) { + statusMessage.hidden = true; + statusMessage.textContent = 'public-key corresponds to private-key'; + importBtn.removeAttribute('disabled'); + return true; + } else { + statusMessage.textContent = 'private-key does not correspond to public-key!' + } + } catch (e) { + statusMessage.textContent = `not a valid private-key: ${e.message || e}`; + } + statusMessage.hidden = false; + importBtn.disabled = true; + return false; +}; + +generateBtn.addEventListener('click', () => { + const privatekey = generatePrivateKey(); + const pubkey = getPublicKey(privatekey); + if (validKeys(privatekey, pubkey)) { + privateKeyInput.value = privatekey; + pubKeyInput.value = pubkey; + statusMessage.textContent = 'private-key created!'; + statusMessage.hidden = false; + } +}); + +importBtn.addEventListener('click', () => { + const privatekey = privateKeyInput.value; + const pubkeyInput = pubKeyInput.value; + if (validKeys(privatekey, pubkeyInput)) { + localStorage.setItem('private_key', privatekey); + localStorage.setItem('pub_key', pubkeyInput); + statusMessage.textContent = 'stored private and public key locally!'; + statusMessage.hidden = false; + config.pubkey = pubkeyInput; + } +}); + +settingsForm.addEventListener('input', () => validKeys(privateKeyInput.value, pubKeyInput.value)); + +privateKeyInput.addEventListener('paste', (event) => { + if (pubKeyInput.value || !event.clipboardData) { + return; + } + if (privateKeyInput.value === '' || ( // either privatekey field is empty + privateKeyInput.selectionStart === 0 // or the whole text is selected and replaced with the clipboard + && privateKeyInput.selectionEnd === privateKeyInput.value.length + )) { // only generate the pubkey if no data other than the text from clipboard will be used + try { + pubKeyInput.value = getPublicKey(event.clipboardData.getData('text')); + } catch(err) {} // settings form will call validKeys on input and display the error + } +}); + +privateTgl.addEventListener('click', () => { + privateKeyInput.type = privateKeyInput.type === 'text' ? 'password' : 'text'; +}); + +privateKeyInput.value = localStorage.getItem('private_key') || ''; +pubKeyInput.value = localStorage.getItem('pub_key') || ''; + +// profile +const profileForm = document.querySelector('form[name="profile"]') as HTMLFormElement; +const profileSubmit = profileForm.querySelector('button[type="submit"]') as HTMLButtonElement; +const profileStatus = document.querySelector('#profilestatus') as HTMLElement; +// const onProfileError = err => { +// profileStatus.hidden = false; +// profileStatus.textContent = err.message +// }; +profileForm.addEventListener('input', (e) => { + if (e.target instanceof HTMLElement) { + if (e.target?.nodeName === 'TEXTAREA') { + updateElemHeight(e.target as HTMLTextAreaElement); + } + } + const form = new FormData(profileForm); + const name = form.get('name'); + const about = form.get('about'); + const picture = form.get('picture'); + profileSubmit.disabled = !(name || about || picture); +}); + +profileForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const form = new FormData(profileForm); + const newProfile = await powEvent({ + kind: 0, + pubkey: config.pubkey, + content: JSON.stringify(Object.fromEntries(form)), + tags: [], + created_at: Math.floor(Date.now() * 0.001) + }, { + difficulty: config.difficulty, + statusElem: profileStatus, + timeout: config.timeout, + }).catch(console.warn); + if (!newProfile) { + profileStatus.textContent = 'publishing profile data canceled'; + profileStatus.hidden = false; + return; + } + const privatekey = localStorage.getItem('private_key'); + if (!privatekey) { + profileStatus.textContent = 'no private key to sign'; + profileStatus.hidden = false; + return; + } + const sig = signEvent(newProfile, privatekey); + // TODO: validateEvent + if (sig) { + publish({...newProfile, sig}, (relay, error) => { + if (error) { + return console.error(error, relay); + } + console.info(`publish request sent to ${relay}`); + profileStatus.textContent = 'profile metadata successfully published'; + profileStatus.hidden = false; + profileSubmit.disabled = true; + }); + } +});