write: move reply and write-new-text note to write.ts

OFF0 2 years ago
parent d4050767ac
commit ad7ad36581
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -1,15 +1,15 @@
import {nip19} from 'nostr-tools'; import {nip19} from 'nostr-tools';
import {zeroLeadingBitsCount} from './utils/crypto'; import {zeroLeadingBitsCount} from './utils/crypto';
import {elem, elemCanvas, elemShrink, parseTextContent, updateElemHeight} from './utils/dom'; import {elem, elemCanvas, parseTextContent} from './utils/dom';
import {bounce, dateTime, formatTime} from './utils/time'; import {bounce, dateTime, formatTime} from './utils/time';
import {getHost, getNoxyUrl, isWssUrl} from './utils/url'; import {getHost, getNoxyUrl, isWssUrl} from './utils/url';
import {powEvent} from './system';
import {sub24hFeed, subNote, subProfile} from './subscriptions' import {sub24hFeed, subNote, subProfile} from './subscriptions'
import {publish} from './relays';
import {getReplyTo, hasEventTag, isMention, sortByCreatedAt, sortEventCreatedAt, validatePow} from './events'; import {getReplyTo, hasEventTag, isMention, sortByCreatedAt, sortEventCreatedAt, validatePow} from './events';
import {clearView, getViewContent, getViewElem, setViewElem, view} from './view'; import {clearView, getViewContent, getViewElem, setViewElem, view} from './view';
import {closeSettingsView, config, toggleSettingsView} from './settings'; import {closeSettingsView, config, toggleSettingsView} from './settings';
import {getReactions, getReactionContents, handleReaction, handleUpvote} from './reactions'; import {getReactions, getReactionContents, handleReaction, handleUpvote} from './reactions';
import {closePublishView, openWriteInput, togglePublishView} from './write';
// curl -H 'accept: application/nostr+json' https://relay.nostr.ch/ // curl -H 'accept: application/nostr+json' https://relay.nostr.ch/
function onEvent(evt, relay) { function onEvent(evt, relay) {
@ -118,8 +118,6 @@ function renderReply(evt, relay) {
setViewElem(evt.id, reply); setViewElem(evt.id, reply);
} }
const restoredReplyTo = localStorage.getItem('reply_to');
config.rerenderFeed = () => { config.rerenderFeed = () => {
clearView(); clearView();
renderFeed(); renderFeed();
@ -190,8 +188,6 @@ function linkPreview(href, id, relay) {
}); });
} }
const writeInput = document.querySelector('textarea[name="message"]');
function createTextNote(evt, relay) { function createTextNote(evt, relay) {
const {host, img, name, time, userName} = getMetadata(evt, relay); const {host, img, name, time, userName} = getMetadata(evt, relay);
const replies = replyList.filter(({replyTo}) => replyTo === evt.id); const replies = replyList.filter(({replyTo}) => replyTo === evt.id);
@ -201,6 +197,20 @@ function createTextNote(evt, relay) {
const didReact = reactions.length && !!reactions.find(reaction => reaction.pubkey === config.pubkey); const didReact = reactions.length && !!reactions.find(reaction => reaction.pubkey === config.pubkey);
const replyFeed = replies[0] ? replies.sort(sortByCreatedAt).map(e => setViewElem(e.id, createTextNote(e, relay))) : []; const replyFeed = replies[0] ? replies.sort(sortByCreatedAt).map(e => setViewElem(e.id, createTextNote(e, relay))) : [];
const [content, {firstLink}] = parseTextContent(evt.content); const [content, {firstLink}] = parseTextContent(evt.content);
const buttons = elem('div', {className: 'buttons'}, [
elem('button', {name: 'reply', type: 'button'}, [
elem('img', {height: 24, width: 24, src: '/assets/comment.svg'})
]),
elem('button', {name: 'star', type: 'button'}, [
elem('img', {
alt: didReact ? '✭' : '✩', // ♥
height: 24, width: 24,
src: `/assets/${didReact ? 'star-fill' : 'star'}.svg`,
title: getReactionContents(evt.id).join(' '),
}),
elem('small', {data: {reactions: ''}}, reactions.length || ''),
]),
]);
const body = elem('div', {className: 'mbox-body'}, [ const body = elem('div', {className: 'mbox-body'}, [
elem('header', { elem('header', {
className: 'mbox-header', className: 'mbox-header',
@ -216,24 +226,10 @@ function createTextNote(evt, relay) {
...content, ...content,
(firstLink && validatePow(evt)) ? linkPreview(firstLink, evt.id, relay) : '', (firstLink && validatePow(evt)) ? linkPreview(firstLink, evt.id, relay) : '',
]), ]),
elem('div', {className: 'buttons'}, [ buttons,
elem('button', {name: 'reply', type: 'button'}, [
elem('img', {height: 24, width: 24, src: '/assets/comment.svg'})
]),
elem('button', {name: 'star', type: 'button'}, [
elem('img', {
alt: didReact ? '✭' : '✩', // ♥
height: 24, width: 24,
src: `/assets/${didReact ? 'star-fill' : 'star'}.svg`,
title: getReactionContents(evt.id).join(' '),
}),
elem('small', {data: {reactions: ''}}, reactions.length || ''),
]),
]),
]); ]);
if (restoredReplyTo === evt.id) { if (localStorage.getItem('reply_to') === evt.id) {
appendReplyForm(body.querySelector('.buttons')); openWriteInput(buttons);
requestAnimationFrame(() => updateElemHeight(writeInput));
} }
return renderArticle([ return renderArticle([
elem('div', {className: 'mbox-img'}, [img]), body, elem('div', {className: 'mbox-img'}, [img]), body,
@ -393,96 +389,6 @@ function getMetadata(evt, relay) {
return {host, img, name, time, userName}; return {host, img, name, time, userName};
} }
const writeForm = document.querySelector('#writeForm');
writeInput.addEventListener('focusout', () => {
const reply_to = localStorage.getItem('reply_to');
if (reply_to && writeInput.value === '') {
writeInput.addEventListener('transitionend', (event) => {
if (!reply_to || reply_to === localStorage.getItem('reply_to') && !writeInput.style.height) { // should prob use some class or data-attr instead of relying on height
writeForm.after(elemShrink(writeInput));
writeForm.remove();
localStorage.removeItem('reply_to');
}
}, {once: true});
}
});
function appendReplyForm(el) {
writeForm.before(elemShrink(writeInput));
writeInput.blur();
writeInput.style.removeProperty('height');
el.after(writeForm);
if (writeInput.value && !writeInput.value.trimRight()) {
writeInput.value = '';
} else {
requestAnimationFrame(() => updateElemHeight(writeInput));
}
requestAnimationFrame(() => writeInput.focus());
}
// send
const sendStatus = document.querySelector('#sendstatus');
const onSendError = err => sendStatus.textContent = err.message;
const publishBtn = document.querySelector('#publish');
writeForm.addEventListener('submit', async (e) => {
e.preventDefault();
const privatekey = localStorage.getItem('private_key');
if (!config.pubkey || !privatekey) {
return onSendError(new Error('no pubkey/privatekey'));
}
const content = writeInput.value.trimRight();
if (!content) {
return onSendError(new Error('message is empty'));
}
const replyTo = localStorage.getItem('reply_to');
const close = () => {
sendStatus.textContent = '';
writeInput.value = '';
writeInput.style.removeProperty('height');
publishBtn.disabled = true;
if (replyTo) {
localStorage.removeItem('reply_to');
publishView.append(writeForm);
}
publishView.hidden = true;
};
const tags = replyTo ? [['e', replyTo]] : []; // , eventRelayMap[replyTo][0]
const newEvent = await powEvent({
kind: 1,
content,
pubkey: config.pubkey,
tags,
created_at: Math.floor(Date.now() * 0.001),
}, {
difficulty: config.difficulty,
statusElem: sendStatus,
timeout: config.timeout,
}).catch(console.warn);
if (!newEvent) {
close();
return;
}
const sig = signEvent(newEvent, privatekey);
// TODO validateEvent
if (sig) {
sendStatus.textContent = 'publishing…';
publish({...newEvent, sig}, (relay, error) => {
if (error) {
return console.log(error, relay);
}
console.info(`publish request sent to ${relay}`);
close();
});
}
});
writeInput.addEventListener('input', () => {
publishBtn.disabled = !writeInput.value.trimRight();
updateElemHeight(writeInput);
});
writeInput.addEventListener('blur', () => sendStatus.textContent = '');
// subscribe and change view // subscribe and change view
function route(path) { function route(path) {
if (path === '/') { if (path === '/') {
@ -514,15 +420,11 @@ window.addEventListener('popstate', (event) => {
route(location.pathname); route(location.pathname);
}); });
const publishView = document.querySelector('#newNote');
const handleLink = (e, a) => { const handleLink = (e, a) => {
if ('nav' in a.dataset) { if ('nav' in a.dataset) {
e.preventDefault(); e.preventDefault();
closeSettingsView(); closeSettingsView();
if (!publishView.hidden) { closePublishView();
publishView.hidden = true;
}
const href = a.getAttribute('href'); const href = a.getAttribute('href');
route(href); route(href);
history.pushState({}, null, href); history.pushState({}, null, href);
@ -534,12 +436,7 @@ const handleButton = (e, button) => {
const id = e.target.closest('[data-id]')?.dataset.id; const id = e.target.closest('[data-id]')?.dataset.id;
switch(button.name) { switch(button.name) {
case 'reply': case 'reply':
if (localStorage.getItem('reply_to') === id) { openWriteInput(button, id);
writeInput.blur();
return;
}
appendReplyForm(button.closest('.buttons'));
localStorage.setItem('reply_to', id);
break; break;
case 'star': case 'star':
const note = replyList.find(r => r.id === id) || textNoteList.find(n => n.id === (id)); const note = replyList.find(r => r.id === id) || textNoteList.find(n => n.id === (id));
@ -549,23 +446,10 @@ const handleButton = (e, button) => {
toggleSettingsView(); toggleSettingsView();
break; break;
case 'new-note': case 'new-note':
if (publishView.hidden) { togglePublishView();
localStorage.removeItem('reply_to'); // should it forget old replyto context?
publishView.append(writeForm);
if (writeInput.value.trimRight()) {
writeInput.style.removeProperty('height');
}
requestAnimationFrame(() => {
updateElemHeight(writeInput);
writeInput.focus();
});
publishView.removeAttribute('hidden');
} else {
publishView.hidden = true;
}
break; break;
case 'back': case 'back':
publishView.hidden = true; closePublishView();
break; break;
} }
// const container = e.target.closest('[data-append]'); // const container = e.target.closest('[data-append]');
@ -587,9 +471,3 @@ document.body.addEventListener('click', (e) => {
handleButton(e, button); handleButton(e, button);
} }
}); });
// document.body.addEventListener('keyup', (e) => {
// if (e.key === 'Escape') {
// hideNewMessage(true);
// }
// });

@ -0,0 +1,154 @@
import {signEvent} from 'nostr-tools';
import {elemShrink, updateElemHeight} from './utils/dom';
import {powEvent} from './system';
import {config} from './settings';
import {publish} from './relays';
// form used to write and publish textnotes for replies and new notes
const writeForm = document.querySelector('#writeForm') as HTMLFormElement;
const writeInput = document.querySelector('textarea[name="message"]') as HTMLTextAreaElement;
// overlay for writing new text notes
const publishView = document.querySelector('#newNote') as HTMLElement;
const openWriteView = () => {
publishView.append(writeForm);
if (writeInput.value.trimRight()) {
writeInput.style.removeProperty('height');
}
requestAnimationFrame(() => {
updateElemHeight(writeInput);
writeInput.focus();
});
publishView.removeAttribute('hidden');
};
export const closePublishView = () => publishView.hidden = true;
export const togglePublishView = () => {
if (publishView.hidden) {
localStorage.removeItem('reply_to'); // should it forget old replyto context?
openWriteView();
} else {
publishView.hidden = true;
}
};
const closeWriteInput = () => writeInput.blur();
export const openWriteInput = (
button: HTMLElement,
id: string,
) => {
appendReplyForm(button.closest('.buttons') as HTMLElement);
localStorage.setItem('reply_to', id);
};
export const toggleWriteInput = (
button: HTMLElement,
id: string,
) => {
if (id && localStorage.getItem('reply_to') === id) {
closeWriteInput();
return;
}
appendReplyForm(button.closest('.buttons') as HTMLElement);
localStorage.setItem('reply_to', id);
};
// const updateWriteInputHeight = () => updateElemHeight(writeInput);
writeInput.addEventListener('focusout', () => {
const reply_to = localStorage.getItem('reply_to');
if (reply_to && writeInput.value === '') {
writeInput.addEventListener('transitionend', (event) => {
if (!reply_to || reply_to === localStorage.getItem('reply_to') && !writeInput.style.height) { // should prob use some class or data-attr instead of relying on height
writeForm.after(elemShrink(writeInput));
writeForm.remove();
localStorage.removeItem('reply_to');
}
}, {once: true});
}
});
// document.body.addEventListener('keyup', (e) => {
// if (e.key === 'Escape') {
// hideNewMessage(true);
// }
// });
const sendStatus = document.querySelector('#sendstatus') as HTMLElement;
const publishBtn = document.querySelector('#publish') as HTMLButtonElement;
const onSendError = (err: Error) => sendStatus.textContent = err.message;
writeForm.addEventListener('submit', async (e) => {
e.preventDefault();
const privatekey = localStorage.getItem('private_key');
if (!config.pubkey || !privatekey) {
return onSendError(new Error('no pubkey/privatekey'));
}
const content = writeInput.value.trimRight();
if (!content) {
return onSendError(new Error('message is empty'));
}
const replyTo = localStorage.getItem('reply_to');
const close = () => {
sendStatus.textContent = '';
writeInput.value = '';
writeInput.style.removeProperty('height');
publishBtn.disabled = true;
if (replyTo) {
localStorage.removeItem('reply_to');
publishView.append(writeForm);
}
publishView.hidden = true;
};
const tags = replyTo ? [['e', replyTo]] : []; // , eventRelayMap[replyTo][0]
const newEvent = await powEvent({
kind: 1,
content,
pubkey: config.pubkey,
tags,
created_at: Math.floor(Date.now() * 0.001),
}, {
difficulty: config.difficulty,
statusElem: sendStatus,
timeout: config.timeout,
}).catch(console.warn);
if (!newEvent) {
close();
return;
}
const sig = signEvent(newEvent, privatekey);
// TODO validateEvent
if (sig) {
sendStatus.textContent = 'publishing…';
publish({...newEvent, sig}, (relay, error) => {
if (error) {
return console.log(error, relay);
}
console.info(`publish request sent to ${relay}`);
close();
});
}
});
writeInput.addEventListener('input', () => {
publishBtn.disabled = !writeInput.value.trimRight();
updateElemHeight(writeInput);
});
writeInput.addEventListener('blur', () => sendStatus.textContent = '');
function appendReplyForm(el: HTMLElement) {
writeForm.before(elemShrink(writeInput));
writeInput.blur();
writeInput.style.removeProperty('height');
el.after(writeForm);
if (writeInput.value && !writeInput.value.trimRight()) {
writeInput.value = '';
} else {
requestAnimationFrame(() => updateElemHeight(writeInput));
}
requestAnimationFrame(() => writeInput.focus());
}
Loading…
Cancel
Save