mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-10-01 17:31:13 +00:00
Add top zaps to thread's primary note
This commit is contained in:
parent
87d1241e85
commit
c7e066121b
@ -34,6 +34,23 @@
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.microAvatar {
|
||||
@include avatar;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
||||
.missingBack {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.iconBackground {
|
||||
@include iconBackground;
|
||||
bottom: -6px;
|
||||
right: -6px;
|
||||
}
|
||||
}
|
||||
|
||||
.xxsAvatar {
|
||||
@include avatar;
|
||||
width: 28px;
|
||||
@ -217,6 +234,13 @@
|
||||
mask-size: contain;
|
||||
}
|
||||
|
||||
.microMissing {
|
||||
@include missing;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.xxsMissing {
|
||||
@include missing;
|
||||
width: 28px;
|
||||
|
@ -11,7 +11,7 @@ import styles from './Avatar.module.scss';
|
||||
|
||||
const Avatar: Component<{
|
||||
src?: string | undefined,
|
||||
size?: "xxs" | "xss" | "xs" | "vvs" | "vs" | "sm" | "md" | "lg" | "xl" | "xxl",
|
||||
size?: "micro" | "xxs" | "xss" | "xs" | "vvs" | "vs" | "sm" | "md" | "lg" | "xl" | "xxl",
|
||||
user?: PrimalUser,
|
||||
highlightBorder?: boolean,
|
||||
id?: string,
|
||||
@ -26,6 +26,7 @@ const Avatar: Component<{
|
||||
const selectedSize = props.size || 'sm';
|
||||
|
||||
const avatarClass = {
|
||||
micro: styles.microAvatar,
|
||||
xxs: styles.xxsAvatar,
|
||||
xss: styles.xssAvatar,
|
||||
xs: styles.xsAvatar,
|
||||
@ -39,6 +40,7 @@ const Avatar: Component<{
|
||||
};
|
||||
|
||||
const missingClass = {
|
||||
micro: styles.microAvatar,
|
||||
xxs: styles.xxsMissing,
|
||||
xss: styles.xssMissing,
|
||||
xs: styles.xsMissing,
|
||||
@ -77,6 +79,7 @@ const Avatar: Component<{
|
||||
let size: MediaSize = 'm';
|
||||
|
||||
switch (selectedSize) {
|
||||
case 'micro':
|
||||
case 'xxs':
|
||||
case 'xss':
|
||||
case 'xs':
|
||||
|
@ -328,6 +328,130 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zapHighlights {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
|
||||
.firstZap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-right: 10px;
|
||||
border-radius: 12px;
|
||||
background: var(--devider);
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
text-decoration: none;
|
||||
|
||||
.amount {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-secondary-2);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--subtile-devider);
|
||||
}
|
||||
}
|
||||
|
||||
.topZaps {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
|
||||
.zapList {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
gap: 6px;
|
||||
max-width: calc(100% - 30px);
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
&::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none !important; /* IE and Edge */
|
||||
scrollbar-width: none !important; /* Firefox */
|
||||
|
||||
.topZap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-right: 10px;
|
||||
border-radius: 12px;
|
||||
background: var(--devider);
|
||||
width: fit-content;
|
||||
text-decoration: none;
|
||||
|
||||
.amount {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-secondary-2);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--subtile-devider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moreZaps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--devider);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
.contextIcon {
|
||||
width: 16px;
|
||||
height: 14px;
|
||||
background-color: var(--text-secondary-2);
|
||||
-webkit-mask: url(../../assets/icons/context.svg) no-repeat 0 / 100%;
|
||||
mask: url(../../assets/icons/context.svg) no-repeat 0 / 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.contextIcon {
|
||||
background-color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { A } from '@solidjs/router';
|
||||
import { batch, Component, Match, Show, Switch } from 'solid-js';
|
||||
import { batch, Component, For, Match, Show, Switch } from 'solid-js';
|
||||
import { PrimalNote, ZapOption } from '../../types/primal';
|
||||
import ParsedNote from '../ParsedNote/ParsedNote';
|
||||
import NoteFooter from './NoteFooter/NoteFooter';
|
||||
|
||||
import styles from './Note.module.scss';
|
||||
import { useThreadContext } from '../../contexts/ThreadContext';
|
||||
import { TopZap, useThreadContext } from '../../contexts/ThreadContext';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import Avatar from '../Avatar/Avatar';
|
||||
@ -13,10 +13,11 @@ import NoteAuthorInfo from './NoteAuthorInfo';
|
||||
import NoteRepostHeader from './NoteRepostHeader';
|
||||
import NoteReplyToHeader from './NoteReplyToHeader';
|
||||
import NoteHeader from './NoteHeader/NoteHeader';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { createStore, unwrap } from 'solid-js/store';
|
||||
import { CustomZapInfo, useAppContext } from '../../contexts/AppContext';
|
||||
import NoteContextTrigger from './NoteContextTrigger';
|
||||
import { date, longDate, veryLongDate } from '../../lib/dates';
|
||||
import { hexToNpub } from '../../lib/keys';
|
||||
|
||||
export type NoteFooterState = {
|
||||
likes: number,
|
||||
@ -129,12 +130,13 @@ const Note: Component<{
|
||||
onCancel: onCancelZap,
|
||||
};
|
||||
|
||||
const openReationModal = () => {
|
||||
const openReactionModal = (openOn = 'likes') => {
|
||||
app?.actions.openReactionModal(props.note.post.id, {
|
||||
likes: footerState.likes,
|
||||
zaps: footerState.zapCount,
|
||||
reposts: footerState.reposts,
|
||||
quotes: 0,
|
||||
openOn,
|
||||
});
|
||||
};
|
||||
|
||||
@ -145,7 +147,7 @@ const Note: Component<{
|
||||
() => {
|
||||
app?.actions.openCustomZapModal(customZapInfo);
|
||||
},
|
||||
openReationModal,
|
||||
openReactionModal,
|
||||
);
|
||||
}
|
||||
|
||||
@ -155,6 +157,16 @@ const Note: Component<{
|
||||
return (likes || 0) + (zaps || 0) + (reposts || 0);
|
||||
};
|
||||
|
||||
const firstZap = () => (threadContext?.topZaps[props.note.post.id] || [])[0];
|
||||
|
||||
const topZaps = () => {
|
||||
return (threadContext?.topZaps[props.note.post.id] || []).slice(1, 8);
|
||||
}
|
||||
|
||||
const zapSender = (zap: TopZap) => {
|
||||
return threadContext?.users.find(u => u.pubkey === zap.sender);
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={noteType() === 'notification'}>
|
||||
@ -209,6 +221,43 @@ const Note: Component<{
|
||||
<ParsedNote note={props.note} width={Math.min(574, window.innerWidth)} />
|
||||
</div>
|
||||
|
||||
<div class={styles.zapHighlights}>
|
||||
<Show when={firstZap()}>
|
||||
<A class={styles.firstZap} href={`/p/${hexToNpub(firstZap().sender)}`}>
|
||||
<Avatar user={zapSender(firstZap())} size="micro" />
|
||||
<div class={styles.amount}>
|
||||
{firstZap().amount_sats.toLocaleString()}
|
||||
</div>
|
||||
<div class={styles.description}>
|
||||
</div>
|
||||
</A>
|
||||
</Show>
|
||||
<div class={styles.topZaps}>
|
||||
<div class={styles.zapList}>
|
||||
<For each={topZaps()}>
|
||||
{zap => (
|
||||
<A class={styles.topZap} href={`/p/${hexToNpub(zap.sender)}`}>
|
||||
<Avatar user={zapSender(zap)} size="micro" />
|
||||
<div class={styles.amount}>
|
||||
{zap.amount_sats.toLocaleString()}
|
||||
</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Show when={topZaps().length > 0}>
|
||||
<button
|
||||
class={styles.moreZaps}
|
||||
onClick={() => openReactionModal('zaps')}
|
||||
>
|
||||
<div class={styles.contextIcon}></div>
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class={styles.time}
|
||||
title={date(props.note.post?.created_at).date.toLocaleString()}
|
||||
@ -218,7 +267,7 @@ const Note: Component<{
|
||||
</span>
|
||||
<button
|
||||
class={styles.reactSummary}
|
||||
onClick={openReationModal}
|
||||
onClick={() => openReactionModal()}
|
||||
>
|
||||
<span class={styles.number}>{reactionSum()}</span> Reactions
|
||||
</button>
|
||||
|
@ -1,7 +1,7 @@
|
||||
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 { Component, createEffect, createSignal, For, onMount, Show } from 'solid-js';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { APP_ID } from '../../App';
|
||||
import { Kind } from '../../constants';
|
||||
@ -43,6 +43,12 @@ const ReactionsModal: Component<{
|
||||
let loadedZaps = 0;
|
||||
let loadedReposts = 0;
|
||||
|
||||
createEffect(() => {
|
||||
if (props.noteId && props.stats.openOn) {
|
||||
setSelectedTab(props.stats.openOn)
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
switch (selectedTab()) {
|
||||
case 'likes':
|
||||
|
@ -133,6 +133,7 @@ export enum Kind {
|
||||
Releases = 10_000_124,
|
||||
ImportResponse = 10_000_127,
|
||||
LinkMetadata = 10_000_128,
|
||||
EventZapInfo = 10_000_129,
|
||||
FilteringReason = 10_000_131,
|
||||
UserFollowerCounts = 10_000_133,
|
||||
SuggestedUsersByCategory = 10_000_134,
|
||||
|
@ -16,6 +16,7 @@ export type ReactionStats = {
|
||||
zaps: number,
|
||||
reposts: number,
|
||||
quotes: number,
|
||||
openOn: string,
|
||||
};
|
||||
|
||||
export type CustomZapInfo = {
|
||||
@ -91,6 +92,7 @@ const initialData: Omit<AppContextStore, 'actions'> = {
|
||||
zaps: 0,
|
||||
reposts: 0,
|
||||
quotes: 0,
|
||||
openOn: 'likes',
|
||||
},
|
||||
showCustomZapModal: false,
|
||||
customZap: undefined,
|
||||
|
@ -40,6 +40,15 @@ import { APP_ID } from "../App";
|
||||
import { useAccountContext } from "./AccountContext";
|
||||
import { setLinkPreviews } from "../lib/notes";
|
||||
|
||||
export type TopZap = {
|
||||
amount_sats: number,
|
||||
created_at: number,
|
||||
event_id: string,
|
||||
receiver: string,
|
||||
sender: string,
|
||||
zap_receipt_id: string,
|
||||
}
|
||||
|
||||
export type ThreadContextStore = {
|
||||
primaryNote: PrimalNote | undefined,
|
||||
noteId: string;
|
||||
@ -49,6 +58,7 @@ export type ThreadContextStore = {
|
||||
page: FeedPage,
|
||||
reposts: Record<string, string> | undefined,
|
||||
lastNote: PrimalNote | undefined,
|
||||
topZaps: Record<string, TopZap[]>,
|
||||
actions: {
|
||||
saveNotes: (newNotes: PrimalNote[]) => void,
|
||||
clearNotes: () => void,
|
||||
@ -78,6 +88,7 @@ export const initialData = {
|
||||
},
|
||||
reposts: {},
|
||||
lastNote: undefined,
|
||||
topZaps: {},
|
||||
};
|
||||
|
||||
|
||||
@ -211,6 +222,22 @@ export const ThreadProvider = (props: { children: ContextChildren }) => {
|
||||
const hints = JSON.parse(content.content);
|
||||
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) {
|
||||
updateStore('topZaps', () => ({ [zapInfo.event_id]: [{ ...zapInfo }]}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (store.topZaps[zapInfo.event_id].find(i => i.zap_receipt_id === zapInfo.zap_receipt_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStore('topZaps', zapInfo.event_id, (zs) => [ ...zs, { ...zapInfo }]);
|
||||
}
|
||||
};
|
||||
|
||||
const savePage = (page: FeedPage) => {
|
||||
|
10
src/types/primal.d.ts
vendored
10
src/types/primal.d.ts
vendored
@ -214,6 +214,13 @@ export type NostrRelayHint = {
|
||||
tags: string[][],
|
||||
};
|
||||
|
||||
export type NostrZapInfo = {
|
||||
kind: Kind.EventZapInfo,
|
||||
content: string,
|
||||
created_at?: number,
|
||||
tags: string[][],
|
||||
};
|
||||
|
||||
export type NostrEventContent =
|
||||
NostrNoteContent |
|
||||
NostrUserContent |
|
||||
@ -243,7 +250,8 @@ export type NostrEventContent =
|
||||
NostrSuggestedUsers |
|
||||
PrimalUserRelays |
|
||||
NostrBookmarks |
|
||||
NostrRelayHint;
|
||||
NostrRelayHint |
|
||||
NostrZapInfo;
|
||||
|
||||
export type NostrEvent = [
|
||||
type: "EVENT",
|
||||
|
Loading…
Reference in New Issue
Block a user