Note quotes reactions

This commit is contained in:
Bojan Mojsilovic 2024-04-24 14:27:34 +02:00
parent a12bfc573d
commit 00f976ae78
17 changed files with 378 additions and 61 deletions

View File

@ -17,6 +17,10 @@
text-decoration: none !important; text-decoration: none !important;
} }
&.altBack {
background-color: var(--background-sheet);
}
.mentionedNoteHeader { .mentionedNoteHeader {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;

View File

@ -15,7 +15,13 @@ import VerificationCheck from '../VerificationCheck/VerificationCheck';
import styles from './EmbeddedNote.module.scss'; import styles from './EmbeddedNote.module.scss';
const EmbeddedNote: Component<{ note: PrimalNote, mentionedUsers?: Record<string, PrimalUser>, includeEmbeds?: boolean, isLast?: boolean}> = (props) => { const EmbeddedNote: Component<{
note: PrimalNote,
mentionedUsers?: Record<string, PrimalUser>,
includeEmbeds?: boolean,
isLast?: boolean,
alternativeBackground?: boolean,
}> = (props) => {
const threadContext = useThreadContext(); const threadContext = useThreadContext();
const intl = useIntl(); const intl = useIntl();
@ -32,11 +38,20 @@ const EmbeddedNote: Component<{ note: PrimalNote, mentionedUsers?: Record<string
return trimVerification(props.note.user?.nip05); return trimVerification(props.note.user?.nip05);
}); });
const klass = () => {
let k = styles.mentionedNote;
k += ' embeddedNote';
if (props.isLast) k+= ' noBottomMargin';
if (props.alternativeBackground) k+= ` ${styles.altBack}`;
return k;
}
const wrapper = (children: JSXElement) => { const wrapper = (children: JSXElement) => {
if (props.includeEmbeds) { if (props.includeEmbeds) {
return ( return (
<div <div
class={`${styles.mentionedNote} embeddedNote ${props.isLast ? 'noBottomMargin' : ''}`} class={klass()}
data-event={props.note.post.id} data-event={props.note.post.id}
data-event-bech32={noteId()} data-event-bech32={noteId()}
> >
@ -48,7 +63,7 @@ const EmbeddedNote: Component<{ note: PrimalNote, mentionedUsers?: Record<string
return ( return (
<A <A
href={`/e/${noteId()}`} href={`/e/${noteId()}`}
class={`${styles.mentionedNote} embeddedNote ${props.isLast ? 'noBottomMargin' : ''}`} class={klass()}
onClick={() => navToThread()} onClick={() => navToThread()}
data-event={props.note.post.id} data-event={props.note.post.id}
data-event-bech32={noteId()} data-event-bech32={noteId()}

View File

@ -23,6 +23,11 @@
border: none; border: none;
} }
&.reactionNote {
background: none;
border-bottom: 1px solid var(--subtile-devider);
}
.header { .header {
position: relative; position: relative;
display: flex; display: flex;

View File

@ -22,7 +22,7 @@ import { thread, zapCustomOption } from '../../translations';
import { useAccountContext } from '../../contexts/AccountContext'; import { useAccountContext } from '../../contexts/AccountContext';
import { uuidv4 } from '../../utils'; import { uuidv4 } from '../../utils';
export type NoteFooterState = { export type NoteReactionsState = {
likes: number, likes: number,
liked: boolean, liked: boolean,
reposts: number, reposts: number,
@ -40,6 +40,7 @@ export type NoteFooterState = {
moreZapsAvailable: boolean, moreZapsAvailable: boolean,
isRepostMenuVisible: boolean, isRepostMenuVisible: boolean,
topZaps: TopZap[], topZaps: TopZap[],
quoteCount: number,
}; };
const Note: Component<{ const Note: Component<{
@ -47,7 +48,9 @@ const Note: Component<{
id?: string, id?: string,
parent?: boolean, parent?: boolean,
shorten?: boolean, shorten?: boolean,
noteType?: 'feed' | 'primary' | 'notification' noteType?: 'feed' | 'primary' | 'notification' | 'reaction'
onClick?: () => void,
quoteCount?: number,
}> = (props) => { }> = (props) => {
const threadContext = useThreadContext(); const threadContext = useThreadContext();
@ -55,15 +58,22 @@ const Note: Component<{
const account = useAccountContext(); const account = useAccountContext();
const intl = useIntl(); const intl = useIntl();
createEffect(() => {
if (props.quoteCount) {
updateReactionsState('quoteCount', () => props.quoteCount || 0);
}
})
const noteType = () => props.noteType || 'feed'; const noteType = () => props.noteType || 'feed';
const repost = () => props.note.repost; const repost = () => props.note.repost;
const navToThread = (note: PrimalNote) => { const navToThread = (note: PrimalNote) => {
props.onClick && props.onClick();
threadContext?.actions.setPrimaryNote(note); threadContext?.actions.setPrimaryNote(note);
}; };
const [reactionsState, updateReactionsState] = createStore<NoteFooterState>({ const [reactionsState, updateReactionsState] = createStore<NoteReactionsState>({
likes: props.note.post.likes, likes: props.note.post.likes,
liked: props.note.post.noteActions.liked, liked: props.note.post.noteActions.liked,
reposts: props.note.post.reposts, reposts: props.note.post.reposts,
@ -81,6 +91,7 @@ const Note: Component<{
moreZapsAvailable: false, moreZapsAvailable: false,
isRepostMenuVisible: false, isRepostMenuVisible: false,
topZaps: [], topZaps: [],
quoteCount: props.quoteCount || 0,
}); });
let noteContextMenu: HTMLDivElement | undefined; let noteContextMenu: HTMLDivElement | undefined;
@ -173,7 +184,7 @@ const Note: Component<{
likes: reactionsState.likes, likes: reactionsState.likes,
zaps: reactionsState.zapCount, zaps: reactionsState.zapCount,
reposts: reactionsState.reposts, reposts: reactionsState.reposts,
quotes: 0, quotes: reactionsState.quoteCount,
openOn, openOn,
}); });
}; };
@ -190,9 +201,9 @@ const Note: Component<{
} }
const reactionSum = () => { 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]); const firstZap = createMemo(() => reactionsState.topZaps[0]);
@ -420,6 +431,50 @@ const Note: Component<{
</div> </div>
</A> </A>
</Match> </Match>
<Match when={noteType() === 'reaction'}>
<A
id={props.id}
class={`${styles.note} ${styles.reactionNote}`}
href={`/e/${props.note?.post.noteId}`}
onClick={() => navToThread(props.note)}
data-event={props.note.post.id}
data-event-bech32={props.note.post.noteId}
draggable={false}
>
<div class={styles.content}>
<div class={styles.leftSide}>
<A href={`/p/${props.note.user.npub}`}>
<Avatar user={props.note.user} size="vs" />
</A>
<Show
when={props.parent}
>
<div class={styles.ancestorLine}></div>
</Show>
</div>
<div class={styles.rightSide}>
<NoteAuthorInfo
author={props.note.user}
time={props.note.post.created_at}
/>
<NoteReplyToHeader note={props.note} />
<div class={styles.message}>
<ParsedNote
note={props.note}
shorten={props.shorten}
width={Math.min(528, window.innerWidth - 72)}
noLightbox={true}
altEmbeds={true}
/>
</div>
</div>
</div>
</A>
</Match>
</Switch> </Switch>
); );
} }

View File

@ -19,7 +19,7 @@ import { getScreenCordinates } from '../../../utils';
import ZapAnimation from '../../ZapAnimation/ZapAnimation'; import ZapAnimation from '../../ZapAnimation/ZapAnimation';
import { CustomZapInfo, useAppContext } from '../../../contexts/AppContext'; import { CustomZapInfo, useAppContext } from '../../../contexts/AppContext';
import NoteFooterActionButton from './NoteFooterActionButton'; import NoteFooterActionButton from './NoteFooterActionButton';
import { NoteFooterState } from '../Note'; import { NoteReactionsState } from '../Note';
import { SetStoreFunction } from 'solid-js/store'; import { SetStoreFunction } from 'solid-js/store';
import BookmarkNote from '../../BookmarkNote/BookmarkNote'; import BookmarkNote from '../../BookmarkNote/BookmarkNote';
import { APP_ID } from '../../../App'; import { APP_ID } from '../../../App';
@ -30,8 +30,8 @@ const NoteFooter: Component<{
note: PrimalNote, note: PrimalNote,
wide?: boolean, wide?: boolean,
id?: string, id?: string,
state: NoteFooterState, state: NoteReactionsState,
updateState: SetStoreFunction<NoteFooterState>, updateState: SetStoreFunction<NoteReactionsState>,
customZapInfo: CustomZapInfo, customZapInfo: CustomZapInfo,
large?: boolean, large?: boolean,
}> = (props) => { }> = (props) => {

View File

@ -132,6 +132,7 @@ const NoteImage: Component<{
data-pswp-height={zoomH()} data-pswp-height={zoomH()}
data-image-group={props.imageGroup} data-image-group={props.imageGroup}
data-cropped={true} data-cropped={true}
target="_blank"
> >
<img <img
id={imgId} id={imgId}

View File

@ -103,3 +103,17 @@
margin-left: 2px; margin-left: 2px;
} }
} }
.reactionNote {
display: flex;
width: fit-content;
align-items: center;
justify-content: center;
height: 48px;
font-weight: 700;
margin-block: 4px;
padding-inline: 12px;
border: 1px solid var(--subtile-devider);
border-radius: var(--border-radius-small);
}

View File

@ -144,6 +144,8 @@ const ParsedNote: Component<{
shorten?: boolean, shorten?: boolean,
isEmbeded?: boolean, isEmbeded?: boolean,
width?: number, width?: number,
noLightbox?: boolean,
altEmbeds?: boolean,
}> = (props) => { }> = (props) => {
const intl = useIntl(); const intl = useIntl();
@ -172,6 +174,8 @@ const ParsedNote: Component<{
}); });
onMount(() => { onMount(() => {
if (props.noLightbox) return;
lightbox.init(); lightbox.init();
}); });
@ -927,6 +931,7 @@ const ParsedNote: Component<{
note={ment} note={ment}
mentionedUsers={props.note.mentionedUsers || {}} mentionedUsers={props.note.mentionedUsers || {}}
isLast={index === content.length-1} isLast={index === content.length-1}
alternativeBackground={props.altEmbeds}
/> />
</div>; </div>;
} }

View File

@ -7,7 +7,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 20px; padding: 16px;
.header { .header {
display: flex; display: flex;
@ -73,7 +73,7 @@
.tab { .tab {
position: relative; position: relative;
display: inline-block; display: inline-block;
padding-inline: 14px; padding-inline: 12px;
padding-block: 2px; padding-block: 2px;
border: none; border: none;
background: none; background: none;
@ -107,6 +107,7 @@
height: 440px; height: 440px;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
padding-right: 8px;
@keyframes fadeIn { @keyframes fadeIn {
from { from {
opacity:0; opacity:0;

View File

@ -1,7 +1,7 @@
import { useIntl } from '@cookbook/solid-intl'; import { useIntl } from '@cookbook/solid-intl';
import { Tabs } from '@kobalte/core'; import { Tabs } from '@kobalte/core';
import { A } from '@solidjs/router'; 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 { createStore } from 'solid-js/store';
import { APP_ID } from '../../App'; import { APP_ID } from '../../App';
import { Kind } from '../../constants'; import { Kind } from '../../constants';
@ -9,20 +9,25 @@ import { useAccountContext } from '../../contexts/AccountContext';
import { ReactionStats } from '../../contexts/AppContext'; import { ReactionStats } from '../../contexts/AppContext';
import { hookForDev } from '../../lib/devTools'; import { hookForDev } from '../../lib/devTools';
import { hexToNpub } from '../../lib/keys'; 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 { truncateNumber2 } from '../../lib/notifications';
import { updateStore } from '../../services/StoreService';
import { subscribeTo } from '../../sockets'; import { subscribeTo } from '../../sockets';
import { convertToNotes } from '../../stores/note';
import { userName } from '../../stores/profile'; import { userName } from '../../stores/profile';
import { actions as tActions, placeholders as tPlaceholders, reactionsModal } from '../../translations'; 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 { parseBolt11 } from '../../utils';
import Avatar from '../Avatar/Avatar'; import Avatar from '../Avatar/Avatar';
import Loader from '../Loader/Loader'; import Loader from '../Loader/Loader';
import Modal from '../Modal/Modal'; import Modal from '../Modal/Modal';
import Note from '../Note/Note';
import Paginator from '../Paginator/Paginator'; import Paginator from '../Paginator/Paginator';
import VerificationCheck from '../VerificationCheck/VerificationCheck'; import VerificationCheck from '../VerificationCheck/VerificationCheck';
import styles from './ReactionsModal.module.scss'; import styles from './ReactionsModal.module.scss';
const ReactionsModal: Component<{ const ReactionsModal: Component<{
id?: string, id?: string,
noteId: string | undefined, noteId: string | undefined,
@ -38,18 +43,33 @@ const ReactionsModal: Component<{
const [likeList, setLikeList] = createStore<any[]>([]); const [likeList, setLikeList] = createStore<any[]>([]);
const [zapList, setZapList] = createStore<any[]>([]); const [zapList, setZapList] = createStore<any[]>([]);
const [repostList, setRepostList] = createStore<any[]>([]); const [repostList, setRepostList] = createStore<any[]>([]);
// const [quotesList, setQuotesList] = createStore<any[]>([]); const [quotesList, setQuotesList] = createStore<PrimalNote[]>([]);
const [quoteCount, setQuoteCount] = createSignal(0);
const [isFetching, setIsFetching] = createSignal(false); const [isFetching, setIsFetching] = createSignal(false);
let loadedLikes = 0; let loadedLikes = 0;
let loadedZaps = 0; let loadedZaps = 0;
let loadedReposts = 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(() => { createEffect(() => {
if (props.noteId && props.stats.openOn) { 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': case 'reposts':
loadedReposts === 0 && getReposts(); loadedReposts === 0 && getReposts();
break; break;
// case 'quotes': case 'quotes':
// loadedQuotes === 0 && getQuotes(); loadedQuotes === 0 && getQuotes();
// break; break;
} }
}); });
@ -76,10 +96,13 @@ const ReactionsModal: Component<{
setZapList(() => []); setZapList(() => []);
setRepostList(() => []); setRepostList(() => []);
setSelectedTab(() => 'likes'); setSelectedTab(() => 'likes');
setQuotesList(() => []);
setQuoteCount(() => 0);
loadedLikes = 0; loadedLikes = 0;
loadedZaps = 0; loadedZaps = 0;
loadedReposts = 0; loadedReposts = 0;
loadedQuotes = 0;
} }
}); });
@ -203,6 +226,7 @@ const ReactionsModal: Component<{
const users: any[] = []; const users: any[] = [];
const unsub = subscribeTo(subId, (type,_, content) => { const unsub = subscribeTo(subId, (type,_, content) => {
if (type === 'EOSE') { if (type === 'EOSE') {
setRepostList((reposts) => [...reposts, ...users]); setRepostList((reposts) => [...reposts, ...users]);
loadedReposts = repostList.length; loadedReposts = repostList.length;
@ -232,44 +256,128 @@ const ReactionsModal: Component<{
getEventReactions(props.noteId, Kind.Repost, subId, offset); getEventReactions(props.noteId, Kind.Repost, subId, offset);
}; };
// const getQuotes = (offset = 0) => { const getQuotes = (offset = 0) => {
// if (!props.noteId) return; 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) => { const unsub = subscribeTo(subId, (type,_, content) => {
// if (type === 'EOSE') { if (type === 'EOSE') {
// setQuotesList((reposts) => [...reposts, ...users]); const pageNotes = convertToNotes(page);
// loadedQuotes = quotesList.length;
// setIsFetching(() => false);
// unsub();
// }
// if (type === 'EVENT') { setQuotesList((notes) => [...notes, ...pageNotes]);
// if (content?.kind === Kind.Metadata) { loadedQuotes = quotesList.length;
// let user = JSON.parse(content.content); setIsFetching(() => false);
unsub();
}
// if (!user.displayName || typeof user.displayName === 'string' && user.displayName.trim().length === 0) { if (type === 'EVENT') {
// user.displayName = user.display_name; if (content?.kind === Kind.Metadata) {
// } const user = content as NostrUserContent;
// user.pubkey = content.pubkey;
// user.npub = hexToNpub(content.pubkey);
// user.created_at = content.created_at;
// users.push(user); page.users[user.pubkey] = { ...user };
// return; return;
// } }
// } if (content?.kind === Kind.Text) {
// }); const message = content as NostrNoteContent;
// setIsFetching(() => true); if (page.messages.find(m => m.id === message.id)) {
// getEventQuotes(props.noteId, subId, offset); 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 ( return (
<Modal <Modal
@ -287,6 +395,12 @@ const ReactionsModal: Component<{
</button> </button>
</div> </div>
<Switch>
<Match when={!isFetching && totalCount() === 0}>
{intl.formatMessage(tPlaceholders.noReactionDetails)}
</Match>
</Switch>
<div class={styles.description}> <div class={styles.description}>
<Tabs.Root value={selectedTab()} onChange={setSelectedTab}> <Tabs.Root value={selectedTab()} onChange={setSelectedTab}>
<Tabs.List class={styles.tabs}> <Tabs.List class={styles.tabs}>
@ -305,9 +419,9 @@ const ReactionsModal: Component<{
{intl.formatMessage(reactionsModal.tabs.reposts, { count: props.stats.reposts })} {intl.formatMessage(reactionsModal.tabs.reposts, { count: props.stats.reposts })}
</Tabs.Trigger> </Tabs.Trigger>
</Show> </Show>
<Show when={props.stats.quotes > 0}> <Show when={quoteCount() > 0}>
<Tabs.Trigger class={styles.tab} value={'quotes'} > <Tabs.Trigger class={styles.tab} value={'quotes'} >
{intl.formatMessage(reactionsModal.tabs.quotes, { count: props.stats.quotes })} {intl.formatMessage(reactionsModal.tabs.quotes, { count: quoteCount() })}
</Tabs.Trigger> </Tabs.Trigger>
</Show> </Show>
@ -319,8 +433,13 @@ const ReactionsModal: Component<{
each={likeList} each={likeList}
fallback={ fallback={
<Show when={!isFetching()}> <Show when={!isFetching()}>
<Show
when={totalCount() > 0}
fallback={intl.formatMessage(tPlaceholders.noReactionDetails)}
>
{intl.formatMessage(tPlaceholders.noLikeDetails)} {intl.formatMessage(tPlaceholders.noLikeDetails)}
</Show> </Show>
</Show>
} }
> >
{admirer => {admirer =>
@ -364,8 +483,13 @@ const ReactionsModal: Component<{
each={zapList} each={zapList}
fallback={ fallback={
<Show when={!isFetching()}> <Show when={!isFetching()}>
<Show
when={totalCount() > 0}
fallback={intl.formatMessage(tPlaceholders.noReactionDetails)}
>
{intl.formatMessage(tPlaceholders.noZapDetails)} {intl.formatMessage(tPlaceholders.noZapDetails)}
</Show> </Show>
</Show>
} }
> >
{zap => {zap =>
@ -416,8 +540,13 @@ const ReactionsModal: Component<{
each={repostList} each={repostList}
fallback={ fallback={
<Show when={!isFetching()}> <Show when={!isFetching()}>
<Show
when={totalCount() > 0}
fallback={intl.formatMessage(tPlaceholders.noReactionDetails)}
>
{intl.formatMessage(tPlaceholders.noRepostDetails)} {intl.formatMessage(tPlaceholders.noRepostDetails)}
</Show> </Show>
</Show>
} }
> >
{reposter => {reposter =>
@ -456,7 +585,36 @@ const ReactionsModal: Component<{
</Show> </Show>
</Tabs.Content> </Tabs.Content>
<Tabs.Content class={styles.tabContent} value={'quotes'}> <Tabs.Content class={styles.tabContent} value={'quotes'}>
All the quotes <For
each={quotesList}
fallback={
<Show when={!isFetching()}>
<Show
when={totalCount() > 0}
fallback={intl.formatMessage(tPlaceholders.noReactionDetails)}
>
{intl.formatMessage(tPlaceholders.noQuoteDetails)}
</Show>
</Show>
}
>
{quote => (
<Note
note={quote}
shorten={true}
noteType="reaction"
onClick={props.onClose}
/>
)}
</For>
<Paginator
loadNextPage={() => {
const len = quotesList.length;
if (len === 0) return;
getQuotes(len);
}}
isSmall={true}
/>
</Tabs.Content> </Tabs.Content>
</Tabs.Root> </Tabs.Root>
</div> </div>

View File

@ -140,6 +140,7 @@ export enum Kind {
UploadChunk = 10_000_135, UploadChunk = 10_000_135,
UserRelays=10_000_139, UserRelays=10_000_139,
RelayHint=10_000_141, RelayHint=10_000_141,
NoteQuoteStats=10_000_143,
WALLET_OPERATION = 10_000_300, WALLET_OPERATION = 10_000_300,
} }

View File

@ -38,7 +38,7 @@ import {
} from "../types/primal"; } from "../types/primal";
import { APP_ID } from "../App"; import { APP_ID } from "../App";
import { useAccountContext } from "./AccountContext"; import { useAccountContext } from "./AccountContext";
import { getEventZaps, setLinkPreviews } from "../lib/notes"; import { getEventQuoteStats, getEventZaps, setLinkPreviews } from "../lib/notes";
import { parseBolt11 } from "../utils"; import { parseBolt11 } from "../utils";
import { getUserProfiles } from "../lib/profile"; import { getUserProfiles } from "../lib/profile";
@ -60,6 +60,7 @@ export type ThreadContextStore = {
reposts: Record<string, string> | undefined, reposts: Record<string, string> | undefined,
lastNote: PrimalNote | undefined, lastNote: PrimalNote | undefined,
topZaps: Record<string, TopZap[]>, topZaps: Record<string, TopZap[]>,
quoteCount: number,
actions: { actions: {
saveNotes: (newNotes: PrimalNote[]) => void, saveNotes: (newNotes: PrimalNote[]) => void,
clearNotes: () => void, clearNotes: () => void,
@ -92,6 +93,7 @@ export const initialData = {
reposts: {}, reposts: {},
lastNote: undefined, lastNote: undefined,
topZaps: {}, topZaps: {},
quoteCount: 0,
}; };
@ -116,6 +118,7 @@ export const ThreadProvider = (props: { children: ContextChildren }) => {
updateStore('noteId', noteId) updateStore('noteId', noteId)
getThread(account?.publicKey, noteId, `thread_${APP_ID}`); getThread(account?.publicKey, noteId, `thread_${APP_ID}`);
fetchTopZaps(noteId); fetchTopZaps(noteId);
fetchNoteQuoteStats(noteId);
updateStore('isFetching', () => true); updateStore('isFetching', () => true);
} }
@ -276,6 +279,12 @@ export const ThreadProvider = (props: { children: ContextChildren }) => {
return; return;
} }
if (content.kind === Kind.NoteQuoteStats) {
const quoteStats = JSON.parse(content.content);
updateStore('quoteCount', () => quoteStats.count || 0);
}
}; };
const savePage = (page: FeedPage) => { const savePage = (page: FeedPage) => {
@ -298,6 +307,10 @@ export const ThreadProvider = (props: { children: ContextChildren }) => {
getUserProfiles(pubkeys, `thread_pk_${APP_ID}`); getUserProfiles(pubkeys, `thread_pk_${APP_ID}`);
}; };
const fetchNoteQuoteStats = (noteId: string) => {
getEventQuoteStats(noteId, `thread_quote_stats_${APP_ID}`)
}
// SOCKET HANDLERS ------------------------------ // SOCKET HANDLERS ------------------------------
const onMessage = (event: MessageEvent) => { const onMessage = (event: MessageEvent) => {
@ -368,6 +381,17 @@ export const ThreadProvider = (props: { children: ContextChildren }) => {
return; return;
} }
} }
if (subId === `thread_quote_stats_${APP_ID}`) {
if (type === 'EOSE') {
savePage(store.page);
}
if (type === 'EVENT') {
updatePage(content);
return;
}
}
}; };
const onSocketClose = (closeEvent: CloseEvent) => { const onSocketClose = (closeEvent: CloseEvent) => {

View File

@ -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 }]}, {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 }]},
]));
};

View File

@ -207,6 +207,7 @@ const Thread: Component = () => {
<Note <Note
note={primaryNote() as PrimalNote} note={primaryNote() as PrimalNote}
noteType="primary" noteType="primary"
quoteCount={threadContext?.quoteCount}
/> />
<Show when={account?.hasPublicKey()}> <Show when={account?.hasPublicKey()}>
<ReplyToNote <ReplyToNote

View File

@ -308,6 +308,8 @@ export const convertToNotes: ConvertToNotes = (page) => {
mentionedNotes, mentionedNotes,
mentionedUsers, mentionedUsers,
replyTo: replyTo && replyTo[1], replyTo: replyTo && replyTo[1],
tags: msg.tags,
id: msg.id,
}; };
}); });
} }

View File

@ -1014,6 +1014,11 @@ export const placeholders = {
description: 'Placeholder when the note is missing', 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: { noLikeDetails: {
id: 'placeholders.noLikeDetails', id: 'placeholders.noLikeDetails',
defaultMessage: 'No details for likes found', defaultMessage: 'No details for likes found',
@ -1029,6 +1034,11 @@ export const placeholders = {
defaultMessage: 'No details for reposts found', defaultMessage: 'No details for reposts found',
description: 'Placeholder when there are no repost details in reactions modal', 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: { addComment: {
id: 'placeholders.addComment', id: 'placeholders.addComment',
defaultMessage: 'Add a comment...', defaultMessage: 'Add a comment...',

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

@ -221,6 +221,13 @@ export type NostrZapInfo = {
tags?: string[][], tags?: string[][],
}; };
export type NostrQuoteStatsInfo = {
kind: Kind.NoteQuoteStats,
content: string,
created_at?: number,
tags?: string[][],
};
export type NostrEventContent = export type NostrEventContent =
NostrNoteContent | NostrNoteContent |
NostrUserContent | NostrUserContent |
@ -251,7 +258,8 @@ export type NostrEventContent =
PrimalUserRelays | PrimalUserRelays |
NostrBookmarks | NostrBookmarks |
NostrRelayHint | NostrRelayHint |
NostrZapInfo; NostrZapInfo |
NostrQuoteStatsInfo;
export type NostrEvent = [ export type NostrEvent = [
type: "EVENT", type: "EVENT",
@ -465,6 +473,8 @@ export type PrimalNote = {
mentionedNotes?: Record<string, PrimalNote>, mentionedNotes?: Record<string, PrimalNote>,
mentionedUsers?: Record<string, PrimalUser>, mentionedUsers?: Record<string, PrimalUser>,
replyTo?: string, replyTo?: string,
id: string,
tags: string[][],
}; };
export type PrimalFeed = { export type PrimalFeed = {