diff --git a/src/error.css b/src/error.css
new file mode 100644
index 0000000..8e24758
--- /dev/null
+++ b/src/error.css
@@ -0,0 +1,36 @@
+#errorOverlay {
+ background: var(--bgcolor-danger);
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ left: 0;
+ overflow: auto;
+ padding: var(--gap);
+ position: fixed;
+ right: 0;
+ top: 0;
+ z-index: 100;
+}
+
+.error-title {
+ margin-top: 0;
+}
+
+#errorOverlay button {
+ background-color: rgba(0 0 0 / .5);
+ border: none;
+ display: inline-block;
+}
+#errorOverlay button:focus {
+ outline: 2px solid white;
+ outline-offset: var(--focus-outline-offset);
+}
+
+#errorOverlay .buttons {
+ max-width: var(--max-width);
+}
+@media (orientation: portrait) {
+ #errorOverlay .buttons {
+ flex-basis: 4rem;
+ }
+}
diff --git a/src/index.html b/src/index.html
index 23f0f3e..8db0b89 100644
--- a/src/index.html
+++ b/src/index.html
@@ -52,6 +52,7 @@
+
diff --git a/src/main.css b/src/main.css
index a60d2f1..01e10b9 100644
--- a/src/main.css
+++ b/src/main.css
@@ -2,6 +2,7 @@
@import "cards.css";
@import "form.css";
@import "write.css";
+@import "error.css";
:root {
/* 5px auto Highlight */
@@ -14,6 +15,7 @@
--focus-outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-outline-color);
--font-small: 1.2rem;
--gap: 2.4rem;
+ --max-width: 96ch;
}
::selection {
diff --git a/src/main.js b/src/main.js
index 5bb97e0..7889cb2 100644
--- a/src/main.js
+++ b/src/main.js
@@ -297,7 +297,7 @@ function handleReaction(evt, relay) {
replies = eventTags.filter((tags) => tags[3] === undefined);
}
if (replies.length !== 1) {
- console.log('call me', evt);
+ // console.log('call me', evt);
return;
}
@@ -740,6 +740,9 @@ function appendReplyForm(el) {
requestAnimationFrame(() => writeInput.focus());
}
+const lockScroll = () => document.body.style.overflow = 'hidden';
+const unlockScroll = () => document.body.style.removeProperty('overflow');
+
const newMessageDiv = document.querySelector('#newMessage');
document.querySelector('#bubble').addEventListener('click', (e) => {
localStorage.removeItem('reply_to'); // should it forget old replyto context?
@@ -749,7 +752,7 @@ document.querySelector('#bubble').addEventListener('click', (e) => {
if (writeInput.value.trimRight()) {
writeInput.style.removeProperty('height');
}
- document.body.style.overflow = 'hidden';
+ lockScroll();
requestAnimationFrame(() => updateElemHeight(writeInput));
});
@@ -760,7 +763,7 @@ document.body.addEventListener('keyup', (e) => {
});
function hideNewMessage(hide) {
- document.body.style.removeProperty('overflow');
+ unlockScroll();
newMessageDiv.hidden = hide;
}
@@ -772,17 +775,19 @@ async function upvote(eventId, relay) {
content: '+',
tags: [['e', eventId, relay, 'reply']],
created_at: Math.floor(Date.now() * 0.001),
- }, difficulty);
- 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, 10).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);
+ }
}
}
@@ -809,7 +814,7 @@ writeForm.addEventListener('submit', async (e) => {
pubkey,
tags,
created_at: Math.floor(Date.now() * 0.001),
- }, difficulty);
+ }, difficulty, 10);
const sig = await signEvent(newEvent, privatekey).catch(onSendError);
if (sig) {
const ev = await pool.publish({...newEvent, sig}, (status, url) => {
@@ -951,7 +956,7 @@ profileForm.addEventListener('submit', async (e) => {
content: JSON.stringify(Object.fromEntries(form)),
tags: [],
created_at: Math.floor(Date.now() * 0.001),
- }, difficulty);
+ }, difficulty, 10);
const sig = await signEvent(newProfile, privatekey).catch(console.error);
if (sig) {
const ev = await pool.publish({...newProfile, sig}, (status, url) => {
@@ -967,6 +972,39 @@ profileForm.addEventListener('submit', async (e) => {
}
});
+const errorOverlay = document.querySelector('#errorOverlay');
+
+function promptError(error, options = {}) {
+ const {onAgain, onCancel} = options;
+ lockScroll();
+ errorOverlay.replaceChildren(
+ elem('h1', {className: 'error-title'}, error),
+ elem('p', {}, 'something went wrong'),
+ elem('div', {className: 'buttons'}, [
+ onCancel ? elem('button', {data: {action: 'close'}}, 'close') : '',
+ onAgain ? elem('button', {data: {action: 'again'}}, 'try again') : '',
+ ]),
+ );
+ const handleOverlayClick = (e) => {
+ const button = e.target.closest('button');
+ if (button) {
+ switch(button.dataset.action) {
+ case 'close':
+ onCancel();
+ break;
+ case 'again':
+ onAgain();
+ break;
+ }
+ errorOverlay.removeEventListener('click', handleOverlayClick);
+ errorOverlay.hidden = true;
+ unlockScroll();
+ }
+ };
+ errorOverlay.addEventListener('click', handleOverlayClick);
+ errorOverlay.hidden = false;
+}
+
/**
* check that the event has the id has the desired number of leading zero bits
* @param {EventObj} evt to validate
@@ -989,14 +1027,20 @@ function validatePow(evt) {
* powEvent returns a rejected promise if the funtion runs for longer than timeout.
* a zero timeout makes mineEvent run without a time limit.
*/
-function powEvent(evt, difficulty, timeout) {
+function powEvent(evt, difficulty, timeout = 0) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js');
worker.onmessage = (msg) => {
worker.terminate();
if (msg.data.error) {
- reject(msg.data.error);
+ promptError(msg.data.error, {
+ onCancel: () => reject('canceled'),
+ onAgain: async () => {
+ const result = await powEvent(evt, difficulty, timeout).catch(console.warn);
+ resolve(result);
+ }
+ })
} else {
resolve(msg.data.event);
}
diff --git a/src/tabs.css b/src/tabs.css
index 57a0c07..39cf5ec 100644
--- a/src/tabs.css
+++ b/src/tabs.css
@@ -43,7 +43,7 @@ input[type="radio"]:checked + label {
}
.tab-content {
- max-width: 96ch;
+ max-width: var(--max-width);
min-height: 200px;
padding: calc(.5 * var(--gap)) 0 100px 0;
}
diff --git a/src/worker.js b/src/worker.js
index ca1e9f0..1077bd1 100644
--- a/src/worker.js
+++ b/src/worker.js
@@ -10,14 +10,14 @@ function mine(event, difficulty, timeout) {
let n = BigInt(0);
event.tags.unshift(['nonce', n.toString(), `${difficulty}`]);
- const start = Math.floor(Date.now() * 0.001);
+ const until = Math.floor(Date.now() * 0.001) + timeout;
console.time('pow');
while (true) {
const now = Math.floor(Date.now() * 0.001);
- // if (now > start + 15) {
- // console.timeEnd('pow');
- // return false;
- // }
+ if (timeout !== 0 && (now > until)) {
+ console.timeEnd('pow');
+ throw 'timeout';
+ }
if (now !== event.created_at) {
event.created_at = now;
// n = BigInt(0); // could reset nonce as we have a new timestamp
@@ -25,7 +25,6 @@ function mine(event, difficulty, timeout) {
event.tags[0][1] = (++n).toString();
const id = getEventHash(event);
if (zeroLeadingBitsCount(id) === difficulty) {
- console.log(event.tags[0][1], id);
console.timeEnd('pow');
return event;
}
@@ -33,11 +32,7 @@ function mine(event, difficulty, timeout) {
}
addEventListener('message', async (msg) => {
- const {
- difficulty,
- event,
- timeout,
- } = msg.data;
+ const {difficulty, event, timeout} = msg.data;
try {
const minedEvent = mine(event, difficulty, timeout);
postMessage({event: minedEvent});