diff --git a/README.md b/README.md index f251efa5..a41384a9 100644 --- a/README.md +++ b/README.md @@ -58,18 +58,24 @@ If you like Coracle and want to support its development, you can donate sats via - [x] Use user relays for feeds - [x] Publish to user relays + target relays: - [x] Add correct recommended relay to tags -- [ ] Support some read/write config on relays page -- [ ] Get real home relays for default pubkeys +- [ ] Relays + - [ ] Support some read/write config + - [ ] Get real home relays for defaults.petnames + - [ ] Add support for astral's relay hack (but don't publish to it) - [ ] Add settings storage on nostr, maybe use kind 0? - [ ] Warn that everything will be cleared on logout -- [ ] Clean up login page to prefer extension, make private key entry "advanced" +- [ ] Login + - [ ] Prefer extension, make private key entry "advanced" + - [ ] Improve login UX for bootstrap delay. Nostr facts? - [ ] Connection management - [ ] Do I need to implement re-connecting now? - [ ] Handle failed connections - [ ] Close connections that haven't been used in a while -- [ ] Improve login UX for bootstrap delay. Nostr facts? - [ ] We often get the root as the reply, figure out why that is, compared to astral/damus -- [ ] Load feeds from network rather than user relays? This could make global feed more useful: global for _my_ relays +- [ ] Load feeds from network rather than user relays? + - Still use "my" relays for global, this could make global feed more useful +- [ ] Figure out migrations from previous version +- [ ] Add relays/mentions to note and reply composition ## 0.2.7 diff --git a/src/App.svelte b/src/App.svelte index 1d9eaaed..ba765e91 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -15,6 +15,7 @@ import {modal, toast, settings, alerts} from "src/app" import {routes} from "src/app/ui" import Anchor from 'src/partials/Anchor.svelte' + import Spinner from 'src/partials/Spinner.svelte' import Modal from 'src/partials/Modal.svelte' import NoteDetailModal from "src/views/NoteDetail.svelte" import PersonSettings from "src/views/PersonSettings.svelte" @@ -211,6 +212,9 @@ {:else if $modal.form === 'person/settings'} + {:else if $modal.message} +

{$modal.message}

+ {/if} {/if} diff --git a/src/agent/data.js b/src/agent/data.js index 1e50c502..4d86e5de 100644 --- a/src/agent/data.js +++ b/src/agent/data.js @@ -28,23 +28,35 @@ people.subscribe($p => { export const getPerson = (pubkey, fallback = false) => $people[pubkey] || (fallback ? {pubkey} : null) +export const updatePeople = async updates => { + // Sync to our in memory copy + people.update($people => ({...$people, ...updates})) + + // Sync to our database + await db.people.bulkPut(Object.values(updates)) +} + // Hooks export const processEvents = async events => { const profileEvents = ensurePlural(events) .filter(e => personKinds.includes(e.kind)) - const profileUpdates = {} + const updates = {} for (const e of profileEvents) { - profileUpdates[e.pubkey] = { + updates[e.pubkey] = { ...getPerson(e.pubkey, true), - ...profileUpdates[e.pubkey], + ...updates[e.pubkey], ...switcherFn(e.kind, { 0: () => JSON.parse(e.content), - 2: () => ({relays: ($people[e.pubkey]?.relays || []).concat(e.content)}), + 2: () => ({ + relays: ($people[e.pubkey]?.relays || []).concat({url: e.content}), + }), 3: () => ({petnames: e.tags}), 12165: () => ({muffle: e.tags}), - 10001: () => ({relays: e.tags.map(t => t[0])}), + 10001: () => ({ + relays: e.tags.map(([url, read, write]) => ({url, read, write})), + }), default: () => { console.log(`Received unsupported event type ${event.kind}`) }, @@ -53,9 +65,5 @@ export const processEvents = async events => { } } - // Sync to our in memory copy - people.update($people => ({...$people, ...profileUpdates})) - - // Sync to our database - await db.people.bulkPut(Object.values(profileUpdates)) + await updatePeople(updates) } diff --git a/src/agent/defaults.js b/src/agent/defaults.js index 6f982f5f..0b5ea3e8 100644 --- a/src/agent/defaults.js +++ b/src/agent/defaults.js @@ -1,15 +1,17 @@ export default { petnames: [ - ["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "fiatjaf", "wss://relay.damus.io"], + ["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "fiatjaf", "wss://nostr-pub.wellorder.net"], ["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", "jb55", "wss://relay.damus.io"], - ["p", "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", "hodlbod", "wss://relay.damus.io"], - ["p", "472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e", "MartyBent", "wss://relay.damus.io"], - ["p", "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", "jack", "wss://relay.damus.io"], + ["p", "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", "hodlbod", "wss://nostr-pub.wellorder.net"], + ["p", "472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e", "MartyBent", "wss://relay.damus.io"], + ["p", "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", "jack", "wss://brb.io"], ["p", "85080d3bad70ccdcd7f74c29a44f55bb85cbcd3dd0cbb957da1d215bdb931204", "preston", "wss://relay.damus.io"], ], relays: [ - 'wss://relay.damus.io', - 'wss://nostr.zebedee.cloud', - 'wss://nostr-pub.wellorder.net', + {url: 'wss://brb.io'}, + {url: 'wss://relay.damus.io'}, + {url: 'wss://nostr.zebedee.cloud'}, + {url: 'wss://nostr-relay.wlvs.space'}, + {url: 'wss://nostr-pub.wellorder.net'}, ], } diff --git a/src/agent/pool.js b/src/agent/pool.js index 9ff1b1f5..c34e89ac 100644 --- a/src/agent/pool.js +++ b/src/agent/pool.js @@ -1,60 +1,94 @@ import {relayInit} from 'nostr-tools' -import {uniqBy, is, filter, identity, prop} from 'ramda' -import {isRelay} from 'src/util/nostr' +import {uniqBy, prop, find, whereEq, is, filter, identity} from 'ramda' import {ensurePlural} from 'hurdak/lib/hurdak' +import {isRelay} from 'src/util/nostr' +import {sleep} from 'src/util/misc' -const relays = {} +const connections = [] -const init = url => { - const relay = relayInit(url) +class Connection { + constructor(url) { + this.nostr = this.init(url) + this.status = 'new' + this.url = url + this.stats = { + count: 0, + timer: 0, + timeouts: 0, + activeCount: 0, + } - relay.url = url - relay.stats = { - count: 0, - timer: 0, - timeouts: 0, - activeCount: 0, + connections.push(this) } + init(url) { + const nostr = relayInit(url) - relay.on('error', () => { - console.log(`failed to connect to ${url}`) - }) + nostr.on('error', () => { + console.log(`failed to connect to ${url}`) + }) - relay.on('disconnect', () => { - delete relays[url] - }) + nostr.on('disconnect', () => { + delete connections[url] + }) - // Do initialization synchonously and wait on retrieval - // so we don't open multiple connections simultaneously - return relay.connect().then( - () => relay, - e => console.log(`Failed to connect to ${url}: ${e}`) - ) + return nostr + } + async connect() { + const shouldConnect = ( + this.status === 'new' + || ( + this.status === 'error' + && Date.now() - this.lastRequest > 30_000 + ) + ) + + if (shouldConnect) { + this.status = 'pending' + + try { + await this.nostr.connect() + this.status = 'ready' + } catch (e) { + console.error(`Failed to connect to ${this.url}: ${e}`) + this.status = 'error' + } + } + + this.lastRequest = Date.now() + + return this + } } -const connect = url => { - if (!relays[url]) { - relays[url] = init(url) - } +const findConnection = url => find(whereEq({url}), connections) - return relays[url] +const connect = async url => { + const conn = findConnection(url) || new Connection(url) + + await Promise.race([conn.connect(), sleep(5000)]) + + if (conn.status === 'ready') { + return conn + } } -const publish = async (urls, event) => { +const publish = async (relays, event) => { return Promise.all( - urls.filter(isRelay).map(async url => { - const relay = await connect(url) + relays.filter(r => r.read !== '!' & isRelay(r.url)).map(async relay => { + const conn = await connect(relay.url) - if (relay) { - return relay.publish(event) + if (conn) { + return conn.nostr.publish(event) } }) ) } -const describeFilter = filter => { +const describeFilter = ({kinds = [], ...filter}) => { let parts = [] + parts.push(kinds.join(',')) + for (const [key, value] of Object.entries(filter)) { if (is(Array, value)) { parts.push(`${key}[${value.length}]`) @@ -63,10 +97,11 @@ const describeFilter = filter => { } } - return parts.join(',') + return '(' + parts.join(',') + ')' } -const subscribe = async (urls, filters) => { +const subscribe = async (relays, filters) => { + relays = uniqBy(prop('url'), relays.filter(r => isRelay(r.url))) filters = ensurePlural(filters) // Create a human readable subscription id for debugging @@ -75,25 +110,22 @@ const subscribe = async (urls, filters) => { filters.map(describeFilter).join(':'), ].join('-') - console.log(filters, id) - const subs = filter(identity, await Promise.all( - urls.filter(isRelay).map(async url => { - const relay = await connect(url) + relays.map(async relay => { + const conn = await connect(relay.url) // If the relay failed to connect, give up - if (!relay) { + if (!conn) { return null } + const sub = conn.nostr.sub(filters, {id}) - const sub = relay.sub(filters, {id}) + sub.conn = conn + sub.conn.stats.activeCount += 1 - sub.relay = relay - sub.relay.stats.activeCount += 1 - - if (sub.relay.stats.activeCount > 10) { - console.warn(`Relay ${url} has >10 active subscriptions`) + if (sub.conn.stats.activeCount > 10) { + console.warn(`Relay ${sub.url} has >10 active subscriptions`) } return sub @@ -103,17 +135,18 @@ const subscribe = async (urls, filters) => { const seen = new Set() return { + subs, unsub: () => { subs.forEach(sub => { sub.unsub() - sub.relay.stats.activeCount -= 1 + sub.conn.stats.activeCount -= 1 }) }, onEvent: cb => { subs.forEach(sub => { sub.on('event', e => { if (!seen.has(e.id)) { - e.seen_on = sub.relay.url + e.seen_on = sub.conn.url seen.add(e.id) cb(e) } @@ -122,60 +155,69 @@ const subscribe = async (urls, filters) => { }, onEose: cb => { subs.forEach(sub => { - sub.on('eose', () => cb(sub.relay.url)) + sub.on('eose', () => cb(sub.conn.url)) }) }, } } -const request = (urls, filters) => { - urls = urls.filter(isRelay) +const request = (relays, filters) => { + relays = uniqBy(prop('url'), relays.filter(r => isRelay(r.url))) return new Promise(async resolve => { - const subscription = await subscribe(urls, filters) + const agg = await subscribe(relays, filters) const now = Date.now() const events = [] const eose = [] - const done = () => { - subscription.unsub() + const attemptToComplete = () => { + // If we have all relays, most after a short timeout, or all after + // a long timeout, go ahead and unsubscribe. + const done = ( + eose.length === agg.subs.length + || Date.now() - now >= 5000 + || ( + Date.now() - now >= 1000 + && eose.length > agg.subs.length - Math.round(agg.subs.length / 10) + ) + ) - resolve(uniqBy(prop('id'), events)) + if (done) { + agg.unsub() + resolve(events) - // Keep track of relay timeouts - urls.forEach(async url => { - if (!eose.includes(url)) { - const relay = await connect(url) + // Keep track of relay timeouts + agg.subs.forEach(async sub => { + if (!eose.includes(sub.conn.url)) { + const conn = findConnection(sub.conn.url) - // Relay may be undefined if we failed to connect - if (relay) { - relay.stats.count += 1 - relay.stats.timer += Date.now() - now - relay.stats.timeouts += 1 + conn.stats.count += 1 + conn.stats.timer += Date.now() - now + conn.stats.timeouts += 1 } - } - }) + }) + } } - subscription.onEvent(e => events.push(e)) + agg.onEvent(e => events.push(e)) - subscription.onEose(async url => { - const relay = await relays[url] + agg.onEose(async url => { + const conn = findConnection(url) - eose.push(url) + if (!eose.includes(url)) { + eose.push(url) - // Keep track of relay timing stats - relay.stats.count += 1 - relay.stats.timer += Date.now() - now - - if (eose.length === urls.length) { - done() + // Keep track of relay timing stats + conn.stats.count += 1 + conn.stats.timer += Date.now() - now } + + attemptToComplete() }) // If a relay takes too long, give up - setTimeout(done, 5000) + setTimeout(attemptToComplete, 5000) }) } -export default {relays, connect, publish, subscribe, request} +export default {connect, publish, subscribe, request} diff --git a/src/app/alerts.js b/src/app/alerts.js index f0fe9b91..edc7b1cb 100644 --- a/src/app/alerts.js +++ b/src/app/alerts.js @@ -3,7 +3,7 @@ import {synced, batch, now} from 'src/util/misc' import {isAlert} from 'src/util/nostr' import {load as _load, listen as _listen, getMuffle, db} from 'src/agent' import loaders from 'src/app/loaders' -import query from 'src/app/query' +import {threadify} from 'src/app' let listener @@ -15,7 +15,7 @@ const onChunk = async (relays, pubkey, events) => { if (events.length > 0) { const context = await loaders.loadContext(relays, events) - const notes = query.threadify(events, context, {muffle: getMuffle()}) + const notes = threadify(events, context, {muffle: getMuffle()}) await db.alerts.bulkPut(notes) diff --git a/src/app/cmd.js b/src/app/cmd.js index d7266005..26f3b0a8 100644 --- a/src/app/cmd.js +++ b/src/app/cmd.js @@ -7,8 +7,8 @@ import {keys, publish, getRelays} from 'src/agent' const updateUser = (relays, updates) => publishEvent(relays, 0, {content: JSON.stringify(updates)}) -const setRelays = relays => - publishEvent(relays, 10001, {tags: relays.map(url => [url, "", ""])}) +const setRelays = (relays, tags) => + publishEvent(relays, 10001, {tags}) const setPetnames = (relays, petnames) => publishEvent(relays, 3, {tags: petnames}) diff --git a/src/app/index.js b/src/app/index.js index a9799f2f..cd8115b4 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -1,11 +1,14 @@ -import {without} from 'ramda' +import {whereEq, sortBy, identity, when, assoc, reject} from 'ramda' +import {createMap, ellipsize} from 'hurdak/lib/hurdak' import {get} from 'svelte/store' -import {getPerson, getRelays, load, keys} from 'src/agent' -import {toast, modal, settings} from 'src/app/ui' +import {renderContent} from 'src/util/html' +import {Tags, displayPerson, findReplyId} from 'src/util/nostr' +import {user, people, getPerson, getRelays, load, keys} from 'src/agent' +import defaults from 'src/agent/defaults' +import {toast, routes, modal, settings} from 'src/app/ui' import cmd from 'src/app/cmd' import alerts from 'src/app/alerts' import loaders from 'src/app/loaders' -import query from 'src/app/query' export {toast, modal, settings, alerts} @@ -23,26 +26,49 @@ export const login = async ({privkey, pubkey}) => { ]) } -export const addRelay = async url => { - const pubkey = get(keys.pubkey) - const person = getPerson(pubkey) - const relays = (person?.relays || []).concat(url) +export const addRelay = async relay => { + const person = get(user) + const modify = relays => relays.concat(relay) - await cmd.setRelays(relays) + // Set to defaults to support anonymous usage + defaults.relays = modify(defaults.relays) - await Promise.all([ - loaders.loadNetwork(getRelays(), pubkey), - alerts.load(getRelays(), pubkey), - alerts.listen(getRelays(), pubkey), - ]) + if (person) { + const relays = modify(person.relays || []) + + // Publish to the new set of relays + await cmd.setRelays(relays, relays) + + await Promise.all([ + loaders.loadNetwork(relays, person.pubkey), + alerts.load(relays, person.pubkey), + alerts.listen(relays, person.pubkey), + ]) + } } export const removeRelay = async url => { - const pubkey = get(keys.pubkey) - const person = getPerson(pubkey) - const relays = person?.relays || [] + const person = get(user) + const modify = relays => reject(whereEq({url}), relays) - await cmd.setRelays(without([url], relays)) + // Set to defaults to support anonymous usage + defaults.relays = modify(defaults.relays) + + if (person) { + await cmd.setRelays(getRelays(), modify(person.relays || [])) + } +} + +export const setRelayWriteCondition = async (url, write) => { + const person = get(user) + const modify = relays => relays.map(when(whereEq({url}), assoc('write', write))) + + // Set to defaults to support anonymous usage + defaults.relays = modify(defaults.relays) + + if (person) { + await cmd.setRelays(getRelays(), modify(person.relays || [])) + } } export const loadNote = async (relays, id) => { @@ -53,10 +79,79 @@ export const loadNote = async (relays, id) => { } const context = await loaders.loadContext(relays, found) - const note = query.annotate(found, context, {showEntire: true, depth: 3}) + const note = annotate(found, context) // Log this for debugging purposes console.log('loadNote', note) return note } + +export const render = (note, {showEntire = false}) => { + const shouldEllipsize = note.content.length > 500 && !showEntire + const $people = get(people) + const peopleByPubkey = createMap( + 'pubkey', + Tags.from(note).type("p").values().all().map(k => $people[k]).filter(identity) + ) + + let content + + // Ellipsize + content = shouldEllipsize ? ellipsize(note.content, 500) : note.content + + // Escape html, replace urls + content = renderContent(content) + + // Mentions + content = content + .replace(/#\[(\d+)\]/g, (tag, i) => { + if (!note.tags[parseInt(i)]) { + return tag + } + + const pubkey = note.tags[parseInt(i)][1] + const person = peopleByPubkey[pubkey] || {pubkey} + const name = displayPerson(person) + const path = routes.person(pubkey) + + return `@${name}` + }) + + return content +} + +export const annotate = (note, context) => { + const reactions = context.filter(e => e.kind === 7 && findReplyId(e) === note.id) + const replies = context.filter(e => e.kind === 1 && findReplyId(e) === note.id) + + return { + ...note, reactions, + person: getPerson(note.pubkey), + replies: sortBy(e => e.created_at, replies).map(r => annotate(r, context)), + } +} + +export const threadify = (events, context, {muffle = []} = {}) => { + const contextById = createMap('id', context) + + // Show parents when possible. For reactions, if there's no parent, + // throw it away. Sort by created date descending + const notes = sortBy( + e => -e.created_at, + events + .map(e => contextById[findReplyId(e)] || (e.kind === 1 ? e : null)) + .filter(e => e && !muffle.includes(e.pubkey)) + ) + + // Annotate our feed with parents, reactions, replies + return notes.map(note => { + let parent = contextById[findReplyId(note)] + + if (parent) { + parent = annotate(parent, context) + } + + return annotate({...note, parent}, context) + }) +} diff --git a/src/app/loaders.js b/src/app/loaders.js index e187c881..e0d34c4d 100644 --- a/src/app/loaders.js +++ b/src/app/loaders.js @@ -1,4 +1,4 @@ -import {uniq, flatten, pluck, groupBy, identity} from 'ramda' +import {uniqBy, prop, uniq, flatten, pluck, groupBy, identity} from 'ramda' import {ensurePlural, createMap, chunk} from 'hurdak/lib/hurdak' import {findReply, personKinds, Tags, getTagValues} from 'src/util/nostr' import {now, timedelta} from 'src/util/misc' @@ -78,7 +78,13 @@ const loadContext = async (relays, notes, {loadParents = true} = {}) => { const parents = getTagValues(parentTags).map(id => eventsById[id]).filter(identity) const parentRelays = Tags.from(parents).relays() - return events.concat(await loadContext(parentRelays, parents, {loadParents: false})) + // We're recurring and so may end up with duplicates here + return uniqBy( + prop('id'), + events.concat( + await loadContext(parentRelays, parents, {loadParents: false}) + ) + ) }) )) } diff --git a/src/app/query.js b/src/app/query.js deleted file mode 100644 index 754052d4..00000000 --- a/src/app/query.js +++ /dev/null @@ -1,84 +0,0 @@ -import {get} from 'svelte/store' -import {sortBy, identity} from 'ramda' -import {createMap, ellipsize} from 'hurdak/lib/hurdak' -import {renderContent} from 'src/util/html' -import {Tags, displayPerson, findReplyId} from 'src/util/nostr' -import {people, getPerson} from 'src/agent' -import {routes} from "src/app/ui" - -const renderNote = (note, {showEntire = false}) => { - const shouldEllipsize = note.content.length > 500 && !showEntire - const $people = get(people) - const peopleByPubkey = createMap( - 'pubkey', - Tags.from(note).type("p").values().all().map(k => $people[k]).filter(identity) - ) - - let content - - // Ellipsize - content = shouldEllipsize ? ellipsize(note.content, 500) : note.content - - // Escape html, replace urls - content = renderContent(content) - - // Mentions - content = content - .replace(/#\[(\d+)\]/g, (tag, i) => { - if (!note.tags[parseInt(i)]) { - return tag - } - - const pubkey = note.tags[parseInt(i)][1] - const person = peopleByPubkey[pubkey] || {pubkey} - const name = displayPerson(person) - const path = routes.person(pubkey) - - return `@${name}` - }) - - return content -} - -const annotate = (note, context, {showEntire = false, depth = 1} = {}) => { - const reactions = context.filter(e => e.kind === 7 && findReplyId(e) === note.id) - const replies = context.filter(e => e.kind === 1 && findReplyId(e) === note.id) - - return { - ...note, reactions, - html: renderNote(note, {showEntire}), - person: getPerson(note.pubkey), - repliesCount: replies.length, - replies: depth === 0 - ? [] - : sortBy(e => e.created_at, replies) - .slice(showEntire ? 0 : -3) - .map(r => annotate(r, context, {depth: depth - 1})) - } -} - -const threadify = (events, context, {muffle = []} = {}) => { - const contextById = createMap('id', context) - - // Show parents when possible. For reactions, if there's no parent, - // throw it away. Sort by created date descending - const notes = sortBy( - e => -e.created_at, - events - .map(e => contextById[findReplyId(e)] || (e.kind === 1 ? e : null)) - .filter(e => e && !muffle.includes(e.pubkey)) - ) - - // Annotate our feed with parents, reactions, replies - return notes.map(note => { - let parent = contextById[findReplyId(note)] - - if (parent) { - parent = annotate(parent, context) - } - - return annotate({...note, parent}, context) - }) -} - -export default {renderNote, threadify, annotate} diff --git a/src/partials/Input.svelte b/src/partials/Input.svelte index ff529978..e5550ce6 100644 --- a/src/partials/Input.svelte +++ b/src/partials/Input.svelte @@ -6,7 +6,7 @@ const className = cx( $$props.class, - "rounded bg-light shadow-inset py-2 px-4 pr-10 text-black w-full placeholder:text-medium", + "rounded bg-light shadow-inset py-2 px-4 pr-10 text-black w-full placeholder:text-placeholder", {"pl-10": $$slots.before, "pr-10": $$slots.after}, ) diff --git a/src/partials/Note.svelte b/src/partials/Note.svelte index f2d2868e..ae3d39f5 100644 --- a/src/partials/Note.svelte +++ b/src/partials/Note.svelte @@ -2,7 +2,7 @@ import cx from 'classnames' import extractUrls from 'extract-urls' import {nip19} from 'nostr-tools' - import {whereEq, reject, propEq, find} from 'ramda' + import {whereEq, pluck, reject, propEq, find} from 'ramda' import {slide} from 'svelte/transition' import {navigate} from 'svelte-routing' import {quantify} from 'hurdak/lib/hurdak' @@ -10,7 +10,7 @@ import {findReply, findReplyId, isLike} from "src/util/nostr" import Preview from 'src/partials/Preview.svelte' import Anchor from 'src/partials/Anchor.svelte' - import {settings, modal} from "src/app" + import {settings, modal, render} from "src/app" import {formatTimestamp} from 'src/util/misc' import Badge from "src/partials/Badge.svelte" import Compose from "src/partials/Compose.svelte" @@ -27,7 +27,8 @@ let reply = null const links = $settings.showLinkPreviews ? extractUrls(note.content) || [] : null - const interactive = !anchorId || anchorId !== note.id + const showEntire = anchorId === note.id + const interactive = !anchorId || !showEntire const relays = getEventRelays(note) let likes, flags, like, flag @@ -48,7 +49,7 @@ const goToParent = async () => { const [id, url] = findReply(note).slice(1) - const relays = getEventRelays(note).concat(url) + const relays = getEventRelays(note).concat({url}) modal.set({note: {id}, relays}) } @@ -117,7 +118,7 @@
{formatTimestamp(note.created_at)} @@ -136,7 +137,7 @@

{:else}
-

{@html note.html}

+

{@html render(note, {showEntire})}

{#each links.slice(-2) as link}
e.stopPropagation()}> @@ -150,7 +151,7 @@ - {note.repliesCount} + {note.replies.length}
0}
- {#if note.repliesCount > 3 && note.replies.length < note.repliesCount} + {#if !showEntire && note.replies.length > 3}
- Show {quantify(note.repliesCount - note.replies.length, 'other reply', 'more replies')} + Show {quantify(note.replies.length - 3, 'other reply', 'more replies')}
{/if} - {#each note.replies as r (r.id)} + {#each note.replies.slice(showEntire ? 0 : -3) as r (r.id)} {/each}
diff --git a/src/partials/Notes.svelte b/src/partials/Notes.svelte index ad5b2669..1908d75d 100644 --- a/src/partials/Notes.svelte +++ b/src/partials/Notes.svelte @@ -6,6 +6,7 @@ import {createScroller} from 'src/util/misc' import Spinner from 'src/partials/Spinner.svelte' import Note from "src/partials/Note.svelte" + import {modal} from "src/app" export let loadNotes export let listenForNotes @@ -30,6 +31,10 @@ }) const scroller = createScroller(async () => { + if ($modal) { + return + } + // Drop notes at the top if there are a lot notes = uniqBy(prop('id'), notes.concat(await loadNotes()).slice(-maxNotes)) }) diff --git a/src/partials/Toggle.svelte b/src/partials/Toggle.svelte index 941f57a5..3e82a078 100644 --- a/src/partials/Toggle.svelte +++ b/src/partials/Toggle.svelte @@ -1,10 +1,14 @@ diff --git a/src/routes/Bech32Entity.svelte b/src/routes/Bech32Entity.svelte index 4a2fe7a9..76be6174 100644 --- a/src/routes/Bech32Entity.svelte +++ b/src/routes/Bech32Entity.svelte @@ -1,4 +1,5 @@
{#if type === "nevent"} - + {:else if type === "note"} {:else if type === "nprofile"} - + {:else if type === "npub"} {/if} diff --git a/src/routes/Login.svelte b/src/routes/Login.svelte index 8e515035..7b25c282 100644 --- a/src/routes/Login.svelte +++ b/src/routes/Login.svelte @@ -34,9 +34,9 @@ const logIn = async ({privkey, pubkey}) => { loading = true - await login({privkey, pubkey}) + login({privkey, pubkey}) - navigate('/notes/network') + navigate('/relays') } const logInWithExtension = async () => { @@ -64,7 +64,7 @@

Welcome!

- To the Nostr Protocol Network + To the Nostr Protocol

diff --git a/src/routes/Person.svelte b/src/routes/Person.svelte index b62bb55d..d22d1ba9 100644 --- a/src/routes/Person.svelte +++ b/src/routes/Person.svelte @@ -59,7 +59,7 @@ following = true const relay = first(relays || getRelays(pubkey)) - const tag = ["p", pubkey, relay, person.name || ""] + const tag = ["p", pubkey, relay.url, person.name || ""] const petnames = reject(t => t[1] === pubkey, $user.petnames).concat(tag) cmd.setPetnames(getRelays(), petnames) diff --git a/src/routes/RelayList.svelte b/src/routes/RelayList.svelte index d1f47c5d..44c0c1cb 100644 --- a/src/routes/RelayList.svelte +++ b/src/routes/RelayList.svelte @@ -1,13 +1,14 @@ -

-
-
-

Get Connected

-

- Relays are hubs for your content and connections. At least one is required to - interact with the network, but you can join as many as you like. -

-
-
+
+
+
+

Your relays

- {#each ($knownRelays || []) as r} - {#if relays.includes(r.url)} -
-
- {r.name || r.url} -

{r.description || ''}

-
- leave(r.url)}> - Leave - -
- {/if} - {/each} - {#if ($knownRelays || []).length > 0} -
-

Other relays

-
- - - - modal.set({form: 'relay', url: q})}> - Add Relay - -
- {/if} - {#each (search(q) || []).slice(0, 50) as r} - {#if !relays.includes(r.url)} -
-
- {r.name || r.url} -

{r.description || ''}

-
- join(r.url)}> - Join - -
- {/if} - {/each} - - Showing {Math.min(($knownRelays || []).length, 50)} of {($knownRelays || []).length} known relays -
+ modal.set({form: 'relay', url: q})}> + Add Relay +
+

+ Relays are hubs for your content and connections. At least one is required to + interact with the network, but you can join as many as you like. +

+
+ {#each relays as {url, write}, i} +
+
+
+ + + {last(url.split('://'))} + + leave(url)}/> +
+

placeholder for description

+
+
+
+ Publish to this relay? + setRelayWriteCondition(url, write === "!" ? "" : "!")} /> +
+
+ {/each} +
+ {#if ($knownRelays || []).length > 0} +
+
+ +

Other relays

+
+ + + + {/if} + {#each (search(q) || []).slice(0, 50) as {url, name, description}} + {#if !find(whereEq({url}), relays)} +
+
+ {name || url} +

{description || ''}

+
+ join(url)}> + Join + +
+ {/if} + {/each} + + Showing {Math.min(($knownRelays || []).length - relays.length, 50)} + of {($knownRelays || []).length - relays.length} known relays +
diff --git a/src/util/nostr.js b/src/util/nostr.js index fb0f1b70..7368bb94 100644 --- a/src/util/nostr.js +++ b/src/util/nostr.js @@ -1,4 +1,4 @@ -import {last, prop, flatten, uniq} from 'ramda' +import {last, identity, objOf, prop, flatten, uniq} from 'ramda' import {nip19} from 'nostr-tools' import {ensurePlural, first} from 'hurdak/lib/hurdak' @@ -12,7 +12,7 @@ export class Tags { return new Tags(ensurePlural(events).flatMap(prop('tags'))) } static wrap(tags) { - return new Tags(tags) + return new Tags(tags.filter(identity)) } all() { return this.tags @@ -24,7 +24,7 @@ export class Tags { return last(this.tags) } relays() { - return uniq(flatten(this.tags).filter(isRelay)) + return uniq(flatten(this.tags).filter(isRelay)).map(objOf('url')) } values() { this.tags = this.tags.map(t => t[1]) @@ -49,12 +49,12 @@ export const getTagValues = tags => tags.map(t => t[1]) export const findReply = e => Tags.from(e).type("e").mark("reply").first() || Tags.from(e).type("e").first() -export const findReplyId = e => first(Tags.wrap(findReply(e)).values()) +export const findReplyId = e => Tags.wrap([findReply(e)]).values().first() export const findRoot = e => Tags.from(e).type("e").mark("root").first() -export const findRootId = e => first(Tags.wrap(findRoot(e)).values()) +export const findRootId = e => Tags.wrap([findRoot(e)]).values().first() export const displayPerson = p => { if (p.name) { @@ -81,4 +81,3 @@ export const isAlert = (e, pubkey) => { } export const isRelay = url => typeof url === 'string' && url.match(/^wss?:\/\/.+/) - diff --git a/src/views/NoteDetail.svelte b/src/views/NoteDetail.svelte index c8daf536..0a8a0c73 100644 --- a/src/views/NoteDetail.svelte +++ b/src/views/NoteDetail.svelte @@ -1,12 +1,11 @@ - -{#if search} -
    - {#await Promise.all(search(q).slice(0, 30).map(n => query.findNote(n.id)))} - - {:then results} - {#each results as e (e.id)} -
  • - -
  • - {/each} - {/await} -
-{:else} - -{/if} diff --git a/src/views/notes/Global.svelte b/src/views/notes/Global.svelte index f00585f7..d9ba6fa4 100644 --- a/src/views/notes/Global.svelte +++ b/src/views/notes/Global.svelte @@ -3,7 +3,7 @@ import {Cursor, now, batch} from 'src/util/misc' import {getRelays, getMuffle, listen, load} from 'src/agent' import loaders from 'src/app/loaders' - import query from 'src/app/query' + import {threadify} from 'src/app' const relays = getRelays() const filter = {kinds: [1, 5, 7]} @@ -13,7 +13,7 @@ listen(relays, {...filter, since: now()}, batch(300, async notes => { const context = await loaders.loadContext(relays, notes) - onNotes(query.threadify(notes, context, {muffle: getMuffle()})) + onNotes(threadify(notes, context, {muffle: getMuffle()})) })) const loadNotes = async () => { @@ -21,11 +21,7 @@ const notes = await load(relays, {...filter, limit, until}) const context = await loaders.loadContext(relays, notes) - console.log('========') - console.log({notes, context}) - console.log(query.threadify(notes, context, {muffle: getMuffle()})) - - return query.threadify(notes, context, {muffle: getMuffle()}) + return threadify(notes, context, {muffle: getMuffle()}) } diff --git a/src/views/notes/Network.svelte b/src/views/notes/Network.svelte index 18cb4a58..fab3f9cd 100644 --- a/src/views/notes/Network.svelte +++ b/src/views/notes/Network.svelte @@ -3,7 +3,7 @@ import {now, Cursor, shuffle, batch} from 'src/util/misc' import {user, getRelays, getFollows, getMuffle, listen, load} from 'src/agent' import loaders from 'src/app/loaders' - import query from 'src/app/query' + import {threadify} from 'src/app' // Get first- and second-order follows. shuffle and slice network so we're not // sending too many pubkeys. This will also result in some variety. @@ -18,7 +18,7 @@ listen(relays, {...filter, since: now()}, batch(300, async notes => { const context = await loaders.loadContext(relays, notes) - onNotes(query.threadify(notes, context, {muffle: getMuffle()})) + onNotes(threadify(notes, context, {muffle: getMuffle()})) })) const loadNotes = async () => { @@ -28,7 +28,7 @@ cursor.onChunk(notes) - return query.threadify(notes, context, {muffle: getMuffle()}) + return threadify(notes, context, {muffle: getMuffle()}) } diff --git a/src/views/person/Likes.svelte b/src/views/person/Likes.svelte index c49a66d2..833aa33a 100644 --- a/src/views/person/Likes.svelte +++ b/src/views/person/Likes.svelte @@ -3,7 +3,7 @@ import {now, batch, Cursor} from 'src/util/misc' import {load, listen, getRelays, getMuffle} from 'src/agent' import loaders from 'src/app/loaders' - import query from 'src/app/query' + import {threadify} from 'src/app' export let pubkey @@ -15,7 +15,7 @@ listen(relays, {...filter, since: now()}, batch(300, async notes => { const context = await loaders.loadContext(relays, notes) - onNotes(query.threadify(notes, context, {muffle: getMuffle()})) + onNotes(threadify(notes, context, {muffle: getMuffle()})) })) const loadNotes = async () => { @@ -23,7 +23,7 @@ const notes = await load(relays, {...filter, limit, until}) const context = await loaders.loadContext(relays, notes) - return query.threadify(notes, context, {muffle: getMuffle()}) + return threadify(notes, context, {muffle: getMuffle()}) } diff --git a/src/views/person/Network.svelte b/src/views/person/Network.svelte index 09072d31..8d252499 100644 --- a/src/views/person/Network.svelte +++ b/src/views/person/Network.svelte @@ -3,7 +3,7 @@ import {now, shuffle, batch, Cursor} from 'src/util/misc' import {getRelays, getFollows, getMuffle, listen, load} from 'src/agent' import loaders from 'src/app/loaders' - import query from 'src/app/query' + import {threadify} from 'src/app' export let pubkey @@ -18,7 +18,7 @@ listen(relays, {...filter, since: now()}, batch(300, async notes => { const context = await loaders.loadContext(relays, notes) - onNotes(query.threadify(notes, context, {muffle: getMuffle()})) + onNotes(threadify(notes, context, {muffle: getMuffle()})) })) const loadNotes = async () => { @@ -26,7 +26,7 @@ const notes = await load(relays, {...filter, limit, until}) const context = await loaders.loadContext(relays, notes) - return query.threadify(notes, context, {muffle: getMuffle()}) + return threadify(notes, context, {muffle: getMuffle()}) } diff --git a/src/views/person/Notes.svelte b/src/views/person/Notes.svelte index 42203e2f..1e9aba28 100644 --- a/src/views/person/Notes.svelte +++ b/src/views/person/Notes.svelte @@ -3,7 +3,7 @@ import {now, batch, Cursor} from 'src/util/misc' import {load, listen, getRelays, getMuffle} from 'src/agent' import loaders from 'src/app/loaders' - import query from 'src/app/query' + import {threadify} from 'src/app' export let pubkey @@ -15,7 +15,7 @@ listen(relays, {...filter, since: now()}, batch(300, async notes => { const context = await loaders.loadContext(relays, notes) - onNotes(query.threadify(notes, context, {muffle: getMuffle()})) + onNotes(threadify(notes, context, {muffle: getMuffle()})) })) const loadNotes = async () => { @@ -23,7 +23,7 @@ const notes = await load(relays, {...filter, limit, until}) const context = await loaders.loadContext(relays, notes) - return query.threadify(notes, context, {muffle: getMuffle()}) + return threadify(notes, context, {muffle: getMuffle()}) } diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 0023475d..a412acc9 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -15,6 +15,7 @@ module.exports = { medium: "#403D39", dark: "#252422", danger: "#ff0000", + placeholder: "#a19989", }, }, plugins: [],