damus.io

damus.io website
git clone git://jb55.com/damus.io
Log | Files | Refs | README | LICENSE

commit e0be7d15c34bba43dbb8c58031f10fdc288c4829
parent 75b7a741752981709b2aa5ba2692b26e389a19c2
Author: William Casarin <jb55@jb55.com>
Date:   Sun, 13 Nov 2022 09:04:42 -0800

Merge remote-tracking branch 'thomas/main'

Diffstat:
Mweb/README.md | 27++++++++++++++++++++++-----
Mweb/css/responsive.css | 37++++++++++---------------------------
Mweb/css/styles.css | 154++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mweb/index.html | 40++++++++++++++++++++++++----------------
Mweb/js/damus.js | 30+++++++++++++++++++-----------
Mweb/js/ui/render.js | 47+++++++++++++++++++++++++----------------------
Mweb/js/ui/util.js | 5+++++
7 files changed, 202 insertions(+), 138 deletions(-)

diff --git a/web/README.md b/web/README.md @@ -4,6 +4,21 @@ Here lies the code for the Damus web app, a client for the Nostr protocol. The goal of this client is to be a better version of Twitter, but not to reproduce all of it's functionality. +[Issue Tracker](https://todo.sr.ht/~tomtom/damus-web-issues) + +## Roadmap + +Here is what is confirmed for development. + + - [ ] Share event + - [ ] Profile view (with ability to follow user) + - [ ] Edit metadata (from profile view) + - [ ] Global timeline view + - [ ] Notifications view + - [ ] Settings view (with ability to configure relays) + - [ ] Multiple reaction picker + - [ ] Direct Messages (subject to discussion) + ## Contribution Guide There are rules to contributing to this client. Please ensure you read them @@ -13,9 +28,8 @@ before making changes and supplying patch notes. - Keep source code organised. Refer to the folder structure. If you have a question, ask it. - Do not include your personal tools in the source code. Use your own scripts - - outside of the project. This does not include build tools such as Make. - - Use tabs. - - Write methods in snake_case. + outside of the project. This does not include build tools such as Make. + - Use tabs & write JS with snake_case. End of discussion. - Do not include binary files. - No NPM (and kin) environments. If you need a file from an external resource mark the location in the "sources" file and add it to the repo. @@ -34,6 +48,9 @@ TODO Write about the style guide. ## Terminology - * Sign Out - Not "log out", "logout", "log off", etc. - * Sign In - Not "login", "log in", "signin", "sign-in", etc. + * Sign Out - Not "log out", "logout", "log off", etc. + * Sign In - Not "login", "log in", "signin", "sign-in", etc. + * Share - Not "boosted", "retweeted", "repost", etc. + * Send - Not "tweet", "toot", "post", etc. + * Link - Not "share". diff --git a/web/css/responsive.css b/web/css/responsive.css @@ -1,5 +1,9 @@ @media (max-width: 800px){ :root { + /* TODO font size should not be controlled by CSS: + * Instead I would prefer user settings. The main reason is the font is + * too small on my desktop when I use the app in column mode. + */ --fsSmall: 10px; --fsNormal: 14px; --fsReduced: 12px; @@ -8,35 +12,19 @@ /* Utility */ .vertical-hide { - display: none; - } - - #content header > label { - padding: 12px; + display: none !important; } /* Application Framework */ - #container { - flex-flow: column-reverse; - width: 100vw; - height: 100vh; + #gnav { + display: initial; } - #content { + #view { width: initial; border-right: none; } - - /* Navigation */ - #nav { - flex-flow: row; - border-top: 1px solid var(--clrBorder); - border-right: none; - padding: 10px; - } - #nav > * { - flex: auto; - margin-bottom: 0; - font-size: 1.3em; + #content header > label { + padding: 12px; } /* Event */ @@ -45,9 +33,4 @@ height: 44px; font-size: 2.2em; } - - /* Post Tools */ - #newpost > :first-child { - width: 0; - } } diff --git a/web/css/styles.css b/web/css/styles.css @@ -30,6 +30,13 @@ /* Font Families */ --ffDefault: "Noto Sans", sans-serif; + + /* Z Layers */ + --zDefault: 0; + --zPFP: 1; + --zHeader: 2; + --zGlobal: 3; + --zModal: 4; } *:focus-visible { /* Technically this is bad and something else should be done to indicate @@ -44,8 +51,6 @@ body { font-size: var(--fsNormal); margin: 0; padding: 0; - width: 100%; - height: 100%; } /* Utilities */ @@ -94,15 +99,29 @@ button.action { .bottom-border { /* TODO rename to bdr-bottom */ border-bottom: solid 1px var(--clrBorder); } +.sticky { + position: sticky; + top: 0; +} +.event-message { + background: #f2f2f2; + padding: 10px; + border-radius: 12px; + color: #444; +} /* Navigation */ #nav { + border-right: 1px solid var(--clrBorder); + padding: 16px; +} +#nav > div { + position: sticky; + top: 16px; display: flex; flex-flow: column; - border-right: 1px solid var(--clrBorder); - padding: 15px; } -#nav > * { +#nav > div > * { margin-bottom: 38px; } #app-icon-logo { @@ -118,34 +137,58 @@ button.nav { color: var(--clrBtn); font-size: 24px; } +#gnav { + display: none; + position: fixed; + bottom: 55px; + right: 55px; + z-index: var(--zGlobal); +} +#gnav button { + position: absolute; + top: 0; + left: 0; + font-size: 24px; + border-radius: 50%; + background: var(--clrText); + color: var(--clrBg); + padding: 5px; + border: transparent 5px solid; + transition: top 0.05s linear; + transform: translateX(-50%) translateY(-50%); + z-index: calc(var(--zGlobal) - 1); +} +#gnav button[role="open-gnav"] { + z-index: var(--zGlobal); + padding: 15px; +} +#gnav.open button[role="sign-out"] { + top: -75px; +} /* Application Framework */ #container { - margin: auto; - max-width: 750px; display: flex; flex-flow: row; - overflow: hidden; } -#content { +#view { flex: auto; - display: flex; - flex-direction: column; border-right: 1px solid var(--clrBorder); - overflow: hidden; width: 750px; + min-height: 100vh; } -#content header > label { - padding: 22px 15px; +#view header { + position: sticky; + top: 0; + z-index: var(--zHeader); + background: var(--clrBg); +} +#view header > label { + padding: 15px; font-size: 22px; font-weight: 800; display: block; } -#view { - height: 100%; - overflow-y: scroll; - flex: 1; -} /* Events & Content */ .event { @@ -161,16 +204,28 @@ button.nav { text-align: center; padding: 15px; } -.userpic { +.userpic { /* TODO remove .userpic and use helper class */ flex-shrink: 1; } - .pfp { - border-radius: 50%; width: 64px; height: 64px; - object-fit: fill; + position: relative; + border-radius: 50%; + z-index: var(--zPFP); font-size: 3.25em; + object-fit: fill; +} +.pfp.deleted { + background: var(--clrText); + font-size: 32px; + color: var(--clrBg); +} +.pfp.deleted > i { + top: 40%; + left: 50%; + position: relative; + transform: translateX(-50%) translateY(-50%); } .event-content { @@ -187,6 +242,22 @@ button.nav { .chatroom-name { font-weight: bold; } +.deleted-comment { + margin-top: 10px; +} +.line-bot { + width: 3px; + height: 100%; + position: relative; + top: -7px; + left: calc(50% - 1px); + background-color: var(--clrBorder); +} +.quote { + margin-left: 10px; + padding: 10px; + display: block; +} #replying-to { max-height: 200px; @@ -220,7 +291,7 @@ button.nav { font-size: var(--fsNormal); } .reactions { - padding-bottom: 15px; + margin-bottom: 15px; } .reaction-group { @@ -258,31 +329,20 @@ button.nav { color: var(--clrHeart); } details.cw summary { - background: #f2f2f2; - padding: 10px; - border-radius: 12px; - color: #444; cursor: pointer; - margin-bottom: 10px; outline: none; + margin-bottom: 10px; } /* Thread Expansion */ .thread-collapsed { padding: 15px; } -.thread-summary { - background: #f2f2f2; - padding: 10px; - border-radius: 12px; - color: #444; - cursor: pointer; -} /* Modal */ .modal { position: fixed; - z-index: 1; + z-index: var(--zModal); left: 0; top: 0; width: 100%; @@ -361,23 +421,3 @@ input[type="text"].cw { font-size: var(--fsReduced); } -.deleted-comment { - border: 2px dashed var(--clrBorder); - border-radius: 10px; - padding: 10px; -} - -.line-bot { - width: 3px; - height: 100%; - margin-top: -7px; - background-color: var(--clrBorder); - margin-left: auto; - margin-right: auto; -} - -.quote { - margin-left: 10px; - padding: 10px; - display: block; -} diff --git a/web/index.html b/web/index.html @@ -6,7 +6,6 @@ <title>Damus</title> <link rel="stylesheet" href="css/styles.css?v=116"> <link rel="stylesheet" href="css/responsive.css?v=7"> - <link rel="stylesheet" href="css/damus.css?v=211"> <link rel="stylesheet" href="css/fontawesome.css?v=2"> <script defer src="js/ui/util.js?v=1"></script> <script defer src="js/ui/render.js?v=1"></script> @@ -21,27 +20,36 @@ // This is our main entry. // https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event addEventListener('DOMContentLoaded', (ev) => { + // TODO fix race condition where profile doesn't load fast enough. damus_web_init(); }); </script> + <nav id="gnav"> + <button class="icon" role="open-gnav" title="Open Menu" onclick="toggle_gnav(this)"> + <i class="fa fa-fw fa-hand-peace"></i> + </button> + <button class="icon" role="sign-out" title="Sign Out" onclick="press_logout()"> + <i class="fa fa-fw fa-arrow-right-from-bracket"></i> + </button> + </nav> <div id="container"> - <div id="nav" class="flex-noshrink"> - <div id="app-icon-logo" class="vertical-hide"> - <i class="fa-regular fa-fw fa-hand-peace"></i> + <div class="flex-fill vertical-hide"></div> + <div id="nav" class="flex-noshrink vertical-hide"> + <div> + <div id="app-icon-logo"> + <!--<img src="https://damus.io/img/damus.svg">--> + <i class="fa-regular fa-fw fa-hand-peace"></i> + </div> + <button class="nav icon" title="Home"> + <i class="fa fa-fw fa-home"></i><span class="hide">Home</span> + </button> + <button onclick="press_logout()" title="Sign Out" class="nav icon"> + <i class="fa fa-fw fa-arrow-right-from-bracket"></i><span class="hide">Sign Out</span> + </button> </div> - <button class="nav icon"> - <i class="fa fa-fw fa-home"></i><span class="hide">Home</span> - </button> - <button onclick="press_logout()" title="Sign Out" class="nav icon"> - <i class="fa fa-fw fa-arrow-right-from-bracket"></i><span class="hide">Sign Out</span> - </button> - </div> - <div id="content"> - <header class="flex-noshrink"> - <label>Home</label> - </header> - <div id="view"></div> </div> + <div id="view"></div> + <div class="flex-fill vertical-hide"></div> </div> <div class="modal closed" id="reply-modal"> <div id="reply-modal-content" class="modal-content"> diff --git a/web/js/damus.js b/web/js/damus.js @@ -129,7 +129,6 @@ async function damus_web_init() if (!model.done_init) { model.loading = false - send_initial_filters(ids.account, model.pubkey, relay) } else { send_home_filters(ids, model, relay) @@ -514,8 +513,13 @@ function handle_profiles_loaded(profiles_id, model, relay) { // stop asking for profiles model.pool.unsubscribe(profiles_id, relay) model.realtime = true - redraw_events(model) + redraw_my_pfp(model) +} + +function redraw_my_pfp(model) { + const html = render_pfp(model.pubkey, model.profiles[model.pubkey]); + document.querySelector(".my-userpic").innerHTML = html; } function debounce(f, interval) { @@ -1029,24 +1033,28 @@ function is_video_url(path) { return VID_REGEX.test(path) } -const URL_REGEX = /(https?:\/\/[^\s]+)[,:)]?(\w|$)/g; +const URL_REGEX = /(^|\s)(https?:\/\/[^\s]+)[,:)]?(\w|$)/g; function linkify(text, show_media) { - return text.replace(URL_REGEX, function(url) { + return text.replace(URL_REGEX, function(match, p1, p2, p3) { + const url = p2+p3 const parsed = new URL(url) - if (show_media && is_img_url(parsed.pathname)) - return ` + let html; + if (show_media && is_img_url(parsed.pathname)) { + html = ` <a target="_blank" href="${url}"> <img class="inline-img" src="${url}"/> </a> `; - else if (show_media && is_video_url(parsed.pathname)) - return ` + } else if (show_media && is_video_url(parsed.pathname)) { + html = ` <video controls class="inline-img" /> <source src="${url}"> </video> `; - else - return `<a target="_blank" rel="noopener noreferrer" href="${url}">${url}</a>`; + } else { + html = `<a target="_blank" rel="noopener noreferrer" href="${url}">${url}</a>`; + } + return p1+html; }) } @@ -1118,7 +1126,7 @@ function format_content(ev, show_media) const open = !!DAMUS.cw_open[ev.id]? "open" : "" return ` <details class="cw" id="cw_${ev.id}" ${open}> - <summary>${cwHTML}</summary> + <summary class="event-message">${cwHTML}</summary> ${body} </details> ` diff --git a/web/js/ui/render.js b/web/js/ui/render.js @@ -4,8 +4,11 @@ function render_home_view(model) { return ` + <header> + <label>Home</label> + </header> <div id="newpost"> - <div><!-- empty to accomodate profile pic --></div> + <div class="my-userpic vertical-hide"><!-- To be loaded. --></div> <div> <textarea placeholder="What's up?" oninput="post_input_changed(this)" class="post-input" id="post-input"></textarea> <div class="post-tools"> @@ -51,7 +54,7 @@ function render_thread_collapsed(model, reply_ev, opts) if (opts.is_composing) return "" return `<div onclick="expand_thread('${reply_ev.id}')" class="thread-collapsed"> - <div class="thread-summary"> + <div class="thread-summary event-message"> More messages in thread available. Click to expand. </div> </div>` @@ -125,12 +128,6 @@ function render_boost(model, ev, opts) { profile: model.profiles[ev.pubkey] } return render_event(model, ev.json_content, opts) - //return ` - //<div class="boost"> - //<div class="boost-text">Reposted by ${render_name_plain(ev.pubkey, profile)}</div> - //${render_event(model, ev.json_content, opts)} - //</div> - //` } function render_comment_body(model, ev, opts) { @@ -166,15 +163,18 @@ function render_boosted_by(model, ev, opts) { function render_deleted_comment_body(ev, deleted) { if (deleted.content) { - const show_media = false return ` - <div class="deleted-comment"> - This comment was deleted. Reason: - <div class="quote">${format_content(deleted, show_media)}</div> + <div class="deleted-comment event-message"> + This content was deleted with reason: + <div class="quote">${format_content(deleted, false)}</div> </div> ` } - return `<div class="deleted-comment">This comment was deleted</div>` + return ` + <div class="deleted-comment event-message"> + This content was deleted. + </div> + ` } function render_event(model, ev, opts={}) { @@ -195,7 +195,7 @@ function render_event(model, ev, opts={}) { const replied_events = render_replied_events(model, ev, opts) - let name = "???" + let name = "" if (!deleted) { name = render_name_plain(ev.pubkey, profile) } @@ -225,11 +225,6 @@ function render_event(model, ev, opts={}) { ` } -function render_pfp(pk, profile, size="normal") { - const name = render_name_plain(pk, profile) - return `<img class="pfp" title="${name}" onerror="this.onerror=null;this.src='${robohash(pk)}';" src="${get_picture(pk, profile)}">` -} - function render_react_onclick(our_pubkey, reacting_to, emoji, reactions) { const reaction = reactions[our_pubkey] if (!reaction) { @@ -260,7 +255,7 @@ function render_reaction(model, reaction) { if (reaction.content === "+" || reaction.content === "") emoji = "❤️" - return render_pfp(reaction.pubkey, profile, "small") + return render_pfp(reaction.pubkey, profile) } function render_action_bar(ev, can_delete) { @@ -332,11 +327,19 @@ function render_name(pk, profile) { } function render_deleted_name() { - return "???" + return "" +} + +function render_pfp(pk, profile) { + const name = render_name_plain(pk, profile) + return `<img class="pfp" title="${name}" onerror="this.onerror=null;this.src='${robohash(pk)}';" src="${get_picture(pk, profile)}">` } function render_deleted_pfp() { - return `<div class="pfp pfp-normal">😵</div>` + return ` + <div class="pfp deleted"> + <i class="fa-solid fa-fw fa-ghost"></i> + </div>` } diff --git a/web/js/ui/util.js b/web/js/ui/util.js @@ -11,3 +11,8 @@ function toggle_cw(el) { input.classList.toggle("hide", !isOn); } +// toggle_gnav hides or shows the global navigation's additional buttons based +// on its opened state. +function toggle_gnav(el) { + el.parentElement.classList.toggle("open"); +}