diff --git a/src/element/badge.css b/src/element/badge.css index 1f5758e..d9ef50c 100644 --- a/src/element/badge.css +++ b/src/element/badge.css @@ -3,7 +3,8 @@ flex-direction: column; align-items: center; gap: 6px; - padding: 6px 0; + background: transparent; + margin: 8px 0; } .badge .badge-details { @@ -23,6 +24,7 @@ .badge .badge-description { margin: 0; color: var(--text-muted); + text-align: center; } .badge .badge-thumbnail { diff --git a/src/element/emoji-pack.css b/src/element/emoji-pack.css index 40f3dd9..a60f7b8 100644 --- a/src/element/emoji-pack.css +++ b/src/element/emoji-pack.css @@ -1,18 +1,22 @@ -.emoji-pack-title { +.emoji-pack { + margin: 8px 0; +} + +.emoji-pack .emoji-pack-title { display: flex; align-items: flex-start; justify-content: space-between; } -.emoji-pack-title .name { +.emoji-pack .emoji-pack-title .name { margin: 0; } -.emoji-pack-title a { +.emoji-pack .emoji-pack-title a { font-size: 14px; } -.emoji-pack-emojis { +.emoji-pack .emoji-pack-emojis { margin-top: 12px; display: flex; flex-direction: row; @@ -20,14 +24,14 @@ gap: 4px; } -.emoji-definition { +.emoji-pack .emoji-definition { display: flex; flex-direction: column; align-items: center; flex: 1; } -.emoji-name { +.emoji-pack .emoji-name { font-size: 10px; } diff --git a/src/element/emoji-pack.tsx b/src/element/emoji-pack.tsx index 4c84aca..59394f5 100644 --- a/src/element/emoji-pack.tsx +++ b/src/element/emoji-pack.tsx @@ -4,7 +4,6 @@ import { type NostrEvent } from "@snort/system"; import { useLogin } from "hooks/login"; import { toEmojiPack } from "hooks/emoji"; import AsyncButton from "element/async-button"; -import { Mention } from "element/mention"; import { findTag } from "utils"; import { USER_EMOJIS } from "const"; import { Login, System } from "index"; @@ -44,12 +43,9 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) { } return ( -
+
-
-

{name}

- -
+

{name}

{login?.pubkey && ( { + className?: string; size?: number; } diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index c4b26c0..51dcbc8 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -141,6 +141,7 @@ export function LiveChat({

Stream Chat

window.open( diff --git a/src/element/markdown.css b/src/element/markdown.css index cd3a323..d52ac78 100644 --- a/src/element/markdown.css +++ b/src/element/markdown.css @@ -19,7 +19,9 @@ line-height: 29px; /* 161.111% */ } -.markdown > img { - max-height: 230px; +.markdown img:not(.emoji):not(.note-avatar) { + max-height: 720px; + margin-top: 8px; width: 100%; + border-radius: 6px; } diff --git a/src/element/mute-button.tsx b/src/element/mute-button.tsx index 15691fc..b008813 100644 --- a/src/element/mute-button.tsx +++ b/src/element/mute-button.tsx @@ -47,7 +47,7 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) { isMuted ? unmute() : mute()} + onClick={() => (isMuted ? unmute() : mute())} > {isMuted ? "Unmute" : "Mute"} diff --git a/src/element/note.css b/src/element/note.css index c2289f7..01c6f3a 100644 --- a/src/element/note.css +++ b/src/element/note.css @@ -1,7 +1,9 @@ .note { - padding: 12px; - border: 1px solid var(--border); - border-radius: 10px; + margin: 8px 0; + color: #fff; + font-size: 15px; + font-weight: 400; + line-height: 22px; } .note .note-header { @@ -10,23 +12,23 @@ } .note .note-header .profile { - font-size: 14px; + font-size: 15px; + font-weight: 600; } -.note .note-avatar { - width: 18px; - height: 18px; +.note .note-header .note-avatar { + width: 24px; + height: 24px; } -.note .note-content { - margin-left: 30px; +.note .note-header .note-link-icon { + color: #909090; } .note .note-content .markdown > * { font-size: 14px; } -.note .note-content .markdown > ul, -.note .note-content .markdown ol { - margin-left: 30px; +.note .note-content .markdown > *:last-child { + margin-bottom: 0; } diff --git a/src/element/note.tsx b/src/element/note.tsx index ddf93fb..6339819 100644 --- a/src/element/note.tsx +++ b/src/element/note.tsx @@ -1,16 +1,24 @@ import "./note.css"; -import { type NostrEvent } from "@snort/system"; +import { type NostrEvent, NostrPrefix } from "@snort/system"; import { Markdown } from "element/markdown"; import { ExternalIconLink } from "element/external-link"; import { Profile } from "element/profile"; +import { hexToBech32 } from "utils"; export function Note({ ev }: { ev: NostrEvent }) { return ( -
+
- +
diff --git a/src/element/stream-cards.css b/src/element/stream-cards.css index f7f5eed..be27ce3 100644 --- a/src/element/stream-cards.css +++ b/src/element/stream-cards.css @@ -1,8 +1,12 @@ -.stream-cards { +.stream-cards, +.edit-container { display: none; } @media (min-width: 1020px) { + .edit-container { + display: block; + } .stream-cards { display: grid; align-items: flex-start; @@ -86,6 +90,7 @@ .new-card h3 { margin: 0; margin-bottom: 12px; + font-weight: 500; } .new-card input[type="text"] { @@ -169,3 +174,73 @@ .stream-card { max-width: 343px; } + +.top-zappers-card .top-zappers-leaderboard { + border: 1px solid; + padding: 4px 8px; + border-radius: 12px; + border-color: var(--border); +} + +.top-zappers-card .top-zapper-container { + display: flex; + align-items: center; + justify-content: space-between; +} + +.top-zapper-container .zap-amount { + display: flex; + align-items: center; + gap: 4px; +} + +.top-zapper-container .top-zapper-amount { + font-size: 18px; + font-weight: 500; + line-height: 22px; +} + +.top-zappers-card .top-zappers-leaderboard { + display: flex; + flex-direction: column; +} + +.top-zapper-container.first .profile { + background: var(--gradient-purple); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.top-zapper-container.second .profile { + background: var(--gradient-yellow); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.top-zapper-container.third .profile { + background: var(--gradient-orange); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.top-zapper-container.live .profile { + background-size: 300% 300%; + animation: animatedgradient 3s ease alternate infinite; +} + +@keyframes animatedgradient { + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0% 50%; + } +} diff --git a/src/element/stream-cards.tsx b/src/element/stream-cards.tsx index 16ea55e..48f588f 100644 --- a/src/element/stream-cards.tsx +++ b/src/element/stream-cards.tsx @@ -12,12 +12,16 @@ import { Icon } from "element/icon"; import { ExternalLink } from "element/external-link"; import { FileUploader } from "element/file-uploader"; import { Markdown } from "element/markdown"; +import { Profile } from "element/profile"; import { useLogin } from "hooks/login"; import { useCards, useUserCards } from "hooks/cards"; +import { useZaps } from "hooks/zaps"; +import useTopZappers from "hooks/top-zappers"; import { CARD, USER_CARDS } from "const"; import { toTag, findTag } from "utils"; import { Login, System } from "index"; import type { Tags } from "types"; +import { formatSats } from "number"; interface CardType { identifier: string; @@ -426,12 +430,14 @@ export function StreamCardEditor({ pubkey, tags }: StreamCardEditorProps) { interface StreamCardsProps { host: string; + isLive: boolean; } -export function ReadOnlyStreamCards({ host }: StreamCardsProps) { +export function ReadOnlyStreamCards({ host, isLive }: StreamCardsProps) { const cards = useCards(host); return (
+ {cards.length === 99 && } {cards.map((ev) => ( ))} @@ -439,7 +445,42 @@ export function ReadOnlyStreamCards({ host }: StreamCardsProps) { ); } -export function StreamCards({ host }: StreamCardsProps) { +interface TopZappersProps { + host: string; + isLive: boolean; + n?: number; +} + +function TopZappers({ host, isLive, n = 5 }: TopZappersProps) { + const zaps = useZaps(host); + const topZappers = useTopZappers(zaps); + return topZappers.length > 0 ? ( +
+

Top Zappers

+
+ {topZappers + .filter((z) => z.pubkey !== "anon") + .slice(0, n) + .map((z, idx) => ( +
+ +
+ +

{formatSats(z.total)}

+
+
+ ))} +
+
+ ) : null; +} + +export function StreamCards({ host, isLive }: StreamCardsProps) { const login = useLogin(); const canEdit = login?.pubkey === host; return ( @@ -447,7 +488,7 @@ export function StreamCards({ host }: StreamCardsProps) { {canEdit ? ( ) : ( - + )} ); diff --git a/src/element/text.css b/src/element/text.css index d8364ab..62e4fdf 100644 --- a/src/element/text.css +++ b/src/element/text.css @@ -1,5 +1,6 @@ -.custom-emoji { - width: 21px; - height: 21px; - display: inline-block; +.text img:not(.emoji):not(.note-avatar) { + max-height: 720px; + margin-top: 8px; + width: 100%; + border-radius: 6px; } diff --git a/src/element/text.tsx b/src/element/text.tsx index db43864..21327dd 100644 --- a/src/element/text.tsx +++ b/src/element/text.tsx @@ -1,3 +1,4 @@ +import "./text.css"; import { useMemo, type ReactNode } from "react"; import { parseNostrLink, validateNostrLink } from "@snort/system"; @@ -31,25 +32,11 @@ function extractLinks(fragments: Fragment[]) { return ( normalizedStr.startsWith("http:") || - normalizedStr.startsWith("https:") || - normalizedStr.startsWith("magnet:") + normalizedStr.startsWith("https:") ); }; if (validateLink()) { - if (!a.startsWith("nostr:")) { - return ( - e.stopPropagation()} - target="_blank" - rel="noreferrer" - className="ext" - > - {a} - - ); - } return {a}; } return a; @@ -204,7 +191,7 @@ export function transformText(ps: Fragment[], tags: Array) { export function Text({ content, tags }: { content: string; tags: string[][] }) { // todo: RTL langugage support const element = useMemo(() => { - return {transformText([content], tags)}; + return {transformText([content], tags)}; }, [content, tags]); return <>{element}; diff --git a/src/hooks/zaps.ts b/src/hooks/zaps.ts new file mode 100644 index 0000000..cd9b778 --- /dev/null +++ b/src/hooks/zaps.ts @@ -0,0 +1,32 @@ +import { useMemo } from "react"; + +import { + EventKind, + NoteCollection, + RequestBuilder, + parseZap, +} from "@snort/system"; +import { useRequestBuilder } from "@snort/system-react"; + +import { System } from "index"; + +export function useZaps(pubkey: string, leaveOpen = false) { + const rb = useMemo(() => { + const rb = new RequestBuilder(`profile-zaps:${pubkey.slice(0, 12)}`); + rb.withOptions({ leaveOpen }); + rb.withFilter().kinds([EventKind.ZapReceipt]).tag("p", [pubkey]); + return rb; + }, [pubkey]); + + const { data } = useRequestBuilder( + System, + NoteCollection, + rb + ); + + return ( + data + ?.map((ev) => parseZap(ev, System.ProfileLoader.Cache)) + .filter((z) => z && z.valid) ?? [] + ); +} diff --git a/src/index.css b/src/index.css index 0031ad1..a0cf362 100644 --- a/src/index.css +++ b/src/index.css @@ -15,7 +15,15 @@ body { --text-muted: #797979; --text-link: #f838d9; --text-danger: #ff563f; - --border: #333; + --surface: #222; + --border: #171717; + --gradient-purple: linear-gradient(135deg, #882bff 0%, #f83838 100%); + --gradient-yellow: linear-gradient(270deg, #adff27 0%, #ffd027 100%); + --gradient-orange: linear-gradient( + 270deg, + #ff5b27 0%, + rgba(255, 182, 39, 0.99) 100% + ); } @media (max-width: 1020px) { @@ -277,3 +285,19 @@ div.paper { height: 15px; margin-bottom: -2px; } + +.surface { + padding: 8px 12px 12px 12px; + background: var(--surface); + border-radius: 10px; +} + +.outline { + padding: 8px 12px 12px 12px; + border-radius: 10px; + border: 1px solid var(--border); +} + +.secondary { + color: #909090; +} diff --git a/src/pages/profile-page.css b/src/pages/profile-page.css index fbd4be0..31a8a8b 100644 --- a/src/pages/profile-page.css +++ b/src/pages/profile-page.css @@ -187,9 +187,7 @@ align-items: center; justify-content: space-between; font-size: 18px; - font-style: normal; font-weight: 500; - line-height: normal; } .profile-page .zapper .zapper-amount { @@ -197,7 +195,6 @@ align-items: center; gap: 4px; font-size: 18px; - font-style: normal; font-weight: 500; line-height: 22px; } diff --git a/src/pages/stream-page.tsx b/src/pages/stream-page.tsx index e34c154..8d0245d 100644 --- a/src/pages/stream-page.tsx +++ b/src/pages/stream-page.tsx @@ -3,6 +3,9 @@ import { parseNostrLink, TaggedRawEvent } from "@snort/system"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import { Helmet } from "react-helmet"; +import { NostrEvent } from "@snort/system"; +import { useUserProfile } from "@snort/system-react"; + import { LiveVideoPlayer } from "element/live-video-player"; import { createNostrLink, @@ -17,13 +20,10 @@ import { useLogin } from "hooks/login"; import { useZapGoal } from "hooks/goals"; import { StreamState, System } from "index"; import { SendZapsDialog } from "element/send-zap"; -import { NostrEvent } from "@snort/system"; -import { useUserProfile } from "@snort/system-react"; import { NewStreamDialog } from "element/new-stream"; import { Tags } from "element/tags"; import { StatePill } from "element/state-pill"; import { StreamCards } from "element/stream-cards"; -import { formatSats } from "number"; import { StreamTimer } from "element/stream-time"; import { ShareMenu } from "element/share-menu"; import { @@ -31,6 +31,7 @@ import { isContentWarningAccepted, } from "element/content-warning"; import { useCurrentStreamFeed } from "hooks/current-stream-feed"; +import { formatSats } from "number"; function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedRawEvent }) { const login = useLogin(); @@ -156,7 +157,7 @@ export function StreamPage() {
- +