mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-10-01 17:31:13 +00:00
Add reactions modal
This commit is contained in:
parent
8ab916e429
commit
109e7f40c2
@ -16,6 +16,8 @@ import Branding from '../Branding/Branding';
|
||||
import BannerIOS, { isIOS } from '../BannerIOS/BannerIOS';
|
||||
import ZapAnimation from '../ZapAnimation/ZapAnimation';
|
||||
import Landing from '../../pages/Landing';
|
||||
import ReactionsModal from '../ReactionsModal/ReactionsModal';
|
||||
import { useAppContext } from '../../contexts/AppContext';
|
||||
|
||||
export const [isHome, setIsHome] = createSignal(false);
|
||||
|
||||
@ -26,6 +28,7 @@ const Layout: Component = () => {
|
||||
const profile = useProfileContext();
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
const app = useAppContext();
|
||||
|
||||
let container: HTMLDivElement | undefined;
|
||||
|
||||
@ -145,6 +148,12 @@ const Layout: Component = () => {
|
||||
<div>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
<ReactionsModal
|
||||
noteId={app?.showReactionsModal}
|
||||
stats={app?.reactionStats}
|
||||
onClose={() => app?.actions.closeReactionModal()}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
@ -26,6 +26,7 @@ import { getScreenCordinates } from '../../utils';
|
||||
const NoteContextMenu: Component<{
|
||||
note: PrimalNote,
|
||||
openCustomZap?: () => void;
|
||||
openReactions?: () => void,
|
||||
id?: string,
|
||||
}> = (props) => {
|
||||
const account = useAccountContext();
|
||||
@ -123,6 +124,14 @@ const NoteContextMenu: Component<{
|
||||
}
|
||||
|
||||
const noteContextForEveryone: MenuItem[] = [
|
||||
{
|
||||
label: intl.formatMessage(tActions.noteContext.reactions),
|
||||
action: () => {
|
||||
props.openReactions && props.openReactions();
|
||||
setContext(false);
|
||||
},
|
||||
icon: 'heart',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(tActions.noteContext.zap),
|
||||
action: () => {
|
||||
@ -192,7 +201,7 @@ const NoteContextMenu: Component<{
|
||||
|
||||
const determineOrient = () => {
|
||||
const coor = getScreenCordinates(context);
|
||||
const height = 380;
|
||||
const height = 440;
|
||||
return (coor.y || 0) + height < window.innerHeight + window.scrollY ? 'down' : 'up';
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,8 @@ import { hookForDev } from '../../../lib/devTools';
|
||||
import NoteContextMenu from '../NoteContextMenu';
|
||||
import { getScreenCordinates } from '../../../utils';
|
||||
import ZapAnimation from '../../ZapAnimation/ZapAnimation';
|
||||
import ReactionsModal from '../../ReactionsModal/ReactionsModal';
|
||||
import { useAppContext } from '../../../contexts/AppContext';
|
||||
|
||||
const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> = (props) => {
|
||||
|
||||
@ -26,6 +28,7 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
const toast = useToastContext();
|
||||
const intl = useIntl();
|
||||
const settings = useSettingsContext();
|
||||
const app = useAppContext();
|
||||
|
||||
let medZapAnimation: HTMLElement | undefined;
|
||||
|
||||
@ -37,6 +40,7 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
const [likes, setLikes] = createSignal(props.note.post.likes);
|
||||
const [reposts, setReposts] = createSignal(props.note.post.reposts);
|
||||
const [replies, setReplies] = createSignal(props.note.post.replies);
|
||||
const [zapCount, setZapCount] = createSignal(props.note.post.zaps);
|
||||
const [zaps, setZaps] = createSignal(props.note.post.satszapped);
|
||||
|
||||
const [isRepostMenuVisible, setIsRepostMenuVisible] = createSignal(false);
|
||||
@ -313,6 +317,7 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
createEffect(() => {
|
||||
|
||||
if (zappedNow()) {
|
||||
setZapCount(c => c + 1);
|
||||
setZaps((z) => z + zappedAmount());
|
||||
setZapped(true);
|
||||
setZappedNow(false);
|
||||
@ -407,6 +412,14 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
openCustomZap={() => {
|
||||
setIsCustomZap(true);
|
||||
}}
|
||||
openReactions={() => {
|
||||
app?.actions.openReactionModal(props.note.post.id, {
|
||||
likes: likes(),
|
||||
zaps: zapCount(),
|
||||
reposts: reposts(),
|
||||
quotes: 0,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -447,7 +460,6 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
setZapped(props.note.post.noteActions.zapped);
|
||||
}}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
288
src/components/ReactionsModal/ReactionsModal.module.scss
Normal file
288
src/components/ReactionsModal/ReactionsModal.module.scss
Normal file
@ -0,0 +1,288 @@
|
||||
.ReactionsModal {
|
||||
position: fixed;
|
||||
width: 432px;
|
||||
color: var(--text-primary);
|
||||
background-color: var(--background-input);
|
||||
border-radius: 8px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
.caption {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
margin: 0px 0px;
|
||||
background-color: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/close.svg) no-repeat center;
|
||||
mask: url(../../assets/icons/close.svg) no-repeat center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-secondary);
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
|
||||
|
||||
.tabs {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding-inline: 8px;
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
padding-top: 22px;
|
||||
border-bottom: 1px solid var(--devider);
|
||||
border-top: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-inline: 14px;
|
||||
padding-block: 2px;
|
||||
border: none;
|
||||
background: none;
|
||||
width: fit-content;
|
||||
height: 28px;
|
||||
margin: 0;
|
||||
margin-bottom: 12px;
|
||||
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tabIndicator {
|
||||
position: absolute;
|
||||
height: 4px;
|
||||
top: 54px;
|
||||
left: 0;
|
||||
border-radius: 2px 2px 0px 0px;
|
||||
background: var(--accent);
|
||||
transition: all 250ms;
|
||||
}
|
||||
|
||||
.tabContent {
|
||||
height: 440px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity:0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity:1;
|
||||
}
|
||||
|
||||
}
|
||||
animation: fadeIn 1s;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.likeItem {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 62px;
|
||||
gap: 12px;
|
||||
padding-inline: 10px;
|
||||
border-radius: var(--border-radius-small);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--subtile-devider);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.likeIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
background: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/heart.svg) no-repeat 0px 0 / 18px 18px;
|
||||
mask: url(../../assets/icons/heart.svg) no-repeat 0px 0 / 18px 18px;
|
||||
}
|
||||
|
||||
.userName {
|
||||
display: flex;
|
||||
justify-items: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
max-width: 248px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zapItem {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 62px;
|
||||
gap: 12px;
|
||||
border-radius: var(--border-radius-small);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--subtile-devider);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.zapAmount {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 40px;
|
||||
height: 42px;
|
||||
|
||||
|
||||
.zapIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
background: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/feed_zap.svg) no-repeat 0px 0 / 18px 18px;
|
||||
mask: url(../../assets/icons/feed_zap.svg) no-repeat 0px 0 / 18px 18px;
|
||||
}
|
||||
.amount {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.zapInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
height: 42px;
|
||||
padding-block: 4px;
|
||||
|
||||
.userName {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
max-width: 248px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.zapMessage {
|
||||
color: var(--text-secondary);
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
max-width: 248px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.repostItem {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 62px;
|
||||
gap: 12px;
|
||||
padding-inline: 10px;
|
||||
border-radius: var(--border-radius-small);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--subtile-devider);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.repostIcon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
background: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/feed_repost.svg) no-repeat 0px 0 / 18px 18px;
|
||||
mask: url(../../assets/icons/feed_repost.svg) no-repeat 0px 0 / 18px 18px;
|
||||
}
|
||||
|
||||
.userName {
|
||||
display: flex;
|
||||
justify-items: flex-start;
|
||||
align-items: center;
|
||||
|
||||
.name {
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
max-width: 248px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
361
src/components/ReactionsModal/ReactionsModal.tsx
Normal file
361
src/components/ReactionsModal/ReactionsModal.tsx
Normal file
@ -0,0 +1,361 @@
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { Tabs } from '@kobalte/core';
|
||||
import { A } from '@solidjs/router';
|
||||
import { Component, createEffect, createSignal, For, Show } from 'solid-js';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { style } from 'solid-js/web';
|
||||
import { APP_ID } from '../../App';
|
||||
import { defaultZapOptions, Kind } from '../../constants';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import { ReactionStats } from '../../contexts/AppContext';
|
||||
import { useSettingsContext } from '../../contexts/SettingsContext';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import { hexToNpub } from '../../lib/keys';
|
||||
import { getEventReactions } from '../../lib/notes';
|
||||
import { truncateNumber } from '../../lib/notifications';
|
||||
import { zapNote, zapProfile } from '../../lib/zap';
|
||||
import { subscribeTo } from '../../sockets';
|
||||
import { userName } from '../../stores/profile';
|
||||
import { toastZapFail, zapCustomOption, actions as tActions, placeholders as tPlaceholders, zapCustomAmount } from '../../translations';
|
||||
import { PrimalNote, PrimalUser, ZapOption } from '../../types/primal';
|
||||
import { parseBolt11 } from '../../utils';
|
||||
import Avatar from '../Avatar/Avatar';
|
||||
import ButtonPrimary from '../Buttons/ButtonPrimary';
|
||||
import Loader from '../Loader/Loader';
|
||||
import Modal from '../Modal/Modal';
|
||||
import TextInput from '../TextInput/TextInput';
|
||||
import { useToastContext } from '../Toaster/Toaster';
|
||||
import VerificationCheck from '../VerificationCheck/VerificationCheck';
|
||||
|
||||
import styles from './ReactionsModal.module.scss';
|
||||
|
||||
const ReactionsModal: Component<{
|
||||
id?: string,
|
||||
noteId: string | undefined,
|
||||
stats: ReactionStats;
|
||||
onClose?: () => void,
|
||||
}> = (props) => {
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const [selectedTab, setSelectedTab] = createSignal('likes');
|
||||
|
||||
const [likeList, setLikeList] = createStore<any[]>([]);
|
||||
const [zapList, setZapList] = createStore<any[]>([]);
|
||||
const [repostList, setRepostList] = createStore<any[]>([]);
|
||||
|
||||
const [isFetching, setIsFetching] = createSignal(false)
|
||||
|
||||
createEffect(() => {
|
||||
switch (selectedTab()) {
|
||||
case 'likes':
|
||||
getLikes();
|
||||
break;
|
||||
case 'zaps':
|
||||
getZaps();
|
||||
break;
|
||||
case 'reposts':
|
||||
getReposts();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.noteId) {
|
||||
setLikeList(() => []);
|
||||
setZapList(() => []);
|
||||
setRepostList(() => []);
|
||||
setSelectedTab(() => 'likes');
|
||||
}
|
||||
});
|
||||
|
||||
const getLikes = () => {
|
||||
if (!props.noteId) return;
|
||||
|
||||
const subId = `nr_l_${props.noteId}_${APP_ID}`;
|
||||
|
||||
const users: any[] = [];
|
||||
|
||||
const unsub = subscribeTo(subId, (type,_, content) => {
|
||||
if (type === 'EOSE') {
|
||||
setLikeList(() => [...users]);
|
||||
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);
|
||||
getEventReactions(props.noteId, Kind.Reaction, subId);
|
||||
};
|
||||
|
||||
const getZaps = () => {
|
||||
if (!props.noteId) return;
|
||||
|
||||
const subId = `nr_z_${props.noteId}_${APP_ID}`;
|
||||
|
||||
const users: Record<string, any> = {};
|
||||
const zaps: any[] = [];
|
||||
|
||||
const unsub = subscribeTo(subId, (type,_, content) => {
|
||||
if (type === 'EOSE') {
|
||||
const zapData = zaps.map((zap => ({
|
||||
...zap,
|
||||
sender: users[zap.pubkey],
|
||||
})));
|
||||
|
||||
setZapList(() => [ ...zapData ]);
|
||||
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[content.pubkey] = { ...user };
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (content?.kind === Kind.Zap) {
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
zaps.push({
|
||||
amount,
|
||||
pubkey: zapInfo.pubkey,
|
||||
message: zapInfo.content,
|
||||
})
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setIsFetching(() => true);
|
||||
getEventReactions(props.noteId, Kind.Zap, subId);
|
||||
};
|
||||
|
||||
const getReposts = () => {
|
||||
if (!props.noteId) return;
|
||||
|
||||
const subId = `nr_r_${props.noteId}_${APP_ID}`;
|
||||
|
||||
const users: any[] = [];
|
||||
|
||||
const unsub = subscribeTo(subId, (type,_, content) => {
|
||||
if (type === 'EOSE') {
|
||||
setRepostList(() => [...users]);
|
||||
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);
|
||||
getEventReactions(props.noteId, Kind.Repost, subId);
|
||||
};
|
||||
|
||||
const totalCount = () => props.stats.likes + props.stats.quotes + props.stats.reposts + props.stats.zaps;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.noteId !== undefined}
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<div id={props.id} class={styles.ReactionsModal}>
|
||||
<div class={styles.header}>
|
||||
<div class={styles.title}>
|
||||
<div class={styles.caption}>
|
||||
{intl.formatMessage(tActions.reactions, { count: totalCount() })}
|
||||
</div>
|
||||
</div>
|
||||
<button class={styles.close} onClick={props.onClose}>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class={styles.description}>
|
||||
<Tabs.Root value={selectedTab()} onChange={setSelectedTab}>
|
||||
<Tabs.List class={styles.tabs}>
|
||||
<Show when={props.stats.likes > 0}>
|
||||
<Tabs.Trigger class={styles.tab} value={'likes'} >
|
||||
Likes ({props.stats.likes})
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<Show when={props.stats.zaps > 0}>
|
||||
<Tabs.Trigger class={styles.tab} value={'zaps'} >
|
||||
Zaps ({props.stats.zaps})
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<Show when={props.stats.reposts > 0}>
|
||||
<Tabs.Trigger class={styles.tab} value={'reposts'} >
|
||||
Reposts ({props.stats.reposts})
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<Show when={props.stats.quotes > 0}>
|
||||
<Tabs.Trigger class={styles.tab} value={'quotes'} >
|
||||
Quotes ({props.stats.quotes})
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
|
||||
<Tabs.Indicator class={styles.tabIndicator} />
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content class={styles.tabContent} value={'likes'}>
|
||||
<Show
|
||||
when={!isFetching()}
|
||||
fallback={<Loader />}
|
||||
>
|
||||
<For
|
||||
each={likeList}
|
||||
>
|
||||
{admirer =>
|
||||
<A
|
||||
href={`/p/${admirer.npub}`}
|
||||
class={styles.likeItem}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
<div class={styles.likeIcon}></div>
|
||||
<Avatar src={admirer.picture} size="vs" />
|
||||
<div class={styles.userName}>
|
||||
<div class={styles.name}>
|
||||
{userName(admirer)}
|
||||
</div>
|
||||
<VerificationCheck user={admirer} />
|
||||
</div>
|
||||
</A>
|
||||
}
|
||||
</For>
|
||||
</Show>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content class={styles.tabContent} value={'zaps'}>
|
||||
<Show
|
||||
when={!isFetching()}
|
||||
fallback={<Loader />}
|
||||
>
|
||||
<For
|
||||
each={zapList}
|
||||
>
|
||||
{zap =>
|
||||
<A
|
||||
href={`/p/${zap.npub}`}
|
||||
class={styles.zapItem}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
<div class={styles.zapAmount}>
|
||||
<div class={styles.zapIcon}></div>
|
||||
<div class={styles.amount}>{truncateNumber(zap.amount)}</div>
|
||||
</div>
|
||||
<Avatar src={zap.sender?.picture} size="vs" />
|
||||
<div class={styles.zapInfo}>
|
||||
<div class={styles.userName}>
|
||||
<div class={styles.name}>
|
||||
{userName(zap.sender)}
|
||||
</div>
|
||||
<VerificationCheck user={zap} />
|
||||
</div>
|
||||
<div class={styles.zapMessage}>
|
||||
{zap.message}
|
||||
</div>
|
||||
</div>
|
||||
</A>
|
||||
}
|
||||
</For>
|
||||
</Show>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content class={styles.tabContent} value={'reposts'}>
|
||||
<Show
|
||||
when={!isFetching()}
|
||||
fallback={<Loader />}
|
||||
>
|
||||
<For
|
||||
each={repostList}
|
||||
>
|
||||
{reposter =>
|
||||
<A
|
||||
href={`/p/${reposter.npub}`}
|
||||
class={styles.repostItem}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
<div class={styles.repostIcon}></div>
|
||||
<Avatar src={reposter.picture} size="vs" />
|
||||
<div class={styles.userName}>
|
||||
<div class={styles.name}>
|
||||
{userName(reposter)}
|
||||
</div>
|
||||
<VerificationCheck user={reposter} />
|
||||
</div>
|
||||
</A>
|
||||
}
|
||||
</For>
|
||||
</Show>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content class={styles.tabContent} value={'quotes'}>
|
||||
All the quotes
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(ReactionsModal);
|
@ -8,14 +8,34 @@ import {
|
||||
useContext
|
||||
} from "solid-js";
|
||||
|
||||
export type ReactionStats = {
|
||||
likes: number,
|
||||
zaps: number,
|
||||
reposts: number,
|
||||
quotes: number,
|
||||
};
|
||||
|
||||
export type AppContextStore = {
|
||||
isInactive: boolean,
|
||||
appState: 'sleep' | 'waking' | 'woke',
|
||||
showReactionsModal: string | undefined,
|
||||
reactionStats: ReactionStats,
|
||||
actions: {
|
||||
openReactionModal: (noteId: string, stats: ReactionStats) => void,
|
||||
closeReactionModal: () => void,
|
||||
},
|
||||
}
|
||||
|
||||
const initialData: AppContextStore = {
|
||||
const initialData: Omit<AppContextStore, 'actions'> = {
|
||||
isInactive: false,
|
||||
appState: 'woke',
|
||||
showReactionsModal: undefined,
|
||||
reactionStats: {
|
||||
likes: 0,
|
||||
zaps: 0,
|
||||
reposts: 0,
|
||||
quotes: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const AppContext = createContext<AppContextStore>();
|
||||
@ -36,6 +56,21 @@ export const AppProvider = (props: { children: JSXElement }) => {
|
||||
}, 3 * 60_000);
|
||||
};
|
||||
|
||||
const openReactionModal = (noteId: string, stats: ReactionStats) => {
|
||||
updateStore('reactionStats', () => ({ ...stats }));
|
||||
updateStore('showReactionsModal', () => noteId);
|
||||
};
|
||||
|
||||
const closeReactionModal = () => {
|
||||
updateStore('reactionStats', () => ({
|
||||
likes: 0,
|
||||
zaps: 0,
|
||||
reposts: 0,
|
||||
quotes: 0,
|
||||
}));
|
||||
updateStore('showReactionsModal', () => undefined);
|
||||
};
|
||||
|
||||
// EFFECTS --------------------------------------
|
||||
|
||||
onMount(() => {
|
||||
@ -73,6 +108,10 @@ export const AppProvider = (props: { children: JSXElement }) => {
|
||||
|
||||
const [store, updateStore] = createStore<AppContextStore>({
|
||||
...initialData,
|
||||
actions: {
|
||||
openReactionModal,
|
||||
closeReactionModal,
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -456,7 +456,6 @@ export const sendEvent = async (event: NostrEvent, relays: Relay[], relaySetting
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const triggerImportEvents = (events: NostrRelaySignedEvent[], subId: string, then?: () => void) => {
|
||||
|
||||
const unsub = subscribeTo(subId, (type) => {
|
||||
@ -469,3 +468,12 @@ export const triggerImportEvents = (events: NostrRelaySignedEvent[], subId: stri
|
||||
|
||||
importEvents(events, subId);
|
||||
};
|
||||
|
||||
|
||||
export const getEventReactions = (eventId: string, kind: number, subid: string) => {
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ["event_actions", { event_id: eventId, kind, limit: 100 }]},
|
||||
]));
|
||||
};
|
||||
|
@ -360,6 +360,11 @@ export const actions = {
|
||||
},
|
||||
},
|
||||
noteContext: {
|
||||
reactions: {
|
||||
id: 'actions.noteContext.reactions',
|
||||
defaultMessage: 'Reactions',
|
||||
description: 'Label for note reactions from context menu',
|
||||
},
|
||||
zap: {
|
||||
id: 'actions.noteContext.zapNote',
|
||||
defaultMessage: 'Custom Zap',
|
||||
@ -421,6 +426,11 @@ export const actions = {
|
||||
defaultMessage: 'Zap',
|
||||
description: 'Label for zap',
|
||||
},
|
||||
reactions: {
|
||||
id: 'actions.reactions',
|
||||
defaultMessage: 'Reactions ({count})',
|
||||
description: 'Label for zap',
|
||||
},
|
||||
};
|
||||
|
||||
export const branding = {
|
||||
|
Loading…
Reference in New Issue
Block a user