Fix alerts

This commit is contained in:
Jonathan Staab 2023-02-06 10:41:48 -06:00
parent efac30809a
commit 9bdb707d27
13 changed files with 93 additions and 84 deletions

View File

@ -1,5 +1,6 @@
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
npm run lint npm run qa:lint
npm run check npm run qa:check

View File

@ -55,7 +55,6 @@ If you like Coracle and want to support its development, you can donate sats via
- Put user detail in a modal? - Put user detail in a modal?
- ReplaceState for settings modals? - ReplaceState for settings modals?
- [ ] Mentions are sorta weird, usually mention self - [ ] Mentions are sorta weird, usually mention self
- [ ] Alerts are not showing likes, just generally screwy. Maybe because I threadify before adding to the db?
- [ ] Change network tab to list relays the user is connected to - [ ] Change network tab to list relays the user is connected to
- [ ] Sync mentions box and in-reply mentions - [ ] Sync mentions box and in-reply mentions
- [ ] Add petnames for channels - [ ] Add petnames for channels
@ -63,11 +62,17 @@ If you like Coracle and want to support its development, you can donate sats via
# Changelog # Changelog
## 0.2.10
- [x] Fixed likes not showing up in alerts
- [x] Raised threshold for pool to 2 so we don't have such a small amount of results
- [x] Wait for profile info on login, navigate to network by default
## 0.2.9 ## 0.2.9
- [x] Fix a bug in pool.subscribe which was causing requests to wait for all connections - [x] Fixed a bug in pool.subscribe which was causing requests to wait for all connections
- [x] Add typescript with pre-commit hook - [x] Added typescript with pre-commit hook
- [x] Fix layout for chat, person pages - [x] Fixed layout for chat, person pages
- [x] Parse relays for kind 3 - [x] Parse relays for kind 3
## 0.2.8 ## 0.2.8

BIN
package-lock.json generated

Binary file not shown.

View File

@ -7,8 +7,10 @@
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint src/*/** --quiet", "qa:lint": "eslint src/*/** --quiet",
"check": "svelte-check --tsconfig ./tsconfig.json --threshold error" "qa:check": "svelte-check --tsconfig ./tsconfig.json --threshold error",
"qa:all": "run-p qa:lint qa:check",
"watch": "find src -type f | entr -r npm run qa:all"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.1.0", "@sveltejs/vite-plugin-svelte": "^1.1.0",
@ -36,6 +38,7 @@
"hurdak": "github:ConsignCloud/hurdak", "hurdak": "github:ConsignCloud/hurdak",
"husky": "^8.0.3", "husky": "^8.0.3",
"nostr-tools": "^1.2.1", "nostr-tools": "^1.2.1",
"npm-run-all": "^4.1.5",
"ramda": "^0.28.0", "ramda": "^0.28.0",
"svelte-check": "^3.0.3", "svelte-check": "^3.0.3",
"svelte-link-preview": "^0.3.3", "svelte-link-preview": "^0.3.3",

View File

@ -12,9 +12,10 @@
import {displayPerson, isLike} from 'src/util/nostr' import {displayPerson, isLike} from 'src/util/nostr'
import {timedelta, now} from 'src/util/misc' import {timedelta, now} from 'src/util/misc'
import {keys, user, pool, getRelays} from 'src/agent' import {keys, user, pool, getRelays} from 'src/agent'
import {modal, toast, settings, logUsage, alerts, messages} from "src/app" import {modal, toast, settings, logUsage, alerts, messages, loadAppData} from "src/app"
import {routes} from "src/app/ui" import {routes} from "src/app/ui"
import Anchor from 'src/partials/Anchor.svelte' import Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.svelte'
import Spinner from 'src/partials/Spinner.svelte' import Spinner from 'src/partials/Spinner.svelte'
import Modal from 'src/partials/Modal.svelte' import Modal from 'src/partials/Modal.svelte'
import SignUp from "src/views/SignUp.svelte" import SignUp from "src/views/SignUp.svelte"
@ -74,9 +75,7 @@
onMount(() => { onMount(() => {
if ($user) { if ($user) {
alerts.load(getRelays(), $user.pubkey) loadAppData($user.pubkey)
alerts.listen(getRelays(), $user.pubkey)
messages.listen(getRelays(), $user.pubkey)
} }
const interval = setInterval(() => { const interval = setInterval(() => {
@ -301,6 +300,13 @@
<PubKeyLogin /> <PubKeyLogin />
{:else if $modal.type === 'person/settings'} {:else if $modal.type === 'person/settings'}
<PersonSettings /> <PersonSettings />
{:else if $modal.type === 'message'}
<Content size="lg">
<div class="text-center">{$modal.message}</div>
{#if $modal.spinner}
<Spinner delay={0} />
{/if}
</Content>
{/if} {/if}
</Modal> </Modal>
{/if} {/if}

View File

@ -192,17 +192,15 @@ const request = (relays, filters, {threshold = 2} = {}): Promise<Record<string,
const eose = [] const eose = []
const attemptToComplete = () => { const attemptToComplete = () => {
const done = ( const allEose = eose.length === relays.length
eose.length === relays.length const atThreshold = eose.filter(url => relaysWithEvents.has(url)).length >= threshold
|| eose.filter(url => relaysWithEvents.has(url)).length >= threshold const hardTimeout = Date.now() - now >= 5000
|| ( const softTimeout = (
Date.now() - now >= 1000 Date.now() - now >= 1000
&& eose.length > relays.length - Math.round(relays.length / 10) && eose.length > relays.length - Math.round(relays.length / 10)
)
|| Date.now() - now >= 5000
) )
if (done) { if (allEose || atThreshold || hardTimeout || softTimeout) {
agg.unsub() agg.unsub()
resolve(events) resolve(events)
} }

View File

@ -1,9 +1,10 @@
import {get} from 'svelte/store' import {get} from 'svelte/store'
import {synced, batch, now} from 'src/util/misc' import {groupBy, pluck, partition, propEq} from 'ramda'
import {isAlert} from 'src/util/nostr' import {synced, timedelta, batch, now} from 'src/util/misc'
import {load as _load, listen as _listen, getMuffle, db} from 'src/agent' import {isAlert, findReplyId} from 'src/util/nostr'
import {load as _load, listen as _listen, db} from 'src/agent'
import loaders from 'src/app/loaders' import loaders from 'src/app/loaders'
import {threadify} from 'src/app' import {annotate} from 'src/app'
let listener let listener
@ -14,21 +15,29 @@ const onChunk = async (relays, pubkey, events) => {
events = events.filter(e => isAlert(e, pubkey)) events = events.filter(e => isAlert(e, pubkey))
if (events.length > 0) { if (events.length > 0) {
const context = await loaders.loadContext(relays, events, {threshold: 2}) const context = await loaders.loadContext(relays, events)
const notes = threadify(events, context, {muffle: getMuffle()}) const [likes, notes] = partition(propEq('kind', 7), events)
const annotatedNotes = notes.map(n => annotate(n, context))
const likesByParent = groupBy(findReplyId, likes)
const likedNotes = context
.filter(e => likesByParent[e.id])
.map(e => annotate({...e, likedBy: pluck('pubkey', likesByParent[e.id])}, context))
await db.table('alerts').bulkPut(notes) await db.table('alerts').bulkPut(annotatedNotes.concat(likedNotes))
mostRecentAlert.update($t => events.reduce((t, e) => Math.max(t, e.created_at), $t)) mostRecentAlert.update($t => events.reduce((t, e) => Math.max(t, e.created_at), $t))
} }
} }
const load = async (relays, pubkey) => { const load = async (relays, pubkey) => {
const since = get(mostRecentAlert) // Include an offset so we don't miss alerts on one relay but not another
const since = get(mostRecentAlert) - timedelta(30, 'days')
// Crank the threshold up since we can afford for this to be slow
const events = await _load( const events = await _load(
relays, relays,
{kinds: [1, 7], '#p': [pubkey], since, limit: 100}, {kinds: [1, 7], '#p': [pubkey], since, limit: 1000},
{threshold: 2} {threshold: 10}
) )
onChunk(relays, pubkey, events) onChunk(relays, pubkey, events)

View File

@ -14,6 +14,15 @@ import loaders from 'src/app/loaders'
export {toast, modal, settings, alerts, messages, logUsage} export {toast, modal, settings, alerts, messages, logUsage}
export const loadAppData = pubkey => {
return Promise.all([
loaders.loadNetwork(getRelays(), pubkey),
alerts.load(getRelays(), pubkey),
alerts.listen(getRelays(), pubkey),
messages.listen(getRelays(), pubkey),
])
}
export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}, usingExtension = false) => { export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}, usingExtension = false) => {
if (privkey) { if (privkey) {
keys.setPrivateKey(privkey) keys.setPrivateKey(privkey)
@ -21,16 +30,18 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
keys.setPublicKey(pubkey) keys.setPublicKey(pubkey)
} }
modal.set({type: 'message', message: "Loading your profile data...", spinner: true})
// Load network and start listening, but don't wait for it // Load network and start listening, but don't wait for it
loaders.loadNetwork(getRelays(), pubkey), loadAppData(pubkey)
alerts.load(getRelays(), pubkey),
alerts.listen(getRelays(), pubkey), // Load our user so we can populate network and show profile info
messages.listen(getRelays(), pubkey) await loaders.loadPeople(getRelays(), [pubkey])
// Not ideal, but the network tab depends on the user's social network being // Not ideal, but the network tab depends on the user's social network being
// loaded, so put them on global when they first log in so we're not slowing // loaded, so put them on global when they first log in so we're not slowing
// down users' first run experience too much // down users' first run experience too much
navigate('/notes/global') navigate('/notes/network')
} }
export const addRelay = async relay => { export const addRelay = async relay => {
@ -46,12 +57,8 @@ export const addRelay = async relay => {
// Publish to the new set of relays // Publish to the new set of relays
await cmd.setRelays(relays, relays) await cmd.setRelays(relays, relays)
await Promise.all([ // Reload alerts, messages, etc
loaders.loadNetwork(relays, person.pubkey), await loadAppData(person.pubkey)
alerts.load(relays, person.pubkey),
alerts.listen(relays, person.pubkey),
messages.listen(getRelays(), person.pubkey)
])
} }
} }
@ -126,6 +133,7 @@ export const annotate = (note, context) => {
export const threadify = (events, context, {muffle = [], showReplies = true} = {}) => { export const threadify = (events, context, {muffle = [], showReplies = true} = {}) => {
const contextById = createMap('id', events.concat(context)) const contextById = createMap('id', events.concat(context))
// Show parents when possible. For reactions, if there's no parent, // Show parents when possible. For reactions, if there's no parent,
// throw it away. Sort by created date descending // throw it away. Sort by created date descending
const notes = sortBy( const notes = sortBy(

View File

@ -2,8 +2,7 @@ import {uniqBy, prop, uniq, flatten, pluck, identity} from 'ramda'
import {ensurePlural, createMap, chunk} from 'hurdak/lib/hurdak' import {ensurePlural, createMap, chunk} from 'hurdak/lib/hurdak'
import {findReply, personKinds, Tags} from 'src/util/nostr' import {findReply, personKinds, Tags} from 'src/util/nostr'
import {now, timedelta} from 'src/util/misc' import {now, timedelta} from 'src/util/misc'
import {load, getPerson} from 'src/agent' import {load, getPerson, getFollows} from 'src/agent'
import defaults from 'src/agent/defaults'
const getStalePubkeys = pubkeys => { const getStalePubkeys = pubkeys => {
// If we're not reloading, only get pubkeys we don't already know about // If we're not reloading, only get pubkeys we don't already know about
@ -28,17 +27,7 @@ const loadPeople = (relays, pubkeys, {kinds = personKinds, force = false, ...opt
} }
const loadNetwork = async (relays, pubkey) => { const loadNetwork = async (relays, pubkey) => {
// Get this user's profile to start with. This may update what relays const tags = Tags.wrap(getFollows(pubkey))
// are available, so don't assign relays to a variable here.
const events = pubkey ? await loadPeople(relays, [pubkey], {force: true}) : []
let petnames = Tags.from(events.filter(e => e.kind === 3)).type("p").all()
// Default to some cool guys we know
if (petnames.length === 0) {
petnames = defaults.petnames
}
const tags = Tags.wrap(petnames)
// Use nip-2 recommended relays to load our user's second-order follows // Use nip-2 recommended relays to load our user's second-order follows
await loadPeople(tags.relays(), tags.values().all()) await loadPeople(tags.relays(), tags.values().all())

View File

@ -178,7 +178,7 @@
</div> </div>
{#if suggestions.length > 0} {#if suggestions.length > 0}
<div class="rounded border border-solid border-medium mt-2" in:fly={{y: 20}}> <div class="rounded border border-solid border-medium mt-2 flex flex-col" in:fly={{y: 20}}>
{#each suggestions as person, i (person.pubkey)} {#each suggestions as person, i (person.pubkey)}
<button <button
class="py-2 px-4 cursor-pointer" class="py-2 px-4 cursor-pointer"

View File

@ -1,10 +1,11 @@
<script> <script>
import {fly} from 'svelte/transition' import {fly} from 'svelte/transition'
import {uniqBy, prop} from 'ramda' import {uniq} from 'ramda'
import {ellipsize, quantify} from 'hurdak/lib/hurdak' import {ellipsize, quantify} from 'hurdak/lib/hurdak'
import Badge from "src/partials/Badge.svelte" import Badge from "src/partials/Badge.svelte"
import {formatTimestamp} from 'src/util/misc' import {formatTimestamp} from 'src/util/misc'
import {killEvent} from 'src/util/html' import {killEvent} from 'src/util/html'
import {getPerson} from 'src/agent'
import {modal} from 'src/app' import {modal} from 'src/app'
export let note export let note
@ -26,11 +27,11 @@
<button <button
class="py-2 px-3 flex flex-col gap-2 text-white cursor-pointer transition-all class="py-2 px-3 flex flex-col gap-2 text-white cursor-pointer transition-all
border border-solid border-black hover:border-medium hover:bg-dark" border border-solid border-black hover:border-medium hover:bg-dark text-left"
on:click={() => modal.set({type: 'note/detail', note})}> on:click={() => modal.set({type: 'note/detail', note})}>
<div class="flex gap-2 items-center justify-between relative"> <div class="flex gap-2 items-center justify-between relative">
<button class="cursor-pointer" on:click={openPopover}> <button class="cursor-pointer" on:click={openPopover}>
{quantify(note.people.length, 'person', 'people')} liked your note. {quantify(note.likedBy.length, 'person', 'people')} liked your note.
</button> </button>
{#if isOpen} {#if isOpen}
<button in:fly={{y: 20}} class="fixed inset-0 z-10" on:click={closePopover} /> <button in:fly={{y: 20}} class="fixed inset-0 z-10" on:click={closePopover} />
@ -39,8 +40,8 @@
in:fly={{y: 20}} in:fly={{y: 20}}
class="absolute top-0 mt-8 py-2 px-4 rounded border border-solid border-medium class="absolute top-0 mt-8 py-2 px-4 rounded border border-solid border-medium
bg-dark grid grid-cols-3 gap-y-2 gap-x-4 z-20"> bg-dark grid grid-cols-3 gap-y-2 gap-x-4 z-20">
{#each uniqBy(prop('pubkey'), note.people) as person (person.pubkey)} {#each uniq(note.likedBy) as pubkey}
<Badge {person} /> <Badge person={getPerson(pubkey)} />
{/each} {/each}
</button> </button>
{/if} {/if}

View File

@ -1,9 +1,11 @@
<script> <script>
import {fade} from 'svelte/transition' import {fade} from 'svelte/transition'
import {Circle2} from 'svelte-loading-spinners' import {Circle2} from 'svelte-loading-spinners'
export let delay = 1000
</script> </script>
<div class="py-20 flex flex-col gap-4 items-center justify-center" in:fade={{delay: 1000}}> <div class="py-20 flex flex-col gap-4 items-center justify-center" in:fade={{delay}}>
<slot /> <slot />
<Circle2 colorOuter="#CCC5B9" colorInner="#403D39" colorCenter="#EB5E28" /> <Circle2 colorOuter="#CCC5B9" colorInner="#403D39" colorCenter="#EB5E28" />
</div> </div>

View File

@ -3,14 +3,14 @@
import {onMount} from 'svelte' import {onMount} from 'svelte'
import {fly} from 'svelte/transition' import {fly} from 'svelte/transition'
import {now, createScroller} from 'src/util/misc' import {now, createScroller} from 'src/util/misc'
import {user, db} from 'src/agent' import {db} from 'src/agent'
import {alerts} from 'src/app' import {alerts} from 'src/app'
import Note from 'src/partials/Note.svelte' import Note from 'src/partials/Note.svelte'
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Like from 'src/partials/Like.svelte' import Like from 'src/partials/Like.svelte'
let limit = 0 let limit = 0
let annotatedNotes = [] let notes = []
onMount(async () => { onMount(async () => {
alerts.lastCheckedAlerts.set(now()) alerts.lastCheckedAlerts.set(now())
@ -19,37 +19,24 @@
limit += 10 limit += 10
const events = await db.table('alerts').toArray() const events = await db.table('alerts').toArray()
const notes = events.filter(e => e.kind === 1)
const likes = events.filter(e => e.kind === 7)
// Combine likes of a single note. Remove grandchild likes notes = sortBy(e => -e.created_at, events).slice(0, limit)
const likesById = {}
for (const like of likes.filter(e => e.parent?.pubkey === $user.pubkey)) {
if (!likesById[like.parent.id]) {
likesById[like.parent.id] = {...like.parent, people: []}
}
likesById[like.parent.id].people.push(like.person)
}
annotatedNotes = sortBy(
e => -e.created_at,
notes
.filter(e => e && e.pubkey !== $user.pubkey)
.concat(Object.values(likesById))
).slice(0, limit)
}) })
}) })
</script> </script>
<Content> <Content>
{#each annotatedNotes as e (e.id)} {#each notes as e (e.id)}
<div in:fly={{y: 20}}> <div in:fly={{y: 20}}>
{#if e.people} {#if e.likedBy}
<Like note={e} /> <Like note={e} />
{:else} {:else}
<Note note={e} /> <Note note={e} />
{/if} {/if}
</div> </div>
{:else}
<Content size="lg" class="text-center">
No alerts found - check back later!
</Content>
{/each} {/each}
</Content> </Content>