Add reaction pagination and adjust width

This commit is contained in:
Bojan Mojsilovic 2024-03-15 14:45:19 +01:00
parent 6bf10e83a1
commit ce6a3c567d
4 changed files with 168 additions and 102 deletions

View File

@ -1,6 +1,6 @@
.ReactionsModal { .ReactionsModal {
position: fixed; position: fixed;
width: 432px; width: 632px;
color: var(--text-primary); color: var(--text-primary);
background-color: var(--background-input); background-color: var(--background-input);
border-radius: 8px; border-radius: 8px;
@ -157,7 +157,7 @@
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
line-height: 16px; line-height: 16px;
max-width: 248px; max-width: 448px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -223,7 +223,7 @@
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
line-height: 16px; line-height: 16px;
max-width: 248px; max-width: 448px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -279,7 +279,7 @@
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
line-height: 16px; line-height: 16px;
max-width: 248px; max-width: 448px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;

View File

@ -3,28 +3,21 @@ import { Tabs } from '@kobalte/core';
import { A } from '@solidjs/router'; import { A } from '@solidjs/router';
import { Component, createEffect, createSignal, For, Show } from 'solid-js'; import { Component, createEffect, createSignal, For, Show } from 'solid-js';
import { createStore } from 'solid-js/store'; import { createStore } from 'solid-js/store';
import { style } from 'solid-js/web';
import { APP_ID } from '../../App'; import { APP_ID } from '../../App';
import { defaultZapOptions, Kind } from '../../constants'; import { Kind } from '../../constants';
import { useAccountContext } from '../../contexts/AccountContext';
import { ReactionStats } from '../../contexts/AppContext'; import { ReactionStats } from '../../contexts/AppContext';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { hookForDev } from '../../lib/devTools'; import { hookForDev } from '../../lib/devTools';
import { hexToNpub } from '../../lib/keys'; import { hexToNpub } from '../../lib/keys';
import { getEventReactions } from '../../lib/notes'; import { getEventReactions } from '../../lib/notes';
import { truncateNumber } from '../../lib/notifications'; import { truncateNumber } from '../../lib/notifications';
import { zapNote, zapProfile } from '../../lib/zap';
import { subscribeTo } from '../../sockets'; import { subscribeTo } from '../../sockets';
import { userName } from '../../stores/profile'; import { userName } from '../../stores/profile';
import { toastZapFail, zapCustomOption, actions as tActions, placeholders as tPlaceholders, zapCustomAmount } from '../../translations'; import { actions as tActions, placeholders as tPlaceholders } from '../../translations';
import { PrimalNote, PrimalUser, ZapOption } from '../../types/primal';
import { parseBolt11 } from '../../utils'; import { parseBolt11 } from '../../utils';
import Avatar from '../Avatar/Avatar'; import Avatar from '../Avatar/Avatar';
import ButtonPrimary from '../Buttons/ButtonPrimary';
import Loader from '../Loader/Loader'; import Loader from '../Loader/Loader';
import Modal from '../Modal/Modal'; import Modal from '../Modal/Modal';
import TextInput from '../TextInput/TextInput'; import Paginator from '../Paginator/Paginator';
import { useToastContext } from '../Toaster/Toaster';
import VerificationCheck from '../VerificationCheck/VerificationCheck'; import VerificationCheck from '../VerificationCheck/VerificationCheck';
import styles from './ReactionsModal.module.scss'; import styles from './ReactionsModal.module.scss';
@ -44,18 +37,22 @@ const ReactionsModal: Component<{
const [zapList, setZapList] = createStore<any[]>([]); const [zapList, setZapList] = createStore<any[]>([]);
const [repostList, setRepostList] = createStore<any[]>([]); const [repostList, setRepostList] = createStore<any[]>([]);
const [isFetching, setIsFetching] = createSignal(false) const [isFetching, setIsFetching] = createSignal(false);
let loadedLikes = 0;
let loadedZaps = 0;
let loadedReposts = 0;
createEffect(() => { createEffect(() => {
switch (selectedTab()) { switch (selectedTab()) {
case 'likes': case 'likes':
getLikes(); loadedLikes === 0 && getLikes();
break; break;
case 'zaps': case 'zaps':
getZaps(); loadedZaps === 0 && getZaps();
break; break;
case 'reposts': case 'reposts':
getReposts(); loadedReposts === 0 && getReposts();
break; break;
} }
}); });
@ -69,7 +66,7 @@ const ReactionsModal: Component<{
} }
}); });
const getLikes = () => { const getLikes = (offset = 0) => {
if (!props.noteId) return; if (!props.noteId) return;
const subId = `nr_l_${props.noteId}_${APP_ID}`; const subId = `nr_l_${props.noteId}_${APP_ID}`;
@ -78,7 +75,8 @@ const ReactionsModal: Component<{
const unsub = subscribeTo(subId, (type,_, content) => { const unsub = subscribeTo(subId, (type,_, content) => {
if (type === 'EOSE') { if (type === 'EOSE') {
setLikeList(() => [...users]); setLikeList((likes) => [ ...likes, ...users ]);
loadedLikes = likeList.length;
setIsFetching(() => false); setIsFetching(() => false);
unsub(); unsub();
} }
@ -102,10 +100,10 @@ const ReactionsModal: Component<{
}); });
setIsFetching(() => true); setIsFetching(() => true);
getEventReactions(props.noteId, Kind.Reaction, subId); getEventReactions(props.noteId, Kind.Reaction, subId, offset);
}; };
const getZaps = () => { const getZaps = (offset = 0) => {
if (!props.noteId) return; if (!props.noteId) return;
const subId = `nr_z_${props.noteId}_${APP_ID}`; const subId = `nr_z_${props.noteId}_${APP_ID}`;
@ -120,7 +118,8 @@ const ReactionsModal: Component<{
sender: users[zap.pubkey], sender: users[zap.pubkey],
}))); })));
setZapList(() => [ ...zapData ]); setZapList((zapItems) => [ ...zapItems, ...zapData ]);
loadedZaps = zapList.length;
setIsFetching(() => false); setIsFetching(() => false);
unsub(); unsub();
} }
@ -174,10 +173,10 @@ const ReactionsModal: Component<{
}); });
setIsFetching(() => true); setIsFetching(() => true);
getEventReactions(props.noteId, Kind.Zap, subId); getEventReactions(props.noteId, Kind.Zap, subId, offset);
}; };
const getReposts = () => { const getReposts = (offset = 0) => {
if (!props.noteId) return; if (!props.noteId) return;
const subId = `nr_r_${props.noteId}_${APP_ID}`; const subId = `nr_r_${props.noteId}_${APP_ID}`;
@ -186,7 +185,8 @@ const ReactionsModal: Component<{
const unsub = subscribeTo(subId, (type,_, content) => { const unsub = subscribeTo(subId, (type,_, content) => {
if (type === 'EOSE') { if (type === 'EOSE') {
setRepostList(() => [...users]); setRepostList((reposts) => [...reposts, ...users]);
loadedReposts = repostList.length;
setIsFetching(() => false); setIsFetching(() => false);
unsub(); unsub();
} }
@ -210,7 +210,7 @@ const ReactionsModal: Component<{
}); });
setIsFetching(() => true); setIsFetching(() => true);
getEventReactions(props.noteId, Kind.Repost, subId); getEventReactions(props.noteId, Kind.Repost, 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;
@ -259,93 +259,144 @@ const ReactionsModal: Component<{
</Tabs.List> </Tabs.List>
<Tabs.Content class={styles.tabContent} value={'likes'}> <Tabs.Content class={styles.tabContent} value={'likes'}>
<Show <For
when={!isFetching()} each={likeList}
fallback={<Loader />} fallback={
<Show when={!isFetching()}>
{intl.formatMessage(tPlaceholders.noLikeDetails)}
</Show>
}
> >
<For {admirer =>
each={likeList} <A
> href={`/p/${admirer.npub}`}
{admirer => class={styles.likeItem}
<A onClick={props.onClose}
href={`/p/${admirer.npub}`} >
class={styles.likeItem} <div class={styles.likeIcon}></div>
onClick={props.onClose} <Avatar src={admirer.picture} size="vs" />
> <div class={styles.userName}>
<div class={styles.likeIcon}></div> <div class={styles.name}>
<Avatar src={admirer.picture} size="vs" /> {userName(admirer)}
<div class={styles.userName}>
<div class={styles.name}>
{userName(admirer)}
</div>
<VerificationCheck user={admirer} />
</div> </div>
</A> <VerificationCheck user={admirer} />
} </div>
</For> </A>
}
</For>
<Show when={likeList.length < props.stats.likes}>
<Paginator
loadNextPage={() => {
const len = likeList.length;
if (len === 0) return;
getLikes(len);
}}
isSmall={true}
/>
</Show>
<Show
when={isFetching()}
>
<Loader />
</Show> </Show>
</Tabs.Content> </Tabs.Content>
<Tabs.Content class={styles.tabContent} value={'zaps'}> <Tabs.Content class={styles.tabContent} value={'zaps'}>
<Show <For
when={!isFetching()} each={zapList}
fallback={<Loader />} fallback={
<Show when={!isFetching()}>
{intl.formatMessage(tPlaceholders.noZapDetails)}
</Show>
}
> >
<For {zap =>
each={zapList} <A
> href={`/p/${zap.npub}`}
{zap => class={styles.zapItem}
<A onClick={props.onClose}
href={`/p/${zap.npub}`} >
class={styles.zapItem} <div class={styles.zapAmount}>
onClick={props.onClose} <div class={styles.zapIcon}></div>
> <div class={styles.amount}>{truncateNumber(zap.amount)}</div>
<div class={styles.zapAmount}> </div>
<div class={styles.zapIcon}></div> <Avatar src={zap.sender?.picture} size="vs" />
<div class={styles.amount}>{truncateNumber(zap.amount)}</div> <div class={styles.zapInfo}>
</div> <div class={styles.userName}>
<Avatar src={zap.sender?.picture} size="vs" /> <div class={styles.name}>
<div class={styles.zapInfo}> {userName(zap.sender)}
<div class={styles.userName}>
<div class={styles.name}>
{userName(zap.sender)}
</div>
<VerificationCheck user={zap} />
</div>
<div class={styles.zapMessage}>
{zap.message}
</div> </div>
<VerificationCheck user={zap} />
</div> </div>
</A> <div class={styles.zapMessage}>
} {zap.message}
</For> </div>
</div>
</A>
}
</For>
<Show when={zapList.length < props.stats.zaps}>
<Paginator
loadNextPage={() => {
const len = zapList.length;
if (len === 0) return;
getZaps(len);
}}
isSmall={true}
/>
</Show>
<Show
when={isFetching()}
>
<Loader />
</Show> </Show>
</Tabs.Content> </Tabs.Content>
<Tabs.Content class={styles.tabContent} value={'reposts'}> <Tabs.Content class={styles.tabContent} value={'reposts'}>
<Show <For
when={!isFetching()} each={repostList}
fallback={<Loader />} fallback={
<Show when={!isFetching()}>
{intl.formatMessage(tPlaceholders.noRepostDetails)}
</Show>
}
> >
<For {reposter =>
each={repostList} <A
> href={`/p/${reposter.npub}`}
{reposter => class={styles.repostItem}
<A onClick={props.onClose}
href={`/p/${reposter.npub}`} >
class={styles.repostItem} <div class={styles.repostIcon}></div>
onClick={props.onClose} <Avatar src={reposter.picture} size="vs" />
> <div class={styles.userName}>
<div class={styles.repostIcon}></div> <div class={styles.name}>
<Avatar src={reposter.picture} size="vs" /> {userName(reposter)}
<div class={styles.userName}>
<div class={styles.name}>
{userName(reposter)}
</div>
<VerificationCheck user={reposter} />
</div> </div>
</A> <VerificationCheck user={reposter} />
} </div>
</For> </A>
}
</For>
<Show when={repostList.length < props.stats.reposts}>
<Paginator
loadNextPage={() => {
const len = repostList.length;
if (len === 0) return;
getReposts(len);
}}
isSmall={true}
/>
</Show>
<Show
when={isFetching()}
>
<Loader />
</Show> </Show>
</Tabs.Content> </Tabs.Content>
<Tabs.Content class={styles.tabContent} value={'quotes'}> <Tabs.Content class={styles.tabContent} value={'quotes'}>

View File

@ -470,10 +470,10 @@ export const triggerImportEvents = (events: NostrRelaySignedEvent[], subId: stri
}; };
export const getEventReactions = (eventId: string, kind: number, subid: string) => { export const getEventReactions = (eventId: string, kind: number, subid: string, offset = 0) => {
sendMessage(JSON.stringify([ sendMessage(JSON.stringify([
"REQ", "REQ",
subid, subid,
{cache: ["event_actions", { event_id: eventId, kind, limit: 100 }]}, {cache: ["event_actions", { event_id: eventId, kind, limit: 20, offset }]},
])); ]));
}; };

View File

@ -982,6 +982,21 @@ export const notifications = {
}; };
export const placeholders = { export const placeholders = {
noLikeDetails: {
id: 'placeholders.noLikeDetails',
defaultMessage: 'No details for likes found',
description: 'Placeholder when there are no like details in reactions modal',
},
noZapDetails: {
id: 'placeholders.noZapDetails',
defaultMessage: 'No details for zaps found',
description: 'Placeholder when there are no zap details in reactions modal',
},
noRepostDetails: {
id: 'placeholders.noRepostDetails',
defaultMessage: 'No details for reposts found',
description: 'Placeholder when there are no repost details in reactions modal',
},
addComment: { addComment: {
id: 'placeholders.addComment', id: 'placeholders.addComment',
defaultMessage: 'Add a comment...', defaultMessage: 'Add a comment...',