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
OFF0 2 years ago
parent 3ab815c30e
commit 4576355b03
Signed by: offbyn
GPG Key ID: 94A2F643C51F37FA

@ -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;
}
@ -62,3 +76,28 @@
padding: 0 0 1rem 0;
margin: 0;
}
.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;
}

@ -32,6 +32,7 @@ export function elem(name = 'div', {data, ...props} = {}, children = []) {
*/
export function multilineText(string) {
return string
.trimRight()
.split('\n')
.reduce((acc, next, i) => acc.concat(i === 0 ? next : [elem('br'), next]), []);
}

@ -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;
}
}

@ -8,6 +8,8 @@
</head>
<body>
<main class="tabbed">
<input type="radio" name="maintabs" id="settings" class="tab">
<label for="settings">profile</label>
<input type="radio" name="maintabs" id="feed" class="tab" checked>
<label for="feed">feed</label>
<!-- <input type="radio" name="maintabs" id="trending" class="tab">
@ -16,21 +18,27 @@
<label for="direct">direct</label>
<input type="radio" name="maintabs" id="chat" class="tab">
<label for="chat">chat</label> -->
<input type="radio" name="maintabs" id="settings" class="tab">
<label for="settings">profile</label>
<div class="tabs">
<div class="tab-content">
<article class="mbox">
<img class="mbox-img" id="bubble" src="assets/comment.svg" alt="">
<div class="mbox-body" id="newMessage">
<form action="#" class="form-inline" id="writeForm">
<artcile>
<svg id="bubble" xmlns="http://www.w3.org/2000/svg" width="24" height="21" viewBox="0 0 80.035 70.031">
<path fill="var(--bgcolor-textinput)" stroke="currentColor" stroke-width="4.927" d="M2.463 31.824q0-4.789 1.893-9.248 1.892-4.46 5.361-8.087 3.47-3.626 8.07-6.333 4.598-2.707 10.324-4.2 5.727-1.493 11.836-1.493 6.107 0 11.834 1.492 5.725 1.494 10.325 4.2 4.599 2.708 8.07 6.334 3.47 3.627 5.362 8.087 1.891 4.46 1.891 9.248 0 5.97-2.967 11.384-2.967 5.414-7.982 9.336-5.015 3.922-11.957 6.248-6.94 2.325-14.576 2.325-7.463 0-14.334-2.221l-8.537 6.038q-4.789 3.02-6.733 1.752-1.943-1.266-.867-7.13l1.77-8.886q-4.2-3.887-6.49-8.71-2.29-4.825-2.29-10.136Z"/>
</svg>
<div id="newMessage" hidden>
<form action="#" id="writeForm" class="form-inline">
<fieldset>
<legend>write a new post</legend>
<textarea name="message" rows="1"></textarea>
<div class="buttons">
<button type="submit" id="publish" disabled>send</button>
</form>
<button type="button" name="back">back</button>
</div>
<small id="sendstatus" class="form-status"></small>
</fieldset>
</form>
</div>
</article>
</artcile>
<div class="cards" id="homefeed"></div>
</div>

@ -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,

@ -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);
}
});

@ -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;
.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 {
.tab > label {
}
}
*/

@ -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;
}
Loading…
Cancel
Save