nip-13: show working msg and cancel btn while mining
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details

mining often takes a few seconds. it can be confusing if nothing
happens when a user is publishing their profile, upvoting a note
or posting a new note.

added visual feedback that nostrweb is working with an option to
cancel the mining process.
pull/55/head
OFF0 1 year ago
parent 898e7265c2
commit 37f0a07cf3
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -97,6 +97,16 @@
overflow: visible;
position: relative;
}
.mbox .buttons {
margin-top: .2rem;
}
.mbox button:not(#publish) {
--bg-color: none;
--border-color: none;
}
.mbox button img + small {
padding-left: .5rem;
}
.mobx-replies {
flex-grow: 1;
position: relative;

@ -97,14 +97,22 @@ textarea:focus {
align-items: center;
display: flex;
flex-basis: 100%;
justify-content: flex-end;
gap: var(--gap);
margin-top: 2rem;
justify-content: start;
margin-top: var(--gap-half);
min-height: 3.2rem;
}
form .buttons,
.form .buttons,
.form-inline .buttons {
flex-basis: fit-content;
margin-top: 0;
justify-content: end;
}
.buttons img,
.buttons small,
.buttons span {
vertical-align: middle;
}
button {
@ -123,24 +131,11 @@ button:focus {
.btn-inline {
--border-color: transparent;
align-items: center;
background: transparent;
color: var(--color);
display: inline-flex;
gap: .5ch;
color: var(--color-accent);
display: inline-block;
line-height: 1;
padding: .6rem;
}
.btn-inline img {
max-height: 18px;
max-width: 18px;
}
.btn-inline img[alt] {
color: #7f7f7f;
line-height: 1px;
}
.btn-inline img[alt]::before {
font-size: 3.4rem;
padding: 0 .6rem;
}
.btn-danger {
@ -156,6 +151,7 @@ button:disabled {
.form-status {
flex-basis: 100%;
flex-grow: 1;
min-height: 1.8rem;
padding: var(--padding);
}

@ -217,7 +217,7 @@ document.body.addEventListener('click', (e) => {
writeInput.blur();
return;
}
appendReplyForm(button);
appendReplyForm(button.closest('.buttons'));
localStorage.setItem('reply_to', id);
return;
}
@ -442,26 +442,24 @@ function createTextNote(evt, relay) {
...content,
(firstLink && validatePow(evt)) ? linkPreview(firstLink, evt.id, relay) : '',
]),
elem('button', {
className: 'btn-inline', name: 'star', type: 'button',
data: {'eventId': evt.id, relay},
}, [
elem('img', {
alt: didReact ? '✭' : '✩', // ♥
height: 24, width: 24,
src: `assets/${didReact ? 'star-fill' : 'star'}.svg`,
title: getReactionList(evt.id).join(' '),
}),
elem('small', {data: {reactions: evt.id}}, hasReactions ? reactionMap[evt.id].length : ''),
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: getReactionList(evt.id).join(' '),
}),
elem('small', {data: {reactions: ''}}, hasReactions ? reactionMap[evt.id].length : ''),
]),
]),
elem('button', {
className: 'btn-inline', name: 'reply', type: 'button',
data: {'eventId': evt.id, relay},
}, [elem('img', {height: 24, width: 24, src: 'assets/comment.svg'})]),
// replies[0] ? elem('div', {className: 'mobx-replies'}, replyFeed.reverse()) : '',
]);
if (restoredReplyTo === evt.id) {
appendReplyForm(body.querySelector('button[name="reply"]'));
appendReplyForm(body.querySelector('.buttons'));
requestAnimationFrame(() => updateElemHeight(writeInput));
}
return renderArticle([
@ -770,7 +768,6 @@ miningTimeoutInput.addEventListener('input', (e) => {
miningTimeoutInput.value = timeout;
async function upvote(eventId, eventPubkey) {
const privatekey = localStorage.getItem('private_key');
const note = replyList.find(r => r.id === eventId) || textNoteList.find(n => n.id === (eventId));
const tags = [
...note.tags
@ -778,25 +775,35 @@ async function upvote(eventId, eventPubkey) {
.map(([a, b]) => [a, b]), // drop optional (nip-10) relay and marker fields
['e', eventId], ['p', eventPubkey], // last e and p tag is the id and pubkey of the note being reacted to (nip-25)
];
const article = (feedDomMap[eventId] || replyDomMap[eventId]);
const reactionBtn = article.querySelector('[name="star"]');
const statusElem = article.querySelector('[data-reactions]');
reactionBtn.disabled = true;
const newReaction = await powEvent({
kind: 7,
pubkey, // TODO: lib could check that this is the pubkey of the key to sign with
content: '+',
tags,
created_at: Math.floor(Date.now() * 0.001),
}, difficulty, timeout).catch(console.warn);
if (newReaction) {
const sig = await signEvent(newReaction, privatekey).catch(console.error);
if (sig) {
const ev = await pool.publish({...newReaction, sig}, (status, url) => {
if (status === 0) {
console.info(`publish request sent to ${url}`);
}
if (status === 1) {
console.info(`event published by ${url}`);
}
}).catch(console.error);
}
}, {difficulty, statusElem, timeout}).catch(console.warn);
if (!newReaction) {
statusElem.textContent = reactionMap[eventId]?.length;
reactionBtn.disabled = false;
return;
}
const privatekey = localStorage.getItem('private_key');
const sig = await signEvent(newReaction, privatekey).catch(console.error);
if (sig) {
statusElem.textContent = 'publishing…';
const ev = await pool.publish({...newReaction, sig}, (status, url) => {
if (status === 0) {
console.info(`publish request sent to ${url}`);
}
if (status === 1) {
console.info(`event published by ${url}`);
}
}).catch(console.error);
reactionBtn.disabled = false;
}
}
@ -816,6 +823,17 @@ writeForm.addEventListener('submit', async (e) => {
return onSendError(new Error('message is empty'));
}
const replyTo = localStorage.getItem('reply_to');
const close = () => {
sendStatus.textContent = '';
writeInput.value = '';
writeInput.style.removeProperty('height');
publish.disabled = true;
if (replyTo) {
localStorage.removeItem('reply_to');
newMessageDiv.append(writeForm);
}
hideNewMessage(true);
};
const tags = replyTo ? [['e', replyTo, eventRelayMap[replyTo][0]]] : [];
const newEvent = await powEvent({
kind: 1,
@ -823,28 +841,23 @@ writeForm.addEventListener('submit', async (e) => {
pubkey,
tags,
created_at: Math.floor(Date.now() * 0.001),
}, difficulty, timeout).catch(console.warn);
if (newEvent) {
const sig = await signEvent(newEvent, privatekey).catch(onSendError);
if (sig) {
const ev = await pool.publish({...newEvent, sig}, (status, url) => {
if (status === 0) {
console.info(`publish request sent to ${url}`);
}
if (status === 1) {
sendStatus.textContent = '';
writeInput.value = '';
writeInput.style.removeProperty('height');
publish.disabled = true;
if (replyTo) {
localStorage.removeItem('reply_to');
newMessageDiv.append(writeForm);
}
hideNewMessage(true);
// console.info(`event published by ${url}`, ev);
}
});
}
}, {difficulty, statusElem: sendStatus, timeout}).catch(console.warn);
if (!newEvent) {
close();
return;
}
const sig = await signEvent(newEvent, privatekey).catch(onSendError);
if (sig) {
sendStatus.textContent = 'publishing…';
const ev = await pool.publish({...newEvent, sig}, (status, url) => {
if (status === 0) {
console.info(`publish request sent to ${url}`);
}
if (status === 1) {
close();
// console.info(`event published by ${url}`, ev);
}
});
}
});
@ -959,29 +972,31 @@ profileForm.addEventListener('input', (e) => {
profileForm.addEventListener('submit', async (e) => {
e.preventDefault();
const form = new FormData(profileForm);
const privatekey = localStorage.getItem('private_key');
const newProfile = await powEvent({
kind: 0,
pubkey,
content: JSON.stringify(Object.fromEntries(form)),
tags: [],
created_at: Math.floor(Date.now() * 0.001),
}, difficulty, timeout).catch(console.warn);
if (newProfile) {
const sig = await signEvent(newProfile, privatekey).catch(console.error);
if (sig) {
const ev = await pool.publish({...newProfile, sig}, (status, url) => {
if (status === 0) {
console.info(`publish request sent to ${url}`);
}
if (status === 1) {
profileStatus.textContent = 'profile metadata successfully published';
profileStatus.hidden = false;
profileSubmit.disabled = true;
}
}).catch(console.error);
}
}, {difficulty, statusElem: profileStatus, timeout}).catch(console.warn);
if (!newProfile) {
profileStatus.textContent = 'publishing profile data canceled';
profileStatus.hidden = false;
return;
}
const privatekey = localStorage.getItem('private_key');
const sig = await signEvent(newProfile, privatekey).catch(console.error);
if (sig) {
const ev = await pool.publish({...newProfile, sig}, (status, url) => {
if (status === 0) {
console.info(`publish request sent to ${url}`);
}
if (status === 1) {
profileStatus.textContent = 'profile metadata successfully published';
profileStatus.hidden = false;
profileSubmit.disabled = true;
}
}).catch(console.error);
}
});
@ -1046,20 +1061,31 @@ function validatePow(evt) {
* a zero timeout makes mineEvent run without a time limit.
* a zero mining target just resolves the promise without trying to find a 'nonce'.
*/
function powEvent(evt, difficulty, timeout) {
function powEvent(evt, options) {
const {difficulty, statusElem, timeout} = options;
if (difficulty === 0) {
return Promise.resolve(evt);
}
const cancelBtn = elem('button', {className: 'btn-inline'}, [elem('small', {}, 'cancel')]);
statusElem.replaceChildren('working…', cancelBtn);
statusElem.hidden = false;
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js');
const onCancel = () => {
worker.terminate();
reject('canceled');
};
cancelBtn.addEventListener('click', onCancel);
worker.onmessage = (msg) => {
worker.terminate();
cancelBtn.removeEventListener('click', onCancel);
if (msg.data.error) {
promptError(msg.data.error, {
onCancel: () => reject('canceled'),
onAgain: async () => {
const result = await powEvent(evt, difficulty, timeout).catch(console.warn);
const result = await powEvent(evt, {difficulty, statusElem, timeout}).catch(console.warn);
resolve(result);
}
})
@ -1070,6 +1096,7 @@ function powEvent(evt, difficulty, timeout) {
worker.onerror = (err) => {
worker.terminate();
cancelBtn.removeEventListener('click', onCancel);
reject(err);
};

Loading…
Cancel
Save