move webv2 to master
no point having a branch for this.
This commit is contained in:
parent
232ca7c087
commit
dfef728d91
9 changed files with 2733 additions and 0 deletions
1
webv2/.gitignore
vendored
Normal file
1
webv2/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
tags
|
7
webv2/Makefile
Normal file
7
webv2/Makefile
Normal file
|
@ -0,0 +1,7 @@
|
|||
tags: fake
|
||||
ctags damus.js nostr.js > $@
|
||||
|
||||
dist:
|
||||
rsync -avzP ./ charon:/www/damus.io/web/
|
||||
|
||||
.PHONY: fake
|
169
webv2/bech32.js
Normal file
169
webv2/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');
|
||||
|
181
webv2/damus.css
Normal file
181
webv2/damus.css
Normal file
|
@ -0,0 +1,181 @@
|
|||
.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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#reply-content {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
border: 0;
|
||||
width: 100%;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
padding: 5px;
|
||||
color: white;
|
||||
background-color: #C073EB;
|
||||
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%;
|
||||
height: 50%;
|
||||
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: 36em;
|
||||
hyphens: auto;
|
||||
word-wrap: break-word;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-kerning: normal;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
font-size: 0.9em;
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
@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 {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 15px 0 15px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.comment {
|
||||
display: flex;
|
||||
font-family: system-ui, sans;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comment p {
|
||||
background-color: rgba(255.0,255.0,255.0,0.1);
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
margin: 0;
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
.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 .info span {
|
||||
font-size: 11px;
|
||||
color: rgba(255.0,255.0,255.0,0.7);
|
||||
}
|
||||
|
||||
@media (max-width: 800px){
|
||||
/* Reverse the order of elements in the user comments,
|
||||
so that the avatar and info appear after the text. */
|
||||
.comment .info {
|
||||
order: 2;
|
||||
width: 50%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pfp {
|
||||
order: 1;
|
||||
margin: 0 15px 0 0;
|
||||
}
|
||||
|
||||
.comment {
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(255.0,255.0,255.0,0.1);
|
||||
}
|
||||
|
||||
.comment p {
|
||||
order: 3;
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
574
webv2/damus.js
Normal file
574
webv2/damus.js
Normal file
|
@ -0,0 +1,574 @@
|
|||
|
||||
let DSTATE
|
||||
|
||||
function uuidv4() {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
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 init_contacts() {
|
||||
return {
|
||||
event: null,
|
||||
friends: new Set(),
|
||||
friend_of_friends: new Set(),
|
||||
}
|
||||
}
|
||||
|
||||
function init_home_model() {
|
||||
return {
|
||||
done_init: false,
|
||||
loading: true,
|
||||
all_events: {},
|
||||
events: [],
|
||||
profiles: {},
|
||||
last_event_of_kind: {},
|
||||
contacts: init_contacts()
|
||||
}
|
||||
}
|
||||
|
||||
async function damus_web_init(thread)
|
||||
{
|
||||
const {RelayPool} = nostrjs
|
||||
const pool = RelayPool(["wss://relay.damus.io"])
|
||||
const now = (new Date().getTime()) / 1000
|
||||
const model = init_home_model()
|
||||
DSTATE = model
|
||||
|
||||
const ids = {
|
||||
comments: "comments",//uuidv4(),
|
||||
profiles: "profiles",//uuidv4(),
|
||||
account: "account",//uuidv4(),
|
||||
home: "home",//uuidv4(),
|
||||
contacts: "contacts",//uuidv4(),
|
||||
notifications: "notifications",//uuidv4(),
|
||||
dms: "dms",//uuidv4(),
|
||||
}
|
||||
|
||||
model.pubkey = get_pubkey()
|
||||
if (!model.pubkey)
|
||||
return
|
||||
model.pool = pool
|
||||
model.el = document.querySelector("#posts")
|
||||
|
||||
pool.on('open', (relay) => {
|
||||
//let authors = followers
|
||||
// TODO: fetch contact list
|
||||
log_debug("relay connected", relay.url)
|
||||
|
||||
if (!model.done_init) {
|
||||
model.loading = false
|
||||
send_initial_filters(ids.account, model.pubkey, relay)
|
||||
} else {
|
||||
send_home_filters(ids, model, relay)
|
||||
}
|
||||
//relay.subscribe(comments_id, {kinds: [1,42], limit: 100})
|
||||
});
|
||||
|
||||
pool.on('event', (relay, sub_id, ev) => {
|
||||
handle_home_event(ids, model, relay, sub_id, ev)
|
||||
})
|
||||
|
||||
pool.on('eose', async (relay, sub_id) => {
|
||||
if (sub_id === ids.home) {
|
||||
handle_comments_loaded(ids.profiles, model, relay)
|
||||
} else if (sub_id === ids.profiles) {
|
||||
handle_profiles_loaded(ids.profiles, model, relay)
|
||||
}
|
||||
})
|
||||
|
||||
return pool
|
||||
}
|
||||
|
||||
let rerender_home_timer
|
||||
function handle_home_event(ids, model, relay, sub_id, ev) {
|
||||
model.all_events[ev.id] = ev
|
||||
|
||||
switch (sub_id) {
|
||||
case ids.home:
|
||||
if (ev.content !== "")
|
||||
insert_event_sorted(model.events, ev)
|
||||
if (model.realtime) {
|
||||
if (rerender_home_timer)
|
||||
clearTimeout(rerender_home_timer)
|
||||
rerender_home_timer = setTimeout(render_home_view.bind(null, model), 200)
|
||||
}
|
||||
break;
|
||||
case ids.account:
|
||||
switch (ev.kind) {
|
||||
case 3:
|
||||
model.loading = false
|
||||
process_contact_event(model, ev)
|
||||
model.done_init = true
|
||||
model.pool.unsubscribe(ids.account, [relay])
|
||||
break
|
||||
case 0:
|
||||
handle_profile_event(model, ev)
|
||||
break
|
||||
}
|
||||
case ids.profiles:
|
||||
try {
|
||||
model.profiles[ev.pubkey] = JSON.parse(ev.content)
|
||||
} catch {
|
||||
console.log("failed to parse", ev.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handle_profile_event(model, ev) {
|
||||
console.log("PROFILE", ev)
|
||||
}
|
||||
|
||||
function send_initial_filters(account_id, pubkey, relay) {
|
||||
const filter = {authors: [pubkey], kinds: [3], limit: 1}
|
||||
relay.subscribe(account_id, filter)
|
||||
}
|
||||
|
||||
function send_home_filters(ids, model, relay) {
|
||||
const friends = contacts_friend_list(model.contacts)
|
||||
friends.push(model.pubkey)
|
||||
|
||||
const contacts_filter = {kinds: [0], authors: friends}
|
||||
const dms_filter = {kinds: [4], limit: 500}
|
||||
const our_dms_filter = {kinds: [4], authors: [ model.pubkey ], limit: 500}
|
||||
const home_filter = {kinds: [1,42,6,7], authors: friends, limit: 500}
|
||||
const notifications_filter = {kinds: [1,42,6,7], "#p": [model.pubkey], limit: 100}
|
||||
|
||||
let home_filters = [home_filter]
|
||||
let notifications_filters = [notifications_filter]
|
||||
let contacts_filters = [contacts_filter]
|
||||
let dms_filters = [dms_filter, our_dms_filter]
|
||||
|
||||
let last_of_kind = {}
|
||||
if (relay) {
|
||||
last_of_kind =
|
||||
model.last_event_of_kind[relay] =
|
||||
model.last_event_of_kind[relay] || {}
|
||||
}
|
||||
|
||||
update_filters_with_since(last_of_kind, home_filters)
|
||||
update_filters_with_since(last_of_kind, contacts_filters)
|
||||
update_filters_with_since(last_of_kind, notifications_filters)
|
||||
update_filters_with_since(last_of_kind, dms_filters)
|
||||
|
||||
const subto = relay? [relay] : undefined
|
||||
model.pool.subscribe(ids.home, home_filters, subto)
|
||||
model.pool.subscribe(ids.contacts, contacts_filters, subto)
|
||||
model.pool.subscribe(ids.notifications, notifications_filters, subto)
|
||||
model.pool.subscribe(ids.dms, dms_filters, subto)
|
||||
}
|
||||
|
||||
function get_since_time(last_event) {
|
||||
if (!last_event) {
|
||||
return null
|
||||
}
|
||||
|
||||
return last_event.created_at - 60 * 10
|
||||
}
|
||||
|
||||
function update_filter_with_since(last_of_kind, filter) {
|
||||
const kinds = filter.kinds || []
|
||||
let initial = null
|
||||
let earliest = kinds.reduce((earliest, kind) => {
|
||||
const last = last_of_kind[kind]
|
||||
let since = get_since_time(last)
|
||||
|
||||
if (!earliest) {
|
||||
if (since === null)
|
||||
return null
|
||||
|
||||
return since
|
||||
}
|
||||
|
||||
if (since === null)
|
||||
return earliest
|
||||
|
||||
return since < earliest ? since : earliest
|
||||
|
||||
}, initial)
|
||||
|
||||
if (earliest)
|
||||
filter.since = earliest
|
||||
}
|
||||
|
||||
function update_filters_with_since(last_of_kind, filters) {
|
||||
for (const filter of filters) {
|
||||
update_filter_with_since(last_of_kind, filter)
|
||||
}
|
||||
}
|
||||
|
||||
function contacts_friend_list(contacts) {
|
||||
return Array.from(contacts.friends)
|
||||
}
|
||||
|
||||
function process_contact_event(model, ev) {
|
||||
load_our_contacts(model.contacts, model.pubkey, ev)
|
||||
load_our_relays(model.pubkey, model.pool, ev)
|
||||
add_contact_if_friend(model.contacts, ev)
|
||||
}
|
||||
|
||||
function add_contact_if_friend(contacts, ev) {
|
||||
if (!contact_is_friend(contacts, ev.pubkey)) {
|
||||
return
|
||||
}
|
||||
|
||||
add_friend_contact(contacts, ev)
|
||||
}
|
||||
|
||||
function contact_is_friend(contacts, pk) {
|
||||
return contacts.friends.has(pk)
|
||||
}
|
||||
|
||||
function add_friend_contact(contacts, contact) {
|
||||
contacts.friends[contact.pubkey] = true
|
||||
|
||||
for (const tag of contact.tags) {
|
||||
if (tag.count >= 2 && tag[0] == "p") {
|
||||
contacts.friend_of_friends.add(tag[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function load_our_relays(our_pubkey, pool, ev) {
|
||||
if (ev.pubkey != our_pubkey)
|
||||
return
|
||||
|
||||
let relays
|
||||
try {
|
||||
relays = JSON.parse(ev.content)
|
||||
} catch (e) {
|
||||
log_debug("error loading relays", e)
|
||||
return
|
||||
}
|
||||
|
||||
for (const relay of Object.keys(relays)) {
|
||||
log_debug("adding relay", relay)
|
||||
if (!pool.has(relay))
|
||||
pool.add(relay)
|
||||
}
|
||||
}
|
||||
|
||||
function log_debug(fmt, ...args) {
|
||||
console.log("[debug] " + fmt, ...args)
|
||||
}
|
||||
|
||||
function load_our_contacts(contacts, our_pubkey, ev) {
|
||||
if (ev.pubkey !== our_pubkey)
|
||||
return
|
||||
|
||||
contacts.event = ev
|
||||
|
||||
for (const tag of ev.tags) {
|
||||
if (tag.length > 1 && tag[0] === "p") {
|
||||
contacts.friends.add(tag[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handle_profiles_loaded(profiles_id, model, relay) {
|
||||
// stop asking for profiles
|
||||
model.pool.unsubscribe(profiles_id, relay)
|
||||
model.realtime = true
|
||||
render_home_view(model)
|
||||
}
|
||||
|
||||
function debounce(f, interval) {
|
||||
let timer = null;
|
||||
let first = true;
|
||||
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
return new Promise((resolve) => {
|
||||
timer = setTimeout(() => resolve(f(...args)), first? 0 : interval);
|
||||
first = false
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// load profiles after comment notes are loaded
|
||||
function handle_comments_loaded(profiles_id, model, relay)
|
||||
{
|
||||
const pubkeys = model.events.reduce((s, ev) => {
|
||||
s.add(ev.pubkey)
|
||||
return s
|
||||
}, new Set())
|
||||
const authors = Array.from(pubkeys)
|
||||
|
||||
// load profiles
|
||||
const filter = {kinds: [0], authors: authors}
|
||||
console.log("subscribe", profiles_id, filter, relay)
|
||||
model.pool.subscribe(profiles_id, filter, relay)
|
||||
}
|
||||
|
||||
function render_home_view(model) {
|
||||
log_debug("rendering home view")
|
||||
model.el.innerHTML = render_events(model)
|
||||
}
|
||||
|
||||
function render_events(model) {
|
||||
const render = render_event.bind(null, model)
|
||||
return model.events.map(render).join("\n")
|
||||
}
|
||||
|
||||
function render_event(model, ev, opts={}) {
|
||||
const profile = model.profiles[ev.pubkey] || {
|
||||
name: "anon",
|
||||
display_name: "Anonymous",
|
||||
}
|
||||
const delta = time_delta(new Date().getTime(), ev.created_at*1000)
|
||||
const pk = ev.pubkey
|
||||
const bar = opts.nobar? "" : render_action_bar(ev)
|
||||
return `
|
||||
<div class="comment">
|
||||
<div class="info">
|
||||
${render_name(ev.pubkey, profile)}
|
||||
<span>${delta}</span>
|
||||
</div>
|
||||
<img class="pfp" onerror="this.onerror=null;this.src='${robohash(pk)}';" src="${get_picture(pk, profile)}">
|
||||
<p>
|
||||
${format_content(ev.content)}
|
||||
|
||||
${bar}
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function close_reply() {
|
||||
const modal = document.querySelector("#reply-modal")
|
||||
modal.style.display = "none";
|
||||
}
|
||||
|
||||
function gather_reply_tags(pubkey, from) {
|
||||
let tags = []
|
||||
for (const tag of from.tags) {
|
||||
if (tag.length >= 2) {
|
||||
if (tag[0] === "e") {
|
||||
tags.push(tag)
|
||||
} else if (tag[0] === "p" && tag[1] !== pubkey) {
|
||||
tags.push(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
tags.push(["e", from.id, "", "reply"])
|
||||
if (from.pubkey !== pubkey)
|
||||
tags.push(["p", from.pubkey])
|
||||
return tags
|
||||
}
|
||||
|
||||
async function create_reply(privkey, pubkey, content, from) {
|
||||
const tags = gather_reply_tags(pubkey, from)
|
||||
const created_at = Math.floor(new Date().getTime() / 1000)
|
||||
const kind = from.kind
|
||||
|
||||
let reply = { pubkey, tags, content, created_at, kind }
|
||||
|
||||
reply.id = await nostrjs.calculate_id(reply)
|
||||
reply.sig = await sign_id(privkey, reply.id)
|
||||
|
||||
return reply
|
||||
}
|
||||
|
||||
async function send_reply() {
|
||||
const modal = document.querySelector("#reply-modal")
|
||||
const replying_to = modal.querySelector("#replying-to")
|
||||
const evid = replying_to.dataset.evid
|
||||
const ev = DSTATE.all_events[evid]
|
||||
|
||||
const { pool } = DSTATE
|
||||
const content = document.querySelector("#reply-content").value
|
||||
const pubkey = get_pubkey()
|
||||
const privkey = get_privkey()
|
||||
|
||||
let reply = await create_reply(privkey, pubkey, content, ev)
|
||||
console.log(nostrjs.event_commitment(reply), reply)
|
||||
pool.send(["EVENT", reply])
|
||||
|
||||
close_reply()
|
||||
}
|
||||
|
||||
function bech32_decode(pubkey) {
|
||||
const decoded = bech32.decode(pubkey)
|
||||
const bytes = fromWords(decoded.words)
|
||||
return nostrjs.hex_encode(bytes)
|
||||
}
|
||||
|
||||
function get_local_state(key) {
|
||||
if (DSTATE[key] != null)
|
||||
return DSTATE[key]
|
||||
|
||||
return localStorage.getItem(key)
|
||||
}
|
||||
|
||||
function set_local_state(key, val) {
|
||||
DSTATE[key] = val
|
||||
localStorage.setItem(key, val)
|
||||
}
|
||||
|
||||
function get_pubkey() {
|
||||
let pubkey = get_local_state('pubkey')
|
||||
|
||||
if (pubkey)
|
||||
return pubkey
|
||||
|
||||
pubkey = prompt("Enter pubkey (hex or npub)")
|
||||
|
||||
if (!pubkey)
|
||||
throw new Error("Need pubkey to continue")
|
||||
|
||||
if (pubkey[0] === "n")
|
||||
pubkey = bech32_decode(pubkey)
|
||||
|
||||
set_local_state('pubkey', pubkey)
|
||||
return pubkey
|
||||
}
|
||||
|
||||
function get_privkey() {
|
||||
let privkey = get_local_state('privkey')
|
||||
|
||||
if (privkey)
|
||||
return privkey
|
||||
|
||||
if (!privkey)
|
||||
privkey = prompt("Enter private key")
|
||||
|
||||
if (!privkey)
|
||||
throw new Error("can't get privkey")
|
||||
|
||||
if (privkey[0] === "n") {
|
||||
privkey = bech32_decode(privkey)
|
||||
}
|
||||
|
||||
set_local_state('privkey', privkey)
|
||||
|
||||
return privkey
|
||||
}
|
||||
|
||||
async function sign_id(privkey, id)
|
||||
{
|
||||
//const digest = nostrjs.hex_decode(id)
|
||||
const sig = await nobleSecp256k1.schnorr.sign(id, privkey)
|
||||
return nostrjs.hex_encode(sig)
|
||||
}
|
||||
|
||||
function reply_to(evid) {
|
||||
const modal = document.querySelector("#reply-modal")
|
||||
const replying = modal.style.display === "none";
|
||||
const replying_to = modal.querySelector("#replying-to")
|
||||
|
||||
replying_to.dataset.evid = evid
|
||||
const ev = DSTATE.all_events[evid]
|
||||
replying_to.innerHTML = render_event(DSTATE, ev, {nobar: true})
|
||||
|
||||
modal.style.display = replying? "block" : "none";
|
||||
}
|
||||
|
||||
function render_action_bar(ev) {
|
||||
return `
|
||||
<a href="javascript:reply_to('${ev.id}')">reply</a>
|
||||
`
|
||||
}
|
||||
|
||||
function convert_quote_blocks(content)
|
||||
{
|
||||
const split = content.split("\n")
|
||||
let blockin = false
|
||||
return split.reduce((str, line) => {
|
||||
if (line !== "" && line[0] === '>') {
|
||||
if (!blockin) {
|
||||
str += "<span class='quote'>"
|
||||
blockin = true
|
||||
}
|
||||
str += sanitize(line.slice(1))
|
||||
} else {
|
||||
if (blockin) {
|
||||
blockin = false
|
||||
str += "</span>"
|
||||
}
|
||||
str += sanitize(line)
|
||||
}
|
||||
return str + "<br/>"
|
||||
}, "")
|
||||
}
|
||||
|
||||
function format_content(content)
|
||||
{
|
||||
return convert_quote_blocks(content)
|
||||
}
|
||||
|
||||
function sanitize(content)
|
||||
{
|
||||
if (!content)
|
||||
return ""
|
||||
return content.replaceAll("<","<").replaceAll(">",">")
|
||||
}
|
||||
|
||||
function robohash(pk) {
|
||||
return "https://robohash.org/" + pk
|
||||
}
|
||||
|
||||
function get_picture(pk, profile)
|
||||
{
|
||||
return sanitize(profile.picture) || robohash(pk)
|
||||
}
|
||||
|
||||
function render_name(pk, profile={})
|
||||
{
|
||||
const display_name = profile.display_name || profile.user
|
||||
const username = profile.name || "anon"
|
||||
const name = display_name || username
|
||||
|
||||
return `<div class="username">${sanitize(name)}</div>`
|
||||
}
|
||||
|
||||
function time_delta(current, previous) {
|
||||
var msPerMinute = 60 * 1000;
|
||||
var msPerHour = msPerMinute * 60;
|
||||
var msPerDay = msPerHour * 24;
|
||||
var msPerMonth = msPerDay * 30;
|
||||
var msPerYear = msPerDay * 365;
|
||||
|
||||
var elapsed = current - previous;
|
||||
|
||||
if (elapsed < msPerMinute) {
|
||||
return Math.round(elapsed/1000) + ' seconds ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerHour) {
|
||||
return Math.round(elapsed/msPerMinute) + ' minutes ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerDay ) {
|
||||
return Math.round(elapsed/msPerHour ) + ' hours ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerMonth) {
|
||||
return Math.round(elapsed/msPerDay) + ' days ago';
|
||||
}
|
||||
|
||||
else if (elapsed < msPerYear) {
|
||||
return Math.round(elapsed/msPerMonth) + ' months ago';
|
||||
}
|
||||
|
||||
else {
|
||||
return Math.round(elapsed/msPerYear ) + ' years ago';
|
||||
}
|
||||
}
|
186
webv2/img/damus-nobg.svg
Normal file
186
webv2/img/damus-nobg.svg
Normal file
|
@ -0,0 +1,186 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="146.15311mm"
|
||||
height="184.664mm"
|
||||
viewBox="0 0 146.15311 184.66401"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2-alpha (0bd5040e, 2022-02-05)"
|
||||
sodipodi:docname="damus-nobg.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:blackoutopacity="0.0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.5946522"
|
||||
inkscape:cx="73.992831"
|
||||
inkscape:cy="206.8436"
|
||||
inkscape:window-width="1435"
|
||||
inkscape:window-height="844"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer2" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient39361">
|
||||
<stop
|
||||
style="stop-color:#0de8ff;stop-opacity:0.78082192;"
|
||||
offset="0"
|
||||
id="stop39357" />
|
||||
<stop
|
||||
style="stop-color:#d600fc;stop-opacity:0.95433789;"
|
||||
offset="1"
|
||||
id="stop39359" />
|
||||
</linearGradient>
|
||||
<inkscape:path-effect
|
||||
effect="bspline"
|
||||
id="path-effect255"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
weight="33.333333"
|
||||
steps="2"
|
||||
helper_size="0"
|
||||
apply_no_weight="true"
|
||||
apply_with_weight="true"
|
||||
only_selected="false" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient2119">
|
||||
<stop
|
||||
style="stop-color:#1c55ff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop2115" />
|
||||
<stop
|
||||
style="stop-color:#7f35ab;stop-opacity:1;"
|
||||
offset="0.5"
|
||||
id="stop2123" />
|
||||
<stop
|
||||
style="stop-color:#ff0bd6;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop2117" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient2119"
|
||||
id="linearGradient2121"
|
||||
x1="10.067794"
|
||||
y1="248.81357"
|
||||
x2="246.56145"
|
||||
y2="7.1864405"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient39361"
|
||||
id="linearGradient39367"
|
||||
x1="62.104473"
|
||||
y1="128.78963"
|
||||
x2="208.25758"
|
||||
y2="128.78963"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Background"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
sodipodi:insensitive="true"
|
||||
style="display:none"
|
||||
transform="translate(-62.104473,-36.457485)">
|
||||
<rect
|
||||
style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583"
|
||||
id="rect61"
|
||||
width="256"
|
||||
height="256"
|
||||
x="-5.3875166e-08"
|
||||
y="-1.0775033e-07"
|
||||
ry="0"
|
||||
inkscape:label="Gradient"
|
||||
sodipodi:insensitive="true" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Logo"
|
||||
sodipodi:insensitive="true"
|
||||
transform="translate(-62.104473,-36.457485)">
|
||||
<path
|
||||
style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z"
|
||||
id="path253"
|
||||
sodipodi:insensitive="true" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Poly"
|
||||
sodipodi:insensitive="true"
|
||||
transform="translate(-62.104473,-36.457485)">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z"
|
||||
id="path4648" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z"
|
||||
id="path9299" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z"
|
||||
id="path9301" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z"
|
||||
id="path9368" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z"
|
||||
id="path9370" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z"
|
||||
id="path9372" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z"
|
||||
id="path9374" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Vertices"
|
||||
transform="translate(-62.104473,-36.457485)">
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path27764"
|
||||
cx="106.86934"
|
||||
cy="142.38014"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle28773"
|
||||
cx="111.54119"
|
||||
cy="99.221161"
|
||||
r="2.0022209" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="circle29091"
|
||||
cx="165.90784"
|
||||
cy="101.36163"
|
||||
r="2.0022209" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.5 KiB |
52
webv2/index.html
Normal file
52
webv2/index.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Damus Web</title>
|
||||
|
||||
<link rel="stylesheet" href="damus.css?v=3">
|
||||
</head>
|
||||
<body>
|
||||
<section class="header">
|
||||
<span class="logo">
|
||||
<img src="img/damus-nobg.svg"/>
|
||||
</span>
|
||||
</section>
|
||||
<div class="container">
|
||||
<div id="posts">
|
||||
</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="send_reply()" id="reply-button">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=3"></script>
|
||||
<script src="damus.js?v=5"></script>
|
||||
<script>
|
||||
const relay = damus_web_init("4e8b44bb43018f79bd3efcdcd71af43814cdf996e0c62adedda1ac33bf5e1371")
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
1203
webv2/noble-secp256k1.js
Normal file
1203
webv2/noble-secp256k1.js
Normal file
File diff suppressed because it is too large
Load diff
360
webv2/nostr.js
Normal file
360
webv2/nostr.js
Normal file
|
@ -0,0 +1,360 @@
|
|||
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.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 = {}
|
||||
|
||||
init_websocket(me)
|
||||
|
||||
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()
|
||||
|
||||
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", 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
|
Loading…
Add table
Add a link
Reference in a new issue