mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Implement reactions
This commit is contained in:
parent
fef1aa6ce3
commit
1fda020029
@ -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.
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -53,7 +53,7 @@
|
||||
42: () => {
|
||||
messages = messages.concat(e)
|
||||
|
||||
ensureAccount(e)
|
||||
ensureAccount(e.pubkey)
|
||||
|
||||
const $prevListItem = last(document.querySelectorAll('.chat-message'))
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -44,6 +44,8 @@
|
||||
} else {
|
||||
await dispatch("account/update", values)
|
||||
|
||||
navigate(`/user/${$user.pubkey}`)
|
||||
|
||||
toast.show("info", "Your profile has been updated!")
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user