
This feature involved a lot of refactoring in order to get working correctly. I wanted to continue using the timeline view for chats thus I used alternative styling & structure for DM event kinds. This worked create since the elements map does not care. There is some queing that has to be done to decrypt message content thus I allow viewing messages even if they haven't been decrypted yet. I think this is good for transparency since you understand what is and is not decrypted. I do think that the UX could improve, because even tho it is fast, it's flashes on new messages. I did not implement saving of latest messages. I will do this later, but this feature is big enough to merge as is: an alpha state that works. I further abstracted profile & name updating to work in a more global manner. Additionally I rewrote code that had attribute scripts to use addEventListener instead. This is needed to happen anyways for security and made the codebase easier to manage.
315 lines
6.6 KiB
JavaScript
315 lines
6.6 KiB
JavaScript
const KIND_METADATA = 0;
|
|
const KIND_NOTE = 1;
|
|
const KIND_RELAY = 2;
|
|
const KIND_CONTACT = 3;
|
|
const KIND_DM = 4;
|
|
const KIND_DELETE = 5;
|
|
const KIND_SHARE = 6;
|
|
const KIND_REACTION = 7;
|
|
const KIND_CHATROOM = 42;
|
|
|
|
const TAG_P = "#p";
|
|
const TAG_E = "#e";
|
|
|
|
const R_HEART = "❤️";
|
|
|
|
const STANDARD_KINDS = [
|
|
KIND_NOTE,
|
|
KIND_DM,
|
|
KIND_DELETE,
|
|
KIND_REACTION,
|
|
KIND_SHARE,
|
|
];
|
|
|
|
function get_local_state(key) {
|
|
if (DAMUS[key] != null)
|
|
return DAMUS[key]
|
|
return localStorage.getItem(key)
|
|
}
|
|
|
|
function set_local_state(key, val) {
|
|
DAMUS[key] = val
|
|
localStorage.setItem(key, val)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
async function broadcast_related_events(ev) {
|
|
ev.tags.reduce((evs, tag) => {
|
|
// cap it at something sane
|
|
if (evs.length >= 5)
|
|
return evs
|
|
const ev = get_tag_event(tag)
|
|
if (!ev)
|
|
return evs
|
|
return evs
|
|
}, [])
|
|
.forEach((ev, i) => {
|
|
// so we don't get rate limited
|
|
setTimeout(() => {
|
|
log_debug("broadcasting related event", ev)
|
|
broadcast_event(ev)
|
|
}, (i+1)*1200)
|
|
});
|
|
}
|
|
|
|
function broadcast_event(ev) {
|
|
DAMUS.pool.send(["EVENT", ev])
|
|
}
|
|
|
|
async function share(evid) {
|
|
const model = DAMUS;
|
|
const e = model.all_events[evid];
|
|
if (!e)
|
|
return;
|
|
let ev = {
|
|
kind: KIND_SHARE,
|
|
created_at: new_creation_time(),
|
|
pubkey: model.pubkey,
|
|
content: JSON.stringify(e),
|
|
tags: [["e", e.id], ["p", e.pubkey]],
|
|
}
|
|
ev.id = await nostrjs.calculate_id(ev);
|
|
ev = await sign_event(ev);
|
|
broadcast_event(ev);
|
|
return ev;
|
|
}
|
|
|
|
async function update_profile(profile={}) {
|
|
let ev = {
|
|
kind: KIND_METADATA,
|
|
created_at: new_creation_time(),
|
|
pubkey: DAMUS.pubkey,
|
|
content: JSON.stringify(profile),
|
|
tags: [],
|
|
};
|
|
ev.id = await nostrjs.calculate_id(ev);
|
|
ev = await sign_event(ev);
|
|
broadcast_event(ev);
|
|
return ev;
|
|
}
|
|
|
|
async function update_contacts() {
|
|
const model = DAMUS;
|
|
const contacts = Array.from(model.contacts.friends);
|
|
const tags = contacts.map((pubkey) => {
|
|
return ["p", pubkey]
|
|
});
|
|
let ev = {
|
|
kind: KIND_CONTACT,
|
|
created_at: new_creation_time(),
|
|
pubkey: model.pubkey,
|
|
content: "",
|
|
tags: tags,
|
|
}
|
|
ev.id = await nostrjs.calculate_id(ev);
|
|
ev = await sign_event(ev);
|
|
broadcast_event(ev);
|
|
return ev;
|
|
}
|
|
|
|
async function sign_event(ev) {
|
|
if (window.nostr && window.nostr.signEvent) {
|
|
const signed = await window.nostr.signEvent(ev)
|
|
if (typeof signed === 'string') {
|
|
ev.sig = signed
|
|
return ev
|
|
}
|
|
return signed
|
|
}
|
|
|
|
const privkey = get_privkey()
|
|
ev.sig = await sign_id(privkey, ev.id)
|
|
return ev
|
|
}
|
|
|
|
function new_reply_tags(ev) {
|
|
const tags = [["e", ev.id, "", "reply"]];
|
|
if (ev.refs.root) {
|
|
tags.push(["e", ev.refs.root, "", "root"]);
|
|
}
|
|
tags.push(["p", ev.pubkey]);
|
|
return tags;
|
|
}
|
|
|
|
async function create_reply(pubkey, content, ev, all=true) {
|
|
const tags = all ? gather_reply_tags(pubkey, ev) : new_reply_tags(ev);
|
|
const created_at = new_creation_time();
|
|
let kind = ev.kind;
|
|
|
|
// convert emoji replies into reactions
|
|
if (is_valid_reaction_content(content))
|
|
kind = 7;
|
|
let reply = {
|
|
pubkey,
|
|
tags,
|
|
content,
|
|
created_at,
|
|
kind
|
|
}
|
|
reply.id = await nostrjs.calculate_id(reply)
|
|
reply = await sign_event(reply)
|
|
return reply
|
|
}
|
|
|
|
async function send_reply(content, replying_to, all=true) {
|
|
const ev = DAMUS.all_events[replying_to]
|
|
if (!ev)
|
|
return;
|
|
|
|
const pubkey = await get_pubkey()
|
|
let reply = await create_reply(pubkey, content, ev, all)
|
|
|
|
broadcast_event(reply)
|
|
broadcast_related_events(reply)
|
|
}
|
|
|
|
async function create_deletion_event(pubkey, target, content="") {
|
|
const created_at = Math.floor(new Date().getTime() / 1000)
|
|
let kind = 5
|
|
|
|
const tags = [["e", target]]
|
|
let del = { pubkey, tags, content, created_at, kind }
|
|
|
|
del.id = await nostrjs.calculate_id(del)
|
|
del = await sign_event(del)
|
|
return del
|
|
}
|
|
|
|
async function delete_post(id, reason) {
|
|
const ev = DAMUS.all_events[id]
|
|
if (!ev)
|
|
return
|
|
|
|
const pubkey = await get_pubkey()
|
|
let del = await create_deletion_event(pubkey, id, reason)
|
|
broadcast_event(del)
|
|
}
|
|
|
|
function model_get_reacts_to(model, pubkey, evid, emoji) {
|
|
const r = model.reactions_to[evid];
|
|
if (!r)
|
|
return;
|
|
for (const id of r.keys()) {
|
|
if (model_is_event_deleted(model, id))
|
|
continue;
|
|
const reaction = model.all_events[id];
|
|
if (!reaction || reaction.pubkey != pubkey)
|
|
continue;
|
|
if (emoji == get_reaction_emoji(reaction))
|
|
return reaction;
|
|
}
|
|
return;
|
|
}
|
|
|
|
function get_reactions(model, evid) {
|
|
const reactions_set = model.reactions_to[evid]
|
|
if (!reactions_set)
|
|
return ""
|
|
|
|
let reactions = []
|
|
for (const id of reactions_set.keys()) {
|
|
if (model_is_event_deleted(model, id))
|
|
continue
|
|
const reaction = model.all_events[id]
|
|
if (!reaction)
|
|
continue
|
|
reactions.push(reaction)
|
|
}
|
|
|
|
const groups = reactions.reduce((grp, r) => {
|
|
const e = get_reaction_emoji(r)
|
|
grp[e] = grp[e] || {}
|
|
grp[e][r.pubkey] = r
|
|
return grp
|
|
}, {})
|
|
|
|
return groups
|
|
}
|
|
|
|
function gather_reply_tags(pubkey, from) {
|
|
let tags = []
|
|
let ids = new Set()
|
|
|
|
if (from.refs && from.refs.root) {
|
|
tags.push(["e", from.refs.root, "", "root"])
|
|
ids.add(from.refs.root)
|
|
}
|
|
|
|
tags.push(["e", from.id, "", "reply"])
|
|
ids.add(from.id)
|
|
|
|
for (const tag of from.tags) {
|
|
if (tag.length >= 2) {
|
|
if (tag[0] === "p" && tag[1] !== pubkey) {
|
|
if (!ids.has(tag[1])) {
|
|
tags.push(["p", tag[1]])
|
|
ids.add(tag[1])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (from.pubkey !== pubkey && !ids.has(from.pubkey)) {
|
|
tags.push(["p", from.pubkey])
|
|
}
|
|
return tags
|
|
}
|
|
|
|
function get_tag_event(tag) {
|
|
const model = DAMUS;
|
|
if (tag.length < 2)
|
|
return null
|
|
if (tag[0] === "e")
|
|
return model.all_events[tag[1]]
|
|
if (tag[0] === "p") {
|
|
let profile = model_get_profile(model, tag[1]);
|
|
if (profile.evid)
|
|
return model.all_events[profile.evid];
|
|
}
|
|
return null
|
|
}
|
|
|
|
function* yield_etags(tags) {
|
|
for (const tag of tags) {
|
|
if (tag.length >= 2 && tag[0] === "e")
|
|
yield tag
|
|
}
|
|
}
|
|
|
|
function get_content_warning(tags) {
|
|
for (const tag of tags) {
|
|
if (tag.length >= 1 && tag[0] === "content-warning")
|
|
return tag[1] || ""
|
|
}
|
|
return null
|
|
}
|
|
|
|
async function get_nip05_pubkey(email) {
|
|
const [user, host] = email.split("@")
|
|
const url = `https://${host}/.well-known/nostr.json?name=${user}`
|
|
|
|
try {
|
|
const res = await fetch(url)
|
|
const json = await res.json()
|
|
log_debug("nip05 data", json)
|
|
return json.names[user]
|
|
} catch (e) {
|
|
log_error("fetching nip05 entry for %s", email, e)
|
|
throw e
|
|
}
|
|
}
|
|
|
|
// TODO rename handle_pubkey to fetch_pubkey
|
|
async function handle_pubkey(pubkey) {
|
|
if (pubkey[0] === "n")
|
|
pubkey = bech32_decode(pubkey)
|
|
if (pubkey.includes("@"))
|
|
pubkey = await get_nip05_pubkey(pubkey)
|
|
set_local_state('pubkey', pubkey)
|
|
return pubkey
|
|
}
|
|
|