mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Batch ensureAccounts, make getter and subscriptions more distinct
This commit is contained in:
parent
395b34554f
commit
0e8d0815c4
@ -1,5 +1,6 @@
|
||||
Bugs
|
||||
|
||||
- [ ] Fix scrolling on note detail
|
||||
- [ ] Ellipsize and hyperlink links
|
||||
- [ ] Be sure to deduplicate all events if needed
|
||||
- [ ] Format text with line breaks - pre, or split/br
|
||||
|
@ -1,14 +1,12 @@
|
||||
<script>
|
||||
import cx from 'classnames'
|
||||
import {onMount} from 'svelte'
|
||||
import {find, last, uniqBy, prop, whereEq} from 'ramda'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {ellipsize} from 'hurdak/src/core'
|
||||
import {hasParent} from 'src/util/html'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import {nostr} from "src/state/nostr"
|
||||
import {dispatch, t} from "src/state/dispatch"
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import {accounts, modal} from "src/state/app"
|
||||
import {user} from "src/state/user"
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
@ -25,8 +23,8 @@
|
||||
let parentId
|
||||
|
||||
$: {
|
||||
like = find(e => e.pubkey === $user.pubkey && e.content === "+", note.reactions)
|
||||
flag = find(e => e.pubkey === $user.pubkey && e.content === "-", note.reactions)
|
||||
like = find(e => e.pubkey === $user?.pubkey && e.content === "+", note.reactions)
|
||||
flag = find(e => e.pubkey === $user?.pubkey && e.content === "-", note.reactions)
|
||||
parentId = prop(1, find(t => last(t) === 'reply' ? t[1] : null, note.tags))
|
||||
}
|
||||
|
||||
|
@ -10,12 +10,12 @@
|
||||
|
||||
onMount(() => {
|
||||
return findNotes(
|
||||
channels.modal,
|
||||
channels.watcher,
|
||||
[{ids: [note.id]},
|
||||
{'#e': [note.id]},
|
||||
// We can't target reaction deletes by e tag, so get them
|
||||
// all so we can support toggling like/flags for our user
|
||||
{kinds: [5], authors: [$user.pubkey]}],
|
||||
{kinds: [5], authors: $user ? [$user.pubkey] : []}],
|
||||
$notes => {
|
||||
note = find(propEq('id', note.id), $notes) || note
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
<script>
|
||||
import {onMount} from 'svelte'
|
||||
import {writable} from 'svelte/store'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {nostr} from 'src/state/nostr'
|
||||
import {channels} from 'src/state/nostr'
|
||||
import {rooms} from 'src/state/app'
|
||||
import Input from "src/partials/Input.svelte"
|
||||
|
||||
@ -18,17 +17,14 @@
|
||||
navigate(`/chat/${id}`)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const sub = nostr.sub({
|
||||
filter: {kinds: [40, 41]},
|
||||
cb: e => {
|
||||
const id = e.kind === 40 ? e.id : e.tags[0][1]
|
||||
onMount(async () => {
|
||||
const events = await channels.getter.all({kinds: [40, 41]})
|
||||
|
||||
$rooms[id] = {id, pubkey: e.pubkey, ...$rooms[id], ...JSON.parse(e.content)}
|
||||
},
|
||||
events.forEach(e => {
|
||||
const id = e.kind === 40 ? e.id : e.tags[0][1]
|
||||
|
||||
$rooms[id] = {id, pubkey: e.pubkey, ...$rooms[id], ...JSON.parse(e.content)}
|
||||
})
|
||||
|
||||
return () => sub.unsub()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -2,15 +2,11 @@
|
||||
import {onMount} from "svelte"
|
||||
import {fly} from 'svelte/transition'
|
||||
import {navigate} from "svelte-routing"
|
||||
import pick from "ramda/src/pick"
|
||||
import {stripExifData} from "src/util/html"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Select from "src/partials/Select.svelte"
|
||||
import Textarea from "src/partials/Textarea.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import RoomList from "src/partials/chat/RoomList.svelte"
|
||||
import {user} from "src/state/user"
|
||||
import {rooms} from "src/state/app"
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import toast from "src/state/toast"
|
||||
@ -37,7 +33,7 @@
|
||||
const submit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!values.name.match(/^\w[\w\-]+\w$/)) {
|
||||
if (!values.name.match(/^\w[\w-]+\w$/)) {
|
||||
toast.show("error", "Names must be comprised of letters, numbers, and dashes only.")
|
||||
} else {
|
||||
const event = await dispatch(values.id ? "room/update" : "room/create", values)
|
||||
|
@ -6,7 +6,7 @@
|
||||
import {switcherFn} from 'hurdak/src/core'
|
||||
import UserBadge from 'src/partials/UserBadge.svelte'
|
||||
import {channels} from 'src/state/nostr'
|
||||
import {rooms, accounts, ensureAccount} from 'src/state/app'
|
||||
import {rooms, accounts, ensureAccounts} from 'src/state/app'
|
||||
import {dispatch} from 'src/state/dispatch'
|
||||
import {user} from 'src/state/user'
|
||||
import RoomList from "src/partials/chat/RoomList.svelte"
|
||||
@ -46,14 +46,14 @@
|
||||
return top + height < bodyRect.height
|
||||
}
|
||||
|
||||
channels.main.sub({
|
||||
return channels.watcher.sub({
|
||||
filter: {kinds: [42, 43, 44], '#e': [room]},
|
||||
cb: e => {
|
||||
switcherFn(e.kind, {
|
||||
42: () => {
|
||||
messages = messages.concat(e)
|
||||
|
||||
ensureAccount(e.pubkey)
|
||||
ensureAccounts([e.pubkey])
|
||||
|
||||
const $prevListItem = last(document.querySelectorAll('.chat-message'))
|
||||
|
||||
|
@ -4,11 +4,8 @@
|
||||
import {navigate} from "svelte-routing"
|
||||
import {copyToClipboard} from "src/util/html"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Select from "src/partials/Select.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import {user} from "src/state/user"
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import toast from "src/state/toast"
|
||||
|
||||
const keypairUrl = 'https://www.cloudflare.com/learning/ssl/how-does-public-key-encryption-work/'
|
||||
|
@ -5,7 +5,6 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import toast from "src/state/toast"
|
||||
import {user} from "src/state/user"
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
|
||||
let privKey = ''
|
||||
|
@ -13,7 +13,7 @@
|
||||
const submit = async e => {
|
||||
e.preventDefault()
|
||||
|
||||
const event = await dispatch("note/create", values.content)
|
||||
await dispatch("note/create", values.content)
|
||||
|
||||
toast.show("info", `Your note has been created!`)
|
||||
|
||||
|
@ -1,35 +1,50 @@
|
||||
<script>
|
||||
import {onMount} from 'svelte'
|
||||
import {get} from 'svelte/store'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {navigate} from "svelte-routing"
|
||||
import {reverse, find, propEq} from 'ramda'
|
||||
import {timedelta, now, formatTimestamp} from 'src/util/misc'
|
||||
import {timedelta, now} from 'src/util/misc'
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import {channels, relays} from "src/state/nostr"
|
||||
import {user} from "src/state/user"
|
||||
import {findNotes} from "src/state/app"
|
||||
import {db} from "src/state/db"
|
||||
import {findNotes, modal} from "src/state/app"
|
||||
|
||||
let stop
|
||||
let notes
|
||||
|
||||
const createNote = () => {
|
||||
navigate("/notes/new")
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
return findNotes(channels.main, {
|
||||
const start = () => {
|
||||
stop = findNotes(channels.watcher, {
|
||||
since: now() - timedelta(1, 'days'),
|
||||
limit: 100,
|
||||
}, $notes => {
|
||||
notes = $notes
|
||||
if ($notes.length) {
|
||||
notes = $notes
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
onMount(() => {
|
||||
const unsub = modal.subscribe($modal => {
|
||||
console.log('modal', $modal)
|
||||
if ($modal) {
|
||||
stop && stop()
|
||||
} else {
|
||||
setTimeout(start, 600)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
stop()
|
||||
unsub()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<ul class="py-8 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each reverse(notes || []) as n (n.id)}
|
||||
{#each (notes || []) as n (n.id)}
|
||||
<li class="border-l border-solid border-medium">
|
||||
<Note interactive note={n} />
|
||||
{#each n.replies as r (r.id)}
|
||||
|
@ -5,7 +5,6 @@
|
||||
import pick from "ramda/src/pick"
|
||||
import {stripExifData} from "src/util/html"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Select from "src/partials/Select.svelte"
|
||||
import Textarea from "src/partials/Textarea.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
@ -39,7 +38,7 @@
|
||||
const submit = async event => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!values.name.match(/^\w[\w\-]+\w$/)) {
|
||||
if (!values.name.match(/^\w[\w-]+\w$/)) {
|
||||
toast.show("error", "Names must be comprised of letters, numbers, and dashes only.")
|
||||
} else {
|
||||
await dispatch("account/update", values)
|
||||
|
@ -1,13 +1,12 @@
|
||||
<script>
|
||||
import {liveQuery} from "dexie"
|
||||
import {propEq} from "ramda"
|
||||
import {fly} from 'svelte/transition'
|
||||
import {fuzzy, hash} from "src/util/misc"
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import {db} from "src/state/db"
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import {nostr, relays} from "src/state/nostr"
|
||||
import {relays} from "src/state/nostr"
|
||||
|
||||
let q = ""
|
||||
let search
|
||||
|
@ -2,13 +2,11 @@
|
||||
import {onMount} from 'svelte'
|
||||
import {reverse} from 'ramda'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {uniqBy, prop} from 'ramda'
|
||||
import {ellipsize} from 'hurdak/src/core'
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
import {now, timedelta} from 'src/util/misc'
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import {channels} from 'src/state/nostr'
|
||||
import {user as currentUser} from 'src/state/user'
|
||||
import {accounts, ensureAccount, findNotes} from "src/state/app"
|
||||
import {accounts, findNotes} from "src/state/app"
|
||||
|
||||
export let pubkey
|
||||
|
||||
@ -18,9 +16,7 @@
|
||||
$: user = $accounts[pubkey]
|
||||
|
||||
onMount(async () => {
|
||||
await ensureAccount(pubkey)
|
||||
|
||||
return findNotes(channels.main, {
|
||||
return findNotes(channels.watcher, {
|
||||
authors: [pubkey],
|
||||
since: now() - timedelta(1, 'days'),
|
||||
limit: 100,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {prop, sortBy, uniqBy, find, last, groupBy} from 'ramda'
|
||||
import {prop, uniq, pluck, sortBy, uniqBy, find, last, groupBy} from 'ramda'
|
||||
import {debounce} from 'throttle-debounce'
|
||||
import {writable, derived, get} from 'svelte/store'
|
||||
import {switcherFn, ensurePlural} from 'hurdak/lib/hurdak'
|
||||
@ -28,38 +28,56 @@ user.subscribe($user => {
|
||||
|
||||
// Utils
|
||||
|
||||
export const ensureAccount = pubkey => {
|
||||
let $account = prop(pubkey, get(accounts))
|
||||
export const ensureAccounts = async pubkeys => {
|
||||
const $accounts = get(accounts)
|
||||
|
||||
if (!$account || $account.lastRefreshed < now() - timedelta(10, 'minutes')) {
|
||||
channels.getter.sub({
|
||||
filter: {kinds: [0], authors: [pubkey]},
|
||||
cb: e => {
|
||||
$account = {
|
||||
...$account,
|
||||
...JSON.parse(e.content),
|
||||
pubkey,
|
||||
lastRefreshed: now(),
|
||||
}
|
||||
// Don't request accounts we recently updated
|
||||
pubkeys = pubkeys.filter(
|
||||
k => !$accounts[k] || $accounts[k].refreshed < now() - timedelta(10, 'minutes')
|
||||
)
|
||||
|
||||
accounts.update($accounts => ({...$accounts, [pubkey]: $account}))
|
||||
},
|
||||
})
|
||||
if (!pubkeys.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const events = await channels.getter.all({kinds: [0], authors: pubkeys})
|
||||
|
||||
accounts.update($accounts => {
|
||||
events.forEach(e => {
|
||||
$accounts[e.pubkey] = {
|
||||
pubkey: e.pubkey,
|
||||
...$accounts[e.pubkey],
|
||||
...JSON.parse(e.content),
|
||||
refreshed: now(),
|
||||
}
|
||||
})
|
||||
|
||||
return $accounts
|
||||
})
|
||||
}
|
||||
|
||||
export const findNotes = (channel, queries, cb) => {
|
||||
const notes = writable([])
|
||||
const reactions = writable([])
|
||||
|
||||
channel.sub({
|
||||
let pubkeys = []
|
||||
|
||||
const refreshAccounts = debounce(300, () => {
|
||||
ensureAccounts(uniq(pubkeys))
|
||||
|
||||
pubkeys = []
|
||||
})
|
||||
|
||||
const closeRequest = channel.sub({
|
||||
filter: ensurePlural(queries).map(q => ({kinds: [1, 5, 7], ...q})),
|
||||
cb: async e => {
|
||||
cb: e => {
|
||||
// Chunk requests to load accounts
|
||||
pubkeys.push(e.pubkey)
|
||||
refreshAccounts()
|
||||
|
||||
switcherFn(e.kind, {
|
||||
1: () => {
|
||||
notes.update($xs => uniqBy(prop('id'), $xs.concat(e)))
|
||||
|
||||
ensureAccount(e.pubkey)
|
||||
},
|
||||
5: () => {
|
||||
const ids = e.tags.map(t => t[1])
|
||||
@ -69,12 +87,8 @@ export const findNotes = (channel, queries, cb) => {
|
||||
},
|
||||
7: () => {
|
||||
reactions.update($xs => $xs.concat(e))
|
||||
|
||||
ensureAccount(e.pubkey)
|
||||
},
|
||||
})
|
||||
|
||||
ensureAccount(e.pubkey)
|
||||
},
|
||||
})
|
||||
|
||||
@ -102,5 +116,12 @@ export const findNotes = (channel, queries, cb) => {
|
||||
}
|
||||
)
|
||||
|
||||
return annotatedNotes.subscribe(debounce(300, cb))
|
||||
const unsubscribe = annotatedNotes.subscribe(debounce(300, cb))
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
|
||||
closeRequest()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,17 +18,17 @@ db
|
||||
|
||||
export const registerRelay = async url => {
|
||||
let json
|
||||
try {
|
||||
const res = await fetch(url.replace(/^ws/, 'http'), {
|
||||
headers: {
|
||||
Accept: 'application/nostr_json',
|
||||
},
|
||||
})
|
||||
// try {
|
||||
// const res = await fetch(url.replace(/^ws/, 'http'), {
|
||||
// headers: {
|
||||
// Accept: 'application/nostr_json',
|
||||
// },
|
||||
// })
|
||||
|
||||
json = await res.json()
|
||||
} catch (e) {
|
||||
json = {}
|
||||
}
|
||||
// json = await res.json()
|
||||
// } catch (e) {
|
||||
// json = {}
|
||||
// }
|
||||
|
||||
db.relays.put({...json, url})
|
||||
}
|
||||
|
@ -2,9 +2,8 @@ import {identity, last, without} from 'ramda'
|
||||
import {getPublicKey} from 'nostr-tools'
|
||||
import {get} from 'svelte/store'
|
||||
import {first, defmulti} from "hurdak/lib/hurdak"
|
||||
import {db} from "src/state/db"
|
||||
import {user} from "src/state/user"
|
||||
import {nostr, relays} from 'src/state/nostr'
|
||||
import {nostr, channels, relays} from 'src/state/nostr'
|
||||
|
||||
// Commands are processed in two layers:
|
||||
// - App-oriented commands are created via dispatch
|
||||
@ -21,7 +20,7 @@ dispatch.addMethod("account/init", async (topic, privkey) => {
|
||||
user.set({name: pubkey.slice(0, 8), privkey, pubkey})
|
||||
|
||||
// Attempt to refresh user data from the network
|
||||
const found = Boolean(await user.refresh())
|
||||
const found = Boolean(await channels.getter.first({authors: [$user.pubkey]}))
|
||||
|
||||
// Tell the caller whether this user was found
|
||||
return {found}
|
||||
|
@ -1,16 +1,80 @@
|
||||
import {writable} from 'svelte/store'
|
||||
import {debounce} from 'throttle-debounce'
|
||||
import {relayPool, getPublicKey} from 'nostr-tools'
|
||||
import {noop} from 'hurdak/lib/hurdak'
|
||||
import {last} from 'ramda'
|
||||
import {first} from 'hurdak/lib/hurdak'
|
||||
import {getLocalJson, setLocalJson} from "src/util/misc"
|
||||
|
||||
export const nostr = relayPool()
|
||||
|
||||
// Initialize nostr channels with a noop query
|
||||
// Track who is subscribing, so we don't go over our limit
|
||||
|
||||
const channel = name => {
|
||||
let active = false
|
||||
let promise = Promise.resolve('init')
|
||||
|
||||
const _chan = {
|
||||
sub: params => {
|
||||
if (active) {
|
||||
throw new Error(`Channel ${name} is already active.`)
|
||||
}
|
||||
|
||||
active = true
|
||||
|
||||
const sub = nostr.sub(params)
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
|
||||
sub.unsub()
|
||||
}
|
||||
},
|
||||
all: filter => {
|
||||
// Wait for any other subscriptions to finish
|
||||
promise = promise.then(() => {
|
||||
return new Promise(resolve => {
|
||||
// Collect results
|
||||
let result = []
|
||||
|
||||
// As long as events are coming in, don't resolve. When
|
||||
// events are no longer streaming, resolve and close the subscription
|
||||
const done = debounce(300, () => {
|
||||
unsub()
|
||||
|
||||
resolve(result)
|
||||
})
|
||||
|
||||
// Create our usbscription, every time we get an event, attempt to complete
|
||||
const unsub = _chan.sub({
|
||||
filter,
|
||||
cb: e => {
|
||||
result.push(e)
|
||||
|
||||
done()
|
||||
},
|
||||
})
|
||||
|
||||
// If our filter doesn't match anything, be sure to resolve the promise
|
||||
setTimeout(done, 1000)
|
||||
})
|
||||
})
|
||||
|
||||
return promise
|
||||
},
|
||||
first: async filter => {
|
||||
return first(await channels.getter.all({...filter, limit: 1}))
|
||||
},
|
||||
last: async filter => {
|
||||
return last(await channels.getter.all({...filter}))
|
||||
},
|
||||
}
|
||||
|
||||
return _chan
|
||||
}
|
||||
|
||||
export const channels = {
|
||||
main: nostr.sub({filter: {ids: []}, cb: noop}),
|
||||
modal: nostr.sub({filter: {ids: []}, cb: noop}),
|
||||
getter: nostr.sub({filter: {ids: []}, cb: noop}),
|
||||
watcher: channel('main'),
|
||||
getter: channel('getter'),
|
||||
}
|
||||
|
||||
// Augment nostr with some extra methods
|
||||
@ -27,40 +91,6 @@ nostr.event = (kind, content = '', tags = []) => {
|
||||
return {kind, content, tags, pubkey, created_at: createdAt}
|
||||
}
|
||||
|
||||
nostr.find = (filter, timeout = 300) => {
|
||||
return new Promise(resolve => {
|
||||
const sub = channels.getter.sub({
|
||||
filter,
|
||||
cb: e => {
|
||||
resolve(e)
|
||||
|
||||
sub.unsub()
|
||||
},
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
resolve(null)
|
||||
|
||||
sub.unsub()
|
||||
}, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
nostr.findLast = (filter, timeout = 300) => {
|
||||
return new Promise(resolve => {
|
||||
let result = null
|
||||
|
||||
channels.getter.sub({
|
||||
filter,
|
||||
cb: e => {
|
||||
result = e
|
||||
},
|
||||
})
|
||||
|
||||
setTimeout(() => resolve(result), timeout)
|
||||
})
|
||||
}
|
||||
|
||||
// Create writable store for relays so we can observe changes in the app
|
||||
|
||||
export const relays = writable(getLocalJson("coracle/relays") || [])
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {writable, get} from "svelte/store"
|
||||
import {getLocalJson, setLocalJson} from "src/util/misc"
|
||||
import {nostr} from 'src/state/nostr'
|
||||
import {nostr, channels} from 'src/state/nostr'
|
||||
|
||||
export const user = writable(getLocalJson("coracle/user"))
|
||||
|
||||
@ -10,22 +10,3 @@ user.subscribe($user => {
|
||||
// Keep nostr in sync
|
||||
nostr.login($user?.privkey)
|
||||
})
|
||||
|
||||
user.refresh = async () => {
|
||||
const $user = get(user)
|
||||
|
||||
if ($user) {
|
||||
const data = await nostr.findLast({authors: [$user.pubkey], kinds: [0]})
|
||||
|
||||
if (data) {
|
||||
user.update($user => ({...$user, ...JSON.parse(data.content)}))
|
||||
}
|
||||
|
||||
return Boolean(data)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for new data every so often
|
||||
setTimeout(() => user.refresh(), 60 * 1000)
|
||||
|
Loading…
Reference in New Issue
Block a user