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:
commit
c3b4ac0d5b
23 changed files with 471 additions and 528 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ TODO.bak
|
||||||
*.mp4
|
*.mp4
|
||||||
channels/index.html
|
channels/index.html
|
||||||
node_modules
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
|
|
@ -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
37
web/README.md
Normal 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.
|
||||||
|
|
|
@ -4,11 +4,25 @@
|
||||||
<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>
|
||||||
|
<script>
|
||||||
|
const relay = 0; // relay is declared for backwards compatibibility.
|
||||||
|
// This is our main entry.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event
|
||||||
|
addEventListener('DOMContentLoaded', (ev) => {
|
||||||
|
damus_web_init();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<div class="flex-fill"></div>
|
<div class="flex-fill"></div>
|
||||||
<div id="nav">
|
<div id="nav">
|
||||||
|
@ -31,8 +45,8 @@
|
||||||
</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">
|
||||||
|
@ -63,13 +77,5 @@
|
||||||
</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>
|
||||||
|
|
111
web/index.js
111
web/index.js
|
@ -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
343
web/js/ui/render.js
Normal 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
13
web/js/ui/util.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue