mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Add alerts page
This commit is contained in:
parent
06ebe73f88
commit
7eb8caf0b1
@ -1,2 +1 @@
|
||||
_VITE_DUFFLEPUD_URL=http://localhost:8000
|
||||
VITE_DUFFLEPUD_URL=https://dufflepud.onrender.com
|
||||
VITE_DUFFLEPUD_URL=http://localhost:8000
|
||||
|
@ -1,5 +1,6 @@
|
||||
Bugs
|
||||
|
||||
- [ ] Add alerts for replies to posts the user liked
|
||||
- [ ] Support bech32 keys/add guide on how to convert
|
||||
- [ ] Loading icon not showing at bottom
|
||||
- [ ] uniq and sortBy are sprinkled all over the place, figure out a better solution
|
||||
|
@ -10,8 +10,9 @@
|
||||
import {Router, Route, links, navigate} from "svelte-routing"
|
||||
import {globalHistory} from "svelte-routing/src/history"
|
||||
import {hasParent} from 'src/util/html'
|
||||
import {timedelta} from 'src/util/misc'
|
||||
import {store as toast} from "src/state/toast"
|
||||
import {channels} from "src/state/nostr"
|
||||
import {channels, epoch} from "src/state/nostr"
|
||||
import {modal, logout, alerts} from "src/state/app"
|
||||
import {user} from 'src/state/user'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
@ -42,36 +43,32 @@
|
||||
let menuIcon
|
||||
let scrollY
|
||||
let suspendedSubs = []
|
||||
let hasAlerts = false
|
||||
let mostRecentAlert = epoch
|
||||
|
||||
export let url = ""
|
||||
|
||||
onMount(() => {
|
||||
// Close menu on click outside
|
||||
document.querySelector("html").addEventListener("click", e => {
|
||||
if (e.target !== menuIcon) {
|
||||
menuIsOpen.set(false)
|
||||
}
|
||||
})
|
||||
// Close menu on click outside
|
||||
document.querySelector("html").addEventListener("click", e => {
|
||||
if (e.target !== menuIcon) {
|
||||
menuIsOpen.set(false)
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
// Poll for new notifications
|
||||
setInterval(throttle(10_000, async () => {
|
||||
(async function pollForNotifications() {
|
||||
if ($user) {
|
||||
const events = await channels.getter.all({
|
||||
limit: 1000,
|
||||
kinds: [1, 7],
|
||||
'#p': $user.id,
|
||||
'#p': $user.pubkey,
|
||||
since: $alerts.since,
|
||||
})
|
||||
|
||||
const items = events//.filter(e => e.pubkey !== $user.pubkey)
|
||||
|
||||
alerts.set({...$alerts, items})
|
||||
|
||||
hasAlerts = items.filter(e => e.created_at > $alerts.since).length > 0
|
||||
} else {
|
||||
hasAlerts = false
|
||||
mostRecentAlert = events.reduce((t, e) => Math.max(t, e.created_at), $alerts.since)
|
||||
}
|
||||
}), 100)
|
||||
|
||||
setTimeout(pollForNotifications, 60_000)
|
||||
})()
|
||||
|
||||
return modal.subscribe($modal => {
|
||||
// Keep scroll position on body, but don't allow scrolling
|
||||
@ -142,7 +139,7 @@
|
||||
<li class="cursor-pointer relative">
|
||||
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/alerts">
|
||||
<i class="fa-solid fa-bell mr-2" /> Alerts
|
||||
{#if hasAlerts}
|
||||
{#if mostRecentAlert > $alerts.since}
|
||||
<div class="w-2 h-2 rounded bg-accent absolute top-3 left-6" />
|
||||
{/if}
|
||||
</a>
|
||||
@ -202,7 +199,7 @@
|
||||
<Anchor external type="unstyled" href="https://github.com/staab/coracle">
|
||||
<h1 class="staatliches text-3xl">Coracle</h1>
|
||||
</Anchor>
|
||||
{#if hasAlerts}
|
||||
{#if mostRecentAlert > $alerts.since}
|
||||
<div class="w-2 h-2 rounded bg-accent absolute top-4 left-12" />
|
||||
{/if}
|
||||
</div>
|
||||
|
18
src/partials/Card.svelte
Normal file
18
src/partials/Card.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<script>
|
||||
import cx from 'classnames'
|
||||
import {fly} from 'svelte/transition'
|
||||
|
||||
export let interactive = false
|
||||
export let invertColors = false
|
||||
</script>
|
||||
|
||||
<li
|
||||
on:click
|
||||
in:fly={{y: 20}}
|
||||
class={cx("py-2 px-3 flex flex-col gap-2 text-white", {
|
||||
"cursor-pointer transition-all": interactive,
|
||||
"border border-solid border-black hover:border-medium hover:bg-dark": interactive && !invertColors,
|
||||
"border border-solid border-dark hover:border-medium hover:bg-medium": interactive && invertColors,
|
||||
})}>
|
||||
<slot />
|
||||
</li>
|
@ -2,7 +2,7 @@
|
||||
import cx from 'classnames'
|
||||
import {find, uniqBy, prop, whereEq} from 'ramda'
|
||||
import {onMount} from 'svelte'
|
||||
import {fly, slide} from 'svelte/transition'
|
||||
import {slide} from 'svelte/transition'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {ellipsize} from 'hurdak/src/core'
|
||||
import {hasParent, toHtml, findLink} from 'src/util/html'
|
||||
@ -14,6 +14,7 @@
|
||||
import {user} from "src/state/user"
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
import UserBadge from "src/partials/UserBadge.svelte"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
|
||||
export let note
|
||||
export let depth = 0
|
||||
@ -99,14 +100,7 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
in:fly={{y: 20}}
|
||||
on:click={onClick}
|
||||
class={cx("py-2 px-3 flex flex-col gap-2 text-white", {
|
||||
"cursor-pointer transition-all": interactive,
|
||||
"border border-solid border-black hover:border-medium hover:bg-dark": interactive && !invertColors,
|
||||
"border border-solid border-dark hover:border-medium hover:bg-medium": interactive && invertColors,
|
||||
})}>
|
||||
<Card on:click={onClick} {interactive} {invertColors}>
|
||||
<div class="flex gap-4 items-center justify-between">
|
||||
<UserBadge user={{...$accounts[note.pubkey], pubkey: note.pubkey}} />
|
||||
<p class="text-sm text-light">{formatTimestamp(note.created_at)}</p>
|
||||
@ -155,7 +149,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{#if reply !== null}
|
||||
<div
|
||||
|
@ -1,34 +1,99 @@
|
||||
<script>
|
||||
import {onMount} from 'svelte'
|
||||
import {propEq} from 'ramda'
|
||||
import {createMap} from 'hurdak/lib/hurdak'
|
||||
import {onMount, onDestroy} from 'svelte'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {writable} from 'svelte/store'
|
||||
import {sortBy, uniqBy, prop} from 'ramda'
|
||||
import {now} from 'src/util/misc'
|
||||
import {alerts, annotateNotes} from 'src/state/app'
|
||||
import {ellipsize} from 'hurdak/src/core'
|
||||
import {user} from 'src/state/user'
|
||||
import {Cursor, epoch} from 'src/state/nostr'
|
||||
import {alerts, annotateAlerts, notesListener, createScroller, modal} from 'src/state/app'
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import UserBadge from "src/partials/UserBadge.svelte"
|
||||
import Note from 'src/partials/Note.svelte'
|
||||
|
||||
let notesById
|
||||
let cursor
|
||||
let listener
|
||||
let scroller
|
||||
let interval
|
||||
let modalUnsub
|
||||
let loading = true
|
||||
let events = writable([])
|
||||
|
||||
onMount(() => {
|
||||
onMount(async () => {
|
||||
// Clear notification badge
|
||||
alerts.set({...$alerts, since: now()})
|
||||
|
||||
annotateNotes($alerts.items.filter(propEq('kind', 1))).then($notes => {
|
||||
notesById = createMap('id', $notes)
|
||||
cursor = new Cursor({kinds: [1, 7], '#p': [$user.pubkey]})
|
||||
listener = await notesListener(events, [{kinds: [1, 5, 7]}], {repliesOnly: true})
|
||||
scroller = createScroller(cursor, async chunk => {
|
||||
// Add chunk context
|
||||
chunk = await annotateAlerts(chunk)
|
||||
|
||||
// Sort and deduplicate
|
||||
events.set(sortBy(n => -n.created_at, uniqBy(prop('id'), $events.concat(chunk))))
|
||||
})
|
||||
|
||||
// Track loading based on cursor cutoff date
|
||||
interval = setInterval(() => {
|
||||
loading = cursor.since > epoch
|
||||
}, 1000)
|
||||
|
||||
modalUnsub = modal.subscribe(async $modal => {
|
||||
if ($modal) {
|
||||
cursor.stop()
|
||||
listener.stop()
|
||||
scroller.stop()
|
||||
} else {
|
||||
cursor.start()
|
||||
listener.start()
|
||||
scroller.start()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
cursor?.stop()
|
||||
listener?.stop()
|
||||
scroller?.stop()
|
||||
modalUnsub?.()
|
||||
clearInterval(interval)
|
||||
})
|
||||
</script>
|
||||
|
||||
{#each $alerts.items as e (e.id)}
|
||||
{#if e.kind === 1 && notesById}
|
||||
<Note interactive note={notesById[e.id]} />
|
||||
{:else if e.kind === 7}
|
||||
hi
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex w-full justify-center items-center py-16">
|
||||
<svelte:window on:scroll={scroller?.start} />
|
||||
|
||||
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each $events as e (e.id)}
|
||||
{#if e.kind === 7}
|
||||
<!-- don't show alerts for likes of replies to this user's notes -->
|
||||
{#if e.parent?.pubkey === $user.pubkey}
|
||||
<li
|
||||
in:fly={{y: 20}}
|
||||
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"
|
||||
on:click={() => modal.set({note: e.parent})}>
|
||||
<div class="flex gap-2 items-center">
|
||||
<UserBadge user={e.user} />
|
||||
<span>liked your note.</span>
|
||||
</div>
|
||||
<div class="ml-6 text-light">
|
||||
{ellipsize(e.parent.content, 240)}
|
||||
</div>
|
||||
</li>
|
||||
{/if}
|
||||
{:else}
|
||||
<li in:fly={{y: 20}}><Note showParent note={e} /></li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if loading}
|
||||
<div in:fly={{y: 20}}><Spinner /></div>
|
||||
{:else if $events.length === 0}
|
||||
<div in:fly={{y: 20}} class="flex w-full justify-center items-center py-16">
|
||||
<div class="text-center max-w-md">
|
||||
No recent activity found.
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{/if}
|
||||
|
@ -25,7 +25,7 @@
|
||||
|
||||
const follow = () => {
|
||||
const petnames = $currentUser.petnames
|
||||
.concat([t("p", user.pubkey, user.name)])
|
||||
.concat([t("p", pubkey, user?.name)])
|
||||
|
||||
dispatch('account/petnames', petnames)
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
|
||||
const unfollow = () => {
|
||||
const petnames = $currentUser.petnames
|
||||
.filter(([_, pubkey]) => pubkey !== user.pubkey)
|
||||
.filter(([_, pubkey]) => pubkey !== pubkey)
|
||||
|
||||
dispatch('account/petnames', petnames)
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
const openAdvanced = () => {
|
||||
modal.set({form: 'user/advanced', user})
|
||||
modal.set({form: 'user/advanced', user: user || {pubkey}})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -189,7 +189,6 @@ export const threadify = async notes => {
|
||||
user: $accounts[note.pubkey],
|
||||
reactions: reactionsByParent[note.id] || [],
|
||||
children: uniqBy(prop('id'), _notes.filter(n => findReply(n) === note.id)).map(annotate),
|
||||
numberOfAncestors: note.tags.filter(([x]) => x === 'e').length,
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,6 +240,42 @@ export const annotateNotes = async (notes, {showParent = false} = {}) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const annotateAlerts = async events => {
|
||||
if (events.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const eventIds = pluck('id', events)
|
||||
const parentIds = events.map(findReply).filter(identity)
|
||||
const filters = [
|
||||
{kinds: [1], ids: parentIds},
|
||||
{kinds: [7], '#e': parentIds},
|
||||
{kinds: [1, 7], '#e': eventIds},
|
||||
]
|
||||
|
||||
const relatedEvents = await channels.getter.all(filters)
|
||||
|
||||
await ensureAccounts(uniq(pluck('pubkey', events.concat(relatedEvents))))
|
||||
|
||||
const $accounts = get(accounts)
|
||||
const reactionsByParent = groupBy(findReply, relatedEvents.filter(e => e.kind === 7 && e.content === '+'))
|
||||
const allNotes = uniqBy(prop('id'), events.concat(relatedEvents).filter(propEq('kind', 1)))
|
||||
const notesById = createMap('id', allNotes)
|
||||
|
||||
const annotate = note => ({
|
||||
...note,
|
||||
user: $accounts[note.pubkey],
|
||||
reactions: reactionsByParent[note.id] || [],
|
||||
children: uniqBy(prop('id'), allNotes.filter(n => findReply(n) === note.id)).map(annotate),
|
||||
})
|
||||
|
||||
return uniqBy(e => e.parent?.id || e.id, events.map(event => {
|
||||
const parentId = findReply(event)
|
||||
|
||||
return {...annotate(event), parent: annotate(notesById[parentId])}
|
||||
}))
|
||||
}
|
||||
|
||||
export const annotateNewNote = async (note) => {
|
||||
await ensureAccounts([note.pubkey])
|
||||
|
||||
@ -249,13 +284,12 @@ export const annotateNewNote = async (note) => {
|
||||
return {
|
||||
...note,
|
||||
user: $accounts[note.pubkey],
|
||||
numberOfAncestors: note.tags.filter(([x]) => x === 'e').length,
|
||||
children: [],
|
||||
reactions: [],
|
||||
}
|
||||
}
|
||||
|
||||
export const notesListener = (notes, filter, {shouldMuffle = false} = {}) => {
|
||||
export const notesListener = (notes, filter, {shouldMuffle = false, repliesOnly = false} = {}) => {
|
||||
const updateNote = (note, id, f) => {
|
||||
if (note.id === id) {
|
||||
return f(note)
|
||||
@ -296,7 +330,7 @@ export const notesListener = (notes, filter, {shouldMuffle = false} = {}) => {
|
||||
const note = await annotateNewNote(e)
|
||||
|
||||
updateNotes(id, n => ({...n, children: n.children.concat(note)}))
|
||||
} else if (!muffle && filterMatches(filter, e)) {
|
||||
} else if (!repliesOnly && !muffle && filterMatches(filter, e)) {
|
||||
const [note] = await threadify([e])
|
||||
|
||||
notes.update($notes => [note].concat($notes))
|
||||
|
Loading…
Reference in New Issue
Block a user