Merge remote-tracking branch 'thomas/main'

web: removed non-working event buttons
Deleted js/index.js
Removed .DS_Store
web: Refactor UI code
web: retab & avoid race condition on main js entry
web: reconstructed project
This commit is contained in:
William Casarin 2022-11-12 11:13:03 -08:00
commit c3b4ac0d5b
23 changed files with 471 additions and 528 deletions

BIN
.DS_Store vendored

Binary file not shown.

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ TODO.bak
*.mp4 *.mp4
channels/index.html channels/index.html
node_modules node_modules
.DS_Store

View file

@ -11,8 +11,4 @@ emojiregex: fake
dist: dist:
rsync -avzP ./ charon:/www/damus.io/web/ rsync -avzP ./ charon:/www/damus.io/web/
distv2:
rsync -avzP ./ charon:/www/damus.io/webv2/
.PHONY: fake .PHONY: fake

37
web/README.md Normal file
View file

@ -0,0 +1,37 @@
# Damus PWA
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.
## Contribution Guide
There are rules to contributing to this client. Please ensure you read them
before making changes and supplying patch notes.
1. No transpilers. All source code should work out of the box.
2. Keep source code organised. Refer to the folder structure. If you have a
question, ask it.
3. 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.
4. Spaces, no tabs.
5. Do not include binary files.
6. 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.
7. Do not write code using experimental browser APIs.
8. Do not write animations in JavaScript, CSS only. Keep them short and snappy.
Animations should not be a forefront, but an enjoyable addition.
9. All new & modified code should be properly documented.
10. Source code should be readable in the browser.
TODO Write about code style requirements & add number of spaces.
## Style Guide
TODO Write about the style guide.
## Terminology
* Sign Out - Not log out, logout, log off, etc.
* Sign In - Not Login, Log In, etc.

View file

@ -4,72 +4,78 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Damus</title> <title>Damus</title>
<link rel="stylesheet" href="styles.css?v=110"> <link rel="stylesheet" href="css/styles.css?v=109">
<link rel="stylesheet" href="damus.css?v=211"> <link rel="stylesheet" href="css/damus.css?v=211">
<link rel="stylesheet" href="fontawesome.css?v=2"> <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>
<script defer src="js/noble-secp256k1.js?v=1"></script>
<script defer src="js/bech32.js?v=1"></script>
<script defer src="js/nostr.js?v=6"></script>
<script defer src="js/damus.js?v=65"></script>
</head> </head>
<body> <body>
<div id="container"> <script>
<div class="flex-fill"></div> const relay = 0; // relay is declared for backwards compatibibility.
<div id="nav"> // This is our main entry.
<div id="app-icon-logo"> // https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event
<i class="fa-regular fa-fw fa-hand-peace"></i> addEventListener('DOMContentLoaded', (ev) => {
</div> damus_web_init();
<div> });
<button class="nav icon"> </script>
<i class="fa fa-fw fa-home"></i><span class="hide">Home</span> <div id="container">
</button></div> <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> <div>
<button class="nav icon"> <button class="nav icon">
<i class="fa fa-fw fa-user"></i><span class="hide">Profile</span> <i class="fa fa-fw fa-user"></i><span class="hide">Profile</span>
</button></div> </button></div>
<div> <div>
<button class="nav icon"> <button class="nav icon">
<i class="fa fa-fw fa-gear"></i><span class="hide">Settings</span> <i class="fa fa-fw fa-gear"></i><span class="hide">Settings</span>
</button></div> </button></div>
--> -->
<div> <div>
<button onclick="press_logout()" class="nav icon"> <button onclick="press_logout()" title="Sign Out" class="nav icon">
<i class="fa fa-fw fa-arrow-right-from-bracket"></i><span class="hide">Logout</span> <i class="fa fa-fw fa-arrow-right-from-bracket"></i><span class="hide">Sign Out</span>
</button></div> </button></div>
</div> </div>
<div id="content"> <div id="content">
<header> <header>
<label>Home</label> <label>Home</label>
</header> </header>
<div id="view"></div> <div id="view"></div>
</div> </div>
<div class="flex-fill"></div> <div class="flex-fill"></div>
</div> </div>
<div class="modal closed" id="reply-modal"> <div class="modal closed" id="reply-modal">
<div id="reply-modal-content" class="modal-content"> <div id="reply-modal-content" class="modal-content">
<header> <header>
<label>Reply To</label> <label>Reply To</label>
<button class="icon" onclick="close_reply()"> <button class="icon" onclick="close_reply()">
<i class="fa fa-xmark"></i> <i class="fa fa-xmark"></i>
</button> </button>
</header> </header>
<div id="replying-to"></div> <div id="replying-to"></div>
<div> <div>
<textarea id="reply-content" class="post-input" <textarea id="reply-content" class="post-input"
placeholder="Write your reply here..."></textarea> placeholder="Write your reply here..."></textarea>
<div class="post-tools"> <div class="post-tools">
<button id="reply-button" class="action" onclick="do_send_reply()"> <button id="reply-button" class="action" onclick="do_send_reply()">
Reply Reply
</button> </button>
</div> </div>
</div> </div>
</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=69"></script>
<script>
// I have to delay loading to wait for nos2x
const relay = setTimeout(damus_web_init, 100)
</script>
</body> </body>
</html> </html>

View file

@ -1,111 +0,0 @@
async function damus_init()
{
const relay = await Relay("wss://relay.damus.io")
const now = (new Date().getTime()) / 1000
const el = document.querySelector("#content")
const model = {events: []}
el.innerHTML = render_initial_content()
model.el = el.querySelector("#home")
relay.subscribe("test_sub_id", {kinds: [1], limit: 20})
relay.event = (sub_id, ev) => {
insert_event_sorted(model.events, ev)
if (model.realtime)
render_home_view(model)
}
relay.eose = () => {
model.realtime = true
render_home_view(model)
}
return relay
}
function render_home_view(model) {
model.el.innerHTML = render_events(model.events)
}
function render_initial_content() {
return `<ul id="home"> </ul>`
}
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
}
}
evs.push(new_ev)
return true
}
function render_events(evs) {
return evs.map(render_event).join("\n")
}
function render_event(ev) {
return `<li>${ev.content}</li>`
}
function Relay(relay, opts={})
{
if (!(this instanceof Relay))
return new Relay(relay, opts)
this.relay = relay
this.opts = opts
const me = this
return new Promise((resolve, reject) => {
const ws = me.ws = new WebSocket(relay);
let resolved = false
ws.onmessage = (m) => { handle_message(me, m) }
ws.onclose = () => { me.close && me.close() }
ws.onerror = () => { me.error && me.error() }
ws.onopen = () => {
if (resolved) {
me.open.bind(me)
return
}
resolved = true
resolve(me)
}
})
}
Relay.prototype.subscribe = function relay_subscribe(sub_id, ...filters) {
const tosend = ["REQ", sub_id, ...filters]
console.log("sending", tosend)
this.ws.send(JSON.stringify(tosend))
}
function handle_message(relay, msg)
{
const data = JSON.parse(msg.data)
if (data.length >= 2) {
switch (data[0]) {
case "EVENT":
if (data.length < 3)
return
return relay.event && relay.event(data[1], data[2])
case "EOSE":
return relay.eose && relay.eose(data[1])
case "NOTICE":
return relay.note && relay.note(...data.slice(1))
}
}
}
const relay = damus_init()

File diff suppressed because one or more lines are too long

343
web/js/ui/render.js Normal file
View file

@ -0,0 +1,343 @@
// This file contains all methods related to rendering UI elements. Rendering
// is done by simple string manipulations & templates. If you need to write
// loops simply write it in code and return strings.
function render_home_view(model) {
return `
<div id="newpost">
<div><!-- empty to accomodate profile pic --></div>
<div>
<textarea placeholder="What's up?" oninput="post_input_changed(this)" 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" disabled>Send</button>
</div>
</div>
</div>
<div id="events"></div>
`
}
function render_home_event(model, ev)
{
let max_depth = 3
if (ev.refs && ev.refs.root && model.expanded.has(ev.refs.root)) {
max_depth = null
}
return render_event(model, ev, {max_depth})
}
function render_events(model) {
return model.events
.filter((ev, i) => i < 140)
.map((ev) => render_home_event(model, ev)).join("\n")
}
function render_reply_line_top(has_top_line) {
const classes = has_top_line ? "" : "invisible"
return `<div class="line-top ${classes}"></div>`
}
function render_reply_line_bot() {
return `<div class="line-bot"></div>`
}
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">
More messages in thread available. Click to expand.
</div>
</div>`
}
function render_replied_events(model, ev, opts)
{
if (!(ev.refs && ev.refs.reply))
return ""
const reply_ev = model.all_events[ev.refs.reply]
if (!reply_ev)
return ""
opts.replies = opts.replies == null ? 1 : opts.replies + 1
if (!(opts.max_depth == null || opts.replies < opts.max_depth))
return render_thread_collapsed(model, reply_ev, opts)
opts.is_reply = true
return render_event(model, reply_ev, opts)
}
function render_replying_to_chat(model, ev) {
const chatroom = (ev.refs.root && model.chatrooms[ev.refs.root]) || {}
const roomname = chatroom.name || ev.refs.root || "??"
const pks = ev.refs.pubkeys || []
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">replying${to_users} in <span class="chatroom-name">${roomname}</span></div>`
}
function render_replying_to(model, ev) {
if (!(ev.refs && ev.refs.reply))
return ""
if (ev.kind === 42)
return render_replying_to_chat(model, ev)
let pubkeys = ev.refs.pubkeys || []
if (pubkeys.length === 0 && ev.refs.reply) {
const replying_to = model.all_events[ev.refs.reply]
if (!replying_to)
return `<div class="replying-to small-txt">reply to ${ev.refs.reply}</div>`
pubkeys = [replying_to.pubkey]
}
const names = ev.refs.pubkeys.map(pk => render_mentioned_name(pk, model.profiles[pk])).join(", ")
return `
<span class="replying-to small-txt">
replying to ${names}
</span>
`
}
function render_unknown_event(model, ev) {
return "Unknown event"
}
function render_boost(model, ev, opts) {
//todo validate content
if (!ev.json_content)
return render_unknown_event(ev)
//const profile = model.profiles[ev.pubkey]
opts.is_boost_event = true
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 render_comment_body(model, ev, opts) {
const can_delete = model.pubkey === ev.pubkey;
const bar = !can_reply(ev) || opts.nobar? "" : render_action_bar(ev, can_delete)
const show_media = !opts.is_composing
return `
<div>
${render_replying_to(model, ev)}
${render_boosted_by(model, ev, opts)}
</div>
<p>
${format_content(ev, show_media)}
</p>
${render_reactions(model, ev)}
${bar}
`
}
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
return `
<div class="deleted-comment">
This comment was deleted. Reason:
<div class="quote">${format_content(deleted, show_media)}</div>
</div>
`
}
return `<div class="deleted-comment">This comment was deleted</div>`
}
function render_event(model, ev, opts={}) {
if (ev.kind === 6)
return render_boost(model, ev, opts)
if (shouldnt_render_event(model, ev, opts))
return ""
delete opts.is_boost_event
model.rendered[ev.id] = true
const profile = model.profiles[ev.pubkey] || DEFAULT_PROFILE
const delta = time_delta(new Date().getTime(), ev.created_at*1000)
const has_bot_line = opts.is_reply
const reply_line_bot = (has_bot_line && render_reply_line_bot()) || ""
const deleted = is_deleted(model, ev.id)
if (deleted && !opts.is_reply)
return ""
const replied_events = render_replied_events(model, ev, opts)
let name = "???"
if (!deleted) {
name = render_name_plain(ev.pubkey, profile)
}
const has_top_line = replied_events !== ""
const border_bottom = has_bot_line ? "" : "bottom-border";
return `
${replied_events}
<div id="ev${ev.id}" class="event ${border_bottom}">
<div class="userpic">
${render_reply_line_top(has_top_line)}
${deleted ? render_deleted_pfp() : render_pfp(ev.pubkey, profile)}
${reply_line_bot}
</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>
`
}
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) {
return `onclick="send_reply('${emoji}', '${reacting_to}')"`
} else {
return `onclick="delete_post('${reaction.id}')"`
}
}
function render_reaction_group(model, emoji, reactions, reacting_to) {
const pfps = Object.keys(reactions).map((pk) => render_reaction(model, reactions[pk]))
let onclick = render_react_onclick(model.pubkey, reacting_to.id, emoji, reactions)
return `
<span ${onclick} class="reaction-group clickable">
<span class="reaction-emoji">
${emoji}
</span>
${pfps.join("\n")}
</span>
`
}
function render_reaction(model, reaction) {
const profile = model.profiles[reaction.pubkey] || DEFAULT_PROFILE
let emoji = reaction.content[0]
if (reaction.content === "+" || reaction.content === "")
emoji = "❤️"
return render_pfp(reaction.pubkey, profile, "small")
}
function render_action_bar(ev, can_delete) {
let delete_html = ""
if (can_delete)
delete_html = `<button class="icon" title="Delete" onclick="delete_post_confirm('${ev.id}')"><i class="fa fa-fw fa-trash"></i></a>`
const groups = get_reactions(DAMUS, ev.id)
const like = "❤️"
const likes = groups[like] || {}
const react_onclick = render_react_onclick(DAMUS.pubkey, ev.id, like, likes)
return `
<div class="action-bar">
<button class="icon" title="Reply" onclick="reply_to('${ev.id}')"><i class="fa fa-fw fa-comment"></i></a>
<button class="icon react heart" ${react_onclick} title="Like"><i class="fa fa-fw fa-heart"></i></a>
<!--<button class="icon" title="Share" onclick=""><i class="fa fa-fw fa-link"></i></a>-->
${delete_html}
<!--<button class="icon" title="View raw Nostr event." onclick=""><i class="fa-solid fa-fw fa-code"></i></a>-->
</div>
`
}
function render_reactions(model, ev) {
const groups = get_reactions(model, ev.id)
let str = ""
for (const emoji of Object.keys(groups)) {
str += render_reaction_group(model, emoji, groups[emoji], ev)
}
return `
<div class="reactions">
${str}
</div>
`
}
// Utility Methods
function render_name_plain(pk, profile=DEFAULT_PROFILE)
{
if (profile.sanitized_name)
return profile.sanitized_name
const display_name = profile.display_name || profile.user
const username = profile.name || "anon"
const name = display_name || username
profile.sanitized_name = sanitize(name)
return profile.sanitized_name
}
function render_pubkey(pk)
{
return pk.slice(-8)
}
function render_username(pk, profile)
{
return (profile && profile.name) || render_pubkey(pk)
}
function render_mentioned_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_deleted_name() {
return "???"
}
function render_deleted_pfp() {
return `<div class="pfp pfp-normal">😵</div>`
}

13
web/js/ui/util.js Normal file
View file

@ -0,0 +1,13 @@
// This file contains utility functions related to UI manipulation. Some code
// may be specific to areas of the UI and others are more utility based. As
// this file grows specific UI area code should be migrated to its own file.
// toggle_cw changes the active stage of the Content Warning for a post. It is
// relative to the element that is pressed.
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);
}