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 = ''; const loadOrGenerateKeys = () => { const storedPubKey = localStorage.getItem('pub_key'); if (storedPubKey) { return storedPubKey; } const privatekey = generatePrivateKey(); const pubkey = getPublicKey(privatekey); localStorage.setItem('private_key', privatekey); localStorage.setItem('pub_key', pubkey); return pubkey; }; let filterDifficulty: number = 0; let difficulty: number = 16; let timeout: number = 5; let rerenderFeed: (() => void) | undefined; /** * global config object * config.pubkey, if not set loaded from localStorage or generate a new key */ export const config = { get pubkey() { if (!pubkey) { pubkey = loadOrGenerateKeys(); } return pubkey; }, set pubkey(value) { console.info(`pubkey was set to ${value}`); pubkey = value; }, get filterDifficulty() { return filterDifficulty; }, get difficulty() { return difficulty; }, get timeout() { return timeout; }, set rerenderFeed(value: () => void) { rerenderFeed = value; } }; const getNumberFromStorage = ( item: string, fallback: number, ) => { const stored = localStorage.getItem(item); if (!stored) { return fallback; } return Number(stored); }; // filter difficulty const filterDifficultyInput = document.querySelector('#filterDifficulty') as HTMLInputElement; const filterDifficultyDisplay = document.querySelector('[data-display="filter_difficulty"]') as HTMLElement; filterDifficultyInput.addEventListener('input', (e) => { localStorage.setItem('filter_difficulty', filterDifficultyInput.value); filterDifficulty = filterDifficultyInput.valueAsNumber; filterDifficultyDisplay.textContent = filterDifficultyInput.value; rerenderFeed && rerenderFeed(); }); filterDifficulty = getNumberFromStorage('filter_difficulty', 0); filterDifficultyInput.valueAsNumber = filterDifficulty; filterDifficultyDisplay.textContent = filterDifficultyInput.value; // mining difficulty target const miningTargetInput = document.querySelector('#miningTarget') as HTMLInputElement; miningTargetInput.addEventListener('input', (e) => { localStorage.setItem('mining_target', miningTargetInput.value); difficulty = miningTargetInput.valueAsNumber; }); // arbitrary difficulty default, still experimenting. difficulty = getNumberFromStorage('mining_target', 16); miningTargetInput.valueAsNumber = difficulty; // mining timeout const miningTimeoutInput = document.querySelector('#miningTimeout') as HTMLInputElement; miningTimeoutInput.addEventListener('input', (e) => { localStorage.setItem('mining_timeout', miningTimeoutInput.value); timeout = miningTimeoutInput.valueAsNumber; }); 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; }); } });