Refactor zapps for note

This commit is contained in:
Bojan Mojsilovic 2024-04-17 16:19:08 +02:00
parent d06c74f466
commit ac21e06df7
5 changed files with 169 additions and 31 deletions

View File

@ -339,12 +339,17 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding-left: 2px;
padding-right: 10px; padding-right: 10px;
padding-block: 2px;
margin: 0;
border-radius: 12px; border-radius: 12px;
background: var(--devider); background: var(--devider);
width: fit-content; width: fit-content;
max-width: 100%; max-width: 100%;
text-decoration: none; text-decoration: none;
border: none;
outline: none;
.amount { .amount {
color: var(--text-primary); color: var(--text-primary);
@ -395,11 +400,16 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding-left: 2px;
padding-right: 10px; padding-right: 10px;
padding-block: 2px;
margin: 0;
border-radius: 12px; border-radius: 12px;
background: var(--devider); background: var(--devider);
width: fit-content; width: fit-content;
text-decoration: none; text-decoration: none;
border: none;
outline: none;
.amount { .amount {
color: var(--text-primary); color: var(--text-primary);

View File

@ -1,5 +1,5 @@
import { A } from '@solidjs/router'; import { A } from '@solidjs/router';
import { batch, Component, For, Match, Show, Switch } from 'solid-js'; import { batch, Component, createMemo, For, Match, Show, Switch } from 'solid-js';
import { PrimalNote, ZapOption } from '../../types/primal'; import { PrimalNote, ZapOption } from '../../types/primal';
import ParsedNote from '../ParsedNote/ParsedNote'; import ParsedNote from '../ParsedNote/ParsedNote';
import NoteFooter from './NoteFooter/NoteFooter'; import NoteFooter from './NoteFooter/NoteFooter';
@ -18,6 +18,7 @@ import { CustomZapInfo, useAppContext } from '../../contexts/AppContext';
import NoteContextTrigger from './NoteContextTrigger'; import NoteContextTrigger from './NoteContextTrigger';
import { date, longDate, veryLongDate } from '../../lib/dates'; import { date, longDate, veryLongDate } from '../../lib/dates';
import { hexToNpub } from '../../lib/keys'; import { hexToNpub } from '../../lib/keys';
import { zapCustomOption } from '../../translations';
export type NoteFooterState = { export type NoteFooterState = {
likes: number, likes: number,
@ -157,14 +158,31 @@ const Note: Component<{
return (likes || 0) + (zaps || 0) + (reposts || 0); return (likes || 0) + (zaps || 0) + (reposts || 0);
}; };
const firstZap = () => (threadContext?.topZaps[props.note.post.id] || [])[0]; const firstZap = createMemo(() => (threadContext?.topZaps[props.note.post.id] || [])[0]);
const topZaps = () => { const topZaps = createMemo(() => {
return (threadContext?.topZaps[props.note.post.id] || []).slice(1, 8); // return (threadContext?.topZaps[props.note.post.id] || []).slice(1);
} const zaps = (threadContext?.topZaps[props.note.post.id] || []).slice(1);
let limit = 0;
let digits = 0;
for (let i=0; i< zaps.length; i++) {
const amount = zaps[i].amount || 0;
const length = Math.log(amount) * Math.LOG10E + 1 | 0;
digits += length;
if (digits > 25 || limit > 6) break;
limit++;
}
return zaps.slice(0, limit);
})
const zapSender = (zap: TopZap) => { const zapSender = (zap: TopZap) => {
return threadContext?.users.find(u => u.pubkey === zap.sender); return threadContext?.users.find(u => u.pubkey === zap.pubkey);
} }
return ( return (
@ -223,25 +241,32 @@ const Note: Component<{
<div class={styles.zapHighlights}> <div class={styles.zapHighlights}>
<Show when={firstZap()}> <Show when={firstZap()}>
<A class={styles.firstZap} href={`/p/${hexToNpub(firstZap().sender)}`}> <button
class={styles.firstZap}
onClick={() => openReactionModal('zaps')}
>
<Avatar user={zapSender(firstZap())} size="micro" /> <Avatar user={zapSender(firstZap())} size="micro" />
<div class={styles.amount}> <div class={styles.amount}>
{firstZap().amount_sats.toLocaleString()} {firstZap().amount.toLocaleString()}
</div> </div>
<div class={styles.description}> <div class={styles.description}>
{firstZap().message}
</div> </div>
</A> </button>
</Show> </Show>
<div class={styles.topZaps}> <div class={styles.topZaps}>
<div class={styles.zapList}> <div class={styles.zapList}>
<For each={topZaps()}> <For each={topZaps()}>
{zap => ( {zap => (
<A class={styles.topZap} href={`/p/${hexToNpub(zap.sender)}`}> <button
class={styles.topZap}
onClick={() => openReactionModal('zaps')}
>
<Avatar user={zapSender(zap)} size="micro" /> <Avatar user={zapSender(zap)} size="micro" />
<div class={styles.amount}> <div class={styles.amount}>
{zap.amount_sats.toLocaleString()} {zap.amount.toLocaleString()}
</div> </div>
</A> </button>
)} )}
</For> </For>
</div> </div>

View File

@ -5,11 +5,12 @@ import { Component, createEffect, createSignal, For, onMount, Show } from 'solid
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';
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 { getEventReactions, getEventZaps } from '../../lib/notes'; import { getEventQuotes, getEventReactions, getEventZaps } from '../../lib/notes';
import { truncateNumber, truncateNumber2 } from '../../lib/notifications'; import { truncateNumber2 } from '../../lib/notifications';
import { subscribeTo } from '../../sockets'; import { subscribeTo } from '../../sockets';
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';
@ -30,18 +31,21 @@ const ReactionsModal: Component<{
}> = (props) => { }> = (props) => {
const intl = useIntl(); const intl = useIntl();
const account = useAccountContext();
const [selectedTab, setSelectedTab] = createSignal('likes'); const [selectedTab, setSelectedTab] = createSignal('likes');
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 [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;
createEffect(() => { createEffect(() => {
if (props.noteId && props.stats.openOn) { if (props.noteId && props.stats.openOn) {
@ -60,6 +64,9 @@ const ReactionsModal: Component<{
case 'reposts': case 'reposts':
loadedReposts === 0 && getReposts(); loadedReposts === 0 && getReposts();
break; break;
// case 'quotes':
// loadedQuotes === 0 && getQuotes();
// break;
} }
}); });
@ -184,7 +191,7 @@ const ReactionsModal: Component<{
}); });
setIsFetching(() => true); setIsFetching(() => true);
getEventZaps(props.noteId, subId, 20, offset); getEventZaps(props.noteId, account?.publicKey, subId, 20, offset);
// getEventReactions(props.noteId, Kind.Zap, subId, offset); // getEventReactions(props.noteId, Kind.Zap, subId, offset);
}; };
@ -225,6 +232,43 @@ const ReactionsModal: Component<{
getEventReactions(props.noteId, Kind.Repost, subId, offset); getEventReactions(props.noteId, Kind.Repost, subId, offset);
}; };
// const getQuotes = (offset = 0) => {
// if (!props.noteId) return;
// const subId = `nr_q_${props.noteId}_${APP_ID}`;
// const users: any[] = [];
// const unsub = subscribeTo(subId, (type,_, content) => {
// if (type === 'EOSE') {
// setQuotesList((reposts) => [...reposts, ...users]);
// loadedQuotes = quotesList.length;
// setIsFetching(() => false);
// unsub();
// }
// if (type === 'EVENT') {
// if (content?.kind === Kind.Metadata) {
// let user = JSON.parse(content.content);
// 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;
// users.push(user);
// return;
// }
// }
// });
// setIsFetching(() => true);
// getEventQuotes(props.noteId, subId, offset);
// };
const totalCount = () => props.stats.likes + props.stats.quotes + props.stats.reposts + props.stats.zaps; const totalCount = () => props.stats.likes + props.stats.quotes + props.stats.reposts + props.stats.zaps;
return ( return (

View File

@ -38,15 +38,15 @@ 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 { setLinkPreviews } from "../lib/notes"; import { getEventZaps, setLinkPreviews } from "../lib/notes";
import { parseBolt11 } from "../utils";
export type TopZap = { export type TopZap = {
amount_sats: number, id: string,
created_at: number, amount: number,
event_id: string, pubkey: string,
receiver: string, message: string,
sender: string, eventId: string,
zap_receipt_id: string,
} }
export type ThreadContextStore = { export type ThreadContextStore = {
@ -112,6 +112,7 @@ export const ThreadProvider = (props: { children: ContextChildren }) => {
clearNotes(); clearNotes();
updateStore('noteId', noteId) updateStore('noteId', noteId)
getThread(account?.publicKey, noteId, `thread_${APP_ID}`); getThread(account?.publicKey, noteId, `thread_${APP_ID}`);
getEventZaps(noteId, account?.publicKey, `thread_zapps_${APP_ID}`, 10, 0);
updateStore('isFetching', () => true); updateStore('isFetching', () => true);
} }
@ -223,19 +224,50 @@ export const ThreadProvider = (props: { children: ContextChildren }) => {
updateStore('page', 'relayHints', (rh) => ({ ...rh, ...hints })); updateStore('page', 'relayHints', (rh) => ({ ...rh, ...hints }));
} }
if (content.kind === Kind.EventZapInfo) {
const zapInfo = JSON.parse(content.content) as TopZap;
if (store.topZaps[zapInfo.event_id] === undefined) { if (content?.kind === Kind.Zap) {
updateStore('topZaps', () => ({ [zapInfo.event_id]: [{ ...zapInfo }]})); const zapTag = content.tags.find(t => t[0] === 'description');
if (!zapTag) return;
const zapInfo = JSON.parse(zapTag[1] || '{}');
let amount = '0';
let bolt11Tag = content?.tags?.find(t => t[0] === 'bolt11');
if (bolt11Tag) {
try {
amount = `${parseBolt11(bolt11Tag[1]) || 0}`;
} catch (e) {
const amountTag = zapInfo.tags.find((t: string[]) => t[0] === 'amount');
amount = amountTag ? amountTag[1] : '0';
}
}
const eventId = (zapInfo.tags.find((t: string[]) => t[0] === 'e') || [])[1];
const zap: TopZap = {
id: zapInfo.id,
amount: parseInt(amount || '0'),
pubkey: zapInfo.pubkey,
message: zapInfo.content,
eventId,
};
if (store.topZaps[eventId] === undefined) {
updateStore('topZaps', () => ({ [eventId]: [{ ...zap }]}));
return; return;
} }
if (store.topZaps[zapInfo.event_id].find(i => i.zap_receipt_id === zapInfo.zap_receipt_id)) { if (store.topZaps[eventId].find(i => i.id === zap.id)) {
return; return;
} }
updateStore('topZaps', zapInfo.event_id, (zs) => [ ...zs, { ...zapInfo }]); updateStore('topZaps', eventId, (zs) => [ ...zs, { ...zap }]);
return;
} }
}; };
@ -299,6 +331,17 @@ export const ThreadProvider = (props: { children: ContextChildren }) => {
return; return;
} }
} }
if (subId === `thread_zapps_${APP_ID}`) {
if (type === 'EOSE') {
savePage(store.page);
}
if (type === 'EVENT') {
updatePage(content);
return;
}
}
}; };
const onSocketClose = (closeEvent: CloseEvent) => { const onSocketClose = (closeEvent: CloseEvent) => {

View File

@ -6,6 +6,7 @@ import LinkPreview from "../components/LinkPreview/LinkPreview";
import { addrRegex, appleMusicRegex, emojiRegex, hashtagRegex, interpunctionRegex, Kind, linebreakRegex, lnRegex, lnUnifiedRegex, mixCloudRegex, nostrNestsRegex, noteRegex, noteRegexLocal, profileRegex, profileRegexG, soundCloudRegex, spotifyRegex, tagMentionRegex, twitchRegex, urlRegex, urlRegexG, wavlakeRegex, youtubeRegex } from "../constants"; import { addrRegex, appleMusicRegex, emojiRegex, hashtagRegex, interpunctionRegex, Kind, linebreakRegex, lnRegex, lnUnifiedRegex, mixCloudRegex, nostrNestsRegex, noteRegex, noteRegexLocal, profileRegex, profileRegexG, soundCloudRegex, spotifyRegex, tagMentionRegex, twitchRegex, urlRegex, urlRegexG, wavlakeRegex, youtubeRegex } from "../constants";
import { sendMessage, subscribeTo } from "../sockets"; import { sendMessage, subscribeTo } from "../sockets";
import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalNote, SendNoteResult } from "../types/primal"; import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalNote, SendNoteResult } from "../types/primal";
import { npubToHex } from "./keys";
import { logError, logInfo, logWarning } from "./logger"; import { logError, logInfo, logWarning } from "./logger";
import { getMediaUrl as getMediaUrlDefault } from "./media"; import { getMediaUrl as getMediaUrlDefault } from "./media";
import { signEvent } from "./nostrAPI"; import { signEvent } from "./nostrAPI";
@ -541,18 +542,33 @@ export const triggerImportEvents = (events: NostrRelaySignedEvent[], subId: stri
export const getEventReactions = (eventId: string, kind: number, subid: string, offset = 0) => { export const getEventReactions = (eventId: string, kind: number, subid: string, offset = 0) => {
const event_id = eventId.startsWith('note1') ? npubToHex(eventId) : eventId;
sendMessage(JSON.stringify([ sendMessage(JSON.stringify([
"REQ", "REQ",
subid, subid,
{cache: ["event_actions", { event_id: eventId, kind, limit: 20, offset }]}, {cache: ["event_actions", { event_id, kind, limit: 20, offset }]},
])); ]));
}; };
export const getEventQuotes = (eventId: string, subid: string, offset = 0) => {
const event_id = eventId.startsWith('note1') ? npubToHex(eventId) : eventId;
export const getEventZaps = (eventId: string, subid: string, limit: number, offset = 0) => {
sendMessage(JSON.stringify([ sendMessage(JSON.stringify([
"REQ", "REQ",
subid, subid,
{cache: ["event_zaps_by_satszapped", { event_id: eventId, limit, offset }]}, {cache: ["note_mentions", { event_id, limit: 20, offset }]},
]));
};
export const getEventZaps = (eventId: string, user_pubkey: string | undefined, subid: string, limit: number, offset = 0) => {
if (!user_pubkey) return;
const event_id = eventId.startsWith('note1') ? npubToHex(eventId) : eventId;
sendMessage(JSON.stringify([
"REQ",
subid,
{cache: ["event_zaps_by_satszapped", { event_id, user_pubkey, limit, offset }]},
])); ]));
}; };