From 00f976ae7884e13a54dfb611132e56db3dab8318 Mon Sep 17 00:00:00 2001 From: Bojan Mojsilovic Date: Wed, 24 Apr 2024 14:27:34 +0200 Subject: [PATCH] Note quotes reactions --- .../EmbeddedNote/EmbeddedNote.module.scss | 4 + src/components/EmbeddedNote/EmbeddedNote.tsx | 21 +- src/components/Note/Note.module.scss | 5 + src/components/Note/Note.tsx | 67 ++++- src/components/Note/NoteFooter/NoteFooter.tsx | 6 +- src/components/NoteImage/NoteImage.tsx | 1 + .../ParsedNote/ParsedNote.module.scss | 14 + src/components/ParsedNote/ParsedNote.tsx | 5 + .../ReactionsModal/ReactionsModal.module.scss | 9 +- .../ReactionsModal/ReactionsModal.tsx | 244 +++++++++++++++--- src/constants.ts | 1 + src/contexts/ThreadContext.tsx | 26 +- src/lib/notes.tsx | 11 + src/pages/Thread.tsx | 1 + src/stores/note.ts | 2 + src/translations.ts | 10 + src/types/primal.d.ts | 12 +- 17 files changed, 378 insertions(+), 61 deletions(-) diff --git a/src/components/EmbeddedNote/EmbeddedNote.module.scss b/src/components/EmbeddedNote/EmbeddedNote.module.scss index e1b3f29..9fb4ada 100644 --- a/src/components/EmbeddedNote/EmbeddedNote.module.scss +++ b/src/components/EmbeddedNote/EmbeddedNote.module.scss @@ -17,6 +17,10 @@ text-decoration: none !important; } + &.altBack { + background-color: var(--background-sheet); + } + .mentionedNoteHeader { display: flex; justify-content: flex-start; diff --git a/src/components/EmbeddedNote/EmbeddedNote.tsx b/src/components/EmbeddedNote/EmbeddedNote.tsx index c849d30..c1933ba 100644 --- a/src/components/EmbeddedNote/EmbeddedNote.tsx +++ b/src/components/EmbeddedNote/EmbeddedNote.tsx @@ -15,7 +15,13 @@ import VerificationCheck from '../VerificationCheck/VerificationCheck'; import styles from './EmbeddedNote.module.scss'; -const EmbeddedNote: Component<{ note: PrimalNote, mentionedUsers?: Record, includeEmbeds?: boolean, isLast?: boolean}> = (props) => { +const EmbeddedNote: Component<{ + note: PrimalNote, + mentionedUsers?: Record, + includeEmbeds?: boolean, + isLast?: boolean, + alternativeBackground?: boolean, +}> = (props) => { const threadContext = useThreadContext(); const intl = useIntl(); @@ -32,11 +38,20 @@ const EmbeddedNote: Component<{ note: PrimalNote, mentionedUsers?: Record { + let k = styles.mentionedNote; + k += ' embeddedNote'; + if (props.isLast) k+= ' noBottomMargin'; + if (props.alternativeBackground) k+= ` ${styles.altBack}`; + + return k; + } + const wrapper = (children: JSXElement) => { if (props.includeEmbeds) { return (
@@ -48,7 +63,7 @@ const EmbeddedNote: Component<{ note: PrimalNote, mentionedUsers?: Record navToThread()} data-event={props.note.post.id} data-event-bech32={noteId()} diff --git a/src/components/Note/Note.module.scss b/src/components/Note/Note.module.scss index c5fb8e9..9d6807e 100644 --- a/src/components/Note/Note.module.scss +++ b/src/components/Note/Note.module.scss @@ -23,6 +23,11 @@ border: none; } + &.reactionNote { + background: none; + border-bottom: 1px solid var(--subtile-devider); + } + .header { position: relative; display: flex; diff --git a/src/components/Note/Note.tsx b/src/components/Note/Note.tsx index 1eb0f64..982a371 100644 --- a/src/components/Note/Note.tsx +++ b/src/components/Note/Note.tsx @@ -22,7 +22,7 @@ import { thread, zapCustomOption } from '../../translations'; import { useAccountContext } from '../../contexts/AccountContext'; import { uuidv4 } from '../../utils'; -export type NoteFooterState = { +export type NoteReactionsState = { likes: number, liked: boolean, reposts: number, @@ -40,6 +40,7 @@ export type NoteFooterState = { moreZapsAvailable: boolean, isRepostMenuVisible: boolean, topZaps: TopZap[], + quoteCount: number, }; const Note: Component<{ @@ -47,7 +48,9 @@ const Note: Component<{ id?: string, parent?: boolean, shorten?: boolean, - noteType?: 'feed' | 'primary' | 'notification' + noteType?: 'feed' | 'primary' | 'notification' | 'reaction' + onClick?: () => void, + quoteCount?: number, }> = (props) => { const threadContext = useThreadContext(); @@ -55,15 +58,22 @@ const Note: Component<{ const account = useAccountContext(); const intl = useIntl(); + createEffect(() => { + if (props.quoteCount) { + updateReactionsState('quoteCount', () => props.quoteCount || 0); + } + }) + const noteType = () => props.noteType || 'feed'; const repost = () => props.note.repost; const navToThread = (note: PrimalNote) => { + props.onClick && props.onClick(); threadContext?.actions.setPrimaryNote(note); }; - const [reactionsState, updateReactionsState] = createStore({ + const [reactionsState, updateReactionsState] = createStore({ likes: props.note.post.likes, liked: props.note.post.noteActions.liked, reposts: props.note.post.reposts, @@ -81,6 +91,7 @@ const Note: Component<{ moreZapsAvailable: false, isRepostMenuVisible: false, topZaps: [], + quoteCount: props.quoteCount || 0, }); let noteContextMenu: HTMLDivElement | undefined; @@ -173,7 +184,7 @@ const Note: Component<{ likes: reactionsState.likes, zaps: reactionsState.zapCount, reposts: reactionsState.reposts, - quotes: 0, + quotes: reactionsState.quoteCount, openOn, }); }; @@ -190,9 +201,9 @@ const Note: Component<{ } const reactionSum = () => { - const { likes, zapCount, reposts } = reactionsState; + const { likes, zapCount, reposts, quoteCount } = reactionsState; - return (likes || 0) + (zapCount || 0) + (reposts || 0); + return (likes || 0) + (zapCount || 0) + (reposts || 0) + (quoteCount || 0); }; const firstZap = createMemo(() => reactionsState.topZaps[0]); @@ -420,6 +431,50 @@ const Note: Component<{
+ + + navToThread(props.note)} + data-event={props.note.post.id} + data-event-bech32={props.note.post.noteId} + draggable={false} + > +
+
+ + + + +
+
+
+ +
+ + + + +
+ +
+
+
+ +
); } diff --git a/src/components/Note/NoteFooter/NoteFooter.tsx b/src/components/Note/NoteFooter/NoteFooter.tsx index b697ef6..2dc5333 100644 --- a/src/components/Note/NoteFooter/NoteFooter.tsx +++ b/src/components/Note/NoteFooter/NoteFooter.tsx @@ -19,7 +19,7 @@ import { getScreenCordinates } from '../../../utils'; import ZapAnimation from '../../ZapAnimation/ZapAnimation'; import { CustomZapInfo, useAppContext } from '../../../contexts/AppContext'; import NoteFooterActionButton from './NoteFooterActionButton'; -import { NoteFooterState } from '../Note'; +import { NoteReactionsState } from '../Note'; import { SetStoreFunction } from 'solid-js/store'; import BookmarkNote from '../../BookmarkNote/BookmarkNote'; import { APP_ID } from '../../../App'; @@ -30,8 +30,8 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string, - state: NoteFooterState, - updateState: SetStoreFunction, + state: NoteReactionsState, + updateState: SetStoreFunction, customZapInfo: CustomZapInfo, large?: boolean, }> = (props) => { diff --git a/src/components/NoteImage/NoteImage.tsx b/src/components/NoteImage/NoteImage.tsx index 217afee..7737702 100644 --- a/src/components/NoteImage/NoteImage.tsx +++ b/src/components/NoteImage/NoteImage.tsx @@ -132,6 +132,7 @@ const NoteImage: Component<{ data-pswp-height={zoomH()} data-image-group={props.imageGroup} data-cropped={true} + target="_blank" > = (props) => { const intl = useIntl(); @@ -172,6 +174,8 @@ const ParsedNote: Component<{ }); onMount(() => { + if (props.noLightbox) return; + lightbox.init(); }); @@ -927,6 +931,7 @@ const ParsedNote: Component<{ note={ment} mentionedUsers={props.note.mentionedUsers || {}} isLast={index === content.length-1} + alternativeBackground={props.altEmbeds} /> ; } diff --git a/src/components/ReactionsModal/ReactionsModal.module.scss b/src/components/ReactionsModal/ReactionsModal.module.scss index 7ea6054..8411db4 100644 --- a/src/components/ReactionsModal/ReactionsModal.module.scss +++ b/src/components/ReactionsModal/ReactionsModal.module.scss @@ -7,7 +7,7 @@ display: flex; flex-direction: column; - padding: 20px; + padding: 16px; .header { display: flex; @@ -73,7 +73,7 @@ .tab { position: relative; display: inline-block; - padding-inline: 14px; + padding-inline: 12px; padding-block: 2px; border: none; background: none; @@ -107,13 +107,14 @@ height: 440px; overflow-x: hidden; overflow-y: scroll; + padding-right: 8px; @keyframes fadeIn { from { - opacity:0; + opacity:0; } to { - opacity:1; + opacity:1; } } diff --git a/src/components/ReactionsModal/ReactionsModal.tsx b/src/components/ReactionsModal/ReactionsModal.tsx index 9a63998..760ca59 100644 --- a/src/components/ReactionsModal/ReactionsModal.tsx +++ b/src/components/ReactionsModal/ReactionsModal.tsx @@ -1,7 +1,7 @@ import { useIntl } from '@cookbook/solid-intl'; import { Tabs } from '@kobalte/core'; import { A } from '@solidjs/router'; -import { Component, createEffect, createSignal, For, onMount, Show } from 'solid-js'; +import { Component, createEffect, createSignal, For, Match, onMount, Show, Switch } from 'solid-js'; import { createStore } from 'solid-js/store'; import { APP_ID } from '../../App'; import { Kind } from '../../constants'; @@ -9,20 +9,25 @@ import { useAccountContext } from '../../contexts/AccountContext'; import { ReactionStats } from '../../contexts/AppContext'; import { hookForDev } from '../../lib/devTools'; import { hexToNpub } from '../../lib/keys'; -import { getEventQuotes, getEventReactions, getEventZaps } from '../../lib/notes'; +import { getEventQuotes, getEventQuoteStats, getEventReactions, getEventZaps, setLinkPreviews } from '../../lib/notes'; import { truncateNumber2 } from '../../lib/notifications'; +import { updateStore } from '../../services/StoreService'; import { subscribeTo } from '../../sockets'; +import { convertToNotes } from '../../stores/note'; import { userName } from '../../stores/profile'; import { actions as tActions, placeholders as tPlaceholders, reactionsModal } from '../../translations'; +import { FeedPage, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalNote, PrimalUser } from '../../types/primal'; import { parseBolt11 } from '../../utils'; import Avatar from '../Avatar/Avatar'; import Loader from '../Loader/Loader'; import Modal from '../Modal/Modal'; +import Note from '../Note/Note'; import Paginator from '../Paginator/Paginator'; import VerificationCheck from '../VerificationCheck/VerificationCheck'; import styles from './ReactionsModal.module.scss'; + const ReactionsModal: Component<{ id?: string, noteId: string | undefined, @@ -38,18 +43,33 @@ const ReactionsModal: Component<{ const [likeList, setLikeList] = createStore([]); const [zapList, setZapList] = createStore([]); const [repostList, setRepostList] = createStore([]); - // const [quotesList, setQuotesList] = createStore([]); + const [quotesList, setQuotesList] = createStore([]); + const [quoteCount, setQuoteCount] = createSignal(0); const [isFetching, setIsFetching] = createSignal(false); let loadedLikes = 0; let loadedZaps = 0; let loadedReposts = 0; - // let loadedQuotes = 0; + let loadedQuotes = 0; + + createEffect(() => { + const count = quoteCount(); + + if (count === 0 && props.stats.quotes > 0) { + setQuoteCount(props.stats.quotes); + } + }) createEffect(() => { if (props.noteId && props.stats.openOn) { - setSelectedTab(props.stats.openOn) + setSelectedTab(props.stats.openOn); + } + }); + + createEffect(() => { + if (props.noteId) { + getQuoteCount(); } }); @@ -64,9 +84,9 @@ const ReactionsModal: Component<{ case 'reposts': loadedReposts === 0 && getReposts(); break; - // case 'quotes': - // loadedQuotes === 0 && getQuotes(); - // break; + case 'quotes': + loadedQuotes === 0 && getQuotes(); + break; } }); @@ -76,10 +96,13 @@ const ReactionsModal: Component<{ setZapList(() => []); setRepostList(() => []); setSelectedTab(() => 'likes'); + setQuotesList(() => []); + setQuoteCount(() => 0); loadedLikes = 0; loadedZaps = 0; loadedReposts = 0; + loadedQuotes = 0; } }); @@ -203,6 +226,7 @@ const ReactionsModal: Component<{ const users: any[] = []; const unsub = subscribeTo(subId, (type,_, content) => { + if (type === 'EOSE') { setRepostList((reposts) => [...reposts, ...users]); loadedReposts = repostList.length; @@ -232,44 +256,128 @@ const ReactionsModal: Component<{ getEventReactions(props.noteId, Kind.Repost, subId, offset); }; - // const getQuotes = (offset = 0) => { - // if (!props.noteId) return; + const getQuotes = (offset = 0) => { + if (!props.noteId) return; - // const subId = `nr_q_${props.noteId}_${APP_ID}`; + const subId = `nr_q_${props.noteId}_${APP_ID}`; - // const users: any[] = []; + let page: FeedPage = { + messages: [], + users: {}, + postStats: {}, + mentions: {}, + noteActions: {}, + }; - // const unsub = subscribeTo(subId, (type,_, content) => { - // if (type === 'EOSE') { - // setQuotesList((reposts) => [...reposts, ...users]); - // loadedQuotes = quotesList.length; - // setIsFetching(() => false); - // unsub(); - // } + const unsub = subscribeTo(subId, (type,_, content) => { + if (type === 'EOSE') { + const pageNotes = convertToNotes(page); - // if (type === 'EVENT') { - // if (content?.kind === Kind.Metadata) { - // let user = JSON.parse(content.content); + setQuotesList((notes) => [...notes, ...pageNotes]); + loadedQuotes = quotesList.length; + setIsFetching(() => false); + unsub(); + } - // if (!user.displayName || typeof user.displayName === 'string' && user.displayName.trim().length === 0) { - // user.displayName = user.display_name; - // } - // user.pubkey = content.pubkey; - // user.npub = hexToNpub(content.pubkey); - // user.created_at = content.created_at; + if (type === 'EVENT') { + if (content?.kind === Kind.Metadata) { + const user = content as NostrUserContent; - // users.push(user); + page.users[user.pubkey] = { ...user }; - // return; - // } - // } - // }); + return; + } + if (content?.kind === Kind.Text) { + const message = content as NostrNoteContent; - // setIsFetching(() => true); - // getEventQuotes(props.noteId, subId, offset); - // }; + if (page.messages.find(m => m.id === message.id)) { + return; + } - const totalCount = () => props.stats.likes + props.stats.quotes + props.stats.reposts + props.stats.zaps; + page.messages.push(message); + return; + } + + if (content?.kind === Kind.NoteStats) { + const statistic = content as NostrStatsContent; + const stat = JSON.parse(statistic.content); + + page.postStats[stat.event_id] = { ...stat }; + return; + } + + if (content?.kind === Kind.Mentions) { + const mentionContent = content as NostrMentionContent; + const mention = JSON.parse(mentionContent.content); + + if (!page.mentions) { + page.mentions = {}; + } + + page.mentions[mention.id] = { ...mention }; + return; + } + + if (content?.kind === Kind.NoteActions) { + const noteActionContent = content as NostrNoteActionsContent; + const noteActions = JSON.parse(noteActionContent.content) as NoteActions; + + page.noteActions[noteActions.event_id] = { ...noteActions }; + + return; + } + + if (content?.kind === Kind.LinkMetadata) { + const metadata = JSON.parse(content.content); + + const data = metadata.resources[0]; + if (!data) { + return; + } + + const preview = { + url: data.url, + title: data.md_title, + description: data.md_description, + mediaType: data.mimetype, + contentType: data.mimetype, + images: [data.md_image], + favicons: [data.icon_url], + }; + + setLinkPreviews(() => ({ [data.url]: preview })); + return; + } + } + }); + + setIsFetching(() => true); + getEventQuotes(props.noteId, subId, offset); + }; + + const getQuoteCount = () => { + if (!props.noteId) return; + + const subId = `nr_qc_${props.noteId}_${APP_ID}`; + + const unsub = subscribeTo(subId, (type,_, content) => { + if (type === 'EOSE') { + unsub(); + } + + if (type === 'EVENT') { + if (content?.kind === Kind.NoteQuoteStats) { + const quoteStats = JSON.parse(content.content); + + setQuoteCount(() => quoteStats.count || 0); + } + } + }); + + getEventQuoteStats(props.noteId, subId); + } + + const totalCount = () => props.stats.likes + (quoteCount() || props.stats.quotes || 0) + props.stats.reposts + props.stats.zaps; return ( + + + {intl.formatMessage(tPlaceholders.noReactionDetails)} + + +
@@ -305,9 +419,9 @@ const ReactionsModal: Component<{ {intl.formatMessage(reactionsModal.tabs.reposts, { count: props.stats.reposts })} - 0}> + 0}> - {intl.formatMessage(reactionsModal.tabs.quotes, { count: props.stats.quotes })} + {intl.formatMessage(reactionsModal.tabs.quotes, { count: quoteCount() })} @@ -319,7 +433,12 @@ const ReactionsModal: Component<{ each={likeList} fallback={ - {intl.formatMessage(tPlaceholders.noLikeDetails)} + 0} + fallback={intl.formatMessage(tPlaceholders.noReactionDetails)} + > + {intl.formatMessage(tPlaceholders.noLikeDetails)} + } > @@ -364,7 +483,12 @@ const ReactionsModal: Component<{ each={zapList} fallback={ - {intl.formatMessage(tPlaceholders.noZapDetails)} + 0} + fallback={intl.formatMessage(tPlaceholders.noReactionDetails)} + > + {intl.formatMessage(tPlaceholders.noZapDetails)} + } > @@ -416,7 +540,12 @@ const ReactionsModal: Component<{ each={repostList} fallback={ - {intl.formatMessage(tPlaceholders.noRepostDetails)} + 0} + fallback={intl.formatMessage(tPlaceholders.noReactionDetails)} + > + {intl.formatMessage(tPlaceholders.noRepostDetails)} + } > @@ -456,7 +585,36 @@ const ReactionsModal: Component<{ - All the quotes + + 0} + fallback={intl.formatMessage(tPlaceholders.noReactionDetails)} + > + {intl.formatMessage(tPlaceholders.noQuoteDetails)} + + + } + > + {quote => ( + + )} + + { + const len = quotesList.length; + if (len === 0) return; + getQuotes(len); + }} + isSmall={true} + />
diff --git a/src/constants.ts b/src/constants.ts index 5061fd1..c1edef9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -140,6 +140,7 @@ export enum Kind { UploadChunk = 10_000_135, UserRelays=10_000_139, RelayHint=10_000_141, + NoteQuoteStats=10_000_143, WALLET_OPERATION = 10_000_300, } diff --git a/src/contexts/ThreadContext.tsx b/src/contexts/ThreadContext.tsx index f266b55..1aa3f53 100644 --- a/src/contexts/ThreadContext.tsx +++ b/src/contexts/ThreadContext.tsx @@ -38,7 +38,7 @@ import { } from "../types/primal"; import { APP_ID } from "../App"; import { useAccountContext } from "./AccountContext"; -import { getEventZaps, setLinkPreviews } from "../lib/notes"; +import { getEventQuoteStats, getEventZaps, setLinkPreviews } from "../lib/notes"; import { parseBolt11 } from "../utils"; import { getUserProfiles } from "../lib/profile"; @@ -60,6 +60,7 @@ export type ThreadContextStore = { reposts: Record | undefined, lastNote: PrimalNote | undefined, topZaps: Record, + quoteCount: number, actions: { saveNotes: (newNotes: PrimalNote[]) => void, clearNotes: () => void, @@ -92,6 +93,7 @@ export const initialData = { reposts: {}, lastNote: undefined, topZaps: {}, + quoteCount: 0, }; @@ -116,6 +118,7 @@ export const ThreadProvider = (props: { children: ContextChildren }) => { updateStore('noteId', noteId) getThread(account?.publicKey, noteId, `thread_${APP_ID}`); fetchTopZaps(noteId); + fetchNoteQuoteStats(noteId); updateStore('isFetching', () => true); } @@ -276,6 +279,12 @@ export const ThreadProvider = (props: { children: ContextChildren }) => { return; } + + if (content.kind === Kind.NoteQuoteStats) { + const quoteStats = JSON.parse(content.content); + + updateStore('quoteCount', () => quoteStats.count || 0); + } }; const savePage = (page: FeedPage) => { @@ -298,6 +307,10 @@ export const ThreadProvider = (props: { children: ContextChildren }) => { getUserProfiles(pubkeys, `thread_pk_${APP_ID}`); }; + const fetchNoteQuoteStats = (noteId: string) => { + getEventQuoteStats(noteId, `thread_quote_stats_${APP_ID}`) + } + // SOCKET HANDLERS ------------------------------ const onMessage = (event: MessageEvent) => { @@ -368,6 +381,17 @@ export const ThreadProvider = (props: { children: ContextChildren }) => { return; } } + + if (subId === `thread_quote_stats_${APP_ID}`) { + if (type === 'EOSE') { + savePage(store.page); + } + + if (type === 'EVENT') { + updatePage(content); + return; + } + } }; const onSocketClose = (closeEvent: CloseEvent) => { diff --git a/src/lib/notes.tsx b/src/lib/notes.tsx index bb2a7d7..e38ae6e 100644 --- a/src/lib/notes.tsx +++ b/src/lib/notes.tsx @@ -572,3 +572,14 @@ export const getEventZaps = (eventId: string, user_pubkey: string | undefined, s {cache: ["event_zaps_by_satszapped", { event_id, user_pubkey, limit, offset }]}, ])); }; + + +export const getEventQuoteStats = (eventId: string, subid: string) => { + const event_id = eventId.startsWith('note1') ? npubToHex(eventId) : eventId; + + sendMessage(JSON.stringify([ + "REQ", + subid, + {cache: ["note_mentions_count", { event_id }]}, + ])); +}; diff --git a/src/pages/Thread.tsx b/src/pages/Thread.tsx index 423a63c..ecdd796 100644 --- a/src/pages/Thread.tsx +++ b/src/pages/Thread.tsx @@ -207,6 +207,7 @@ const Thread: Component = () => { { mentionedNotes, mentionedUsers, replyTo: replyTo && replyTo[1], + tags: msg.tags, + id: msg.id, }; }); } diff --git a/src/translations.ts b/src/translations.ts index d7296b1..d56b447 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -1014,6 +1014,11 @@ export const placeholders = { description: 'Placeholder when the note is missing', }, }, + noReactionDetails: { + id: 'placeholders.noReactionDetails', + defaultMessage: 'No details for rections found', + description: 'Placeholder when there are no reaction details in reactions modal', + }, noLikeDetails: { id: 'placeholders.noLikeDetails', defaultMessage: 'No details for likes found', @@ -1029,6 +1034,11 @@ export const placeholders = { defaultMessage: 'No details for reposts found', description: 'Placeholder when there are no repost details in reactions modal', }, + noQuoteDetails: { + id: 'placeholders.noQuoteDetails', + defaultMessage: 'No details for quotes found', + description: 'Placeholder when there are no quote details in reactions modal', + }, addComment: { id: 'placeholders.addComment', defaultMessage: 'Add a comment...', diff --git a/src/types/primal.d.ts b/src/types/primal.d.ts index d89dcb5..2016bbf 100644 --- a/src/types/primal.d.ts +++ b/src/types/primal.d.ts @@ -221,6 +221,13 @@ export type NostrZapInfo = { tags?: string[][], }; +export type NostrQuoteStatsInfo = { + kind: Kind.NoteQuoteStats, + content: string, + created_at?: number, + tags?: string[][], +}; + export type NostrEventContent = NostrNoteContent | NostrUserContent | @@ -251,7 +258,8 @@ export type NostrEventContent = PrimalUserRelays | NostrBookmarks | NostrRelayHint | - NostrZapInfo; + NostrZapInfo | + NostrQuoteStatsInfo; export type NostrEvent = [ type: "EVENT", @@ -465,6 +473,8 @@ export type PrimalNote = { mentionedNotes?: Record, mentionedUsers?: Record, replyTo?: string, + id: string, + tags: string[][], }; export type PrimalFeed = {