From 3914a3c0c9e0027a839b65c3ec18bf2341687b1b Mon Sep 17 00:00:00 2001 From: Bojan Mojsilovic Date: Mon, 27 May 2024 15:18:27 +0200 Subject: [PATCH] Enable reactions --- .../ArticlePreview/ArticlePreview.tsx | 168 ++++++++++-- .../Note/NoteFooter/ArticleFooter.tsx | 244 +++++++++--------- src/contexts/AccountContext.tsx | 9 +- src/contexts/AppContext.tsx | 4 +- src/contexts/ReadsContext.tsx | 2 + src/lib/notes.tsx | 6 +- src/lib/zap.ts | 47 +++- src/pages/Reads.tsx | 16 +- src/stores/note.ts | 21 ++ src/types/primal.d.ts | 11 + 10 files changed, 365 insertions(+), 163 deletions(-) diff --git a/src/components/ArticlePreview/ArticlePreview.tsx b/src/components/ArticlePreview/ArticlePreview.tsx index 21df000..b2c3067 100644 --- a/src/components/ArticlePreview/ArticlePreview.tsx +++ b/src/components/ArticlePreview/ArticlePreview.tsx @@ -1,11 +1,15 @@ import { A } from '@solidjs/router'; -import { Component, createEffect, For, JSXElement, Show } from 'solid-js'; +import { batch, Component, createEffect, For, JSXElement, Show } from 'solid-js'; import { createStore } from 'solid-js/store'; import { Portal } from 'solid-js/web'; +import { useAccountContext } from '../../contexts/AccountContext'; +import { CustomZapInfo, useAppContext } from '../../contexts/AppContext'; +import { useThreadContext } from '../../contexts/ThreadContext'; import { shortDate } from '../../lib/dates'; import { hookForDev } from '../../lib/devTools'; import { userName } from '../../stores/profile'; -import { PrimalArticle } from '../../types/primal'; +import { PrimalArticle, ZapOption } from '../../types/primal'; +import { uuidv4 } from '../../utils'; import Avatar from '../Avatar/Avatar'; import { NoteReactionsState } from '../Note/Note'; import ArticleFooter from '../Note/NoteFooter/ArticleFooter'; @@ -21,17 +25,20 @@ const ArticlePreview: Component<{ article: PrimalArticle, }> = (props) => { + const app = useAppContext(); + const account = useAccountContext(); + const thread = useThreadContext(); const [reactionsState, updateReactionsState] = createStore({ - likes: 0, - liked: false, - reposts: 0, - reposted: false, - replies: 0, - replied: false, - zapCount: 0, - satsZapped: 0, - zapped: false, + likes: props.article.likes, + liked: props.article.noteActions.liked, + reposts: props.article.reposts, + reposted: props.article.noteActions.reposted, + replies: props.article.replies, + replied: props.article.noteActions.replied, + zapCount: props.article.zaps, + satsZapped: props.article.satszapped, + zapped: props.article.noteActions.zapped, zappedAmount: 0, zappedNow: false, isZapping: false, @@ -44,6 +51,136 @@ const ArticlePreview: Component<{ quoteCount: 0, }); + let latestTopZap: string = ''; + let latestTopZapFeed: string = ''; + + const onConfirmZap = (zapOption: ZapOption) => { + app?.actions.closeCustomZapModal(); + batch(() => { + updateReactionsState('zappedAmount', () => zapOption.amount || 0); + updateReactionsState('satsZapped', (z) => z + (zapOption.amount || 0)); + updateReactionsState('zapped', () => true); + updateReactionsState('showZapAnim', () => true) + }); + + addTopZap(zapOption); + addTopZapFeed(zapOption) + }; + + const onSuccessZap = (zapOption: ZapOption) => { + app?.actions.closeCustomZapModal(); + app?.actions.resetCustomZap(); + + const pubkey = account?.publicKey; + + if (!pubkey) return; + + batch(() => { + updateReactionsState('zapCount', (z) => z + 1); + updateReactionsState('isZapping', () => false); + updateReactionsState('showZapAnim', () => false); + updateReactionsState('hideZapIcon', () => false); + updateReactionsState('zapped', () => true); + }); + }; + + const onFailZap = (zapOption: ZapOption) => { + app?.actions.closeCustomZapModal(); + app?.actions.resetCustomZap(); + batch(() => { + updateReactionsState('zappedAmount', () => -(zapOption.amount || 0)); + updateReactionsState('satsZapped', (z) => z - (zapOption.amount || 0)); + updateReactionsState('isZapping', () => false); + updateReactionsState('showZapAnim', () => false); + updateReactionsState('hideZapIcon', () => false); + updateReactionsState('zapped', () => props.article.noteActions.zapped); + }); + + removeTopZap(zapOption); + removeTopZapFeed(zapOption); + }; + + const onCancelZap = (zapOption: ZapOption) => { + app?.actions.closeCustomZapModal(); + app?.actions.resetCustomZap(); + batch(() => { + updateReactionsState('zappedAmount', () => -(zapOption.amount || 0)); + updateReactionsState('satsZapped', (z) => z - (zapOption.amount || 0)); + updateReactionsState('isZapping', () => false); + updateReactionsState('showZapAnim', () => false); + updateReactionsState('hideZapIcon', () => false); + updateReactionsState('zapped', () => props.article.noteActions.zapped); + }); + + removeTopZap(zapOption); + removeTopZapFeed(zapOption); + }; + + const addTopZap = (zapOption: ZapOption) => { + const pubkey = account?.publicKey; + + if (!pubkey) return; + + const oldZaps = [ ...reactionsState.topZaps ]; + + latestTopZap = uuidv4() as string; + + const newZap = { + amount: zapOption.amount || 0, + message: zapOption.message || '', + pubkey, + eventId: props.article.id, + id: latestTopZap, + }; + + if (!thread?.users.find((u) => u.pubkey === pubkey)) { + thread?.actions.fetchUsers([pubkey]) + } + + const zaps = [ ...oldZaps, { ...newZap }].sort((a, b) => b.amount - a.amount); + updateReactionsState('topZaps', () => [...zaps]); + }; + + const removeTopZap = (zapOption: ZapOption) => { + const zaps = reactionsState.topZaps.filter(z => z.id !== latestTopZap); + updateReactionsState('topZaps', () => [...zaps]); + }; + + + const addTopZapFeed = (zapOption: ZapOption) => { + const pubkey = account?.publicKey; + + if (!pubkey) return; + + const oldZaps = [ ...reactionsState.topZapsFeed ]; + + latestTopZapFeed = uuidv4() as string; + + const newZap = { + amount: zapOption.amount || 0, + message: zapOption.message || '', + pubkey, + eventId: props.article.id, + id: latestTopZapFeed, + }; + + const zaps = [ ...oldZaps, { ...newZap }].sort((a, b) => b.amount - a.amount).slice(0, 4); + updateReactionsState('topZapsFeed', () => [...zaps]); + } + + const removeTopZapFeed = (zapOption: ZapOption) => { + const zaps = reactionsState.topZapsFeed.filter(z => z.id !== latestTopZapFeed); + updateReactionsState('topZapsFeed', () => [...zaps]); + }; + + const customZapInfo: () => CustomZapInfo = () => ({ + note: props.article, + onConfirm: onConfirmZap, + onSuccess: onSuccessZap, + onFail: onFailZap, + onCancel: onCancelZap, + }); + return (
@@ -101,13 +238,8 @@ const ArticlePreview: Component<{ note={props.article} state={reactionsState} updateState={updateReactionsState} - customZapInfo={{ - note: props.article, - onConfirm: () => {}, - onSuccess: () => {}, - onFail: () => {}, - onCancel: () => {}, - }} + customZapInfo={customZapInfo()} + onZapAnim={addTopZapFeed} />
diff --git a/src/components/Note/NoteFooter/ArticleFooter.tsx b/src/components/Note/NoteFooter/ArticleFooter.tsx index 761099c..cf40730 100644 --- a/src/components/Note/NoteFooter/ArticleFooter.tsx +++ b/src/components/Note/NoteFooter/ArticleFooter.tsx @@ -8,7 +8,7 @@ import { useToastContext } from '../../Toaster/Toaster'; import { useIntl } from '@cookbook/solid-intl'; import { truncateNumber } from '../../../lib/notifications'; -import { canUserReceiveZaps, zapNote } from '../../../lib/zap'; +import { canUserReceiveZaps, zapArticle, zapNote } from '../../../lib/zap'; import { useSettingsContext } from '../../../contexts/SettingsContext'; import zapMD from '../../../assets/lottie/zap_md_2.json'; @@ -135,167 +135,167 @@ const ArticleFooter: Component<{ const doReply = () => {}; const doLike = async (e: MouseEvent) => { - // e.preventDefault(); - // e.stopPropagation(); + e.preventDefault(); + e.stopPropagation(); - // if (!account) { - // return; - // } + if (!account) { + return; + } - // if (!account.hasPublicKey()) { - // account.actions.showGetStarted(); - // return; - // } + if (!account.hasPublicKey()) { + account.actions.showGetStarted(); + return; + } - // if (account.relays.length === 0) { - // toast?.sendWarning( - // intl.formatMessage(t.noRelaysConnected), - // ); - // return; - // } + if (account.relays.length === 0) { + toast?.sendWarning( + intl.formatMessage(t.noRelaysConnected), + ); + return; + } - // const success = await account.actions.addLike(props.note); + const success = await account.actions.addLike(props.note); - // if (success) { - // batch(() => { - // props.updateState('likes', (l) => l + 1); - // props.updateState('liked', () => true); - // }); - // } + if (success) { + batch(() => { + props.updateState('likes', (l) => l + 1); + props.updateState('liked', () => true); + }); + } }; const startZap = (e: MouseEvent | TouchEvent) => { - // e.preventDefault(); - // e.stopPropagation(); + e.preventDefault(); + e.stopPropagation(); - // if (!account?.hasPublicKey()) { - // account?.actions.showGetStarted(); - // props.updateState('isZapping', () => false); - // return; - // } + if (!account?.hasPublicKey()) { + account?.actions.showGetStarted(); + props.updateState('isZapping', () => false); + return; + } - // if (account.relays.length === 0) { - // toast?.sendWarning( - // intl.formatMessage(t.noRelaysConnected), - // ); - // return; - // } + if (account.relays.length === 0) { + toast?.sendWarning( + intl.formatMessage(t.noRelaysConnected), + ); + return; + } - // if (!canUserReceiveZaps(props.note.user)) { - // toast?.sendWarning( - // intl.formatMessage(t.zapUnavailable), - // ); - // props.updateState('isZapping', () => false); - // return; - // } + if (!canUserReceiveZaps(props.note.author)) { + toast?.sendWarning( + intl.formatMessage(t.zapUnavailable), + ); + props.updateState('isZapping', () => false); + return; + } - // quickZapDelay = setTimeout(() => { - // app?.actions.openCustomZapModal(props.customZapInfo); - // props.updateState('isZapping', () => true); - // }, 500); + quickZapDelay = setTimeout(() => { + app?.actions.openCustomZapModal(props.customZapInfo); + props.updateState('isZapping', () => true); + }, 500); }; const commitZap = (e: MouseEvent | TouchEvent) => { - // e.preventDefault(); - // e.stopPropagation(); + e.preventDefault(); + e.stopPropagation(); - // clearTimeout(quickZapDelay); + clearTimeout(quickZapDelay); - // if (!account?.hasPublicKey()) { - // account?.actions.showGetStarted(); - // return; - // } + if (!account?.hasPublicKey()) { + account?.actions.showGetStarted(); + return; + } - // if (account.relays.length === 0 || !canUserReceiveZaps(props.note.user)) { - // return; - // } + if (account.relays.length === 0 || !canUserReceiveZaps(props.note.author)) { + return; + } - // if (app?.customZap === undefined) { - // doQuickZap(); - // } + if (app?.customZap === undefined) { + doQuickZap(); + } }; const animateZap = () => { - // setTimeout(() => { - // props.updateState('hideZapIcon', () => true); + setTimeout(() => { + props.updateState('hideZapIcon', () => true); - // if (!medZapAnimation) { - // return; - // } + if (!medZapAnimation) { + return; + } - // let newLeft = props.wide ? 15 : 13; - // let newTop = props.wide ? -6 : -6; + let newLeft = props.wide ? 15 : 13; + let newTop = props.wide ? -6 : -6; - // if (props.large) { - // newLeft = 2; - // newTop = -9; - // } + if (props.large) { + newLeft = 2; + newTop = -9; + } - // medZapAnimation.style.left = `${newLeft}px`; - // medZapAnimation.style.top = `${newTop}px`; + medZapAnimation.style.left = `${newLeft}px`; + medZapAnimation.style.top = `${newTop}px`; - // const onAnimDone = () => { - // batch(() => { - // props.updateState('showZapAnim', () => false); - // props.updateState('hideZapIcon', () => false); - // props.updateState('zapped', () => true); - // }); - // medZapAnimation?.removeEventListener('complete', onAnimDone); - // } + const onAnimDone = () => { + batch(() => { + props.updateState('showZapAnim', () => false); + props.updateState('hideZapIcon', () => false); + props.updateState('zapped', () => true); + }); + medZapAnimation?.removeEventListener('complete', onAnimDone); + } - // medZapAnimation.addEventListener('complete', onAnimDone); + medZapAnimation.addEventListener('complete', onAnimDone); - // try { - // // @ts-ignore - // medZapAnimation.seek(0); - // // @ts-ignore - // medZapAnimation.play(); - // } catch (e) { - // console.warn('Failed to animte zap:', e); - // onAnimDone(); - // } - // }, 10); + try { + // @ts-ignore + medZapAnimation.seek(0); + // @ts-ignore + medZapAnimation.play(); + } catch (e) { + console.warn('Failed to animte zap:', e); + onAnimDone(); + } + }, 10); }; const doQuickZap = async () => { - // if (!account?.hasPublicKey()) { - // account?.actions.showGetStarted(); - // return; - // } + if (!account?.hasPublicKey()) { + account?.actions.showGetStarted(); + return; + } - // const amount = settings?.defaultZap.amount || 10; - // const message = settings?.defaultZap.message || ''; - // const emoji = settings?.defaultZap.emoji; + const amount = settings?.defaultZap.amount || 10; + const message = settings?.defaultZap.message || ''; + const emoji = settings?.defaultZap.emoji; - // batch(() => { - // props.updateState('isZapping', () => true); - // props.updateState('satsZapped', (z) => z + amount); - // props.updateState('showZapAnim', () => true); - // }); + batch(() => { + props.updateState('isZapping', () => true); + props.updateState('satsZapped', (z) => z + amount); + props.updateState('showZapAnim', () => true); + }); - // props.onZapAnim && props.onZapAnim({ amount, message, emoji }) + props.onZapAnim && props.onZapAnim({ amount, message, emoji }) - // setTimeout(async () => { - // const success = await zapNote(props.note, account.publicKey, amount, message, account.relays); + setTimeout(async () => { + const success = await zapArticle(props.note, account.publicKey, amount, message, account.relays); - // props.updateState('isZapping', () => false); + props.updateState('isZapping', () => false); - // if (success) { - // props.customZapInfo.onSuccess({ - // emoji, - // amount, - // message, - // }); + if (success) { + props.customZapInfo.onSuccess({ + emoji, + amount, + message, + }); - // return; - // } + return; + } - // props.customZapInfo.onFail({ - // emoji, - // amount, - // message, - // }); - // }, lottieDuration()); + props.customZapInfo.onFail({ + emoji, + amount, + message, + }); + }, lottieDuration()); } diff --git a/src/contexts/AccountContext.tsx b/src/contexts/AccountContext.tsx index 30f60c5..3ac15c7 100644 --- a/src/contexts/AccountContext.tsx +++ b/src/contexts/AccountContext.tsx @@ -21,6 +21,7 @@ import { PrimalNote, PrimalUser, NostrEventContent, + PrimalArticle, } from '../types/primal'; import { Kind, pinEncodePrefix, relayConnectingTimeout } from "../constants"; import { isConnected, refreshSocketListeners, removeSocketListeners, socket, subscribeTo, reset, subTo } from "../sockets"; @@ -79,7 +80,7 @@ export type AccountContextStore = { showNewNoteForm: () => void, hideNewNoteForm: () => void, setActiveUser: (user: PrimalUser) => void, - addLike: (note: PrimalNote) => Promise, + addLike: (note: PrimalNote | PrimalArticle) => Promise, setPublicKey: (pubkey: string | undefined) => void, addFollow: (pubkey: string, cb?: (remove: boolean, pubkey: string) => void) => void, removeFollow: (pubkey: string, cb?: (remove: boolean, pubkey: string) => void) => void, @@ -517,15 +518,15 @@ export function AccountProvider(props: { children: JSXElement }) { updateStore('showNewNoteForm', () => false); }; - const addLike = async (note: PrimalNote) => { - if (store.likes.includes(note.post.id)) { + const addLike = async (note: PrimalNote | PrimalArticle) => { + if (store.likes.includes(note.id)) { return false; } const { success } = await sendLike(note, store.relays, store.relaySettings); if (success) { - updateStore('likes', (likes) => [ ...likes, note.post.id]); + updateStore('likes', (likes) => [ ...likes, note.id]); saveLikes(store.publicKey, store.likes); } diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx index e067504..6c9714e 100644 --- a/src/contexts/AppContext.tsx +++ b/src/contexts/AppContext.tsx @@ -7,7 +7,7 @@ import { onMount, useContext } from "solid-js"; -import { PrimalNote, PrimalUser, ZapOption } from "../types/primal"; +import { PrimalArticle, PrimalNote, PrimalUser, ZapOption } from "../types/primal"; import { CashuMint } from "@cashu/cashu-ts"; @@ -21,7 +21,7 @@ export type ReactionStats = { export type CustomZapInfo = { profile?: PrimalUser, - note?: PrimalNote, + note?: PrimalNote | PrimalArticle, onConfirm: (zapOption: ZapOption) => void, onSuccess: (zapOption: ZapOption) => void, onFail: (zapOption: ZapOption) => void, diff --git a/src/contexts/ReadsContext.tsx b/src/contexts/ReadsContext.tsx index af16a42..b795937 100644 --- a/src/contexts/ReadsContext.tsx +++ b/src/contexts/ReadsContext.tsx @@ -489,6 +489,7 @@ export const ReadsProvider = (props: { children: ContextChildren }) => { if (content.kind === Kind.NoteStats) { const statistic = content as NostrStatsContent; const stat = JSON.parse(statistic.content); + console.log('READS STATS: ', stat) if (scope) { updateStore(scope, 'page', 'postStats', @@ -523,6 +524,7 @@ export const ReadsProvider = (props: { children: ContextChildren }) => { const noteActionContent = content as NostrNoteActionsContent; const noteActions = JSON.parse(noteActionContent.content) as NoteActions; + console.log('READS ACTIONS: ', content) if (scope) { updateStore(scope, 'page', 'noteActions', (actions) => ({ ...actions, [noteActions.event_id]: { ...noteActions } }) diff --git a/src/lib/notes.tsx b/src/lib/notes.tsx index d73c397..96d6ac2 100644 --- a/src/lib/notes.tsx +++ b/src/lib/notes.tsx @@ -308,13 +308,13 @@ export const importEvents = (events: NostrRelaySignedEvent[], subid: string) => type NostrEvent = { content: string, kind: number, tags: string[][], created_at: number }; -export const sendLike = async (note: PrimalNote, relays: Relay[], relaySettings?: NostrRelays) => { +export const sendLike = async (note: PrimalNote | PrimalArticle, relays: Relay[], relaySettings?: NostrRelays) => { const event = { content: '+', kind: Kind.Reaction, tags: [ - ['e', note.post.id], - ['p', note.post.pubkey], + ['e', note.id], + ['p', note.pubkey], ], created_at: Math.floor((new Date()).getTime() / 1000), }; diff --git a/src/lib/zap.ts b/src/lib/zap.ts index 0ee38d6..39e8e7d 100644 --- a/src/lib/zap.ts +++ b/src/lib/zap.ts @@ -1,7 +1,7 @@ import { bech32 } from "@scure/base"; // @ts-ignore Bad types in nostr-tools import { nip57, Relay, utils } from "nostr-tools"; -import { PrimalNote, PrimalUser } from "../types/primal"; +import { PrimalArticle, PrimalNote, PrimalUser } from "../types/primal"; import { logError } from "./logger"; import { enableWebLn, sendPayment, signEvent } from "./nostrAPI"; @@ -50,6 +50,51 @@ export const zapNote = async (note: PrimalNote, sender: string | undefined, amou } } +export const zapArticle = async (note: PrimalArticle, sender: string | undefined, amount: number, comment = '', relays: Relay[]) => { + if (!sender) { + return false; + } + + const callback = await getZapEndpoint(note.author); + + if (!callback) { + return false; + } + + const sats = Math.round(amount * 1000); + + let payload = { + profile: note.pubkey, + event: note.msg.id, + amount: sats, + relays: relays.map(r => r.url) + }; + + if (comment.length > 0) { + // @ts-ignore + payload.comment = comment; + } + + const zapReq = nip57.makeZapRequest(payload); + + try { + const signedEvent = await signEvent(zapReq); + + const event = encodeURIComponent(JSON.stringify(signedEvent)); + + const r2 = await (await fetch(`${callback}?amount=${sats}&nostr=${event}`)).json(); + const pr = r2.pr; + + await enableWebLn(); + await sendPayment(pr); + + return true; + } catch (reason) { + console.error('Failed to zap: ', reason); + return false; + } +} + export const zapProfile = async (profile: PrimalUser, sender: string | undefined, amount: number, comment = '', relays: Relay[]) => { if (!sender || !profile) { return false; diff --git a/src/pages/Reads.tsx b/src/pages/Reads.tsx index 90a81e1..95cb7c7 100644 --- a/src/pages/Reads.tsx +++ b/src/pages/Reads.tsx @@ -27,13 +27,14 @@ import { PrimalUser } from '../types/primal'; import Avatar from '../components/Avatar/Avatar'; import { userName } from '../stores/profile'; import { useAccountContext } from '../contexts/AccountContext'; -import { feedNewPosts, placeholders, branding } from '../translations'; +import { reads, branding } from '../translations'; import Search from '../components/Search/Search'; import { setIsHome } from '../components/Layout/Layout'; import PageTitle from '../components/PageTitle/PageTitle'; import { useAppContext } from '../contexts/AppContext'; import { useReadsContext } from '../contexts/ReadsContext'; import ArticlePreview from '../components/ArticlePreview/ArticlePreview'; +import PageCaption from '../components/PageCaption/PageCaption'; const Home: Component = () => { @@ -133,18 +134,7 @@ const Home: Component = () => { -
- -
- -
- -
+ diff --git a/src/stores/note.ts b/src/stores/note.ts index d3529df..9327f11 100644 --- a/src/stores/note.ts +++ b/src/stores/note.ts @@ -366,6 +366,7 @@ export const convertToNotes: ConvertToNotes = (page, topZaps) => { replyTo: replyTo && replyTo[1], tags: msg.tags, id: msg.id, + pubkey: msg.pubkey, topZaps: [ ...tz ], }; }); @@ -391,6 +392,7 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => { const kind = msg.kind; const user = page?.users[msg.pubkey]; + const stat = page?.postStats[msg.id]; const mentionIds = Object.keys(mentions) let userMentionIds = msg.tags?.reduce((acc, t) => t[0] === 'p' ? [...acc, t[1]] : acc, []); @@ -447,8 +449,18 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => { const wordCount = page.wordCount ? page.wordCount[message.id] || 0 : 0; + + const noActions = { + event_id: msg.id, + liked: false, + replied: false, + reposted: false, + zapped: false, + }; + let article: PrimalArticle = { id: msg.id, + pubkey: msg.pubkey, title: '', summary: '', image: '', @@ -462,6 +474,15 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => { mentionedNotes, mentionedUsers, wordCount, + noteActions: (page.noteActions && page.noteActions[msg.id]) ?? noActions, + likes: stat?.likes || 0, + mentions: stat?.mentions || 0, + reposts: stat?.reposts || 0, + replies: stat?.replies || 0, + zaps: stat?.zaps || 0, + score: stat?.score || 0, + score24h: stat?.score24h || 0, + satszapped: stat?.satszapped || 0, }; msg.tags.forEach(tag => { diff --git a/src/types/primal.d.ts b/src/types/primal.d.ts index 7257ba8..6b6e2fc 100644 --- a/src/types/primal.d.ts +++ b/src/types/primal.d.ts @@ -497,6 +497,7 @@ export type PrimalNote = { mentionedUsers?: Record, replyTo?: string, id: string, + pubkey: string, tags: string[][], topZaps: TopZap[], }; @@ -516,9 +517,19 @@ export type PrimalArticle = { mentionedUsers?: Record, replyTo?: string, id: string, + pubkey: string, naddr: string, msg: NostrNoteContent, wordCount: number, + noteActions: NoteActions, + likes: number, + mentions: number, + reposts: number, + replies: number, + zaps: number, + score: number, + score24h: number, + satszapped: number, }; export type PrimalFeed = {