Get main notes stuff working

This commit is contained in:
Jonathan Staab 2022-12-16 20:46:15 -08:00
parent 0bfe584bab
commit 9f6601cbdc
10 changed files with 205 additions and 75 deletions

View File

@ -37,4 +37,8 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg
# Workers
- [ ] Check firefox
- [ ] Check firefox - in dev it won't work, but it should in production
- [ ] Re-implement muffle
- 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/

View File

@ -54,6 +54,8 @@
}
})
window.addEventListener('unhandledrejection', e => console.error(e))
onMount(() => {
// Poll for new notifications
(async function pollForNotifications() {

View File

@ -1,13 +1,13 @@
<script>
import cx from 'classnames'
import {find, uniqBy, prop, whereEq} from 'ramda'
import {onMount} from 'svelte'
import {liveQuery} from 'dexie'
import {slide} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {hasParent, findLink} from 'src/util/html'
import {renderNote} from 'src/util/notes'
import Preview from 'src/partials/Preview.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import relay from 'src/relay'
import {dispatch} from "src/state/dispatch"
import {findReply} from "src/state/nostr"
import {accounts, settings, modal} from "src/state/app"
@ -23,21 +23,17 @@
export let showEntire = false
export let invertColors = false
let link = null
let like = null
let flag = null
let reply = null
let interactive = null
$: {
like = find(e => e.pubkey === $user?.pubkey && e.content === "+", note.reactions)
flag = find(e => e.pubkey === $user?.pubkey && e.content === "-", note.reactions)
interactive = !anchorId || anchorId !== note.id
}
const link = $settings.showLinkPreviews ? findLink(note.content) : null
const interactive = !anchorId || anchorId !== note.id
const like = liveQuery(() => relay.findReaction(note.id, {content: "+", pubkey: $user?.pubkey}))
const flag = liveQuery(() => relay.findReaction(note.id, {content: "-", pubkey: $user?.pubkey}))
const likes = liveQuery(() => relay.countReactions(note.id, {content: "+"}))
const flags = liveQuery(() => relay.countReactions(note.id, {content: "-"}))
const replies = liveQuery(() => relay.filterReplies(note.id))
onMount(async () => {
link = $settings.showLinkPreviews ? findLink(note.content) : null
})
relay.ensureContext(note)
const onClick = e => {
if (!['I'].includes(e.target.tagName) && !hasParent('a', e.target)) {
@ -111,10 +107,10 @@
Reply to <Anchor on:click={goToParent}>{findReply(note).slice(0, 8)}</Anchor>
</small>
{/if}
{#if flag}
{#if $flag}
<p class="text-light border-l-2 border-solid border-medium pl-4">
You have flagged this content as offensive.
<Anchor on:click={() => deleteReaction(flag)}>Unflag</Anchor>
<Anchor on:click={() => deleteReaction($flag)}>Unflag</Anchor>
</p>
{:else}
<p class="text-ellipsis overflow-hidden">
@ -130,17 +126,17 @@
<i
class="fa-solid fa-reply cursor-pointer"
on:click={startReply} />
{note.children.length}
{$replies?.length}
</div>
<div class={cx({'text-accent': like})}>
<div class={cx({'text-accent': $like})}>
<i
class="fa-solid fa-heart cursor-pointer"
on:click={() => like ? deleteReaction(like) : react("+")} />
{uniqBy(prop('pubkey'), note.reactions.filter(whereEq({content: '+'}))).length}
on:click={() => $like ? deleteReaction($like) : react("+")} />
{$likes}
</div>
<div>
<i class="fa-solid fa-flag cursor-pointer" on:click={() => react("-")} />
{uniqBy(prop('pubkey'), note.reactions.filter(whereEq({content: '-'}))).length}
{$flags}
</div>
</div>
{/if}
@ -169,9 +165,9 @@
{/if}
{#if depth > 0}
{#each uniqBy(prop('id'), note.children) as child (child.id)}
{#each ($replies || []) as r (r.id)}
<div class="ml-5 border-l border-solid border-medium">
<svelte:self note={child} depth={depth - 1} {invertColors} {anchorId} />
<svelte:self note={r} depth={depth - 1} {invertColors} {anchorId} />
</div>
{/each}
{/if}

View File

@ -1,26 +1,59 @@
<script>
import {liveQuery} from 'dexie'
import {onMount, onDestroy} from 'svelte'
import {prop, identity, concat, uniqBy, groupBy} from 'ramda'
import {createMap} from 'hurdak/lib/hurdak'
import {findReply, findRoot} from 'src/nostr/tags'
import {fly} from 'svelte/transition'
import {uniqBy, sortBy, reject, prop} from 'ramda'
import {createScroller, getMuffleValue, threadify, notesListener} from "src/util/notes"
import Spinner from "src/partials/Spinner.svelte"
import Note from "src/partials/Note.svelte"
import relay from 'src/relay'
import {modal} from "src/state/app"
export let filter
export let shouldMuffle = false
const notes = liveQuery(() => relay.filterEvents(filter).toArray())
const toThread = async notes => {
const ancestorIds = concat(notes.map(findRoot), notes.map(findReply)).filter(identity)
const ancestors = await relay.filterEvents({kinds: [1], ids: ancestorIds}).toArray()
const allNotes = uniqBy(prop('id'), notes.concat(ancestors))
const notesById = createMap('id', allNotes)
const notesByRoot = groupBy(
n => {
const rootId = findRoot(n)
const parentId = findReply(n)
// Actually dereference the notes in case we weren't able to retrieve them
if (notesById[rootId]) {
return rootId
}
if (notesById[parentId]) {
return parentId
}
return n.id
},
allNotes
)
return Object.keys(notesByRoot).map(id => notesById[id])
}
const notes = relay.lq(async () => {
let data = relay.filterEvents(filter).limit(10)
if (shouldMuffle) {
data = data
}
return await toThread(await data.reverse().sortBy('created_at'))
})
</script>
<!-- <svelte:window on:scroll={scroller?.start} /> -->
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
{#each ($notes || []) as n (n.id)}
<li>{n.content}</li>
<!-- <li><Note interactive note={n} depth={2} /></li> -->
<li><Note interactive note={n} depth={2} /></li>
{:else}
<li class="p-20 text-center" in:fly={{y: 20}}>No notes found.</li>
{/each}

View File

@ -1,26 +1,38 @@
import Dexie from 'dexie'
import {defmulti} from 'hurdak/lib/hurdak'
import {now, timedelta} from 'src/util/misc'
import {filterTags} from 'src/nostr/tags'
import {worker} from 'src/relay/worker'
export const db = new Dexie('coracle/relay')
db.version(1).stores({
db.version(2).stores({
events: '++id, pubkey, created_at, kind, content',
users: '++pubkey, name, about',
tags: 'type, value, event',
tags: '++key, event, value',
})
window.db = db
// Hooks
db.events.hook('creating', (id, {pubkey, tags}, t) => {
// We can't return a promise, so use setTimeout instead
setTimeout(async () => {
const user = await db.users.where('pubkey').equals(pubkey).first()
db.events.hook('creating', (id, e, t) => {
setTimeout(() => {
for (const tag of e.tags) {
db.tags.put({
id: [id, ...tag.slice(0, 2)].join(':'),
event: id,
type: tag[0],
value: tag[1],
relay: tag[2],
mark: tag[3],
})
}
// Throttle updates for users
if (!user || user.updated_at < now() - timedelta(1, 'hours')) {
worker.post('user/update', user || {pubkey, updated_at: 0})
if (e.kind === 5) {
const eventIds = filterTags({tag: "e"}, e)
db.events.where('id').anyOf(eventIds).delete()
db.tags.where('event').anyOf(eventIds).delete()
}
})
})

View File

@ -1,23 +1,83 @@
import {liveQuery} from 'dexie'
import {pluck, isNil} from 'ramda'
import {ensurePlural, first} from 'hurdak/lib/hurdak'
import {now, timedelta} from 'src/util/misc'
import {db} from 'src/relay/db'
import {worker} from 'src/relay/worker'
import {ensurePlural} from 'hurdak/lib/hurdak'
const filterEvents = ({kinds, ids, authors, ...filter}) => {
let t = db.events
// Livequery appears to swallow errors
const lq = f => liveQuery(async () => {
try {
return await f()
} catch (e) {
console.error(e)
}
})
if (kinds) {
t = t.where('kind').anyOf(ensurePlural(kinds))
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()
// Throttle updates for users
if (!user || user.updated_at < now() - timedelta(1, 'hours')) {
worker.post('user/update', user || {pubkey: e.pubkey, updated_at: 0})
}
if (ids) {
t = t.where('id').anyOf(ensurePlural(ids))
}
if (authors) {
t = t.where('pubkey').anyOf(ensurePlural(authors))
}
return t
// TODO optimize this like user above so we're not double-fetching
worker.post('event/fetchContext', e)
}
export default {db, worker, filterEvents}
const prefilterEvents = filter => {
if (filter.ids) {
return db.events.where('id').anyOf(ensurePlural(filter.ids))
}
if (filter.authors) {
return db.events.where('pubkey').anyOf(ensurePlural(filter.authors))
}
if (filter.kinds) {
return db.events.where('kind').anyOf(ensurePlural(filter.kinds))
}
return db.events
}
const filterEvents = filter => {
return prefilterEvents(filter)
.filter(e => {
if (filter.ids && !filter.ids.includes(e.id)) return false
if (filter.authors && !filter.authors.includes(e.pubkey)) return false
if (filter.kinds && !filter.kinds.includes(e.kind)) return false
if (!isNil(filter.content) && filter.content !== e.content) return false
return true
})
}
const filterReplies = async (id, filter) => {
const tags = db.tags.where('value').equals(id).filter(t => t.mark === 'reply')
const ids = pluck('event', await tags.toArray())
const replies = await filterEvents({...filter, kinds: [1], ids}).toArray()
return replies
}
const filterReactions = async (id, filter) => {
const tags = db.tags.where('value').equals(id).filter(t => t.mark === 'reply')
const ids = pluck('event', await tags.toArray())
const reactions = await filterEvents({...filter, kinds: [7], ids}).toArray()
return reactions
}
const findReaction = async (id, filter) =>
first(await filterReactions(id, filter))
const countReactions = async (id, filter) =>
(await filterReactions(id, filter)).length
export default {
db, worker, lq, ensureContext, filterEvents, filterReactions, countReactions,
findReaction, filterReplies,
}

View File

@ -1,5 +1,4 @@
<script>
import {writable} from 'svelte/store'
import {navigate} from 'svelte-routing'
import {timedelta} from 'src/util/misc'
import Anchor from "src/partials/Anchor.svelte"
@ -10,10 +9,7 @@
export let activeTab
const globalNotes = writable([])
const followNotes = writable([])
const authors = $user ? $user.petnames.map(t => t[1]) : []
const setActiveTab = tab => navigate(`/notes/${tab}`)
</script>
@ -34,9 +30,9 @@
</div>
</div>
{:else if activeTab === 'follows'}
<Notes notes={followNotes} filter={{kinds: [1], authors}} shouldMuffle />
<Notes filter={{kinds: [1], authors}} shouldMuffle />
{:else}
<Notes delta={timedelta(1, 'minutes')} notes={globalNotes} filter={{kinds: [1]}} shouldMuffle />
<Notes filter={{kinds: [1]}} shouldMuffle />
{/if}
<div class="fixed bottom-0 right-0 p-8">
<a

View File

@ -2,6 +2,7 @@ import {identity, isNil, uniqBy, last, without} from 'ramda'
import {get} from 'svelte/store'
import {first, defmulti} from "hurdak/lib/hurdak"
import {user} from "src/state/user"
import relay from 'src/relay'
import {nostr, relays} from 'src/state/nostr'
import {ensureAccounts} from 'src/state/app'
@ -34,7 +35,7 @@ dispatch.addMethod("account/update", async (topic, updates) => {
user.set({...get(user), ...updates})
// Tell the network
await nostr.publish(nostr.event(0, JSON.stringify(updates)))
await relay.worker.post('event/publish', nostr.event(0, JSON.stringify(updates)))
})
dispatch.addMethod("account/petnames", async (topic, petnames) => {
@ -44,7 +45,7 @@ dispatch.addMethod("account/petnames", async (topic, petnames) => {
user.set({...$user, petnames})
// Tell the network
await nostr.publish(nostr.event(3, '', petnames))
await relay.worker.post('event/publish', nostr.event(3, '', petnames))
})
dispatch.addMethod("account/muffle", async (topic, muffle) => {
@ -54,7 +55,7 @@ dispatch.addMethod("account/muffle", async (topic, muffle) => {
user.set({...$user, muffle})
// Tell the network
await nostr.publish(nostr.event(12165, '', muffle))
await relay.worker.post('event/publish', nostr.event(12165, '', muffle))
})
dispatch.addMethod("relay/join", async (topic, url) => {
@ -74,7 +75,7 @@ dispatch.addMethod("relay/leave", (topic, url) => {
dispatch.addMethod("room/create", async (topic, room) => {
const event = nostr.event(40, JSON.stringify(room))
await nostr.publish(event)
await relay.worker.post('event/publish', event)
return event
})
@ -82,7 +83,7 @@ dispatch.addMethod("room/create", async (topic, room) => {
dispatch.addMethod("room/update", async (topic, {id, ...room}) => {
const event = nostr.event(41, JSON.stringify(room), [t("e", id)])
await nostr.publish(event)
await relay.worker.post('event/publish', event)
return event
})
@ -90,7 +91,7 @@ dispatch.addMethod("room/update", async (topic, {id, ...room}) => {
dispatch.addMethod("message/create", async (topic, roomId, content) => {
const event = nostr.event(42, content, [t("e", roomId, "root")])
await nostr.publish(event)
await relay.worker.post('event/publish', event)
return event
})
@ -98,7 +99,7 @@ dispatch.addMethod("message/create", async (topic, roomId, content) => {
dispatch.addMethod("note/create", async (topic, content, tags=[]) => {
const event = nostr.event(1, content, tags)
await nostr.publish(event)
await relay.worker.post('event/publish', event)
return event
})
@ -107,7 +108,7 @@ dispatch.addMethod("reaction/create", async (topic, content, e) => {
const tags = copyTags(e, [t("p", e.pubkey), t("e", e.id, 'reply')])
const event = nostr.event(7, content, tags)
await nostr.publish(event)
await relay.worker.post('event/publish', event)
return event
})
@ -116,7 +117,7 @@ dispatch.addMethod("reply/create", async (topic, content, e) => {
const tags = copyTags(e, [t("p", e.pubkey), t("e", e.id, 'reply')])
const event = nostr.event(1, content, tags)
await nostr.publish(event)
await relay.worker.post('event/publish', event)
return event
})
@ -124,7 +125,7 @@ dispatch.addMethod("reply/create", async (topic, content, e) => {
dispatch.addMethod("event/delete", async (topic, ids) => {
const event = nostr.event(5, '', ids.map(id => t("e", id)))
await nostr.publish(event)
await relay.worker.post('event/publish', event)
return event
})

View File

@ -1,6 +1,7 @@
import {writable} from "svelte/store"
import {getLocalJson, setLocalJson} from "src/util/misc"
import {nostr} from 'src/state/nostr'
import relay from 'src/relay'
export const user = writable(getLocalJson("coracle/user"))
@ -10,8 +11,10 @@ user.subscribe($user => {
// Keep nostr in sync
if ($user?.privkey) {
nostr.login($user.privkey)
relay.worker.post('pool/setPrivateKey', $user.privkey)
} else if ($user?.pubkey) {
nostr.pubkeyLogin($user.pubkey)
relay.worker.post('pool/setPublicKey', $user.pubkey)
}
// Migrate data from old formats

View File

@ -1,6 +1,7 @@
import {relayPool} from 'nostr-tools'
import {defmulti, noop, uuid} from 'hurdak/lib/hurdak'
import {now, timedelta} from "src/util/misc"
import {filterTags} from "src/nostr/tags"
// ============================================================================
// Utils/config
@ -66,17 +67,26 @@ onmessage.addMethod('pool/setPrivateKey', withPayload(privkey => {
}))
onmessage.addMethod('pool/setPublicKey', withPayload(pubkey => {
pool._privkey = pubkey
// TODO fix this, it ain't gonna work
pool.registerSigningFunction(async event => {
const {sig} = await window.nostr.signEvent(event)
return sig
})
pool._pubkey = pubkey
}))
onmessage.addMethod('event/publish', withPayload(event => {
pool.publish(event)
post('events/put', event)
}))
onmessage.addMethod('user/update', withPayload(async user => {
if (!user.pubkey) throw new Error("Invalid user")
req({
const sub = req({
filter: {kinds: [0], authors: [user.pubkey], since: user.updated_at},
onEvent: e => {
try {
@ -86,7 +96,20 @@ onmessage.addMethod('user/update', withPayload(async user => {
}
},
onEose: () => {
sub.unsub()
post('users/put', {...user, updated_at: now()})
},
})
}))
onmessage.addMethod('event/fetchContext', withPayload(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(),
})
}))