From 4576355b0390a3d69affddbf98956701199165fb Mon Sep 17 00:00:00 2001 From: OFF0 Date: Sun, 4 Dec 2022 23:12:54 +0100 Subject: [PATCH] layout: visual interface improvements - writing a new message is now presented in full-screen, so that there are no distractions, i.e. other posts - added back button and listen to esc key to close new message - on portrait mode the navigation buttons are now positioned at the bottom of the screen - write new message botton (bubble) is also positioned bottomright - replies now use a line to the last reply instead of indentation, better use of available space, especially on small screen - ignore newlines at the end of a post - added subtile growin effect to the multiline textfield, to hint that the textarea is growing with more content --- src/cards.css | 57 +++++++++++++++++++++++----- src/domutil.js | 7 ++-- src/form.css | 100 ++++++++++++++++++++++++++++++++++++++++--------- src/index.html | 28 +++++++++----- src/main.css | 7 +++- src/main.js | 69 ++++++++++++++++++++++++---------- src/tabs.css | 49 +++++++++++++++++------- src/write.css | 51 +++++++++++++++++++++++++ 8 files changed, 295 insertions(+), 73 deletions(-) create mode 100644 src/write.css diff --git a/src/cards.css b/src/cards.css index 5db1daa..af579e2 100644 --- a/src/cards.css +++ b/src/cards.css @@ -1,26 +1,40 @@ /* https://developer.mozilla.org/en-US/docs/Web/CSS/Layout_cookbook/Media_objects */ .mbox { + --profileimg-size: 5rem; align-items: center; display: flex; flex-direction: row; - flex-wrap: nowrap; + flex-wrap: wrap; margin-bottom: 1rem; + padding: 0 calc(.25 * var(--gap)); +} +.mbox:last-child { + margin-bottom: 0; +} +.mbox .mbox { + padding: 0; } .mbox-img { - --size: 5rem; align-self: start; - flex-basis: var(--size); - height: var(--size); + background-color: var(--bgcolor-textinput); + border-radius: var(--profileimg-size); + border: 1px solid transparent; + flex-basis: var(--profileimg-size); + height: var(--profileimg-size); margin-right: 1rem; - margin-top: .5ch; - max-width: var(--size); - max-width: var(--size); + /* padding-top: .5ch; */ + max-width: var(--profileimg-size); + max-width: var(--profileimg-size); + outline: .5rem solid var(--bgcolor); + overflow: hidden; + position: relative; + z-index: 2; } .mbox-updated-contact .mbox-img, .mbox-recommend-server .mbox-img { - --size: 4.5ch; + --profileimg-size: 4.5ch; margin-left: 3ch; margin-right: 3.5ch; } @@ -61,4 +75,29 @@ .mbox-updated-contact { padding: 0 0 1rem 0; margin: 0; -} \ No newline at end of file +} + +.mbox { + overflow: hidden; +} +.mbox .mbox { + overflow: visible; + position: relative; +} +.mobx-replies { + flex-grow: 1; + position: relative; +} +.mbox .mbox::before, +.mobx-replies::before { + background-color: var(--bgcolor-inactive); + border: none; + content: ""; + display: block; + height: 100vh; + left: calc(.5 * var(--profileimg-size)); + margin-left: -.2rem; + position: absolute; + top: -100vh; + width: .4rem; +} diff --git a/src/domutil.js b/src/domutil.js index dced403..3d6409e 100644 --- a/src/domutil.js +++ b/src/domutil.js @@ -31,7 +31,8 @@ export function elem(name = 'div', {data, ...props} = {}, children = []) { * @return Array */ export function multilineText(string) { - return string - .split('\n') - .reduce((acc, next, i) => acc.concat(i === 0 ? next : [elem('br'), next]), []); + return string + .trimRight() + .split('\n') + .reduce((acc, next, i) => acc.concat(i === 0 ? next : [elem('br'), next]), []); } diff --git a/src/form.css b/src/form.css index c7e26ba..2353c07 100644 --- a/src/form.css +++ b/src/form.css @@ -1,30 +1,46 @@ +:root { + --transition-duration: .25s; + --transition-timing-function: ease-out; +} + form, .form { + --padding: 1.2rem; display: flex; flex-direction: column; + padding: var(--gap); +} + +fieldset { + /* ignore this container */ + border: none; + display: contents; +} + +legend { + display: none; + width: 100%; +} +#newMessage legend { + display: block; } input, textarea { color: var(--color); font-size: 1.6rem; - margin-bottom: 1.2rem; - padding: 1.3rem 1.8rem; } button, label { + color: var(--color); cursor: pointer; display: block; font-size: 1.6rem; margin-bottom: 0; - padding: 1.3rem 1.8rem; + padding: var(--padding); text-indent: 0; - transition: background-color .25s; -} - -label { - color: var(--color-accent); + transition: background-color var(--transition-duration); } input[type="password"], @@ -34,7 +50,8 @@ textarea { border: .2rem solid #b7b7b7; border-radius: .2rem; display: block; - margin: 0; + margin: 0 0 1.2rem 0; + padding: var(--padding); } input[type="password"]:focus, input[type="text"]:focus, @@ -44,27 +61,41 @@ textarea:focus { outline: var(--focus-outline); } textarea { - max-height: 50vh; + /* max-height: 64vh; */ min-height: 20px; resize: none; - transition: min-height .1s ease-out, height .1s ease-out; + transition: min-height var(--transition-duration) var(--transition-timing-function), + height var(--transition-duration) var(--transition-timing-function); } textarea:focus { min-height: 3.5rem; } - +#newMessage textarea { + min-height: 10rem; +} +#newMessage textarea:focus { + min-height: 18rem; +} .buttons { align-items: center; display: flex; + flex-basis: 100%; justify-content: flex-end; + gap: var(--gap); margin-top: 2rem; min-height: 3.2rem; } +.form-inline .buttons { + flex-basis: fit-content; + margin-top: 0; +} button { - background-color: var(--bgcolor-accent); - border: none; + --bg-color: var(--bgcolor-accent); + --border-color: var(--bgcolor-accent); + background-color: var(--bg-color); + border: 0.2rem solid var(--border-color); border-radius: .2rem; cursor: pointer; outline-offset: 1px; @@ -75,6 +106,7 @@ button:focus { } .btn-inline { + --border-color: transparent; align-items: center; background: transparent; color: var(--color); @@ -100,7 +132,8 @@ button:focus { } button:disabled { - background-color: var(--bgcolor-inactive); + --bg-color: var(--bgcolor-inactive); + --border-color: var(--bgcolor-inactive); cursor: default; } @@ -110,15 +143,19 @@ button:disabled { } .form-status { + flex-basis: 100%; flex-grow: 1; padding: 1rem 1.8rem; } .form-inline { + --padding: 1.2rem 1.3rem; display: flex; flex-direction: row; flex-grow: 1; - gap: 1rem; + flex-wrap: wrap; + gap: 0 var(--gap); + padding: 0; } .cards .form-inline button, .cards .form-inline input[type="text"], @@ -128,7 +165,10 @@ button:disabled { .form-inline input[type="text"], .form-inline textarea { + flex-basis: 50%; flex-grow: 1; + flex-shrink: 1; + min-width: 100px; margin-bottom: 0; } @@ -136,10 +176,36 @@ button:disabled { flex-grow: 0; } -.form-inline button#publish { +button#publish { + align-self: end; + order: 2; +} +button[name="back"] { + display: none; +} +#newMessage button[name="back"] { align-self: end; + display: inherit; +} + +#sendstatus { + order: 1; } .focus-active { } + +.shrink-out { + animation-duration: var(--transition-duration); + animation-name: lineInserted; + transition: max-height var(--transition-duration) var(--transition-timing-function); +} +@keyframes lineInserted { + from { + max-height: 50px; + } + to { + max-height: 0px; + } +} \ No newline at end of file diff --git a/src/index.html b/src/index.html index 5fecca9..41a57d7 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,8 @@
+ + - -
-
- -
-
- - + + + + + -
+
diff --git a/src/main.css b/src/main.css index 6375161..5ba113b 100644 --- a/src/main.css +++ b/src/main.css @@ -1,6 +1,7 @@ @import "tabs.css"; @import "cards.css"; @import "form.css"; +@import "write.css"; :root { /* 5px auto Highlight */ @@ -12,6 +13,7 @@ --focus-outline-width: 2px; --focus-outline: var(--focus-outline-width) var(--focus-outline-style) var(--focus-outline-color); --font-small: 1.2rem; + --gap: 2.4rem; } ::selection { @@ -26,7 +28,7 @@ @media (prefers-color-scheme: light) { html { --bgcolor: #fdfefa; - --bgcolor-accent: #37ff1d; + --bgcolor-accent: #7badfc; --bgcolor-inactive: #bababa; --bgcolor-textinput: #fff; --color: rgb(68 68 68); @@ -39,7 +41,7 @@ html { --bgcolor: #191919; --bgcolor-accent: #1e437d; - --bgcolor-inactive: #333333; + --bgcolor-inactive: #434343; --bgcolor-textinput: #0e0e0e; --color: #fff; --color-accent: #bbb;; @@ -65,6 +67,7 @@ body { color: var(--color); font-size: 1.6rem; line-height: 1.5; + margin: 0; } body, diff --git a/src/main.js b/src/main.js index 44e838b..e3f0146 100644 --- a/src/main.js +++ b/src/main.js @@ -3,8 +3,8 @@ import {elem, multilineText} from './domutil.js'; 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.nostr.info', {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}); @@ -112,7 +112,7 @@ function handleReaction(evt, relay) { if (evt.pubkey === pubkey) { const star = button.querySelector('img[src$="star.svg"]'); star.setAttribute('src', 'assets/star-fill.svg'); - star.setAttribute('title', reactionMap[eventId]) + star.setAttribute('title', reactionMap[eventId]); } } } @@ -155,7 +155,7 @@ setInterval(() => { function createTextNote(evt, relay) { const {host, img, isReply, replies, time, userName} = getMetadata(evt, relay); - const isLongContent = evt.content.length > 280; + const isLongContent = evt.content.trimRight().length > 280; const content = isLongContent ? `${evt.content.slice(0, 280)}…` : evt.content; const hasReactions = reactionMap[evt.id]?.length > 0; const didReact = hasReactions && !!reactionMap[evt.id].find(reaction => reaction.pubkey === pubkey); @@ -190,10 +190,16 @@ function createTextNote(evt, relay) { 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"]')); + requestAnimationFrame(() => updateElemHeight(writeInput)); + } + return rendernArticle([ + elem('div', {className: 'mbox-img'}, [img]), body, replies[0] ? elem('div', {className: 'mobx-replies'}, replyFeed.reverse()) : '', ]); - if (restoredReplyTo === evt.id) appendReplyForm(body.querySelector('button[name="reply"]')); - return rendernArticle([img, body]); } function handleReply(evt, relay) { @@ -214,7 +220,7 @@ function renderReply(evt, relay) { let replyContainer = article.querySelector('.mobx-replies'); if (!replyContainer) { replyContainer = elem('div', {className: 'mobx-replies'}); - article.querySelector('.mbox-body').append(replyContainer); + article.append(replyContainer); } const reply = createTextNote(evt, relay); replyContainer.append(reply); @@ -291,7 +297,7 @@ function renderRecommendServer(evt, relay) { ]), ` recommends server: ${evt.content}`, ]); - return rendernArticle([img, body], {className: 'mbox-recommend-server', data: {relay: evt.content}}); + return rendernArticle([elem('div', {className: 'mbox-img'}, [img]), body], {className: 'mbox-recommend-server', data: {relay: evt.content}}); } function rendernArticle(content, props = {}) { @@ -349,7 +355,6 @@ function getMetadata(evt, relay) { const userName = user?.metadata[relay]?.name || evt.pubkey.slice(0, 8); const userAbout = user?.metadata[relay]?.about || ''; const img = elem('img', { - className: 'mbox-img', src: userImg, alt: `${userName} ${host}`, title: `${userName} on ${host} ${userAbout}`, @@ -378,19 +383,45 @@ const writeForm = document.querySelector('#writeForm'); const writeInput = document.querySelector('textarea[name="message"]'); function appendReplyForm(el) { + const shrink = elem('div', {className: 'shrink-out'}); + shrink.style.height = `${writeInput.style.height || writeInput.getBoundingClientRect().height}px`; + shrink.addEventListener('animationend', () => shrink.remove()); + writeForm.before(shrink); + writeInput.blur(); + writeInput.style.removeProperty('height'); el.after(writeForm); - el.after(sendStatus); - writeInput.focus(); + if (writeInput.value && !writeInput.value.trimRight()) { + writeInput.value = ''; + } else { + requestAnimationFrame(() => updateElemHeight(writeInput)); + } + requestAnimationFrame(() => writeInput.focus()); } const newMessageDiv = document.querySelector('#newMessage'); document.querySelector('#bubble').addEventListener('click', (e) => { - localStorage.removeItem('reply_to'); + localStorage.removeItem('reply_to'); // should it forget old replyto context? newMessageDiv.prepend(writeForm); - newMessageDiv.append(sendStatus); + hideNewMessage(false); writeInput.focus(); + if (writeInput.value.trimRight()) { + writeInput.style.removeProperty('height'); + } + document.body.style.overflow = 'hidden'; + requestAnimationFrame(() => updateElemHeight(writeInput)); }); +document.body.addEventListener('keyup', (e) => { + if (e.key === 'Escape') { + hideNewMessage(true); + } +}); + +function hideNewMessage(hide) { + document.body.style.removeProperty('overflow'); + newMessageDiv.hidden = hide; +} + async function upvote(eventId, relay) { const privatekey = localStorage.getItem('private_key'); const newReaction = { @@ -415,10 +446,7 @@ async function upvote(eventId, relay) { // send const sendStatus = document.querySelector('#sendstatus'); -const onSendError = err => { - sendStatus.textContent = err.message; - sendStatus.hidden = false; -}; +const onSendError = err => sendStatus.textContent = err.message; const publish = document.querySelector('#publish'); writeForm.addEventListener('submit', async (e) => { e.preventDefault(); @@ -447,14 +475,13 @@ writeForm.addEventListener('submit', async (e) => { console.info(`publish request sent to ${url}`); } if (status === 1) { - sendStatus.hidden = true; + sendStatus.textContent = ''; writeInput.value = ''; writeInput.style.removeProperty('height'); publish.disabled = true; if (replyTo) { localStorage.removeItem('reply_to'); newMessageDiv.append(writeForm); - newMessageDiv.append(sendStatus); } // console.info(`event published by ${url}`, ev); } @@ -547,4 +574,8 @@ document.body.addEventListener('click', (e) => { delete append.dataset.append; return; } + const back = e.target.closest('[name="back"]') + if (back) { + hideNewMessage(true); + } }); diff --git a/src/tabs.css b/src/tabs.css index e92c055..7591b5b 100644 --- a/src/tabs.css +++ b/src/tabs.css @@ -1,4 +1,7 @@ -.tabs { margin-top: 4rem; } +.tabs { + flex-basis: 100%; + margin-top: 4rem; +} .tabs .tab-content { display: none; } #feed:checked ~ .tabs .tab-content:nth-child(1), #trending:checked ~ .tabs .tab-content:nth-child(2), @@ -15,13 +18,17 @@ input[type="radio"].tab { } .tab + label { + background-color: var(--bgcolor-textinput); border: none; color: var(--color); display: inline-block; + margin-left: var(--gap); + margin-top: var(--gap); outline: 2px solid var(--bgcolor-accent); padding: 1rem 1.5em; position: relative; top: 1px; + z-index: 11; } input[type="radio"]:checked + label { background: var(--bgcolor-accent); @@ -38,18 +45,34 @@ input[type="radio"]:checked + label { .tab-content { max-width: 96ch; min-height: 200px; + padding: calc(.5 * var(--gap)) 0 100px 0; } - -/* - - - -.tab { - float: left; -} - -.tab > label { +.tabbed { + align-items: start; + display: flex; + flex-wrap: wrap; } +@media (orientation: portrait) { + .tabbed { + align-items: start; + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + justify-content: start; + } + .tabs { + height: 100vh; + margin-top: 0; + order: 1; + overflow: scroll; + width: 100vw; + } + .tab + label { + margin-top: calc(-3 * var(--gap)); + margin-left: var(--gap); + order: 2; + } + .cards { - -*/ \ No newline at end of file + } +} \ No newline at end of file diff --git a/src/write.css b/src/write.css new file mode 100644 index 0000000..158bfd3 --- /dev/null +++ b/src/write.css @@ -0,0 +1,51 @@ +#bubble { + bottom: 4rem; + height: 10rem; + padding: 0; + position: fixed; + right: 5rem; + width: 10rem; + z-index: 12; +} +@media (orientation: portrait) { + #bubble { + bottom: calc(2 * var(--gap)); + right: var(--gap); + } +} + +#newMessage { + align-items: center; + display: flex; + height: 100vh; + justify-content: center; + position: fixed; + top: 0; + width: 100vw; + z-index: 20; +} + +#newMessage #writeForm { + align-items: start; + background-color: var(--bgcolor); + display: flex; + flex-direction: row; + flex-grow: 1; + flex-wrap: wrap; + gap: 0; + justify-content: end; + max-height: 100vh; + min-height: 64vh; + outline: 100vh solid var(--bgcolor); + overflow-y: auto; + padding: 2rem; +} + +#newMessage .form-inline textarea { + flex-basis: 100%; + margin: var(--gap) 0; +} + +#newMessage .buttons { + align-self: end; +} \ No newline at end of file