diff --git a/README.md b/README.md index 53d36db9..7c80593a 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg - [ ] Check firefox - in dev it won't work, but it should in production - [ ] Re-implement muffle +- [ ] Rename users/accounts to people - https://vitejs.dev/guide/features.html#web-workers - https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers - https://web.dev/module-workers/ diff --git a/src/App.svelte b/src/App.svelte index 7b14c711..50d84035 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -16,9 +16,9 @@ import {modal, logout, alerts} from "src/state/app" import {user} from 'src/state/user' import Anchor from 'src/partials/Anchor.svelte' - import NoteDetail from "src/partials/NoteDetail.svelte" + import NoteDetail from "src/views/NoteDetail.svelte" import NotFound from "src/routes/NotFound.svelte" - // import Search from "src/routes/Search.svelte" + import Search from "src/routes/Search.svelte" import Alerts from "src/routes/Alerts.svelte" import Notes from "src/routes/Notes.svelte" import Login from "src/routes/Login.svelte" @@ -27,7 +27,7 @@ import Keys from "src/routes/Keys.svelte" import RelayList from "src/routes/RelayList.svelte" import AddRelay from "src/routes/AddRelay.svelte" - // import UserDetail from "src/routes/UserDetail.svelte" + import UserDetail from "src/routes/UserDetail.svelte" // import UserAdvanced from "src/routes/UserAdvanced.svelte" import NoteCreate from "src/routes/NoteCreate.svelte" // import Chat from "src/routes/Chat.svelte" @@ -54,8 +54,6 @@ } }) - window.addEventListener('unhandledrejection', e => console.error(e)) - onMount(() => { // Poll for new notifications (async function pollForNotifications() { @@ -96,13 +94,7 @@
- + - {#key params.pubkey + params.activeTab} + {#key params.pubkey} {/key} - --> diff --git a/src/partials/Likes.svelte b/src/partials/Likes.svelte deleted file mode 100644 index fb8ad55d..00000000 --- a/src/partials/Likes.svelte +++ /dev/null @@ -1,79 +0,0 @@ - - - - -
    - {#each $notes as n (n.id)} -
  • - {:else} - {#if loading} -
  • - {:else} -
  • No notes found.
  • - {/if} - {/each} -
diff --git a/src/partials/NoteDetail.svelte b/src/partials/NoteDetail.svelte deleted file mode 100644 index eef2b530..00000000 --- a/src/partials/NoteDetail.svelte +++ /dev/null @@ -1,52 +0,0 @@ - - -{#each $notes as n (n.id)} -
- -
-{:else} - -{/each} diff --git a/src/partials/Notes.svelte b/src/partials/Notes.svelte deleted file mode 100644 index 8718f253..00000000 --- a/src/partials/Notes.svelte +++ /dev/null @@ -1,59 +0,0 @@ - - -
    - {#each ($notes || []) as n (n.id)} -
  • - {/each} -
- -{#if $notes?.length === 0} -
-
- No notes found. -
-
-{/if} diff --git a/src/relay/db.js b/src/relay/db.js index 7f25c5a0..e377cf78 100644 --- a/src/relay/db.js +++ b/src/relay/db.js @@ -3,8 +3,8 @@ import {filterTags} from 'src/util/nostr' export const db = new Dexie('coracle/relay') -db.version(2).stores({ - events: '++id, pubkey, created_at, kind, content', +db.version(3).stores({ + events: '++id, pubkey, created_at, kind, content, reply, root', users: '++pubkey, name, about', tags: '++key, event, value', }) diff --git a/src/relay/index.js b/src/relay/index.js index 6b0b3858..8e709f05 100644 --- a/src/relay/index.js +++ b/src/relay/index.js @@ -18,13 +18,21 @@ const lq = f => liveQuery(async () => { const ensureContext = async e => { // We can't return a promise, so use setTimeout instead - const user = await db.users.where('pubkey').equals(e.pubkey).first() + const user = await db.users.where('pubkey').equals(e.pubkey).first() || { + muffle: [], + petnames: [], + updated_at: 0, + pubkey: e.pubkey, + } // Throttle updates for users - if (!user || user.updated_at < now() - timedelta(1, 'hours')) { - await pool.updateUser(user || {pubkey: e.pubkey, updated_at: 0}) + if (user.updated_at < now() - timedelta(1, 'hours')) { + Object.assign(user, await pool.getUserInfo({pubkey: e.pubkey, ...user})) } + // Even if we didn't find a match, save it so we don't keep trying to refresh + db.users.put({...user, updated_at: now()}) + // TODO optimize this like user above so we're not double-fetching await pool.fetchContext(e) } @@ -79,6 +87,25 @@ const findReaction = async (id, filter) => const countReactions = async (id, filter) => (await filterReactions(id, filter)).length +const findNote = async id => { + const [note, children] = await Promise.all([ + db.events.get(id), + db.events.where('reply').equals(id), + ]) + + const [replies, reactions, user, html] = await Promise.all([ + children.clone().filter(e => e.kind === 1).toArray(), + children.clone().filter(e => e.kind === 7).toArray(), + db.users.get(note.pubkey), + renderNote(note, {showEntire: false}), + ]) + + return { + ...note, reactions, user, html, + replies: await Promise.all(replies.map(r => findNote(r.id))), + } +} + const renderNote = async (note, {showEntire = false}) => { const shouldEllipsize = note.content.length > 500 && !showEntire const content = shouldEllipsize ? ellipsize(note.content, 500) : note.content @@ -105,5 +132,5 @@ const renderNote = async (note, {showEntire = false}) => { export default { db, pool, lq, ensureContext, filterEvents, filterReactions, countReactions, - findReaction, filterReplies, renderNote, + findReaction, filterReplies, findNote, renderNote, } diff --git a/src/relay/pool.js b/src/relay/pool.js index 1ba8fe56..4aa18a77 100644 --- a/src/relay/pool.js +++ b/src/relay/pool.js @@ -1,7 +1,8 @@ +import {uniqBy, prop} from 'ramda' import {relayPool, getPublicKey} from 'nostr-tools' import {noop, switcherFn, uuid} from 'hurdak/lib/hurdak' -import {now, timedelta} from "src/util/misc" -import {filterTags} from "src/util/nostr" +import {now, randomChoice, timedelta} from "src/util/misc" +import {filterTags, findReply, findRoot} from "src/util/nostr" import {db} from 'src/relay/db' // ============================================================================ @@ -9,43 +10,78 @@ import {db} from 'src/relay/db' const pool = relayPool() -const post = (topic, payload) => postMessage({topic, payload}) - -const req = ({filter, onEvent, onEose = noop}) => { - // If we don't have any relays, we'll wait forever for an eose, but - // we already know we're done. Use a timeout since callers are - // expecting this to be async and we run into errors otherwise. - if (pool.relays.length === 0) { - onEose() - - return {unsub: noop} +class Channel { + constructor(name) { + this.name = name + this.p = Promise.resolve() } + async sub(filter, onEvent, onEose = noop) { + // If we don't have any relays, we'll wait forever for an eose, but + // we already know we're done. Use a timeout since callers are + // expecting this to be async and we run into errors otherwise. + if (Object.keys(pool.relays).length === 0) { + setTimeout(onEose) - const eoseRelays = [] - return pool.sub({filter, cb: onEvent}, uuid(), r => { - eoseRelays.push(r) - - if (eoseRelays.length === pool.relays.length) { - onEose() + return {unsub: noop} } - }) + + // Grab our spot in the queue, save resolve for later + let resolve + let p = this.p + this.p = new Promise(r => { + resolve = r + }) + + // Make sure callers have to wait for the previous sub to be done + // before they can get a new one. + await p + + // Start our subscription, wait for all relays to eose before + // calling it done + const eoseRelays = [] + const sub = pool.sub({filter, cb: onEvent}, this.name, r => { + eoseRelays.push(r) + + if (eoseRelays.length === Object.keys(pool.relays).length) { + onEose() + } + }) + + return { + unsub: () => { + sub.unsub() + + resolve() + } + } + } + all(filter) { + /* eslint no-async-promise-executor: 0 */ + return new Promise(async resolve => { + const result = [] + + const sub = await this.sub( + filter, + e => result.push(e), + r => { + sub.unsub() + + resolve(uniqBy(prop('id'), result)) + }, + ) + }) + } } -// ============================================================================ -// Start up a subscription to get recent data and listen for new stuff +export const channels = [ + new Channel('a'), + new Channel('b'), + new Channel('c'), +] -const lastSync = now() - timedelta(1, 'days') +const req = filter => randomChoice(channels).all(filter) -req({ - filter: { - kinds: [1], - since: lastSync, - limit: 10, - }, - onEvent: e => { - post('events/put', e) - }, -}) +const prepEvent = e => ({...e, root: findRoot(e), reply: findReply(e)}) // ============================================================================ // Listen to messages posted from the main application @@ -81,47 +117,37 @@ const setPublicKey = pubkey => { const publishEvent = event => { pool.publish(event) - db.events.put(event) + db.events.put(prepEvent(event)) } -const updateUser = async user => { - if (!user.pubkey) throw new Error("Invalid user") +const loadEvents = async filter => { + const events = await req(filter) - user = {muffle: [], petnames: [], ...user} + db.events.bulkPut(events.map(prepEvent)) +} - const sub = req({ - filter: { - kinds: [0, 3, 12165], - authors: [user.pubkey], - since: user.updated_at, - }, - onEvent: e => { - switcherFn(e.kind, { - 0: () => Object.assign(user, JSON.parse(e.content)), - 3: () => Object.assign(user, {petnames: e.tags}), - 12165: () => Object.assign(user, {muffle: e.tags}), - }) - }, - onEose: () => { - sub.unsub() +const getUserInfo = async user => { + for (const e of await req({kinds: [0, 3, 12165], authors: [user.pubkey]})) { + switcherFn(e.kind, { + 0: () => Object.assign(user, JSON.parse(e.content)), + 3: () => Object.assign(user, {petnames: e.tags}), + 12165: () => Object.assign(user, {muffle: e.tags}), + }) + } - db.users.put({...user, updated_at: now()}) - }, - }) + return user } const fetchContext = async event => { - const sub = req({ - filter: [ - {kinds: [5, 7], '#e': [event.id]}, - {kinds: [5], 'ids': filterTags({tag: "e"}, event)}, - ], - onEvent: e => post('events/put', e), - onEose: () => sub.unsub(), - }) + const events = await req([ + {kinds: [5, 7], '#e': [event.id]}, + {kinds: [5], 'ids': filterTags({tag: "e"}, event)}, + ]) + + db.events.bulkPut(events.map(prepEvent)) } export default { getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey, - publishEvent, updateUser, fetchContext, + publishEvent, loadEvents, getUserInfo, fetchContext, } diff --git a/src/routes/Alerts.svelte b/src/routes/Alerts.svelte index 3b66cf47..5d89b647 100644 --- a/src/routes/Alerts.svelte +++ b/src/routes/Alerts.svelte @@ -7,7 +7,7 @@ import {user} from 'src/state/user' import {alerts, modal} from 'src/state/app' import UserBadge from "src/partials/UserBadge.svelte" - import Note from 'src/partials/Note.svelte' + import Note from 'src/views/Note.svelte' const events = relay.lq(async () => { const events = await relay diff --git a/src/routes/Notes.svelte b/src/routes/Notes.svelte index 0441acce..7e67552c 100644 --- a/src/routes/Notes.svelte +++ b/src/routes/Notes.svelte @@ -3,7 +3,7 @@ import {timedelta} from 'src/util/misc' import Anchor from "src/partials/Anchor.svelte" import Tabs from "src/partials/Tabs.svelte" - import Notes from "src/partials/Notes.svelte" + import Notes from "src/views/Notes.svelte" import {relays} from "src/state/nostr" import {user} from "src/state/user" diff --git a/src/routes/Search.svelte b/src/routes/Search.svelte index 18a41b54..3f32ae2c 100644 --- a/src/routes/Search.svelte +++ b/src/routes/Search.svelte @@ -1,70 +1,14 @@ - -
  • - +
    {#if type === 'people'} - + +{:else if type === 'notes'} + {/if} - -{#if type === 'notes'} -
      - {#each (results || []) as e (e.id)} -
    • - -
    • - {/each} -
    -{/if} - - - - -{#if $relays.length === 0} -
    -
    - You aren't yet connected to any relays. Please click here to get started. -
    -
    -{/if} - diff --git a/src/routes/UserDetail.svelte b/src/routes/UserDetail.svelte index 0bbd9a30..d906b171 100644 --- a/src/routes/UserDetail.svelte +++ b/src/routes/UserDetail.svelte @@ -1,24 +1,20 @@ @@ -52,15 +48,15 @@
    + style="background-image: url({$user?.picture})" />
    -

    {user?.name || pubkey.slice(0, 8)}

    +

    {$user?.name || pubkey.slice(0, 8)}

    {#if $currentUser && $currentUser.pubkey !== pubkey} {/if}
    -

    {user?.about || ''}

    +

    {$user?.about || ''}

    {#if $currentUser?.pubkey === pubkey} @@ -80,11 +76,18 @@
    + {#if activeTab === 'notes'} - + {:else if activeTab === 'likes'} - + {:else if activeTab === 'network'} - +{#if $user} + t[1])}} /> +{:else} +
    + Unable to show network for this user. +
    +{/if} {/if} diff --git a/src/state/dispatch.js b/src/state/dispatch.js index 95f16233..d4231af7 100644 --- a/src/state/dispatch.js +++ b/src/state/dispatch.js @@ -23,10 +23,10 @@ dispatch.addMethod("account/init", async (topic, { privkey, pubkey }) => { }) // Make sure we have data for this user - await ensureAccounts([pubkey], {force: true}) + const {name} = await relay.pool.updateUser({pubkey}) // Tell the caller whether this user was found - return {found: Boolean(get(user).name)} + return {found: Boolean(name)} }) dispatch.addMethod("account/update", async (topic, updates) => { @@ -58,8 +58,6 @@ dispatch.addMethod("account/muffle", async (topic, muffle) => { }) dispatch.addMethod("relay/join", async (topic, url) => { - const $user = get(user) - relays.update(r => r.concat(url)) }) diff --git a/src/util/misc.js b/src/util/misc.js index ebe39e32..6cbaf290 100644 --- a/src/util/misc.js +++ b/src/util/misc.js @@ -1,4 +1,5 @@ -import pluck from "ramda/src/pluck" +import {pluck} from "ramda" +import {debounce} from 'throttle-debounce' import Fuse from "fuse.js/dist/fuse.min.js" export const fuzzy = (data, opts = {}) => { @@ -50,3 +51,28 @@ export const formatTimestamp = ts => { export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) +export const createScroller = loadMore => { + const onScroll = debounce(1000, async () => { + /* eslint no-constant-condition: 0 */ + + while (true) { + // While we have empty space, fill it + const {scrollY, innerHeight} = window + const {scrollHeight} = document.body + + if (scrollY + innerHeight + 600 < scrollHeight) { + break + } + + loadMore() + + await sleep(1000) + } + }) + + onScroll() + + return onScroll +} + +export const randomChoice = xs => xs[Math.floor(Math.random() * xs.length)] diff --git a/src/views/Likes.svelte b/src/views/Likes.svelte new file mode 100644 index 00000000..506f67b2 --- /dev/null +++ b/src/views/Likes.svelte @@ -0,0 +1,26 @@ + + +{#if $notes} +
      + {#each $notes as n (n.id)} +
    • + {:else} +
    • No notes found.
    • + {/each} +
    +{/if} diff --git a/src/partials/Note.svelte b/src/views/Note.svelte similarity index 77% rename from src/partials/Note.svelte rename to src/views/Note.svelte index 06106da3..2f3133d6 100644 --- a/src/partials/Note.svelte +++ b/src/views/Note.svelte @@ -1,6 +1,6 @@ + +{#if $observable} +
    + +
    +{/if} diff --git a/src/views/Notes.svelte b/src/views/Notes.svelte new file mode 100644 index 00000000..8f7e0891 --- /dev/null +++ b/src/views/Notes.svelte @@ -0,0 +1,80 @@ + + + + +
      + {#each ($notes || []) as n (n.id)} +
    • + {/each} +
    + +{#if $notes?.length === 0} +
    +
    + No notes found. +
    +
    +{:else} + +{/if} diff --git a/src/views/SearchNotes.svelte b/src/views/SearchNotes.svelte new file mode 100644 index 00000000..043bab16 --- /dev/null +++ b/src/views/SearchNotes.svelte @@ -0,0 +1,35 @@ + + +
      + {#each results as e (e.id)} +
    • + +
    • + {/each} +
    diff --git a/src/views/SearchPeople.svelte b/src/views/SearchPeople.svelte new file mode 100644 index 00000000..88a487aa --- /dev/null +++ b/src/views/SearchPeople.svelte @@ -0,0 +1,42 @@ + + +