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 1461 additions and 88 deletions

View File

@ -1,5 +1,6 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint
npm run check
npm run qa:lint
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?
- ReplaceState for settings modals?
- [ ] 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
- [ ] Sync mentions box and in-reply mentions
- [ ] Add petnames for channels
@ -63,11 +62,17 @@ If you like Coracle and want to support its development, you can donate sats via
# 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
- [x] Fix a bug in pool.subscribe which was causing requests to wait for all connections
- [x] Add typescript with pre-commit hook
- [x] Fix layout for chat, person pages
- [x] Fixed a bug in pool.subscribe which was causing requests to wait for all connections
- [x] Added typescript with pre-commit hook
- [x] Fixed layout for chat, person pages
- [x] Parse relays for kind 3
## 0.2.8

1372
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,8 +7,10 @@
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint src/*/** --quiet",
"check": "svelte-check --tsconfig ./tsconfig.json --threshold error"
"qa:lint": "eslint src/*/** --quiet",
"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": {
"@sveltejs/vite-plugin-svelte": "^1.1.0",
@ -36,6 +38,7 @@
"hurdak": "github:ConsignCloud/hurdak",
"husky": "^8.0.3",
"nostr-tools": "^1.2.1",
"npm-run-all": "^4.1.5",
"ramda": "^0.28.0",
"svelte-check": "^3.0.3",
"svelte-link-preview": "^0.3.3",

View File

@ -12,9 +12,10 @@
import {displayPerson, isLike} from 'src/util/nostr'
import {timedelta, now} from 'src/util/misc'
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 Anchor from 'src/partials/Anchor.svelte'
import Content from 'src/partials/Content.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import Modal from 'src/partials/Modal.svelte'
import SignUp from "src/views/SignUp.svelte"
@ -74,9 +75,7 @@
onMount(() => {
if ($user) {
alerts.load(getRelays(), $user.pubkey)
alerts.listen(getRelays(), $user.pubkey)
messages.listen(getRelays(), $user.pubkey)
loadAppData($user.pubkey)
}
const interval = setInterval(() => {
@ -301,6 +300,13 @@
<PubKeyLogin />
{:else if $modal.type === 'person/settings'}
<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}
</Modal>
{/if}

View File

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

View File

@ -1,9 +1,10 @@
import {get} from 'svelte/store'
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 {groupBy, pluck, partition, propEq} from 'ramda'
import {synced, timedelta, batch, now} from 'src/util/misc'
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 {threadify} from 'src/app'
import {annotate} from 'src/app'
let listener
@ -14,21 +15,29 @@ const onChunk = async (relays, pubkey, events) => {
events = events.filter(e => isAlert(e, pubkey))
if (events.length > 0) {
const context = await loaders.loadContext(relays, events, {threshold: 2})
const notes = threadify(events, context, {muffle: getMuffle()})
const context = await loaders.loadContext(relays, events)
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))
}
}
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(
relays,
{kinds: [1, 7], '#p': [pubkey], since, limit: 100},
{threshold: 2}
{kinds: [1, 7], '#p': [pubkey], since, limit: 1000},
{threshold: 10}
)
onChunk(relays, pubkey, events)

View File

@ -14,6 +14,15 @@ import loaders from 'src/app/loaders'
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) => {
if (privkey) {
keys.setPrivateKey(privkey)
@ -21,16 +30,18 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
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
loaders.loadNetwork(getRelays(), pubkey),
alerts.load(getRelays(), pubkey),
alerts.listen(getRelays(), pubkey),
messages.listen(getRelays(), pubkey)
loadAppData(pubkey)
// Load our user so we can populate network and show profile info
await loaders.loadPeople(getRelays(), [pubkey])
// 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
// down users' first run experience too much
navigate('/notes/global')
navigate('/notes/network')
}
export const addRelay = async relay => {
@ -46,12 +57,8 @@ export const addRelay = async relay => {
// 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),
messages.listen(getRelays(), person.pubkey)
])
// Reload alerts, messages, etc
await loadAppData(person.pubkey)
}
}
@ -126,6 +133,7 @@ export const annotate = (note, context) => {
export const threadify = (events, context, {muffle = [], showReplies = true} = {}) => {
const contextById = createMap('id', events.concat(context))
// Show parents when possible. For reactions, if there's no parent,
// throw it away. Sort by created date descending
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 {findReply, personKinds, Tags} from 'src/util/nostr'
import {now, timedelta} from 'src/util/misc'
import {load, getPerson} from 'src/agent'
import defaults from 'src/agent/defaults'
import {load, getPerson, getFollows} from 'src/agent'
const getStalePubkeys = pubkeys => {
// 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) => {
// Get this user's profile to start with. This may update what relays
// 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)
const tags = Tags.wrap(getFollows(pubkey))
// Use nip-2 recommended relays to load our user's second-order follows
await loadPeople(tags.relays(), tags.values().all())

View File

@ -178,7 +178,7 @@
</div>
{#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)}
<button
class="py-2 px-4 cursor-pointer"

View File

@ -1,10 +1,11 @@
<script>
import {fly} from 'svelte/transition'
import {uniqBy, prop} from 'ramda'
import {uniq} from 'ramda'
import {ellipsize, quantify} from 'hurdak/lib/hurdak'
import Badge from "src/partials/Badge.svelte"
import {formatTimestamp} from 'src/util/misc'
import {killEvent} from 'src/util/html'
import {getPerson} from 'src/agent'
import {modal} from 'src/app'
export let note
@ -26,11 +27,11 @@
<button
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})}>
<div class="flex gap-2 items-center justify-between relative">
<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>
{#if isOpen}
<button in:fly={{y: 20}} class="fixed inset-0 z-10" on:click={closePopover} />
@ -39,8 +40,8 @@
in:fly={{y: 20}}
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">
{#each uniqBy(prop('pubkey'), note.people) as person (person.pubkey)}
<Badge {person} />
{#each uniq(note.likedBy) as pubkey}
<Badge person={getPerson(pubkey)} />
{/each}
</button>
{/if}

View File

@ -1,9 +1,11 @@
<script>
import {fade} from 'svelte/transition'
import {Circle2} from 'svelte-loading-spinners'
export let delay = 1000
</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 />
<Circle2 colorOuter="#CCC5B9" colorInner="#403D39" colorCenter="#EB5E28" />
</div>

View File

@ -3,14 +3,14 @@
import {onMount} from 'svelte'
import {fly} from 'svelte/transition'
import {now, createScroller} from 'src/util/misc'
import {user, db} from 'src/agent'
import {db} from 'src/agent'
import {alerts} from 'src/app'
import Note from 'src/partials/Note.svelte'
import Content from 'src/partials/Content.svelte'
import Like from 'src/partials/Like.svelte'
let limit = 0
let annotatedNotes = []
let notes = []
onMount(async () => {
alerts.lastCheckedAlerts.set(now())
@ -19,37 +19,24 @@
limit += 10
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
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)
notes = sortBy(e => -e.created_at, events).slice(0, limit)
})
})
</script>
<Content>
{#each annotatedNotes as e (e.id)}
{#each notes as e (e.id)}
<div in:fly={{y: 20}}>
{#if e.people}
{#if e.likedBy}
<Like note={e} />
{:else}
<Note note={e} />
{/if}
</div>
{:else}
<Content size="lg" class="text-center">
No alerts found - check back later!
</Content>
{/each}
</Content>