diff --git a/src/components/Avatar/Avatar.module.scss b/src/components/Avatar/Avatar.module.scss index 7f1982e..a84ff69 100644 --- a/src/components/Avatar/Avatar.module.scss +++ b/src/components/Avatar/Avatar.module.scss @@ -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; diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index 39d4a80..e1f0bb3 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -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': diff --git a/src/components/Note/Note.module.scss b/src/components/Note/Note.module.scss index 2235aeb..1ed9251 100644 --- a/src/components/Note/Note.module.scss +++ b/src/components/Note/Note.module.scss @@ -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); + } + } + } + } + } + + } } diff --git a/src/components/Note/Note.tsx b/src/components/Note/Note.tsx index f4cd00f..3f612da 100644 --- a/src/components/Note/Note.tsx +++ b/src/components/Note/Note.tsx @@ -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 ( @@ -209,6 +221,43 @@ const Note: Component<{ +
+ + + +
+ {firstZap().amount_sats.toLocaleString()} +
+
+
+
+
+
+ + + 0}> + + +
+
+ +
diff --git a/src/components/ReactionsModal/ReactionsModal.tsx b/src/components/ReactionsModal/ReactionsModal.tsx index 1bdbed2..32c5280 100644 --- a/src/components/ReactionsModal/ReactionsModal.tsx +++ b/src/components/ReactionsModal/ReactionsModal.tsx @@ -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': diff --git a/src/constants.ts b/src/constants.ts index 158bae6..5061fd1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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, diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx index 57a5f55..d7c96b7 100644 --- a/src/contexts/AppContext.tsx +++ b/src/contexts/AppContext.tsx @@ -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 = { zaps: 0, reposts: 0, quotes: 0, + openOn: 'likes', }, showCustomZapModal: false, customZap: undefined, diff --git a/src/contexts/ThreadContext.tsx b/src/contexts/ThreadContext.tsx index 9e42432..a626110 100644 --- a/src/contexts/ThreadContext.tsx +++ b/src/contexts/ThreadContext.tsx @@ -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 | undefined, lastNote: PrimalNote | undefined, + topZaps: Record, 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) => { diff --git a/src/types/primal.d.ts b/src/types/primal.d.ts index c8eb515..48292a8 100644 --- a/src/types/primal.d.ts +++ b/src/types/primal.d.ts @@ -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",