From 1fda0200299c81bb479c800e9011126eb59474f6 Mon Sep 17 00:00:00 2001 From: Jonathan Staab Date: Fri, 25 Nov 2022 21:04:47 -0800 Subject: [PATCH] Implement reactions --- README.md | 2 +- src/partials/Note.svelte | 47 +++++++++++++++++++++-- src/partials/NoteDetail.svelte | 9 ++++- src/routes/ChatRoom.svelte | 2 +- src/routes/NoteCreate.svelte | 7 ++++ src/routes/Notes.svelte | 23 ++++++------ src/routes/Profile.svelte | 2 + src/state/app.js | 69 +++++++++++++++++++++++++++++++++- src/state/dispatch.js | 37 +++++++++++++++--- 9 files changed, 172 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 938b3bb6..28368e2f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/partials/Note.svelte b/src/partials/Note.svelte index befa4567..0db14e69 100644 --- a/src/partials/Note.svelte +++ b/src/partials/Note.svelte @@ -1,21 +1,49 @@
  • {ellipsize(note.content, 240)}

    - - - +
    + + {note.replies.length} +
    +
    + like ? deleteReaction(like) : react("+")} /> + {uniqBy(prop('pubkey'), note.reactions.filter(whereEq({content: '+'}))).length} +
    +
    + flag ? deleteReaction(flag) : react("-")} /> + {uniqBy(prop('pubkey'), note.reactions.filter(whereEq({content: '-'}))).length} +
  • diff --git a/src/partials/NoteDetail.svelte b/src/partials/NoteDetail.svelte index a42b0ac3..26316945 100644 --- a/src/partials/NoteDetail.svelte +++ b/src/partials/NoteDetail.svelte @@ -1,12 +1,17 @@ diff --git a/src/routes/ChatRoom.svelte b/src/routes/ChatRoom.svelte index fe502442..c9d2367b 100644 --- a/src/routes/ChatRoom.svelte +++ b/src/routes/ChatRoom.svelte @@ -53,7 +53,7 @@ 42: () => { messages = messages.concat(e) - ensureAccount(e) + ensureAccount(e.pubkey) const $prevListItem = last(document.querySelectorAll('.chat-message')) diff --git a/src/routes/NoteCreate.svelte b/src/routes/NoteCreate.svelte index 14a33aab..7d3a6be0 100644 --- a/src/routes/NoteCreate.svelte +++ b/src/routes/NoteCreate.svelte @@ -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") + } + })
    diff --git a/src/routes/Notes.svelte b/src/routes/Notes.svelte index 8284ddb5..53b309d6 100644 --- a/src/routes/Notes.svelte +++ b/src/routes/Notes.svelte @@ -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() })
      - {#each reverse(notes) as n} + {#each reverse(notes || []) as n (n.id)} {/each}
    diff --git a/src/routes/Profile.svelte b/src/routes/Profile.svelte index 446e41dc..40510636 100644 --- a/src/routes/Profile.svelte +++ b/src/routes/Profile.svelte @@ -44,6 +44,8 @@ } else { await dispatch("account/update", values) + navigate(`/user/${$user.pubkey}`) + toast.show("info", "Your profile has been updated!") } } diff --git a/src/state/app.js b/src/state/app.js index 6c9985bf..8620bbd8 100644 --- a/src/state/app.js +++ b/src/state/app.js @@ -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() + } +} diff --git a/src/state/dispatch.js b/src/state/dispatch.js index deac66d9..70283a79 100644 --- a/src/state/dispatch.js +++ b/src/state/dispatch.js @@ -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 +}