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() {