Implement reactions

This commit is contained in:
Jonathan Staab 2022-11-25 21:04:47 -08:00
parent fef1aa6ce3
commit 1fda020029
9 changed files with 172 additions and 26 deletions

View File

@ -1,10 +1,10 @@
Bugs
- [ ] Make sure to unsubscribe from all stores when necessary
- [ ] Format text with line breaks - pre, or split/br
- [ ] Reduce concurrent subscriptions
- [ ] Remove dexie, or use it instead of localstorage for cached data
- [ ] rename /user to /users
- [ ] Add fallback redirect to /notes, ditch / route
- [ ] Memoize room list, currently every time the user switches chat rooms it pulls the full list
- [ ] Fix toast, it gets in the way. Make it smaller and dismissable.

View File

@ -1,21 +1,49 @@
<script>
import cx from 'classnames'
import {onMount} from 'svelte'
import {find, 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 {nostr} from "src/state/nostr"
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'
import UserBadge from "src/partials/UserBadge.svelte"
export let note
export let interactive = false
let like = null
let flag = null
$: {
like = find(e => e.pubkey === $user.pubkey && e.content === "+", note.reactions)
}
$: {
flag = find(e => e.pubkey === $user.pubkey && e.content === "-", note.reactions)
}
const onClick = e => {
if (!['I'].includes(e.target.tagName) && !hasParent('a', e.target)) {
modal.set({note})
}
}
const react = content => {
if ($user) {
dispatch('reaction/create', content, note)
} else {
navigate('/login')
}
}
const deleteReaction = e => {
dispatch('event/delete', [e.id])
}
</script>
<li
@ -32,9 +60,22 @@
<div class="ml-6 flex flex-col gap-2">
<p>{ellipsize(note.content, 240)}</p>
<div class="flex gap-6 text-light">
<i class="fa-solid fa-reply cursor-pointer" />
<i class="fa-solid fa-heart cursor-pointer" />
<i class="fa-solid fa-flag cursor-pointer" />
<div>
<i class="fa-solid fa-reply cursor-pointer" />
{note.replies.length}
</div>
<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}
</div>
<div class={cx({'text-accent': flag})}>
<i
class="fa-solid fa-flag cursor-pointer"
on:click={() => flag ? deleteReaction(flag) : react("-")} />
{uniqBy(prop('pubkey'), note.reactions.filter(whereEq({content: '-'}))).length}
</div>
</div>
</div>
</li>

View File

@ -1,12 +1,17 @@
<script>
import {onMount} from 'svelte'
import {ensureAccount} from 'src/state/app'
import {findNotes} from "src/state/app"
import Note from 'src/partials/Note.svelte'
export let note
onMount(() => {
ensureAccount(note.account)
return findNotes(
[{ids: [note.id]}, {'#e': [note.id]}],
$notes => {
note = $notes[0] || note
}
)
})
</script>

View File

@ -53,7 +53,7 @@
42: () => {
messages = messages.concat(e)
ensureAccount(e)
ensureAccount(e.pubkey)
const $prevListItem = last(document.querySelectorAll('.chat-message'))

View File

@ -5,6 +5,7 @@
import Textarea from "src/partials/Textarea.svelte"
import Button from "src/partials/Button.svelte"
import {dispatch} from "src/state/dispatch"
import {user} from "src/state/user"
import toast from "src/state/toast"
let values = {}
@ -18,6 +19,12 @@
navigate('/notes')
}
onMount(() => {
if (!$user) {
navigate("/login")
}
})
</script>
<div class="m-auto">

View File

@ -3,37 +3,36 @@
import {get} from 'svelte/store'
import {fly} from 'svelte/transition'
import {navigate} from "svelte-routing"
import {prop, reverse, last} from 'ramda'
import {reverse, find, propEq} from 'ramda'
import {timedelta, now, formatTimestamp} from 'src/util/misc'
import Anchor from "src/partials/Anchor.svelte"
import Note from "src/partials/Note.svelte"
import {nostr, relays} from "src/state/nostr"
import {user} from "src/state/user"
import {accounts, ensureAccount} from "src/state/app"
import {findNotes, modal} from "src/state/app"
import {db} from "src/state/db"
let notes = []
let notes
const createNote = () => {
navigate("/notes/new")
}
onMount(() => {
const sub = nostr.sub({
filter: {kinds: [1], since: new Date().valueOf() / 1000 - 7 * 24 * 60 * 60},
cb: e => {
notes = notes.concat(e)
return findNotes({
since: new Date().valueOf() / 1000 - 7 * 24 * 60 * 60,
}, $notes => {
notes = $notes
ensureAccount(e.pubkey)
},
// if ($modal?.note) {
// modal.set({note: find(propEq('id', $modal.note.id), $notes)})
// }
})
return () => sub.unsub()
})
</script>
<ul class="py-8 flex flex-col gap-4 max-w-xl m-auto">
{#each reverse(notes) as n}
{#each reverse(notes || []) as n (n.id)}
<Note interactive note={n} />
{/each}
</ul>

View File

@ -44,6 +44,8 @@
} else {
await dispatch("account/update", values)
navigate(`/user/${$user.pubkey}`)
toast.show("info", "Your profile has been updated!")
}
}

View File

@ -1,5 +1,6 @@
import {prop} from 'ramda'
import {writable, get} from 'svelte/store'
import {prop, find, last, groupBy} from 'ramda'
import {writable, derived, get} from 'svelte/store'
import {switcherFn, ensurePlural} from 'hurdak/lib/hurdak'
import {getLocalJson, setLocalJson, now, timedelta} from "src/util/misc"
import {user} from 'src/state/user'
import {nostr} from 'src/state/nostr'
@ -24,6 +25,8 @@ user.subscribe($user => {
}
})
// Utils
export const ensureAccount = pubkey => {
let $account = prop(pubkey, get(accounts))
@ -47,3 +50,65 @@ export const ensureAccount = pubkey => {
}, 1000)
}
}
export const findNotes = (queries, cb) => {
const notes = writable([])
const reactions = writable([])
const sub = nostr.sub({
filter: ensurePlural(queries).map(q => ({kinds: [1, 5, 7], ...q})),
cb: async e => {
switcherFn(e.kind, {
1: () => {
notes.update($xs => $xs.concat(e))
ensureAccount(e.pubkey)
},
5: () => {
const ids = e.tags.map(t => t[1])
notes.update($xs => $xs.filter(({id}) => !id.includes(ids)))
reactions.update($xs => $xs.filter(({id}) => !id.includes(ids)))
},
7: () => {
reactions.update($xs => $xs.concat(e))
ensureAccount(e.pubkey)
},
})
ensureAccount(e.pubkey)
},
})
const annotatedNotes = derived(
[notes, reactions, accounts],
([$notes, $reactions, $accounts]) => {
const repliesById = groupBy(
n => find(t => last(t) === 'reply', n.tags)[1],
$notes.filter(n => n.tags.map(last).includes('reply'))
)
const reactionsById = groupBy(
n => find(t => last(t) === 'reply', n.tags)[1],
$reactions.filter(n => n.tags.map(last).includes('reply'))
)
const annotate = n => ({
...n,
user: $accounts[n.pubkey],
replies: (repliesById[n.id] || []).map(reply => annotate(reply)),
reactions: (reactionsById[n.id] || []).map(reaction => annotate(reaction)),
})
return $notes.map(annotate)
}
)
const unsubscribe = annotatedNotes.subscribe(cb)
return () => {
sub.unsub()
unsubscribe()
}
}

View File

@ -1,7 +1,7 @@
import {identity, without} from 'ramda'
import {getPublicKey} from 'nostr-tools'
import {get} from 'svelte/store'
import {defmulti} from "hurdak/lib/hurdak"
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'
@ -52,8 +52,7 @@ dispatch.addMethod("room/create", async (topic, room) => {
})
dispatch.addMethod("room/update", async (topic, {id, ...room}) => {
const [relay] = get(relays)
const event = nostr.event(41, JSON.stringify(room), [["e", id, relay]])
const event = nostr.event(41, JSON.stringify(room), [t("e", id)])
await nostr.publish(event)
@ -61,8 +60,7 @@ dispatch.addMethod("room/update", async (topic, {id, ...room}) => {
})
dispatch.addMethod("message/create", async (topic, roomId, content) => {
const [relay] = get(relays)
const event = nostr.event(42, content, [["e", roomId, relay, "root"]])
const event = nostr.event(42, content, [t("e", roomId, "root")])
await nostr.publish(event)
@ -76,3 +74,32 @@ dispatch.addMethod("note/create", async (topic, content) => {
return event
})
dispatch.addMethod("reaction/create", async (topic, content, e) => {
const tags = e.tags.filter(tag => tag[0].includes(["e", "p"])).map(t => t.slice(0, 2))
const event = nostr.event(7, content, tags.concat([t("p", e.pubkey), t("e", e.id, 'reply')]))
await nostr.publish(event)
return event
})
dispatch.addMethod("event/delete", async (topic, ids) => {
const event = nostr.event(5, '', ids.map(id => t("e", id)))
await nostr.publish(event)
return event
})
// utils
const t = (type, content, marker) => {
const tag = [type, content, first(get(relays))]
if (marker) {
tag.push(marker)
}
return tag
}