diff --git a/esbuildconf.js b/esbuildconf.js index c0508d4..43e48a8 100644 --- a/esbuildconf.js +++ b/esbuildconf.js @@ -14,10 +14,11 @@ export const options = { 'src/assets/heart-fill.svg', 'src/assets/star.svg', 'src/assets/star-fill.svg', + 'src/favicon.ico', ], outdir: 'dist', //entryNames: '[name]-[hash]', TODO: replace urls in index.html with hashed paths - loader: {'.html': 'copy', '.svg': 'copy'}, + loader: {'.html': 'copy', '.svg': 'copy', '.ico': 'copy'}, bundle: true, platform: 'browser', minify: false, // TODO: true for release and enable sourcemap diff --git a/src/assets/bubble.svg b/src/assets/bubble.svg index 583c79f..9b9d947 100644 --- a/src/assets/bubble.svg +++ b/src/assets/bubble.svg @@ -1,4 +1,6 @@ - + + + diff --git a/src/cards.css b/src/cards.css index 2d91ac1..5db1daa 100644 --- a/src/cards.css +++ b/src/cards.css @@ -17,6 +17,8 @@ max-width: var(--size); max-width: var(--size); } + +.mbox-updated-contact .mbox-img, .mbox-recommend-server .mbox-img { --size: 4.5ch; margin-left: 3ch; @@ -27,7 +29,6 @@ flex-basis: calc(100% - 64px - 1rem); flex-grow: 0; flex-shrink: 1; - max-width: 96ch; word-break: break-word; } @@ -38,15 +39,26 @@ margin-top: 0; } .mbox-header time, -.mbox-username { +.mbox-username, +.mbox-updated-contact, +.mbox mbox-recommend-server { + color: var(--color-accent); } +.mbox-updated-contact .mbox-body, .mbox-recommend-server .mbox-body { display: block; flex-basis: 100%; font-size: var(--font-small); + overflow: scroll; } +.mbox-updated-contact .mbox-header, .mbox-recommend-server .mbox-header { display: inline; } + +.mbox-updated-contact { + padding: 0 0 1rem 0; + margin: 0; +} \ No newline at end of file diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000..9b1f7e6 Binary files /dev/null and b/src/favicon.ico differ diff --git a/src/form.css b/src/form.css index 3f6a634..1f6b11b 100644 --- a/src/form.css +++ b/src/form.css @@ -1,4 +1,7 @@ -form { +form, +.form { + display: flex; + flex-direction: column; } input, @@ -17,6 +20,7 @@ label { margin-bottom: 0; padding: 1.3rem 1.8rem; text-indent: 0; + transition: background-color .25s; } label { @@ -25,11 +29,11 @@ label { input[type="password"], input[type="text"] { + background: var(--bgcolor-textinput); border: .2rem solid #b7b7b7; border-radius: .2rem; display: block; margin: 0; - width: 100%; } input[type="password"]:focus, input[type="text"]:focus { @@ -78,6 +82,10 @@ button:focus { font-size: 3.4rem; } +.btn-danger { + background: var(--bgcolor-danger); +} + button:disabled { background-color: var(--bgcolor-inactive); cursor: default; @@ -95,6 +103,7 @@ button:disabled { .form-inline { display: flex; + flex-direction: row; flex-grow: 1; gap: 1rem; } diff --git a/src/index.html b/src/index.html index 8b81010..16958a0 100644 --- a/src/index.html +++ b/src/index.html @@ -2,30 +2,30 @@ - nostr sandbox + nostr
- + - +
- +
-
+ - +
@@ -46,6 +46,10 @@
+
diff --git a/src/main.css b/src/main.css index 7052d73..61d53db 100644 --- a/src/main.css +++ b/src/main.css @@ -24,23 +24,25 @@ @media (prefers-color-scheme: light) { html { - --bgcolor: #fff; - --bgcolor-accent: #ff731d; + --bgcolor: #fdfefa; + --bgcolor-accent: #37ff1d; --bgcolor-inactive: #bababa; + --bgcolor-textinput: #fff; --color: rgb(68 68 68); --color-accent: #ff731d; - --bgcolor-danger: rgb(255 0 0); + --bgcolor-danger: rgb(255, 80, 80); } } @media (prefers-color-scheme: dark) { html { --bgcolor: #191919; - --bgcolor-accent: #2d4263; - --bgcolor-inactive: #535353; - --color: #c84b31; - --color-accent: #ecdbba; - --bgcolor-danger: rgb(255 0 0); + --bgcolor-accent: #1e437d; + --bgcolor-inactive: #333333; + --bgcolor-textinput: #0e0e0e; + --color: #fff; + --color-accent: #bbb;; + --bgcolor-danger: rgb(169, 0, 0); } img { @@ -81,14 +83,27 @@ time { background-color: var(--bgcolor-danger); } +a { + color: var(--color-accent); +} + a:focus { border-radius: var(--focus-border-radius); outline: var(--focus-outline); outline-offset: 0; } +a:visited { + color: darkmagenta; +} + img[alt] { font-size: .9rem; text-align: center; word-break: break-all; } + +pre { + margin: 0; + padding: .5rem 0; +} diff --git a/src/main.js b/src/main.js index 858eb9f..9fca2fd 100644 --- a/src/main.js +++ b/src/main.js @@ -4,23 +4,14 @@ import {dateTime, formatTime} from './timeutil.js'; // curl -H 'accept: application/nostr+json' https://nostr.x1ddos.ch const pool = relayPool(); pool.addRelay('wss://relay.nostr.info', {read: true, write: true}); -// pool.addRelay('wss://relay.damus.io', {read: true, write: true}); +pool.addRelay('wss://relay.damus.io', {read: true, write: true}); pool.addRelay('wss://nostr.x1ddos.ch', {read: true, write: true}); // pool.addRelay('wss://nostr.openchain.fr', {read: true, write: true}); // pool.addRelay('wss://nostr.bitcoiner.social/', {read: true, write: true}); // read only // pool.addRelay('wss://nostr.rocks', {read: true, write: false}); - -let max = 0; - function onEvent(evt, relay) { - if (evt.id === '209eefe6c940377fa8730853a75d1b4bb31bd929d79') { - console.log(evt) - } - // if (max++ >= 223) { - // return subscription.unsub(); - // } switch (evt.kind) { case 0: handleMetadata(evt, relay); @@ -32,7 +23,7 @@ function onEvent(evt, relay) { handleRecommendServer(evt, relay); break; case 3: - updateContactList(evt, relay); + // handleContactList(evt, relay); break; case 7: handleReaction(evt, relay); @@ -41,7 +32,13 @@ function onEvent(evt, relay) { } } -let pubkey = localStorage.getItem('pub_key') +let pubkey = localStorage.getItem('pub_key') || (() => { + const privatekey = generatePrivateKey(); + const pubkey = getPublicKey(privatekey); + localStorage.setItem('private_key', privatekey); + localStorage.setItem('pub_key', pubkey); + return pubkey; +})(); const subscription = pool.sub({ cb: onEvent, @@ -55,7 +52,7 @@ const subscription = pool.sub({ // '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', // jb55 // ], // since: new Date(Date.now() - (24 * 60 * 60 * 1000)), - limit: 400, + limit: 500, } }); @@ -112,9 +109,10 @@ function handleReaction(evt, relay) { const button = article.querySelector('button[name="star"]'); const reactions = button.querySelector('[data-reactions]'); reactions.textContent = reactionMap[eventId].length; - console.log(evt.pubkey, pubkey) if (evt.pubkey === pubkey) { - button.querySelector('img[src$="star.svg"]').setAttribute('src', 'assets/star-fill.svg'); + const star = button.querySelector('img[src$="star.svg"]'); + star.setAttribute('src', 'assets/star-fill.svg'); + star.setAttribute('title', reactionMap[eventId]) } } } @@ -131,14 +129,8 @@ const sortByCreatedAt = (evt1, evt2) => { return evt1.created_at > evt2.created_at ? -1 : 1; }; -// let debounceDebugMessageTimer; function renderFeed() { const sortedFeeds = textNoteList.sort(sortByCreatedAt).reverse(); - // debug - // clearTimeout(debounceDebugMessageTimer); - // debounceDebugMessageTimer = setTimeout(() => { - // console.log(`${sortedFeeds.reverse().map(e => dateTime.format(e.created_at * 1000)).join('\n')}`) - // }, 2000); sortedFeeds.forEach((textNoteEvent, i) => { if (feedDomMap[textNoteEvent.id]) { // TODO check eventRelayMap if event was published to different relays @@ -178,22 +170,26 @@ function createTextNote(evt, relay) { elem('strong', {className: 'mbox-username'}, userName), ' ', elem('time', {dateTime: time.toISOString()}, formatTime(time)), - ` kind:${evt.kind} ${evt.id}`, ]), ]), elem('div', {data: isLongContent ? {append: evt.content.slice(280)} : null}, content), - elem('button', { - className: 'btn-inline', name: 'reply', type: 'button', - data: {'eventId': evt.id, relay}, - }, [elem('img', {height: 24, width: 24, src: 'assets/comment.svg'})]), 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`}), // ♥ + elem('img', { + alt: didReact ? '✭' : '✩', // ♥ + height: 24, width: 24, + src: `assets/${didReact ? 'star-fill' : 'star'}.svg`, + title: reactionMap[evt.id]?.map(({content}) => content).join(' '), + }), elem('small', {data: {reactions: evt.id}}, hasReactions ? reactionMap[evt.id].length : ''), ]), - replies[0] ? elem('div', {className: 'mobx-replies'}, replyFeed) : '', + 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()) : '', ]); return rendernArticle([img, body]); } @@ -210,8 +206,7 @@ function handleReply(evt, relay) { function renderReply(evt, relay) { const eventId = evt.tags[0][1]; // TODO: double check const article = feedDomMap[eventId] || replyDomMap[eventId]; - if (!article) { - // root article has not been rendered + if (!article) { // root article has not been rendered return; } let replyContainer = article.querySelector('.mobx-replies'); @@ -245,6 +240,45 @@ function handleRecommendServer(evt, relay) { feedDomMap[evt.id] = art; } +function handleContactList(evt, relay) { + if (feedDomMap[evt.id]) { + return; + } + const art = renderUpdateContact(evt, relay); + if (textNoteList.length < 2) { + feedContainer.append(art); + return; + } + const closestTextNotes = textNoteList.sort(sortEventCreatedAt(evt.created_at)); + feedDomMap[closestTextNotes[0].id].after(art); + feedDomMap[evt.id] = art; + // const user = userList.find(u => u.pupkey === evt.pubkey); + // if (user) { + // console.log(`TODO: add contact list for ${evt.pubkey.slice(0, 8)} on ${relay}`, evt.tags); + // } else { + // tempContactList[relay] = tempContactList[relay] + // ? [...tempContactList[relay], evt] + // : [evt]; + // } +} + +function renderUpdateContact(evt, relay) { + const {img, time, userName} = getMetadata(evt, relay); + const body = elem('div', {className: 'mbox-body', title: dateTime.format(time)}, [ + elem('header', {className: 'mbox-header'}, [ + elem('small', {}, [ + + ]), + ]), + elem('pre', {title: JSON.stringify(evt.content)}, [ + elem('strong', {}, userName), + ' updated contacts: ', + JSON.stringify(evt.tags), + ]), + ]); + return rendernArticle([img, body], {className: 'mbox-updated-contact'}); +} + function renderRecommendServer(evt, relay) { const {img, time, userName} = getMetadata(evt, relay); const body = elem('div', {className: 'mbox-body', title: dateTime.format(time)}, [ @@ -264,7 +298,7 @@ function rendernArticle(content, props = {}) { } const userList = []; -const tempContactList = {}; +// const tempContactList = {}; function handleMetadata(evt, relay) { try { @@ -280,9 +314,7 @@ function setMetadata(evt, relay, content) { const user = userList.find(u => u.pubkey === evt.pubkey); if (!user) { userList.push({ - metadata: { - [relay]: content - }, + metadata: {[relay]: content}, pubkey: evt.pubkey, }); } else { @@ -292,12 +324,12 @@ function setMetadata(evt, relay, content) { ...content, }; } - if (tempContactList[relay]) { - const updates = tempContactList[relay].filter(update => update.pubkey === evt.pubkey); - if (updates) { - // console.log('TODO: add contact list (kind 3)', updates); - } - } + // if (tempContactList[relay]) { + // const updates = tempContactList[relay].filter(update => update.pubkey === evt.pubkey); + // if (updates) { + // console.log('TODO: add contact list (kind 3)', updates); + // } + // } } const getHost = (url) => { @@ -326,19 +358,6 @@ function getMetadata(evt, relay) { return {host, img, isReply, replies, time, userName}; } -function updateContactList(evt, relay) { - const user = userList.find(u => u.pupkey === evt.pubkey); - if (user) { - console.log(`TODO: add contact list for ${evt.pubkey.slice(0, 8)} on ${relay}`, evt.tags); - } else { - tempContactList[relay] = tempContactList[relay] - ? [...tempContactList[relay], evt] - : [evt]; - } -} - -// check pool.status - // reply const writeForm = document.querySelector('#writeForm'); const input = document.querySelector('input[name="message"]'); @@ -351,8 +370,9 @@ feedContainer.addEventListener('click', (e) => { lastReplyBtn.hidden = false; } lastReplyBtn = button; - button.hidden = true; + // button.hidden = true; button.after(writeForm); + button.after(sendStatus); writeForm.hidden = false; replyTo = ['e', button.dataset.eventId, button.dataset.relay]; input.focus(); @@ -364,6 +384,14 @@ feedContainer.addEventListener('click', (e) => { } }); +const newMessageDiv = document.querySelector('#newMessage'); +document.querySelector('#bubble').addEventListener('click', (e) => { + replyTo = null; + newMessageDiv.prepend(writeForm); + newMessageDiv.append(sendStatus); + input.focus(); +}); + async function upvote(eventId, relay) { const privatekey = localStorage.getItem('private_key'); const newReaction = { @@ -393,7 +421,8 @@ const onSendError = err => { sendStatus.hidden = false; }; const publish = document.querySelector('#publish'); -publish.addEventListener('click', async () => { +writeForm.addEventListener('submit', async (e) => { + e.preventDefault(); // const pubkey = localStorage.getItem('pub_key'); const privatekey = localStorage.getItem('private_key'); if (!pubkey || !privatekey) { @@ -424,7 +453,8 @@ publish.addEventListener('click', async () => { lastReplyBtn.hidden = false; lastReplyBtn = null; replyTo = null; - document.querySelector('#newMessage').append(writeForm); + newMessageDiv.append(writeForm); + newMessageDiv.append(sendStatus); } // console.info(`event published by ${url}`, ev); } @@ -433,6 +463,7 @@ publish.addEventListener('click', async () => { }); input.addEventListener('input', () => publish.disabled = !input.value); +input.addEventListener('blur', () => sendStatus.textContent = ''); // settings const form = document.querySelector('form[name="settings"]'); @@ -502,4 +533,4 @@ document.body.addEventListener('click', (e) => { delete append.dataset.append; return; } -}); \ No newline at end of file +}); diff --git a/src/tabs.css b/src/tabs.css index 2edcedf..e92c055 100644 --- a/src/tabs.css +++ b/src/tabs.css @@ -1,4 +1,4 @@ - +.tabs { margin-top: 4rem; } .tabs .tab-content { display: none; } #feed:checked ~ .tabs .tab-content:nth-child(1), #trending:checked ~ .tabs .tab-content:nth-child(2), @@ -43,9 +43,6 @@ input[type="radio"]:checked + label { /* -.tabs { - position: relative; -} .tab { float: left;