damus.io

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

commit 9f2af26fa614986dc12ff78375723a7bddc58835
parent b5a2b5ec2bcefa67f65c405e45c0209141fdf894
Author: Thomas Mathews <thomas.c.mathews@gmail.com>
Date:   Thu, 17 Nov 2022 20:40:00 -0800

web: Add profile view.

Note that this is incomplete as I do not fetch the events for the user
and render them. I am also missing the state check for if you follow the
user.

Diffstat:
Mweb/css/styles.css | 41++++++++++++++++++++++++++++++++++++++++-
Aweb/icon/message-user.svg | 2++
Aweb/icon/pubkey.svg | 2++
Mweb/index.html | 36++++++++++++++++++++++++++++++++++++
Mweb/js/damus.js | 10+---------
Mweb/js/ui/render.js | 37+++++++++++++++++++++----------------
Mweb/js/ui/util.js | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aweb/js/util.js | 15+++++++++++++++
8 files changed, 181 insertions(+), 26 deletions(-)

diff --git a/web/css/styles.css b/web/css/styles.css @@ -37,6 +37,9 @@ --zHeader: 2; --zGlobal: 3; --zModal: 4; + + /* Icon */ + --iconSize: 24px; } *:focus-visible { /* Technically this is bad and something else should be done to indicate @@ -62,7 +65,7 @@ body { flex: none; } .hide { - display: none; + display: none !important; } .loader { font-size: 24px; @@ -95,6 +98,10 @@ button.action { color: white; font-weight: 800; } +img.icon { + width: var(--iconSize); + height: var(--iconSize); +} .float-right { float: right; } @@ -114,6 +121,9 @@ button.action { border-radius: 12px; color: #444; } +.flex { + display: flex; +} /* Icon */ @@ -458,3 +468,32 @@ input[type="text"].cw { font-size: var(--fsReduced); } +/* Profile */ + +.pfp.jumbo { + width: 128px; + height: 128px; +} +[role="profile-info"] { + padding: 15px; + padding-top: 0; +} +.profile-tools { + flex: 1; + text-align: right; +} +.profile-tools > button { + vertical-align: middle; +} +.profile-tools > button.icon { + margin-right: 20px; +} +p[role="profile-desc"] { + margin-bottom: 0; +} +label[role="profile-nip5"] { + margin-top: 15px; + font-weight: 800; + display: block; +} + diff --git a/web/icon/message-user.svg b/web/icon/message-user.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M448 64H64C28.65 64 0 92.65 0 128v256c0 35.35 28.65 64 64 64h384c35.35 0 64-28.65 64-64V128C512 92.65 483.3 64 448 64zM64 112h384c8.822 0 16 7.178 16 16v22.16l-166.8 138.1c-23.19 19.28-59.34 19.27-82.47 .0156L48 150.2V128C48 119.2 55.18 112 64 112zM448 400H64c-8.822 0-16-7.178-16-16V212.7l136.1 113.4C204.3 342.8 229.8 352 256 352s51.75-9.188 71.97-25.98L464 212.7V384C464 392.8 456.8 400 448 400z"/></svg>+ \ No newline at end of file diff --git a/web/icon/pubkey.svg b/web/icon/pubkey.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M220.6 261.4L121.9 360L168.1 407C178.3 416.4 178.3 431.6 168.1 440.1C159.6 450.3 144.4 450.3 135 440.1L88 393.9L57.94 424L104.1 471C114.3 480.4 114.3 495.6 104.1 504.1C95.6 514.3 80.4 514.3 71.03 504.1L7.029 440.1C-2.343 431.6-2.343 416.4 7.029 407L186.6 227.4C169.9 203.9 160 175.1 160 144C160 64.47 224.5 0 304 0C383.5 0 448 64.47 448 144C448 223.5 383.5 288 304 288C272.9 288 244.1 278.1 220.6 261.4zM304 240C357 240 400 197 400 144C400 90.98 357 48 304 48C250.1 48 208 90.98 208 144C208 197 250.1 240 304 240z"/></svg>+ \ No newline at end of file diff --git a/web/index.html b/web/index.html @@ -7,6 +7,7 @@ <link rel="stylesheet" href="css/styles.css?v=120"> <link rel="stylesheet" href="css/responsive.css?v=10"> <link rel="stylesheet" href="css/fontawesome.css?v=2"> + <script defer src="js/util.js?v=1"></script> <script defer src="js/ui/util.js?v=6"></script> <script defer src="js/ui/render.js?v=13"></script> <script defer src="js/noble-secp256k1.js?v=1"></script> @@ -22,6 +23,7 @@ damus_web_init() }); </script> + <nav id="gnav" class=""> <button class="icon" role="open-gnav" title="Open Menu" onclick="toggle_gnav(this)"> <img class="icon svg invert" src="icon/logo.svg"/> @@ -40,6 +42,7 @@ <img class="icon svg invert" src="icon/sign-out.svg"/> </button> </nav> + <div id="container"> <div class="flex-fill vertical-hide"></div> <div id="nav" class="flex-noshrink vertical-hide"> @@ -96,9 +99,42 @@ </header> <div class="events"></div> </div> + <div id="thread-view" class="hide"> + <header> + <label>Thread</label> + </header> + <div class="events"></div> + </div> + <div id="profile-view" class="hide"> + <header> + <label role="profile-name">Profile</label> + </header> + <div role="profile-info" class="bottom-border"> + <div class="flex"> + <img role="profile-image" class="pfp jumbo" src="" /> + <div class="profile-tools"> + <!-- + <button class="icon" title="Message User" role="message-user"> + <img class="icon" src="icon/message-user.svg"/></button> + --> + <button class="icon" role="copy-pk" + data-pk="" onclick="click_copy_pk(this)" title="Copy Public Key"> + <img class="icon" src="icon/pubkey.svg"/></button> + <button class="action" role="follow-user" data-pk="" + onclick="click_toggle_follow_user(this)">Follow</button> + </div> + </div> + <div> + <label role="profile-nip5"></label> + <p role="profile-desc"></p> + </div> + </div> + <div class="events"></div> + </div> </div> <div class="flex-fill vertical-hide"></div> </div> + <div class="modal closed" id="reply-modal"> <div id="reply-modal-content" class="modal-content"> <header> diff --git a/web/js/damus.js b/web/js/damus.js @@ -66,6 +66,7 @@ function init_home_model() { seen: new Set(), }, notifications: init_timeline('notifications'), + profile: init_timeline('profile'), }, pow: 0, // pow difficulty target deleted: {}, @@ -106,15 +107,6 @@ function update_title(model) { update_notification_markers(has_notes) } -// update_notification_markers will find all markers and hide or show them -// based on the passed in state of 'active'. -function update_notification_markers(active) { - let els = document.querySelectorAll(".new-notifications") - for (const el of els) { - el.classList.toggle("hide", !active) - } -} - function notice_chatroom(state, id) { if (!state.chatrooms[id]) diff --git a/web/js/ui/render.js b/web/js/ui/render.js @@ -131,12 +131,11 @@ function render_boosted_by(ev, opts) { if (!b) { return "" } - // TODO encapsulate username as link/button! return ` - <div class="boosted-by">Shared by - <span class="username" data-pubkey="${b.pubkey}">${render_name_plain(b.pubkey, b.profile)}</span> - </div> - ` + <div class="boosted-by"> + Shared by ${render_name(b.pubkey, b.profile)} + </div> + ` } function render_deleted_comment_body(ev, deleted) { @@ -177,7 +176,7 @@ function render_event(damus, view, ev, opts={}) { let name = "" if (!deleted) { - name = render_name_plain(ev.pubkey, profile) + name = render_name_plain(profile) } const has_top_line = replied_events !== "" @@ -192,9 +191,7 @@ function render_event(damus, view, ev, opts={}) { </div> <div class="event-content"> <div class="info"> - <span class="username" data-pubkey="${ev.pubkey}" data-name="${name}"> - ${name} - </span> + ${render_name(ev.pubkey, profile)} <span class="timestamp">${delta}</span> </div> <div class="comment"> @@ -275,8 +272,10 @@ function render_reactions(model, ev) { // Utility Methods -function render_name_plain(pk, profile=DEFAULT_PROFILE) -{ +/* render_name_plain takes in a profile and tries it's best to return a string + * that is best suited for the profile. + */ +function render_name_plain(profile=DEFAULT_PROFILE) { if (profile.sanitized_name) return profile.sanitized_name @@ -299,11 +298,15 @@ function render_username(pk, profile) } function render_mentioned_name(pk, profile) { - return `<span class="username">@${render_username(pk, profile)}</span>` + return render_name(pk, profile, "@"); + //return `<span class="username">@${render_username(pk, profile)}</span>` } -function render_name(pk, profile) { - return `<div class="username">${render_name_plain(pk, profile)}</div>` +function render_name(pk, profile, prefix="") { + return ` + <span class="username clickable" onclick="show_profile('${pk}')" + data-pk="${pk}">${prefix}${render_name_plain(profile)} + </span>` } function render_deleted_name() { @@ -311,8 +314,10 @@ function render_deleted_name() { } 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)}">` + const name = render_name_plain(profile) + return `<img class="pfp" title="${name}" + onerror="this.onerror=null;this.src='${robohash(pk)}';" + src="${get_picture(pk, profile)}">` } function render_deleted_pfp() { diff --git a/web/js/ui/util.js b/web/js/ui/util.js @@ -41,3 +41,67 @@ function init_message_textareas() { post_input_changed(el); } } + +// update_notification_markers will find all markers and hide or show them +// based on the passed in state of 'active'. +function update_notification_markers(active) { + let els = document.querySelectorAll(".new-notifications") + for (const el of els) { + el.classList.toggle("hide", !active) + } +} + +/* show_profile updates the current view to the profile display and updates the + * information to the relevant profile based on the public key passed. + * TODO handle async waiting for relay not yet connected + */ +function show_profile(pk) { + switch_view("profile"); + const profile = DAMUS.profiles[pk]; + const el = find_node("#profile-view"); + // TODO show loading indicator then render + + find_node("[role='profile-image']", el).src = get_picture(pk, profile); + find_nodes("[role='profile-name']", el).forEach(el => { + el.innerText = render_name_plain(profile); + }); + + const el_nip5 = find_node("[role='profile-nip5']", el) + el_nip5.innerText = profile.nip05; + el_nip5.classList.toggle("hide", !profile.nip05); + + const el_desc = find_node("[role='profile-desc']", el) + el_desc.innerHTML = newlines_to_br(profile.about); + el_desc.classList.toggle("hide", !profile.about); + + find_node("button[role='copy-pk']", el).dataset.pk = pk; + + const btn_follow = find_node("button[role='follow-user']", el) + btn_follow.dataset.pk = pk; + // TODO check follow status + btn_follow.innerText = 1 == 1 ? "Follow" : "Unfollow"; + btn_follow.classList.toggle("hide", pk == DAMUS.pubkey); +} + +/* newlines_to_br takes a string and converts all newlines to HTML 'br' tags. + */ +function newlines_to_br(str="") { + return str.split("\n").reduce((acc, part, index) => { + return acc + part + "<br/>"; + }, ""); +} + +/* click_copy_pk writes the element's dataset.pk field to the users OS' + * clipboard. No we don't use fallback APIs, use a recent browser. + */ +function click_copy_pk(el) { + // TODO show toast + navigator.clipboard.writeText(el.dataset.pk); +} + +/* click_follow_user sends the event to the relay to subscribe the active user + * to the target public key of the element's dataset.pk field. + */ +function click_toggle_follow_user(el) { + alert("sorry not implemented"); +} diff --git a/web/js/util.js b/web/js/util.js @@ -0,0 +1,15 @@ +/* find_node is a short name for document.querySelector, it also takes in a + * parent element to search on. + */ +function find_node(selector, parentEl) { + const el = parentEl ? parentEl : document; + return el.querySelector(selector) +} + +/* find_nodes is a short name for document.querySelectorAll, it also takes in a + * parent element to search on. + */ +function find_nodes(selector, parentEl) { + const el = parentEl ? parentEl : document; + return el.querySelectorAll(selector) +}