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
169
web/js/bech32.js
Normal file
169
web/js/bech32.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
var ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
||||
var ALPHABET_MAP = {};
|
||||
for (var z = 0; z < ALPHABET.length; z++) {
|
||||
var x = ALPHABET.charAt(z);
|
||||
ALPHABET_MAP[x] = z;
|
||||
}
|
||||
function polymodStep(pre) {
|
||||
var b = pre >> 25;
|
||||
return (((pre & 0x1ffffff) << 5) ^
|
||||
(-((b >> 0) & 1) & 0x3b6a57b2) ^
|
||||
(-((b >> 1) & 1) & 0x26508e6d) ^
|
||||
(-((b >> 2) & 1) & 0x1ea119fa) ^
|
||||
(-((b >> 3) & 1) & 0x3d4233dd) ^
|
||||
(-((b >> 4) & 1) & 0x2a1462b3));
|
||||
}
|
||||
function prefixChk(prefix) {
|
||||
var chk = 1;
|
||||
for (var i = 0; i < prefix.length; ++i) {
|
||||
var c = prefix.charCodeAt(i);
|
||||
if (c < 33 || c > 126)
|
||||
return 'Invalid prefix (' + prefix + ')';
|
||||
chk = polymodStep(chk) ^ (c >> 5);
|
||||
}
|
||||
chk = polymodStep(chk);
|
||||
for (var i = 0; i < prefix.length; ++i) {
|
||||
var v = prefix.charCodeAt(i);
|
||||
chk = polymodStep(chk) ^ (v & 0x1f);
|
||||
}
|
||||
return chk;
|
||||
}
|
||||
function convertbits(data, inBits, outBits, pad) {
|
||||
var value = 0;
|
||||
var bits = 0;
|
||||
var maxV = (1 << outBits) - 1;
|
||||
var result = [];
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
value = (value << inBits) | data[i];
|
||||
bits += inBits;
|
||||
while (bits >= outBits) {
|
||||
bits -= outBits;
|
||||
result.push((value >> bits) & maxV);
|
||||
}
|
||||
}
|
||||
if (pad) {
|
||||
if (bits > 0) {
|
||||
result.push((value << (outBits - bits)) & maxV);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (bits >= inBits)
|
||||
return 'Excess padding';
|
||||
if ((value << (outBits - bits)) & maxV)
|
||||
return 'Non-zero padding';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function toWords(bytes) {
|
||||
return convertbits(bytes, 8, 5, true);
|
||||
}
|
||||
function fromWordsUnsafe(words) {
|
||||
var res = convertbits(words, 5, 8, false);
|
||||
if (Array.isArray(res))
|
||||
return res;
|
||||
}
|
||||
function fromWords(words) {
|
||||
var res = convertbits(words, 5, 8, false);
|
||||
if (Array.isArray(res))
|
||||
return res;
|
||||
throw new Error(res);
|
||||
}
|
||||
function getLibraryFromEncoding(encoding) {
|
||||
var ENCODING_CONST;
|
||||
if (encoding === 'bech32') {
|
||||
ENCODING_CONST = 1;
|
||||
}
|
||||
else {
|
||||
ENCODING_CONST = 0x2bc830a3;
|
||||
}
|
||||
function encode(prefix, words, LIMIT) {
|
||||
LIMIT = LIMIT || 90;
|
||||
if (prefix.length + 7 + words.length > LIMIT)
|
||||
throw new TypeError('Exceeds length limit');
|
||||
prefix = prefix.toLowerCase();
|
||||
// determine chk mod
|
||||
var chk = prefixChk(prefix);
|
||||
if (typeof chk === 'string')
|
||||
throw new Error(chk);
|
||||
var result = prefix + '1';
|
||||
for (var i = 0; i < words.length; ++i) {
|
||||
var x = words[i];
|
||||
if (x >> 5 !== 0)
|
||||
throw new Error('Non 5-bit word');
|
||||
chk = polymodStep(chk) ^ x;
|
||||
result += ALPHABET.charAt(x);
|
||||
}
|
||||
for (var i = 0; i < 6; ++i) {
|
||||
chk = polymodStep(chk);
|
||||
}
|
||||
chk ^= ENCODING_CONST;
|
||||
for (var i = 0; i < 6; ++i) {
|
||||
var v = (chk >> ((5 - i) * 5)) & 0x1f;
|
||||
result += ALPHABET.charAt(v);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function __decode(str, LIMIT) {
|
||||
LIMIT = LIMIT || 90;
|
||||
if (str.length < 8)
|
||||
return str + ' too short';
|
||||
if (str.length > LIMIT)
|
||||
return 'Exceeds length limit';
|
||||
// don't allow mixed case
|
||||
var lowered = str.toLowerCase();
|
||||
var uppered = str.toUpperCase();
|
||||
if (str !== lowered && str !== uppered)
|
||||
return 'Mixed-case string ' + str;
|
||||
str = lowered;
|
||||
var split = str.lastIndexOf('1');
|
||||
if (split === -1)
|
||||
return 'No separator character for ' + str;
|
||||
if (split === 0)
|
||||
return 'Missing prefix for ' + str;
|
||||
var prefix = str.slice(0, split);
|
||||
var wordChars = str.slice(split + 1);
|
||||
if (wordChars.length < 6)
|
||||
return 'Data too short';
|
||||
var chk = prefixChk(prefix);
|
||||
if (typeof chk === 'string')
|
||||
return chk;
|
||||
var words = [];
|
||||
for (var i = 0; i < wordChars.length; ++i) {
|
||||
var c = wordChars.charAt(i);
|
||||
var v = ALPHABET_MAP[c];
|
||||
if (v === undefined)
|
||||
return 'Unknown character ' + c;
|
||||
chk = polymodStep(chk) ^ v;
|
||||
// not in the checksum?
|
||||
if (i + 6 >= wordChars.length)
|
||||
continue;
|
||||
words.push(v);
|
||||
}
|
||||
if (chk !== ENCODING_CONST)
|
||||
return 'Invalid checksum for ' + str;
|
||||
return { prefix: prefix, words: words };
|
||||
}
|
||||
function decodeUnsafe(str, LIMIT) {
|
||||
var res = __decode(str, LIMIT);
|
||||
if (typeof res === 'object')
|
||||
return res;
|
||||
}
|
||||
function decode(str, LIMIT) {
|
||||
var res = __decode(str, LIMIT);
|
||||
if (typeof res === 'object')
|
||||
return res;
|
||||
throw new Error(res);
|
||||
}
|
||||
return {
|
||||
decodeUnsafe: decodeUnsafe,
|
||||
decode: decode,
|
||||
encode: encode,
|
||||
toWords: toWords,
|
||||
fromWordsUnsafe: fromWordsUnsafe,
|
||||
fromWords: fromWords
|
||||
};
|
||||
}
|
||||
|
||||
const bech32 = getLibraryFromEncoding('bech32');
|
||||
const bech32m = getLibraryFromEncoding('bech32m');
|
||||
|
1170
web/js/damus.js
Normal file
1170
web/js/damus.js
Normal file
File diff suppressed because one or more lines are too long
1203
web/js/noble-secp256k1.js
Normal file
1203
web/js/noble-secp256k1.js
Normal file
File diff suppressed because it is too large
Load diff
366
web/js/nostr.js
Normal file
366
web/js/nostr.js
Normal file
|
@ -0,0 +1,366 @@
|
|||
const nostrjs = (function nostrlib() {
|
||||
const WS = typeof WebSocket !== 'undefined' ? WebSocket : require('ws')
|
||||
|
||||
function RelayPool(relays, opts)
|
||||
{
|
||||
if (!(this instanceof RelayPool))
|
||||
return new RelayPool(relays)
|
||||
|
||||
this.onfn = {}
|
||||
this.relays = []
|
||||
|
||||
for (const relay of relays) {
|
||||
this.add(relay)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
RelayPool.prototype.close = function relayPoolClose() {
|
||||
for (const relay of this.relays) {
|
||||
relay.close()
|
||||
}
|
||||
}
|
||||
|
||||
RelayPool.prototype.on = function relayPoolOn(method, fn) {
|
||||
for (const relay of this.relays) {
|
||||
this.onfn[method] = fn
|
||||
relay.onfn[method] = fn.bind(null, relay)
|
||||
}
|
||||
}
|
||||
|
||||
RelayPool.prototype.has = function relayPoolHas(relayUrl) {
|
||||
for (const relay of this.relays) {
|
||||
if (relay.url === relayUrl)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
RelayPool.prototype.setupHandlers = function relayPoolSetupHandlers()
|
||||
{
|
||||
// setup its message handlers with the ones we have already
|
||||
const keys = Object.keys(this.onfn)
|
||||
for (const handler of keys) {
|
||||
for (const relay of this.relays) {
|
||||
relay.onfn[handler] = this.onfn[handler].bind(null, relay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RelayPool.prototype.remove = function relayPoolRemove(url) {
|
||||
let i = 0
|
||||
|
||||
for (const relay of this.relays) {
|
||||
if (relay.url === url) {
|
||||
relay.ws && relay.ws.close()
|
||||
this.relays = this.replays.splice(i, 1)
|
||||
return true
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
RelayPool.prototype.subscribe = function relayPoolSubscribe(sub_id, filters, relay_ids) {
|
||||
const relays = relay_ids ? this.find_relays(relay_ids) : this.relays
|
||||
for (const relay of relays) {
|
||||
relay.subscribe(sub_id, filters)
|
||||
}
|
||||
}
|
||||
|
||||
RelayPool.prototype.unsubscribe = function relayPoolUnsubscibe(sub_id, relay_ids) {
|
||||
const relays = relay_ids ? this.find_relays(relay_ids) : this.relays
|
||||
for (const relay of relays) {
|
||||
relay.unsubscribe(sub_id)
|
||||
}
|
||||
}
|
||||
|
||||
RelayPool.prototype.send = function relayPoolSend(payload, relay_ids) {
|
||||
const relays = relay_ids ? this.find_relays(relay_ids) : this.relays
|
||||
for (const relay of relays) {
|
||||
relay.send(payload)
|
||||
}
|
||||
}
|
||||
|
||||
RelayPool.prototype.add = function relayPoolAdd(relay) {
|
||||
if (relay instanceof Relay) {
|
||||
if (this.has(relay.url))
|
||||
return false
|
||||
|
||||
this.relays.push(relay)
|
||||
this.setupHandlers()
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.has(relay))
|
||||
return false
|
||||
|
||||
const r = Relay(relay, this.opts)
|
||||
this.relays.push(r)
|
||||
this.setupHandlers()
|
||||
return true
|
||||
}
|
||||
|
||||
RelayPool.prototype.find_relays = function relayPoolFindRelays(relay_ids) {
|
||||
if (relay_ids instanceof Relay)
|
||||
return [relay_ids]
|
||||
|
||||
if (relay_ids.length === 0)
|
||||
return []
|
||||
|
||||
if (!relay_ids[0])
|
||||
throw new Error("what!?")
|
||||
|
||||
if (relay_ids[0] instanceof Relay)
|
||||
return relay_ids
|
||||
|
||||
return this.relays.reduce((acc, relay) => {
|
||||
if (relay_ids.some((rid) => relay.url === rid))
|
||||
acc.push(relay)
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
Relay.prototype.wait_connected = async function relay_wait_connected(data) {
|
||||
let retry = 1000
|
||||
while (true) {
|
||||
if (!this.ws || this.ws.readyState !== 1) {
|
||||
await sleep(retry)
|
||||
retry *= 1.5
|
||||
}
|
||||
else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function Relay(relay, opts={})
|
||||
{
|
||||
if (!(this instanceof Relay))
|
||||
return new Relay(relay, opts)
|
||||
|
||||
this.url = relay
|
||||
this.opts = opts
|
||||
|
||||
if (opts.reconnect == null)
|
||||
opts.reconnect = true
|
||||
|
||||
const me = this
|
||||
me.onfn = {}
|
||||
|
||||
try {
|
||||
init_websocket(me)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
function init_websocket(me) {
|
||||
let ws
|
||||
try {
|
||||
ws = me.ws = new WS(me.url);
|
||||
} catch(e) {
|
||||
return null
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let resolved = false
|
||||
ws.onmessage = (m) => { handle_nostr_message(me, m) }
|
||||
ws.onclose = () => {
|
||||
if (me.onfn.close)
|
||||
me.onfn.close()
|
||||
if (me.reconnecting)
|
||||
return reject(new Error("close during reconnect"))
|
||||
if (!me.manualClose && me.opts.reconnect)
|
||||
reconnect(me)
|
||||
}
|
||||
ws.onerror = () => {
|
||||
if (me.onfn.error)
|
||||
me.onfn.error()
|
||||
if (me.reconnecting)
|
||||
return reject(new Error("error during reconnect"))
|
||||
if (me.opts.reconnect)
|
||||
reconnect(me)
|
||||
}
|
||||
ws.onopen = () => {
|
||||
if (me.onfn.open)
|
||||
me.onfn.open()
|
||||
else
|
||||
console.log("no onopen???", me)
|
||||
|
||||
if (resolved) return
|
||||
|
||||
resolved = true
|
||||
resolve(me)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function reconnect(me)
|
||||
{
|
||||
const reconnecting = true
|
||||
let n = 100
|
||||
try {
|
||||
me.reconnecting = true
|
||||
await init_websocket(me)
|
||||
me.reconnecting = false
|
||||
} catch {
|
||||
//console.error(`error thrown during reconnect... trying again in ${n} ms`)
|
||||
await sleep(n)
|
||||
n *= 1.5
|
||||
}
|
||||
}
|
||||
|
||||
Relay.prototype.on = function relayOn(method, fn) {
|
||||
this.onfn[method] = fn
|
||||
}
|
||||
|
||||
Relay.prototype.close = function relayClose() {
|
||||
if (this.ws) {
|
||||
this.manualClose = true
|
||||
this.ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
Relay.prototype.subscribe = function relay_subscribe(sub_id, filters) {
|
||||
if (Array.isArray(filters))
|
||||
this.send(["REQ", sub_id, ...filters])
|
||||
else
|
||||
this.send(["REQ", sub_id, filters])
|
||||
}
|
||||
|
||||
Relay.prototype.unsubscribe = function relay_unsubscribe(sub_id) {
|
||||
this.send(["CLOSE", sub_id])
|
||||
}
|
||||
|
||||
Relay.prototype.send = async function relay_send(data) {
|
||||
await this.wait_connected()
|
||||
this.ws.send(JSON.stringify(data))
|
||||
}
|
||||
|
||||
function handle_nostr_message(relay, msg)
|
||||
{
|
||||
let data
|
||||
try {
|
||||
data = JSON.parse(msg.data)
|
||||
} catch (e) {
|
||||
console.error("handle_nostr_message", msg, e)
|
||||
return
|
||||
}
|
||||
if (data.length >= 2) {
|
||||
switch (data[0]) {
|
||||
case "EVENT":
|
||||
if (data.length < 3)
|
||||
return
|
||||
return relay.onfn.event && relay.onfn.event(data[1], data[2])
|
||||
case "EOSE":
|
||||
return relay.onfn.eose && relay.onfn.eose(data[1])
|
||||
case "NOTICE":
|
||||
return relay.onfn.notice && relay.onfn.notice(...data.slice(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sha256(message) {
|
||||
if (crypto.subtle) {
|
||||
const buffer = await crypto.subtle.digest('SHA-256', message);
|
||||
return new Uint8Array(buffer);
|
||||
} else if (require) {
|
||||
const { createHash } = require('crypto');
|
||||
const hash = createHash('sha256');
|
||||
[message].forEach((m) => hash.update(m));
|
||||
return Uint8Array.from(hash.digest());
|
||||
} else {
|
||||
throw new Error("The environment doesn't have sha256 function");
|
||||
}
|
||||
}
|
||||
|
||||
async function calculate_id(ev) {
|
||||
const commit = event_commitment(ev)
|
||||
const buf = new TextEncoder().encode(commit);
|
||||
return hex_encode(await sha256(buf))
|
||||
}
|
||||
|
||||
function event_commitment(ev) {
|
||||
const {pubkey,created_at,kind,tags,content} = ev
|
||||
return JSON.stringify([0, pubkey, created_at, kind, tags, content])
|
||||
}
|
||||
|
||||
function hex_char(val) {
|
||||
if (val < 10)
|
||||
return String.fromCharCode(48 + val)
|
||||
if (val < 16)
|
||||
return String.fromCharCode(97 + val - 10)
|
||||
}
|
||||
|
||||
function hex_encode(buf) {
|
||||
let str = ""
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
const c = buf[i]
|
||||
str += hex_char(c >> 4)
|
||||
str += hex_char(c & 0xF)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
function char_to_hex(cstr) {
|
||||
const c = cstr.charCodeAt(0)
|
||||
// c >= 0 && c <= 9
|
||||
if (c >= 48 && c <= 57) {
|
||||
return c - 48;
|
||||
}
|
||||
// c >= a && c <= f
|
||||
if (c >= 97 && c <= 102) {
|
||||
return c - 97 + 10;
|
||||
}
|
||||
// c >= A && c <= F
|
||||
if (c >= 65 && c <= 70) {
|
||||
return c - 65 + 10;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
function hex_decode(str, buflen)
|
||||
{
|
||||
let bufsize = buflen || 33
|
||||
let c1, c2
|
||||
let i = 0
|
||||
let j = 0
|
||||
let buf = new Uint8Array(bufsize)
|
||||
let slen = str.length
|
||||
while (slen > 1) {
|
||||
if (-1==(c1 = char_to_hex(str[j])) || -1==(c2 = char_to_hex(str[j+1])))
|
||||
return null;
|
||||
if (!bufsize)
|
||||
return null;
|
||||
j += 2
|
||||
slen -= 2
|
||||
buf[i++] = (c1 << 4) | c2
|
||||
bufsize--;
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
return {
|
||||
RelayPool,
|
||||
calculate_id,
|
||||
event_commitment,
|
||||
hex_encode,
|
||||
hex_decode,
|
||||
}
|
||||
})()
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports)
|
||||
module.exports = nostrjs
|
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