commit dfdfcf58ca6acb86a795130d7e79be4ea0a5a93a
parent 572122b581e133007cda747426ec12d64230e10f
Author: Thomas Mathews <thomas.c.mathews@gmail.com>
Date: Fri, 11 Nov 2022 12:23:03 -0800
Updated styling to look like twitter.
Diffstat:
M | web/damus.css | | | 324 | +++---------------------------------------------------------------------------- |
M | web/damus.js | | | 200 | +++++++++++++++++++++++++++++++++++++++++++++++++------------------------------ |
M | web/index.html | | | 104 | ++++++++++++++++++++++++++++++++++++++++++++++++------------------------------- |
A | web/styles.css | | | 301 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 500 insertions(+), 429 deletions(-)
diff --git a/web/damus.css b/web/damus.css
@@ -1,29 +1,8 @@
-.header {
- display: flex;
- margin: 30px 0 30px 0;
- flex-direction: column;
- align-items: center;
-}
-
-.logo {
- margin-bottom: 0;
- letter-spacing: -0.05em;
-}
-
-
-.logo img {
- padding-right: 18px;
- width: 60px;
-}
-
-#content-warning-input {
- width: 100%;
-}
-
-#content-warning-input-container {
- width: 100%;
- margin-bottom: 10px;
-}
+/*
+ * This code is not maintained it needs to be cleaned up and deleted. The code
+ * resides here as a working base to migrate to styles.css. Original author is
+ * JB55.
+ */
.deleted-comment {
border: 2px dashed white;
@@ -31,313 +10,32 @@
padding: 10px;
}
-.clickable {
- cursor: pointer;
-}
-
.inline-img {
width: 100%;
border-radius: 8px;
}
-.boost {
-}
-
-.boost-text {
- padding: 0 10px 10px 10px;
- color: #b6ffa8;
- margin: auto;
- font-size: 0.8em;
-}
-
-.chatroom-name {
- color: orange;
- font-weight: bold;
-}
-
+/*
.line-top {
- width: 2px;
+ width: 3px;
height: 5px;
- background-color: #eac3ff;
+ background-color: var(--clrBorder);
margin-left: auto;
margin-right: auto;
-}
+}*/
.line-bot {
- width: 2px;
+ width: 3px;
height: 100%;
margin-top: -7px;
- background-color: #eac3ff;
+ background-color: var(--clrBorder);
margin-left: auto;
margin-right: auto;
}
-body {
- min-height: 100vh;
- font-family: system-ui, sans;
-}
-
-#reply-top {
- display: flex;
- align-items: center;
-}
-
-#reply-modal {
- position: fixed; /* Stay in place */
- z-index: 1; /* Sit on top */
- left: 0;
- top: 0;
- width: 100%; /* Full width */
- height: 100%; /* Full height */
- background: rgba(0,0,0,0.4);
-}
-
-summary {
- cursor: pointer;
-}
-
-details {
- background-color: rgba(255,255,255,0.2);
- padding: 10px;
- margin-right: 30px;
- border-radius: 20px;
-}
-
-input {
- background-color: rgba(255,255,255,0.2);
- border: 0;
- height: 30px;
- color: white;
- border-radius: 5px;
-}
-
-textarea {
- background-color: rgba(255,255,255,0.2);
- border: 0;
- width: 100%;
- height: 40px;
- color: white;
- border-radius: 5px;
-}
-
-#newpost {
- margin: 0 auto 20px auto;
- width: 80%
-}
-
-::placeholder {
- color: rgba(255,255,255,0.7);
-}
-
-.post-group {
- display: flex;
-}
-
-#post-button {
- width: 50px;
-}
-
-.post-group > textarea {
- margin-right: 10px;
-}
-
-.small-txt {
- font-size: 0.6em;
- color: rgba(255,255,255,0.8);
-}
-
-.reaction-group {
- display: inline-flex;
- background-color: rgba(255,255,255,0.15);
- padding: 4px;
- border-radius: 5px;
-}
-
-.reaction-group img {
- margin-left: -8px;
-}
-
-.reaction-emoji {
- margin-right: 14px;
-}
-
-button {
- border: 0;
- padding: 5px;
- color: white;
- background-color: #9202e2;
- border-radius: 5px;
-}
-
-.close {
- margin: 0 8px 0 8px;
- font-size: 1.3em;
- font-weight: bold;
- text-decoration: none;
- color: white;
-}
-
-.small-text {
- font-size: 12px;
-}
-
-#reply-modal-content {
- /*display: none; */
- padding: 10px;
- margin: 10% auto;
- width: 60%;
- overflow: auto; /* Enable scroll if needed */
- background: linear-gradient(0deg, #A74BDB 0%, #C45BFE 100%);
- border-radius: 15px;
-}
-
-html {
- line-height: 1.5;
- font-size: 20px;
- font-family: "Georgia", sans-serif;
-
- color: white;
- background: linear-gradient(45deg, rgba(28,85,255,1) 0%, rgba(127,53,171,1) 59%, #C45BFE 100%);
-}
-.container {
- margin: 0 auto;
- max-width: 48em;
- hyphens: auto;
- word-wrap: break-word;
- text-rendering: optimizeLegibility;
- font-kerning: normal;
-}
-@media (max-width: 600px) {
- .container {
- font-size: 0.9em;
- }
-}
-@media print {
- .container {
- background-color: transparent;
- color: black;
- font-size: 12pt;
- }
- p, h2, h3 {
- orphans: 3;
- widows: 3;
- }
- h2, h3, h4 {
- page-break-after: avoid;
- }
-}
-
-.pfp {
- border-radius: 50%;
-}
-
-.pfp-small {
- width: 28px;
- height: 28px;
-}
-
-.pfp-normal {
- margin: 0 15px 0 15px;
- width: 60px;
- height: 60px;
- font-size: 2.4em;
-}
-
-
-.thread-collapsed {
- margin: 0 auto 5px auto;
- width: 56.3%;
-}
-
-.comment {
- display: flex;
- font-family: system-ui, sans;
- margin-bottom: 20px;
- flex-wrap: wrap;
- /*align-items: center;*/
-}
-
-.comment .comment-body {
- background-color: rgba(255.0,255.0,255.0,0.1);
- padding: 10px;
- border-radius: 8px;
- margin: 0;
- width: 55%;
-}
-
-.comment-body p {
- margin: 0 0 10px 0;
-}
-
-.comment .info {
- text-align: right;
- width: 18%;
- line-height: 0.8em;
-}
-
.quote {
- border-left: 2px solid white;
margin-left: 10px;
padding: 10px;
- background-color: rgba(255.0,255.0,255.0,0.1);
display: block;
}
-.comment .timestamp {
- font-size: 11px;
- color: rgba(255.0,255.0,255.0,0.7);
-}
-
-.invisible {
- visibility: hidden;
-}
-
-@media (max-width: 800px){
- /* Reverse the order of elements in the user comments,
- so that the avatar and info appear after the text. */
-
- .pfp {
- margin: 0 0 0 0;
- }
-
- .pfpbox {
- order: 1;
- margin-right: 10px;
- }
-
- .comment .info {
- order: 2;
- text-align: left;
- width: 70%;
- }
-
- .comment .comment-body {
- width: 100%;
- order: 3;
- margin: 5px 0 0 0;
- }
-
- .line-top {
- height: 20px;
- }
-
- .line-bottom {
- /*display: none;*/
- }
-
- .comment {
- margin: 0 0 0 0;
- align-items: center;
- border-radius: 8px;
- /*background-color: rgba(255.0,255.0,255.0,0.1);*/
- }
-
- #newpost {
- width: 100%;
- }
-
- .comment p {
- order: 3;
- width: 100%;
- }
-}
diff --git a/web/damus.js b/web/damus.js
@@ -8,21 +8,18 @@ function uuidv4() {
}
function insert_event_sorted(evs, new_ev) {
- for (let i = 0; i < evs.length; i++) {
- const ev = evs[i]
-
- if (new_ev.id === ev.id) {
- return false
- }
-
- if (new_ev.created_at > ev.created_at) {
- evs.splice(i, 0, new_ev)
- return true
- }
+ for (let i = 0; i < evs.length; i++) {
+ const ev = evs[i]
+ if (new_ev.id === ev.id) {
+ return false
}
-
- evs.push(new_ev)
- return true
+ if (new_ev.created_at > ev.created_at) {
+ evs.splice(i, 0, new_ev)
+ return true
+ }
+ }
+ evs.push(new_ev)
+ return true
}
function init_contacts() {
@@ -260,8 +257,6 @@ function process_event(model, ev)
process_json_content(ev)
else if (ev.kind === 5)
process_deletion_event(model, ev)
- else if (ev.kind === 0)
- process_profile_event(model, ev)
const last_notified = get_local_state('last_notified_date')
if (notified && (last_notified == null || ((ev.created_at*1000) > last_notified))) {
@@ -294,10 +289,6 @@ function should_add_to_home(ev)
let rerender_home_timer
function handle_home_event(ids, model, relay, sub_id, ev) {
- // ignore duplicates
- if (model.all_events[ev.id])
- return
-
model.all_events[ev.id] = ev
process_event(model, ev)
@@ -320,23 +311,22 @@ function handle_home_event(ids, model, relay, sub_id, ev) {
model.done_init = true
model.pool.unsubscribe(ids.account, [relay])
break
+ case 0:
+ handle_profile_event(model, ev)
+ break
}
case ids.profiles:
- break
+ try {
+ model.profile_events[ev.pubkey] = ev
+ model.profiles[ev.pubkey] = JSON.parse(ev.content)
+ } catch {
+ console.log("failed to parse", ev.content)
+ }
}
}
-function process_profile_event(model, ev) {
- const prev_ev = model.profile_events[ev.pubkey]
- if (prev_ev && prev_ev.created_at > ev.created_at)
- return
-
- model.profile_events[ev.pubkey] = ev
- try {
- model.profiles[ev.pubkey] = JSON.parse(ev.content)
- } catch(e) {
- log_debug("failed to parse profile contents", ev)
- }
+function handle_profile_event(model, ev) {
+ console.log("PROFILE", ev)
}
function send_initial_filters(account_id, pubkey, relay) {
@@ -553,10 +543,18 @@ function setup_home_event_handlers(events_el)
function redraw_home_view(model) {
model.view_el.innerHTML = render_home_view(model)
model.events_el = document.querySelector("#events")
- if (model.events.length > 0)
+ if (model.events.length > 0) {
redraw_events(model)
- else
- model.events_el.innerText = "Loading..."
+ } else {
+ model.events_el.innerHTML= `
+ <div class="loading-events">
+ <span class="loader" title="Loading...">
+ <i class="fa-solid fa-fw fa-spin fa-hurricane"
+ style="--fa-animation-duration: 0.5s;"></i>
+ </span>
+ </div>
+ `
+ }
}
async function send_post() {
@@ -567,7 +565,7 @@ async function send_post() {
const content = input_el.value
const created_at = Math.floor(new Date().getTime() / 1000)
const kind = 1
- const tags = cw? [["content-warning", cw]] : []
+ const tags = cw ? [["content-warning", cw]] : []
const pubkey = await get_pubkey()
const {pool} = DSTATE
@@ -600,17 +598,19 @@ async function sign_event(ev) {
function render_home_view(model) {
return `
<div id="newpost">
- <div id="content-warning-input-container">
- <input id="content-warning-input" type="text" placeholder="Content Warning (nsfw, politics, etc)"></input>
+ <div><!-- empty to accomodate profile pic --></div>
+ <div>
+ <textarea placeholder="What's up?" class="post-input" id="post-input"></textarea>
+ <div class="post-tools">
+ <input id="content-warning-input" class="cw hide" type="text" placeholder="Reason"/>
+ <button title="Mark this message as sensitive." onclick="toggle_cw(this)" class="cw icon">
+ <i class="fa-solid fa-triangle-exclamation"></i>
+ </button>
+ <button onclick="send_post(this)" class="action" id="post-button">Send</button>
+ </div>
</div>
-
- <div class="post-group">
- <textarea placeholder="What's on your mind?" id="post-input"></textarea>
- <button onclick="send_post(this)" id="post-button">Post</button>
- </div>
- </div>
- <div id="events">
</div>
+ <div id="events"></div>
`
}
@@ -690,7 +690,11 @@ function render_thread_collapsed(model, reply_ev, opts)
{
if (opts.is_composing)
return ""
- return `<div onclick="expand_thread('${reply_ev.id}')" class="thread-collapsed clickable">...</div>`
+ return `<div onclick="expand_thread('${reply_ev.id}')" class="thread-collapsed">
+ <div class="thread-summary">
+ More messages in thread available. Click to expand.
+ </div>
+ </div>`
}
function* yield_etags(tags)
@@ -735,7 +739,7 @@ function render_replying_to_chat(model, ev) {
const names = pks.map(pk => render_mentioned_name(pk, model.profiles[pk])).join(", ")
const to_users = pks.length === 0 ? "" : ` to ${names}`
- return `<div class="replying-to small-txt">replying${to_users} in <span class="chatroom-name">${roomname}</span></div>`
+ return `<div class="replying-to">replying${to_users} in <span class="chatroom-name">${roomname}</span></div>`
}
function render_replying_to(model, ev) {
@@ -795,14 +799,19 @@ function render_boost(model, ev, opts) {
if (!ev.json_content)
return render_unknown_event(ev)
- const profile = model.profiles[ev.pubkey]
+ //const profile = model.profiles[ev.pubkey]
opts.is_boost_event = true
- 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>
- `
+ opts.boosted = {
+ pubkey: ev.pubkey,
+ 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 shouldnt_render_event(model, ev, opts) {
@@ -818,13 +827,14 @@ function render_deleted_pfp() {
}
function render_comment_body(model, ev, opts) {
- const bar = !can_reply(ev) || opts.nobar? "" : render_action_bar(ev)
+ const canDelete = !(model.pubkey !== ev.pubkey)
+ const bar = !can_reply(ev) || opts.nobar? "" : render_action_bar(ev, canDelete)
const show_media = !opts.is_composing
return `
<div>
${render_replying_to(model, ev)}
- ${render_delete_post(model, ev)}
+ ${render_boosted_by(model, ev, opts)}
</div>
<p>
${format_content(ev, show_media)}
@@ -834,6 +844,19 @@ function render_comment_body(model, ev, opts) {
`
}
+function render_boosted_by(model, ev, opts) {
+ const b = opts.boosted
+ 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>
+ `
+}
+
function render_deleted_comment_body(ev, deleted) {
if (deleted.content) {
const show_media = false
@@ -844,8 +867,6 @@ function render_deleted_comment_body(ev, deleted) {
</div>
`
}
-
-
return `<div class="deleted-comment">This comment was deleted</div>`
}
@@ -868,20 +889,29 @@ function render_event(model, ev, opts={}) {
const replied_events = render_replied_events(model, ev, opts)
+ let name = "???"
+ if (!deleted) {
+ name = render_name_plain(ev.pubkey, profile)
+ }
+
return `
${replied_events}
- <div id="ev${ev.id}" class="comment">
- <div class="info">
- ${deleted ? render_deleted_name() : render_name(ev.pubkey, profile)}
- <span class="timestamp">${delta}</span>
- </div>
- <div class="pfpbox">
+ <div id="ev${ev.id}" class="event">
+ <div class="userpic">
${render_reply_line_top(replied_events === "")}
${deleted ? render_deleted_pfp() : render_pfp(ev.pubkey, profile)}
${reply_line_bot}
- </div>
- <div class="comment-body">
- ${deleted ? render_deleted_comment_body(ev, deleted) : render_comment_body(model, ev, opts)}
+ </div>
+ <div class="event-content">
+ <div class="info">
+ <span class="username" data-pubkey="${ev.pubkey}" data-name="${name}">
+ ${name}
+ </span>
+ <span class="timestamp">${delta}</span>
+ </div>
+ <div class="comment">
+ ${deleted ? render_deleted_comment_body(ev, deleted) : render_comment_body(model, ev, opts)}
+ </div>
</div>
</div>
`
@@ -889,7 +919,7 @@ function render_event(model, ev, opts={}) {
function render_pfp(pk, profile, size="normal") {
const name = render_name_plain(pk, profile)
- return `<img title="${name}" class="pfp pfp-${size}" onerror="this.onerror=null;this.src='${robohash(pk)}';" src="${get_picture(pk, profile)}">`
+ return `<img title="${name}" onerror="this.onerror=null;this.src='${robohash(pk)}';" src="${get_picture(pk, profile)}">`
}
const REACTION_REGEX = /^[#*0-9]\uFE0F?\u20E3|[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23ED-\u23EF\u23F1\u23F2\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB\u25FC\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692\u2694-\u2697\u2699\u269B\u269C\u26A0\u26A7\u26AA\u26B0\u26B1\u26BD\u26BE\u26C4\u26C8\u26CF\u26D1\u26D3\u26E9\u26F0-\u26F5\u26F7\u26F8\u26FA\u2702\u2708\u2709\u270F\u2712\u2714\u2716\u271D\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u27A1\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B55\u3030\u303D\u3297\u3299]\uFE0F?|[\u261D\u270C\u270D](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\u270A\u270B](?:\uD83C[\uDFFB-\uDFFF])?|[\u23E9-\u23EC\u23F0\u23F3\u25FD\u2693\u26A1\u26AB\u26C5\u26CE\u26D4\u26EA\u26FD\u2705\u2728\u274C\u274E\u2753-\u2755\u2795-\u2797\u27B0\u27BF\u2B50]|\u26F9(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\u2764\uFE0F?(?:\u200D(?:\uD83D\uDD25|\uD83E\uDE79))?|\uD83C(?:[\uDC04\uDD70\uDD71\uDD7E\uDD7F\uDE02\uDE37\uDF21\uDF24-\uDF2C\uDF36\uDF7D\uDF96\uDF97\uDF99-\uDF9B\uDF9E\uDF9F\uDFCD\uDFCE\uDFD4-\uDFDF\uDFF5\uDFF7]\uFE0F?|[\uDF85\uDFC2\uDFC7](?:\uD83C[\uDFFB-\uDFFF])?|[\uDFC3\uDFC4\uDFCA](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDFCB\uDFCC](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDCCF\uDD8E\uDD91-\uDD9A\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF84\uDF86-\uDF93\uDFA0-\uDFC1\uDFC5\uDFC6\uDFC8\uDFC9\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF8-\uDFFF]|\uDDE6\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF]|\uDDE7\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF]|\uDDE8\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF]|\uDDE9\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF]|\uDDEA\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA]|\uDDEB\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7]|\uDDEC\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE]|\uDDED\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA]|\uDDEE\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9]|\uDDEF\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5]|\uDDF0\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF]|\uDDF1\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE]|\uDDF2\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF]|\uDDF3\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF]|\uDDF4\uD83C\uDDF2|\uDDF5\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE]|\uDDF6\uD83C\uDDE6|\uDDF7\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC]|\uDDF8\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF]|\uDDF9\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF]|\uDDFA\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF]|\uDDFB\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA]|\uDDFC\uD83C[\uDDEB\uDDF8]|\uDDFD\uD83C\uDDF0|\uDDFE\uD83C[\uDDEA\uDDF9]|\uDDFF\uD83C[\uDDE6\uDDF2\uDDFC]|\uDFF3\uFE0F?(?:\u200D(?:\u26A7\uFE0F?|\uD83C\uDF08))?|\uDFF4(?:\u200D\u2620\uFE0F?|\uDB40\uDC67\uDB40\uDC62\uDB40(?:\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDC73\uDB40\uDC63\uDB40\uDC74|\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F)?)|\uD83D(?:[\uDC08\uDC26](?:\u200D\u2B1B)?|[\uDC3F\uDCFD\uDD49\uDD4A\uDD6F\uDD70\uDD73\uDD76-\uDD79\uDD87\uDD8A-\uDD8D\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA\uDECB\uDECD-\uDECF\uDEE0-\uDEE5\uDEE9\uDEF0\uDEF3]\uFE0F?|[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDC8F\uDC91\uDCAA\uDD7A\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC](?:\uD83C[\uDFFB-\uDFFF])?|[\uDC6E\uDC70\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD74\uDD90](?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?|[\uDC00-\uDC07\uDC09-\uDC14\uDC16-\uDC25\uDC27-\uDC3A\uDC3C-\uDC3E\uDC40\uDC44\uDC45\uDC51-\uDC65\uDC6A\uDC79-\uDC7B\uDC7D-\uDC80\uDC84\uDC88-\uDC8E\uDC90\uDC92-\uDCA9\uDCAB-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDDA4\uDDFB-\uDE2D\uDE2F-\uDE34\uDE37-\uDE44\uDE48-\uDE4A\uDE80-\uDEA2\uDEA4-\uDEB3\uDEB7-\uDEBF\uDEC1-\uDEC5\uDED0-\uDED2\uDED5-\uDED7\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uDC15(?:\u200D\uD83E\uDDBA)?|\uDC3B(?:\u200D\u2744\uFE0F?)?|\uDC41\uFE0F?(?:\u200D\uD83D\uDDE8\uFE0F?)?|\uDC68(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?\uDC68\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D\uDC68\uD83C[\uDFFB-\uDFFE])))?))?|\uDC69(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:\uDC8B\u200D\uD83D)?[\uDC68\uDC69]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D(?:[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?|\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFC-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFD-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFD\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D\uD83D(?:[\uDC68\uDC69]|\uDC8B\u200D\uD83D[\uDC68\uDC69])\uD83C[\uDFFB-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83D[\uDC68\uDC69]\uD83C[\uDFFB-\uDFFE])))?))?|\uDC6F(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDD75(?:\uFE0F|\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|\uDE2E(?:\u200D\uD83D\uDCA8)?|\uDE35(?:\u200D\uD83D\uDCAB)?|\uDE36(?:\u200D\uD83C\uDF2B\uFE0F?)?)|\uD83E(?:[\uDD0C\uDD0F\uDD18-\uDD1F\uDD30-\uDD34\uDD36\uDD77\uDDB5\uDDB6\uDDBB\uDDD2\uDDD3\uDDD5\uDEC3-\uDEC5\uDEF0\uDEF2-\uDEF8](?:\uD83C[\uDFFB-\uDFFF])?|[\uDD26\uDD35\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD4\uDDD6-\uDDDD](?:\uD83C[\uDFFB-\uDFFF])?(?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDDDE\uDDDF](?:\u200D[\u2640\u2642]\uFE0F?)?|[\uDD0D\uDD0E\uDD10-\uDD17\uDD20-\uDD25\uDD27-\uDD2F\uDD3A\uDD3F-\uDD45\uDD47-\uDD76\uDD78-\uDDB4\uDDB7\uDDBA\uDDBC-\uDDCC\uDDD0\uDDE0-\uDDFF\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC2\uDECE-\uDEDB\uDEE0-\uDEE8]|\uDD3C(?:\u200D[\u2640\u2642]\uFE0F?|\uD83C[\uDFFB-\uDFFF])?|\uDDD1(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1))|\uD83C(?:\uDFFB(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFC-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFC(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFD-\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFD(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFE(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFD\uDFFF]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?|\uDFFF(?:\u200D(?:[\u2695\u2696\u2708]\uFE0F?|\u2764\uFE0F?\u200D(?:\uD83D\uDC8B\u200D)?\uD83E\uDDD1\uD83C[\uDFFB-\uDFFE]|\uD83C[\uDF3E\uDF73\uDF7C\uDF84\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E(?:[\uDDAF-\uDDB3\uDDBC\uDDBD]|\uDD1D\u200D\uD83E\uDDD1\uD83C[\uDFFB-\uDFFF])))?))?|\uDEF1(?:\uD83C(?:\uDFFB(?:\u200D\uD83E\uDEF2\uD83C[\uDFFC-\uDFFF])?|\uDFFC(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFD-\uDFFF])?|\uDFFD(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])?|\uDFFE(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFD\uDFFF])?|\uDFFF(?:\u200D\uD83E\uDEF2\uD83C[\uDFFB-\uDFFE])?))?)$/
@@ -990,7 +1020,7 @@ function render_reactions(model, ev) {
function close_reply() {
const modal = document.querySelector("#reply-modal")
- modal.style.display = "none";
+ modal.classList.add("closed");
}
function gather_reply_tags(pubkey, from) {
@@ -1179,7 +1209,7 @@ async function sign_id(privkey, id)
function reply_to(evid) {
const modal = document.querySelector("#reply-modal")
- const replying = modal.style.display === "none";
+ modal.classList.remove("closed")
const replying_to = modal.querySelector("#replying-to")
replying_to.dataset.evid = evid
@@ -1189,10 +1219,18 @@ function reply_to(evid) {
modal.style.display = replying? "block" : "none";
}
-function render_action_bar(ev) {
+function render_action_bar(ev, canDelete=false) {
+ let deleteHTML = ""
+ if (canDelete) {
+ deleteHTML = `<button class="icon" title="Delete" onclick="like('${ev.id}')"><i class="fa fa-fw fa-trash"></i></a>`
+ }
return `
<div class="action-bar">
- <a href="javascript:reply_to('${ev.id}')">reply</a>
+ <button class="icon" title="Reply" onclick="reply_to('${ev.id}')"><i class="fa fa-fw fa-comment"></i></a>
+ <button class="icon react heart" title="Like" onclick=""><i class="fa fa-fw fa-heart"></i></a>
+ <button class="icon" title="Share" onclick=""><i class="fa fa-fw fa-link"></i></a>
+ ${deleteHTML}
+ <button class="icon" title="View raw Nostr event." onclick=""><i class="fa-solid fa-fw fa-code"></i></a>
</div>
`
}
@@ -1207,7 +1245,7 @@ function is_video_url(path) {
return VID_REGEX.test(path)
}
-const URL_REGEX = /(https?:\/\/[^\s]+)[,:)]?(\w|$)/g;
+const URL_REGEX = /(https?:\/\/[^\s\):]+)/g;
function linkify(text, show_media) {
return text.replace(URL_REGEX, function(url) {
const parsed = new URL(url)
@@ -1283,11 +1321,16 @@ function format_content(ev, show_media)
let cw = get_content_warning(ev.tags)
if (cw !== null) {
- cw = cw === ""? "Content Warning" : `Content Warning: ${cw}`
+ let cwHTML = "This content has been marked as sensitive"
+ if (cw === "") {
+ cwHTML += "."
+ } else {
+ cwHTML += ` due to: "<span>${cw}</span>".`
+ }
const open = !!DSTATE.cw_open[ev.id]? "open" : ""
return `
<details class="cw" id="cw_${ev.id}" ${open}>
- <summary>${cw}</summary>
+ <summary>${cwHTML}</summary>
${body}
</details>
`
@@ -1378,3 +1421,10 @@ function time_delta(current, previous) {
return Math.round(elapsed/msPerYear ) + ' years ago';
}
}
+
+function toggle_cw(el) {
+ el.classList.toggle("active");
+ const isOn = el.classList.contains("active");
+ const input = el.parentElement.querySelector("input.cw");
+ input.classList.toggle("hide", !isOn);
+}
diff --git a/web/index.html b/web/index.html
@@ -3,49 +3,71 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
-
<title>Damus</title>
-
- <link rel="stylesheet" href="damus.css?v=21">
+ <link rel="stylesheet" href="styles.css?v=17">
+ <link rel="stylesheet" href="damus.css?v=20">
+ <link rel="stylesheet"
+ href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css">
</head>
<body>
- <section class="header">
- <span class="logo">
- <img src="img/damus-nobg.svg"/>
- </span>
- </section>
- <div id="view" class="container">
- </div>
-
- <div style="display: none" id="reply-modal">
- <div id="reply-modal-content">
- <span id="reply-top">
- <a class="close" href="javascript:close_reply()">✕</a>
- <span class="small-text">
- Replying to...
- </span>
- </span>
-
- <div id="replying-to">
- </div>
-
- <div>
- <textarea id="reply-content"></textarea>
- </div>
-
- <div style="float:right">
- <button onclick="do_send_reply()" id="reply-button">Reply</button>
- </div>
- </div>
- </div>
- <script src="noble-secp256k1.js?v=1"></script>
- <script src="bech32.js?v=1"></script>
- <script src="nostr.js?v=6"></script>
- <script src="damus.js?v=59"></script>
- <script>
- // I have to delay loading to wait for nos2x
- const relay = setTimeout(damus_web_init, 100)
- </script>
+ <div id="container">
+ <div class="flex-fill"></div>
+ <div id="nav">
+ <div id="app-icon-logo">
+ <i class="fa-regular fa-fw fa-hand-peace"></i>
+ </div>
+ <div>
+ <button class="nav icon">
+ <i class="fa fa-fw fa-home"></i><span class="hide">Home</span>
+ </button></div>
+ <div>
+ <button class="nav icon">
+ <i class="fa fa-fw fa-user"></i><span class="hide">Profile</span>
+ </button></div>
+ <div>
+ <button class="nav icon">
+ <i class="fa fa-fw fa-gear"></i><span class="hide">Settings</span>
+ </button></div>
+ <div>
+ <button class="nav icon">
+ <i class="fa-regular fa-fw fa-circle-question"></i><span class="hide">Help</span>
+ </button></div>
+ </div>
+ <div id="content">
+ <header>
+ <label>Home</label>
+ </header>
+ <div id="view"></div>
+ </div>
+ <div class="flex-fill"></div>
+ </div>
+ <div class="modal closed" id="reply-modal">
+ <div id="reply-modal-content" class="modal-content">
+ <header>
+ <label>Reply To</label>
+ <button class="icon" onclick="close_reply()">
+ <i class="fa fa-xmark"></i>
+ </button>
+ </header>
+ <div id="replying-to"></div>
+ <div>
+ <textarea id="reply-content" class="post-input"
+ placeholder="Write your reply here..."></textarea>
+ <div class="post-tools">
+ <button id="reply-button" class="action" onclick="do_send_reply()">
+ Reply
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <script src="noble-secp256k1.js?v=1"></script>
+ <script src="bech32.js?v=1"></script>
+ <script src="nostr.js?v=6"></script>
+ <script src="damus.js?v=57"></script>
+ <script>
+ // I have to delay loading to wait for nos2x
+ const relay = setTimeout(damus_web_init, 100)
+ </script>
</body>
</html>
-
diff --git a/web/styles.css b/web/styles.css
@@ -0,0 +1,301 @@
+/* Hello welcome to my styles. Here are some notes.
+ *
+ * - I use Noto Sans because it supports most languages.
+ * - I will implement light mode first and then attempt dark.
+ * - All variables should be declared at the top.
+ * - Use as little as possible, write from scratch, and utilize helper
+ * classes.
+ * - No transpilers!
+ */
+
+@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;800&display=swap');
+
+:root {
+ --clrBg: #fff;
+ --clrPanel: #f9f9f9;
+ --clrBorder: #f2f2f2;
+ --clrBtn: #202020;
+ --clrText: #202020;
+ --clrTextLight: #868686;
+ --clrTextLighter: #abb4ca;
+ --clrHeart: #ff5050;
+ --clrWarn: #fbc560;
+
+ --fsSmall: 12px;
+ --fsNormal: 16px;
+ --fsReduced: 14px;
+ --fsEnlarged: 18px;
+
+ --ffDefault: "Noto Sans", sans-serif;
+}
+*:focus-visible {
+ /* Technically this is bad and something else should be done to indicate
+ * that something is in focus.
+ */
+ outline: none;
+}
+body {
+ background: var(--clrBg);
+ color: var(--clrText);
+ font-family: "Noto Sans", sans-serif;
+ font-size: var(--fsNormal);
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+}
+
+/* Utilities */
+.flex-fill {
+ flex: 1;
+}
+.hide {
+ display: none;
+}
+.loader {
+ font-size: 24px;
+}
+button {
+ cursor: pointer;
+}
+button.icon {
+ border: none;
+ background: transparent;
+ padding: 0;
+ margin: 0;
+}
+button.action {
+ border: none;
+ border-radius: 50px;
+ background: #171717;
+ padding: 10px 15px;
+ font-size: 16px;
+ color: white;
+ font-weight: 800;
+}
+.float-right {
+ float: right;
+}
+
+/* Application Framework */
+button.nav {
+ color: var(--clrBtn);
+ font-size: 24px;
+ padding: 15px 25px;
+}
+
+#app-icon-logo {
+ font-size: 28px;
+ text-align: center;
+ padding: 20px;
+}
+#container {
+ display: flex;
+ flex-direction: row;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+}
+#nav {
+ flex-shrink: 1;
+ border-right: 1px solid var(--clrBorder);
+}
+#content {
+ display: flex;
+ flex-direction: column;
+ border-right: 1px solid var(--clrBorder);
+}
+#content header > label {
+ padding: 22px 15px;
+ font-size: 22px;
+ font-weight: 800;
+ display: block;
+}
+#view {
+ width: 750px;
+ overflow-y: scroll;
+ flex: 1;
+}
+
+/* Events & Content */
+#events > .event {
+ border-bottom: solid 1px var(--clrBorder);
+}
+.event {
+ display: flex;
+ flex-direction: row;
+ padding: 15px;
+ transition: background-color 0.2s linear;
+}
+.event:hover {
+ background-color: var(--clrPanel);
+}
+.loading-events {
+ text-align: center;
+ padding: 15px;
+}
+.userpic {
+ flex-shrink: 1;
+}
+.userpic > img {
+ border-radius: 50%;
+ width: 64px;
+ height: 64px;
+ object-fit: fill;
+}
+.event-content {
+ flex: 1;
+ padding-left: 15px;
+}
+.event-content > .info {
+ display: inline-block;
+}
+.username {
+ font-weight: 800;
+ font-size: var(--fsReduced);
+}
+.chatroom-name {
+ font-weight: bold;
+}
+.timestamp, .replying-to, .boosted-by {
+ font-size: var(--fsSmall);
+ color: var(--clrTextLight);
+}
+.comment {
+ word-break: break-word;
+}
+.action-bar {
+}
+.action-bar > button {
+ margin-right: 25px;
+ color: var(--clrTextLighter);
+ font-size: var(--fsNormal);
+}
+.reactions {
+ padding-bottom: 15px;
+}
+.reaction-group img {
+ width: 18px;
+ height: 18px;
+ object-fit: cover;
+ border-radius: 50%;
+ margin-left: -8px;
+ vertical-align: top;
+}
+.reaction-group img:first-of-type {
+ margin-left: 0px;
+}
+.reaction-emoji {
+}
+.action-bar button.icon {
+ transition: color 0.3s linear;
+}
+.action-bar button.icon:hover {
+ color: var(--clrText);
+}
+.action-bar button.heart:hover {
+ color: var(--clrHeart);
+}
+details.cw summary {
+ background: #f2f2f2;
+ padding: 10px;
+ border-radius: 12px;
+ color: #444;
+ cursor: pointer;
+ 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;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0,0,0,0.4);
+ opacity: 1;
+ transition: opacity 0.2s linear;
+}
+.modal.closed {
+ opacity: 0;
+ pointer-events: none;
+}
+.modal-content {
+ padding: 20px;
+ overflow: auto;
+ border-radius: 15px;
+ background: var(--clrPanel);
+ max-width: 700px;
+ margin: 0 auto;
+ margin-top: 35px;
+}
+.modal header {
+ display: flex;
+}
+.modal header label {
+ font-weight: 800;
+ font-size: var(--fsEnlarged);
+ flex: 1;
+}
+.modal header button {
+ font-size: 24px;
+}
+
+/* Post & Reply */
+#newpost {
+ padding: 0px 15px 15px;
+ display: flex;
+ flex-direction: row;
+ border-bottom: solid 1px var(--clrBorder);
+}
+#newpost > :first-child {
+ width: 64px;
+}
+#newpost > :last-child {
+ padding-left: 15px;
+ flex: 1;
+}
+textarea.post-input {
+ display: block;
+ width: 100%;
+ border: none;
+ background: transparent;
+ color: var(--clrText);
+ font-size: var(--fsEnlarged);
+ font-family: var(--ffDefault);
+ margin: 10px 0;
+ padding: 0;
+ box-sizing: border-box;
+ resize: vertical;
+}
+
+.post-tools {
+ text-align: right;
+}
+.post-tools > button.icon {
+ margin-right: 10px;
+ font-size: var(--fsEnlarged);
+ color: var(--clrTextLight);
+}
+.post-tools > button.icon.cw.active {
+ color: var(--clrWarn);
+}
+input[type="text"].cw {
+ border: none;
+ border-bottom: solid 2px var(--clrWarn);
+ font-size: var(--fsReduced);
+}
+