move webv2 to master

no point having a branch for this.
This commit is contained in:
William Casarin 2022-10-27 14:11:43 -07:00
parent 232ca7c087
commit dfef728d91
9 changed files with 2733 additions and 0 deletions

1
webv2/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
tags

7
webv2/Makefile Normal file
View 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
View 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
View 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
View 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("<","&lt;").replaceAll(">","&gt;")
}
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
View 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
View 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

File diff suppressed because it is too large Load diff

360
webv2/nostr.js Normal file
View 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