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:
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");
+}