Enable reactions

This commit is contained in:
Bojan Mojsilovic 2024-05-27 15:18:27 +02:00
parent cca4d11df0
commit 3914a3c0c9
10 changed files with 365 additions and 163 deletions

View File

@ -1,11 +1,15 @@
import { A } from '@solidjs/router'; 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 { createStore } from 'solid-js/store';
import { Portal } from 'solid-js/web'; 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 { shortDate } from '../../lib/dates';
import { hookForDev } from '../../lib/devTools'; import { hookForDev } from '../../lib/devTools';
import { userName } from '../../stores/profile'; 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 Avatar from '../Avatar/Avatar';
import { NoteReactionsState } from '../Note/Note'; import { NoteReactionsState } from '../Note/Note';
import ArticleFooter from '../Note/NoteFooter/ArticleFooter'; import ArticleFooter from '../Note/NoteFooter/ArticleFooter';
@ -21,17 +25,20 @@ const ArticlePreview: Component<{
article: PrimalArticle, article: PrimalArticle,
}> = (props) => { }> = (props) => {
const app = useAppContext();
const account = useAccountContext();
const thread = useThreadContext();
const [reactionsState, updateReactionsState] = createStore<NoteReactionsState>({ const [reactionsState, updateReactionsState] = createStore<NoteReactionsState>({
likes: 0, likes: props.article.likes,
liked: false, liked: props.article.noteActions.liked,
reposts: 0, reposts: props.article.reposts,
reposted: false, reposted: props.article.noteActions.reposted,
replies: 0, replies: props.article.replies,
replied: false, replied: props.article.noteActions.replied,
zapCount: 0, zapCount: props.article.zaps,
satsZapped: 0, satsZapped: props.article.satszapped,
zapped: false, zapped: props.article.noteActions.zapped,
zappedAmount: 0, zappedAmount: 0,
zappedNow: false, zappedNow: false,
isZapping: false, isZapping: false,
@ -44,6 +51,136 @@ const ArticlePreview: Component<{
quoteCount: 0, 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 ( return (
<A class={styles.article} href={`/e/${props.article.naddr}`}> <A class={styles.article} href={`/e/${props.article.naddr}`}>
<div class={styles.header}> <div class={styles.header}>
@ -101,13 +238,8 @@ const ArticlePreview: Component<{
note={props.article} note={props.article}
state={reactionsState} state={reactionsState}
updateState={updateReactionsState} updateState={updateReactionsState}
customZapInfo={{ customZapInfo={customZapInfo()}
note: props.article, onZapAnim={addTopZapFeed}
onConfirm: () => {},
onSuccess: () => {},
onFail: () => {},
onCancel: () => {},
}}
/> />
</div> </div>

View File

@ -8,7 +8,7 @@ import { useToastContext } from '../../Toaster/Toaster';
import { useIntl } from '@cookbook/solid-intl'; import { useIntl } from '@cookbook/solid-intl';
import { truncateNumber } from '../../../lib/notifications'; import { truncateNumber } from '../../../lib/notifications';
import { canUserReceiveZaps, zapNote } from '../../../lib/zap'; import { canUserReceiveZaps, zapArticle, zapNote } from '../../../lib/zap';
import { useSettingsContext } from '../../../contexts/SettingsContext'; import { useSettingsContext } from '../../../contexts/SettingsContext';
import zapMD from '../../../assets/lottie/zap_md_2.json'; import zapMD from '../../../assets/lottie/zap_md_2.json';
@ -135,167 +135,167 @@ const ArticleFooter: Component<{
const doReply = () => {}; const doReply = () => {};
const doLike = async (e: MouseEvent) => { const doLike = async (e: MouseEvent) => {
// e.preventDefault(); e.preventDefault();
// e.stopPropagation(); e.stopPropagation();
// if (!account) { if (!account) {
// return; return;
// } }
// if (!account.hasPublicKey()) { if (!account.hasPublicKey()) {
// account.actions.showGetStarted(); account.actions.showGetStarted();
// return; return;
// } }
// if (account.relays.length === 0) { if (account.relays.length === 0) {
// toast?.sendWarning( toast?.sendWarning(
// intl.formatMessage(t.noRelaysConnected), intl.formatMessage(t.noRelaysConnected),
// ); );
// return; return;
// } }
// const success = await account.actions.addLike(props.note); const success = await account.actions.addLike(props.note);
// if (success) { if (success) {
// batch(() => { batch(() => {
// props.updateState('likes', (l) => l + 1); props.updateState('likes', (l) => l + 1);
// props.updateState('liked', () => true); props.updateState('liked', () => true);
// }); });
// } }
}; };
const startZap = (e: MouseEvent | TouchEvent) => { const startZap = (e: MouseEvent | TouchEvent) => {
// e.preventDefault(); e.preventDefault();
// e.stopPropagation(); e.stopPropagation();
// if (!account?.hasPublicKey()) { if (!account?.hasPublicKey()) {
// account?.actions.showGetStarted(); account?.actions.showGetStarted();
// props.updateState('isZapping', () => false); props.updateState('isZapping', () => false);
// return; return;
// } }
// if (account.relays.length === 0) { if (account.relays.length === 0) {
// toast?.sendWarning( toast?.sendWarning(
// intl.formatMessage(t.noRelaysConnected), intl.formatMessage(t.noRelaysConnected),
// ); );
// return; return;
// } }
// if (!canUserReceiveZaps(props.note.user)) { if (!canUserReceiveZaps(props.note.author)) {
// toast?.sendWarning( toast?.sendWarning(
// intl.formatMessage(t.zapUnavailable), intl.formatMessage(t.zapUnavailable),
// ); );
// props.updateState('isZapping', () => false); props.updateState('isZapping', () => false);
// return; return;
// } }
// quickZapDelay = setTimeout(() => { quickZapDelay = setTimeout(() => {
// app?.actions.openCustomZapModal(props.customZapInfo); app?.actions.openCustomZapModal(props.customZapInfo);
// props.updateState('isZapping', () => true); props.updateState('isZapping', () => true);
// }, 500); }, 500);
}; };
const commitZap = (e: MouseEvent | TouchEvent) => { const commitZap = (e: MouseEvent | TouchEvent) => {
// e.preventDefault(); e.preventDefault();
// e.stopPropagation(); e.stopPropagation();
// clearTimeout(quickZapDelay); clearTimeout(quickZapDelay);
// if (!account?.hasPublicKey()) { if (!account?.hasPublicKey()) {
// account?.actions.showGetStarted(); account?.actions.showGetStarted();
// return; return;
// } }
// if (account.relays.length === 0 || !canUserReceiveZaps(props.note.user)) { if (account.relays.length === 0 || !canUserReceiveZaps(props.note.author)) {
// return; return;
// } }
// if (app?.customZap === undefined) { if (app?.customZap === undefined) {
// doQuickZap(); doQuickZap();
// } }
}; };
const animateZap = () => { const animateZap = () => {
// setTimeout(() => { setTimeout(() => {
// props.updateState('hideZapIcon', () => true); props.updateState('hideZapIcon', () => true);
// if (!medZapAnimation) { if (!medZapAnimation) {
// return; return;
// } }
// let newLeft = props.wide ? 15 : 13; let newLeft = props.wide ? 15 : 13;
// let newTop = props.wide ? -6 : -6; let newTop = props.wide ? -6 : -6;
// if (props.large) { if (props.large) {
// newLeft = 2; newLeft = 2;
// newTop = -9; newTop = -9;
// } }
// medZapAnimation.style.left = `${newLeft}px`; medZapAnimation.style.left = `${newLeft}px`;
// medZapAnimation.style.top = `${newTop}px`; medZapAnimation.style.top = `${newTop}px`;
// const onAnimDone = () => { const onAnimDone = () => {
// batch(() => { batch(() => {
// props.updateState('showZapAnim', () => false); props.updateState('showZapAnim', () => false);
// props.updateState('hideZapIcon', () => false); props.updateState('hideZapIcon', () => false);
// props.updateState('zapped', () => true); props.updateState('zapped', () => true);
// }); });
// medZapAnimation?.removeEventListener('complete', onAnimDone); medZapAnimation?.removeEventListener('complete', onAnimDone);
// } }
// medZapAnimation.addEventListener('complete', onAnimDone); medZapAnimation.addEventListener('complete', onAnimDone);
// try { try {
// // @ts-ignore // @ts-ignore
// medZapAnimation.seek(0); medZapAnimation.seek(0);
// // @ts-ignore // @ts-ignore
// medZapAnimation.play(); medZapAnimation.play();
// } catch (e) { } catch (e) {
// console.warn('Failed to animte zap:', e); console.warn('Failed to animte zap:', e);
// onAnimDone(); onAnimDone();
// } }
// }, 10); }, 10);
}; };
const doQuickZap = async () => { const doQuickZap = async () => {
// if (!account?.hasPublicKey()) { if (!account?.hasPublicKey()) {
// account?.actions.showGetStarted(); account?.actions.showGetStarted();
// return; return;
// } }
// const amount = settings?.defaultZap.amount || 10; const amount = settings?.defaultZap.amount || 10;
// const message = settings?.defaultZap.message || ''; const message = settings?.defaultZap.message || '';
// const emoji = settings?.defaultZap.emoji; const emoji = settings?.defaultZap.emoji;
// batch(() => { batch(() => {
// props.updateState('isZapping', () => true); props.updateState('isZapping', () => true);
// props.updateState('satsZapped', (z) => z + amount); props.updateState('satsZapped', (z) => z + amount);
// props.updateState('showZapAnim', () => true); props.updateState('showZapAnim', () => true);
// }); });
// props.onZapAnim && props.onZapAnim({ amount, message, emoji }) props.onZapAnim && props.onZapAnim({ amount, message, emoji })
// setTimeout(async () => { setTimeout(async () => {
// const success = await zapNote(props.note, account.publicKey, amount, message, account.relays); const success = await zapArticle(props.note, account.publicKey, amount, message, account.relays);
// props.updateState('isZapping', () => false); props.updateState('isZapping', () => false);
// if (success) { if (success) {
// props.customZapInfo.onSuccess({ props.customZapInfo.onSuccess({
// emoji, emoji,
// amount, amount,
// message, message,
// }); });
// return; return;
// } }
// props.customZapInfo.onFail({ props.customZapInfo.onFail({
// emoji, emoji,
// amount, amount,
// message, message,
// }); });
// }, lottieDuration()); }, lottieDuration());
} }

View File

@ -21,6 +21,7 @@ import {
PrimalNote, PrimalNote,
PrimalUser, PrimalUser,
NostrEventContent, NostrEventContent,
PrimalArticle,
} from '../types/primal'; } from '../types/primal';
import { Kind, pinEncodePrefix, relayConnectingTimeout } from "../constants"; import { Kind, pinEncodePrefix, relayConnectingTimeout } from "../constants";
import { isConnected, refreshSocketListeners, removeSocketListeners, socket, subscribeTo, reset, subTo } from "../sockets"; import { isConnected, refreshSocketListeners, removeSocketListeners, socket, subscribeTo, reset, subTo } from "../sockets";
@ -79,7 +80,7 @@ export type AccountContextStore = {
showNewNoteForm: () => void, showNewNoteForm: () => void,
hideNewNoteForm: () => void, hideNewNoteForm: () => void,
setActiveUser: (user: PrimalUser) => void, setActiveUser: (user: PrimalUser) => void,
addLike: (note: PrimalNote) => Promise<boolean>, addLike: (note: PrimalNote | PrimalArticle) => Promise<boolean>,
setPublicKey: (pubkey: string | undefined) => void, setPublicKey: (pubkey: string | undefined) => void,
addFollow: (pubkey: string, cb?: (remove: boolean, pubkey: string) => void) => void, addFollow: (pubkey: string, cb?: (remove: boolean, pubkey: string) => void) => void,
removeFollow: (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); updateStore('showNewNoteForm', () => false);
}; };
const addLike = async (note: PrimalNote) => { const addLike = async (note: PrimalNote | PrimalArticle) => {
if (store.likes.includes(note.post.id)) { if (store.likes.includes(note.id)) {
return false; return false;
} }
const { success } = await sendLike(note, store.relays, store.relaySettings); const { success } = await sendLike(note, store.relays, store.relaySettings);
if (success) { if (success) {
updateStore('likes', (likes) => [ ...likes, note.post.id]); updateStore('likes', (likes) => [ ...likes, note.id]);
saveLikes(store.publicKey, store.likes); saveLikes(store.publicKey, store.likes);
} }

View File

@ -7,7 +7,7 @@ import {
onMount, onMount,
useContext useContext
} from "solid-js"; } from "solid-js";
import { PrimalNote, PrimalUser, ZapOption } from "../types/primal"; import { PrimalArticle, PrimalNote, PrimalUser, ZapOption } from "../types/primal";
import { CashuMint } from "@cashu/cashu-ts"; import { CashuMint } from "@cashu/cashu-ts";
@ -21,7 +21,7 @@ export type ReactionStats = {
export type CustomZapInfo = { export type CustomZapInfo = {
profile?: PrimalUser, profile?: PrimalUser,
note?: PrimalNote, note?: PrimalNote | PrimalArticle,
onConfirm: (zapOption: ZapOption) => void, onConfirm: (zapOption: ZapOption) => void,
onSuccess: (zapOption: ZapOption) => void, onSuccess: (zapOption: ZapOption) => void,
onFail: (zapOption: ZapOption) => void, onFail: (zapOption: ZapOption) => void,

View File

@ -489,6 +489,7 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
if (content.kind === Kind.NoteStats) { if (content.kind === Kind.NoteStats) {
const statistic = content as NostrStatsContent; const statistic = content as NostrStatsContent;
const stat = JSON.parse(statistic.content); const stat = JSON.parse(statistic.content);
console.log('READS STATS: ', stat)
if (scope) { if (scope) {
updateStore(scope, 'page', 'postStats', updateStore(scope, 'page', 'postStats',
@ -523,6 +524,7 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
const noteActionContent = content as NostrNoteActionsContent; const noteActionContent = content as NostrNoteActionsContent;
const noteActions = JSON.parse(noteActionContent.content) as NoteActions; const noteActions = JSON.parse(noteActionContent.content) as NoteActions;
console.log('READS ACTIONS: ', content)
if (scope) { if (scope) {
updateStore(scope, 'page', 'noteActions', updateStore(scope, 'page', 'noteActions',
(actions) => ({ ...actions, [noteActions.event_id]: { ...noteActions } }) (actions) => ({ ...actions, [noteActions.event_id]: { ...noteActions } })

View File

@ -308,13 +308,13 @@ export const importEvents = (events: NostrRelaySignedEvent[], subid: string) =>
type NostrEvent = { content: string, kind: number, tags: string[][], created_at: number }; 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 = { const event = {
content: '+', content: '+',
kind: Kind.Reaction, kind: Kind.Reaction,
tags: [ tags: [
['e', note.post.id], ['e', note.id],
['p', note.post.pubkey], ['p', note.pubkey],
], ],
created_at: Math.floor((new Date()).getTime() / 1000), created_at: Math.floor((new Date()).getTime() / 1000),
}; };

View File

@ -1,7 +1,7 @@
import { bech32 } from "@scure/base"; import { bech32 } from "@scure/base";
// @ts-ignore Bad types in nostr-tools // @ts-ignore Bad types in nostr-tools
import { nip57, Relay, utils } from "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 { logError } from "./logger";
import { enableWebLn, sendPayment, signEvent } from "./nostrAPI"; 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[]) => { export const zapProfile = async (profile: PrimalUser, sender: string | undefined, amount: number, comment = '', relays: Relay[]) => {
if (!sender || !profile) { if (!sender || !profile) {
return false; return false;

View File

@ -27,13 +27,14 @@ import { PrimalUser } from '../types/primal';
import Avatar from '../components/Avatar/Avatar'; import Avatar from '../components/Avatar/Avatar';
import { userName } from '../stores/profile'; import { userName } from '../stores/profile';
import { useAccountContext } from '../contexts/AccountContext'; import { useAccountContext } from '../contexts/AccountContext';
import { feedNewPosts, placeholders, branding } from '../translations'; import { reads, branding } from '../translations';
import Search from '../components/Search/Search'; import Search from '../components/Search/Search';
import { setIsHome } from '../components/Layout/Layout'; import { setIsHome } from '../components/Layout/Layout';
import PageTitle from '../components/PageTitle/PageTitle'; import PageTitle from '../components/PageTitle/PageTitle';
import { useAppContext } from '../contexts/AppContext'; import { useAppContext } from '../contexts/AppContext';
import { useReadsContext } from '../contexts/ReadsContext'; import { useReadsContext } from '../contexts/ReadsContext';
import ArticlePreview from '../components/ArticlePreview/ArticlePreview'; import ArticlePreview from '../components/ArticlePreview/ArticlePreview';
import PageCaption from '../components/PageCaption/PageCaption';
const Home: Component = () => { const Home: Component = () => {
@ -133,18 +134,7 @@ const Home: Component = () => {
<Search /> <Search />
</Wormhole> </Wormhole>
<div class={styles.normalCentralHeader}> <PageCaption title={intl.formatMessage(reads.pageTitle)} />
<HomeHeader
hasNewPosts={hasNewPosts}
loadNewContent={loadNewContent}
newPostCount={newPostCount}
newPostAuthors={newPostAuthors}
/>
</div>
<div class={styles.phoneCentralHeader}>
<HomeHeaderPhone />
</div>
<StickySidebar> <StickySidebar>
<HomeSidebar /> <HomeSidebar />

View File

@ -366,6 +366,7 @@ export const convertToNotes: ConvertToNotes = (page, topZaps) => {
replyTo: replyTo && replyTo[1], replyTo: replyTo && replyTo[1],
tags: msg.tags, tags: msg.tags,
id: msg.id, id: msg.id,
pubkey: msg.pubkey,
topZaps: [ ...tz ], topZaps: [ ...tz ],
}; };
}); });
@ -391,6 +392,7 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => {
const kind = msg.kind; const kind = msg.kind;
const user = page?.users[msg.pubkey]; const user = page?.users[msg.pubkey];
const stat = page?.postStats[msg.id];
const mentionIds = Object.keys(mentions) const mentionIds = Object.keys(mentions)
let userMentionIds = msg.tags?.reduce((acc, t) => t[0] === 'p' ? [...acc, t[1]] : acc, []); 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 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 = { let article: PrimalArticle = {
id: msg.id, id: msg.id,
pubkey: msg.pubkey,
title: '', title: '',
summary: '', summary: '',
image: '', image: '',
@ -462,6 +474,15 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => {
mentionedNotes, mentionedNotes,
mentionedUsers, mentionedUsers,
wordCount, 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 => { msg.tags.forEach(tag => {

11
src/types/primal.d.ts vendored
View File

@ -497,6 +497,7 @@ export type PrimalNote = {
mentionedUsers?: Record<string, PrimalUser>, mentionedUsers?: Record<string, PrimalUser>,
replyTo?: string, replyTo?: string,
id: string, id: string,
pubkey: string,
tags: string[][], tags: string[][],
topZaps: TopZap[], topZaps: TopZap[],
}; };
@ -516,9 +517,19 @@ export type PrimalArticle = {
mentionedUsers?: Record<string, PrimalUser>, mentionedUsers?: Record<string, PrimalUser>,
replyTo?: string, replyTo?: string,
id: string, id: string,
pubkey: string,
naddr: string, naddr: string,
msg: NostrNoteContent, msg: NostrNoteContent,
wordCount: number, wordCount: number,
noteActions: NoteActions,
likes: number,
mentions: number,
reposts: number,
replies: number,
zaps: number,
score: number,
score24h: number,
satszapped: number,
}; };
export type PrimalFeed = { export type PrimalFeed = {