feat: badges #58
@ -9,6 +9,9 @@ export const USER_CARDS = 17_777 as EventKind;
|
|||||||
export const CARD = 37_777 as EventKind;
|
export const CARD = 37_777 as EventKind;
|
||||||
export const MUTED = 10_000 as EventKind;
|
export const MUTED = 10_000 as EventKind;
|
||||||
|
|
||||||
|
export const DAY = 60 * 60 * 24;
|
||||||
|
export const WEEK = 7 * DAY;
|
||||||
|
|
||||||
export const defaultRelays = {
|
export const defaultRelays = {
|
||||||
"wss://relay.snort.social": { read: true, write: true },
|
"wss://relay.snort.social": { read: true, write: true },
|
||||||
"wss://nos.lol": { read: true, write: true },
|
"wss://nos.lol": { read: true, write: true },
|
||||||
|
@ -13,7 +13,7 @@ interface EventProps {
|
|||||||
export function Event({ link }: EventProps) {
|
export function Event({ link }: EventProps) {
|
||||||
const event = useEvent(link);
|
const event = useEvent(link);
|
||||||
|
|
||||||
if (event && event.kind === GOAL) {
|
if (event?.kind === GOAL) {
|
||||||
return (
|
return (
|
||||||
<div className="event-container">
|
<div className="event-container">
|
||||||
<Goal ev={event} />
|
<Goal ev={event} />
|
||||||
@ -21,7 +21,7 @@ export function Event({ link }: EventProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event && event.kind === EventKind.TextNote) {
|
if (event?.kind === EventKind.TextNote) {
|
||||||
return (
|
return (
|
||||||
<div className="event-container">
|
<div className="event-container">
|
||||||
<Note ev={event} />
|
<Note ev={event} />
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { type NostrLink } from "@snort/system";
|
import { type NostrLink, EventKind } from "@snort/system";
|
||||||
|
|
||||||
import { useEvent } from "hooks/event";
|
import { useEvent } from "hooks/event";
|
||||||
import { EMOJI_PACK } from "const";
|
import { EMOJI_PACK } from "const";
|
||||||
import { EmojiPack } from "element/emoji-pack";
|
import { EmojiPack } from "element/emoji-pack";
|
||||||
|
import { Badge } from "element/badge";
|
||||||
|
|
||||||
interface AddressProps {
|
interface AddressProps {
|
||||||
link: NostrLink;
|
link: NostrLink;
|
||||||
@ -15,5 +16,9 @@ export function Address({ link }: AddressProps) {
|
|||||||
return <EmojiPack ev={event} />;
|
return <EmojiPack ev={event} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event?.kind === EventKind.Badge) {
|
||||||
|
return <Badge ev={event} />;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
36
src/element/badge.css
Normal file
36
src/element/badge.css
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
.badge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge .badge-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge .badge-name {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge .badge-description {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge .badge-thumbnail {
|
||||||
|
max-width: 120px;
|
||||||
|
aspect-ration: 4/3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge .badge-description {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
21
src/element/badge.tsx
Normal file
21
src/element/badge.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import "./badge.css";
|
||||||
|
import type { NostrEvent } from "@snort/system";
|
||||||
|
import { findTag } from "utils";
|
||||||
|
|
||||||
|
export function Badge({ ev }: { ev: NostrEvent }) {
|
||||||
|
const name = findTag(ev, "name") || findTag(ev, "d");
|
||||||
|
const description = findTag(ev, "description");
|
||||||
|
const thumb = findTag(ev, "thumb");
|
||||||
|
const image = findTag(ev, "image");
|
||||||
|
return (
|
||||||
|
<div className="badge">
|
||||||
|
<img className="badge-thumbnail" src={thumb || image} alt={name} />
|
||||||
|
<div className="badge-details">
|
||||||
|
<h4 className="badge-name">{name}</h4>
|
||||||
|
{description?.length > 0 && (
|
||||||
|
<p className="badge-description">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -12,18 +12,14 @@ import { System } from "../index";
|
|||||||
import { formatSats } from "../number";
|
import { formatSats } from "../number";
|
||||||
import { EmojiPicker } from "./emoji-picker";
|
import { EmojiPicker } from "./emoji-picker";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { Emoji } from "./emoji";
|
import { Emoji as EmojiComponent } from "./emoji";
|
||||||
import { Profile } from "./profile";
|
import { Profile } from "./profile";
|
||||||
import { Text } from "element/text";
|
import { Text } from "element/text";
|
||||||
import { SendZapsDialog } from "./send-zap";
|
import { SendZapsDialog } from "./send-zap";
|
||||||
import { findTag } from "../utils";
|
import { findTag } from "../utils";
|
||||||
import type { EmojiPack } from "../hooks/emoji";
|
import type { EmojiPack } from "../hooks/emoji";
|
||||||
import { useLogin } from "../hooks/login";
|
import { useLogin } from "../hooks/login";
|
||||||
|
import type { Badge, Emoji } from "types";
|
||||||
interface Emoji {
|
|
||||||
id: string;
|
|
||||||
native?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function emojifyReaction(reaction: string) {
|
function emojifyReaction(reaction: string) {
|
||||||
if (reaction === "+") {
|
if (reaction === "+") {
|
||||||
@ -40,11 +36,13 @@ export function ChatMessage({
|
|||||||
ev,
|
ev,
|
||||||
reactions,
|
reactions,
|
||||||
emojiPacks,
|
emojiPacks,
|
||||||
|
badges,
|
||||||
}: {
|
}: {
|
||||||
ev: NostrEvent;
|
ev: NostrEvent;
|
||||||
streamer: string;
|
streamer: string;
|
||||||
reactions: readonly NostrEvent[];
|
reactions: readonly NostrEvent[];
|
||||||
emojiPacks: EmojiPack[];
|
emojiPacks: EmojiPack[];
|
||||||
|
badges: Badge[];
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const inView = useIntersectionObserver(ref, {
|
const inView = useIntersectionObserver(ref, {
|
||||||
@ -81,6 +79,9 @@ export function ChatMessage({
|
|||||||
return messageZaps.reduce((acc, z) => acc + z.amount, 0);
|
return messageZaps.reduce((acc, z) => acc + z.amount, 0);
|
||||||
}, [zaps, ev]);
|
}, [zaps, ev]);
|
||||||
const hasZaps = totalZaps > 0;
|
const hasZaps = totalZaps > 0;
|
||||||
|
const awardedBadges = badges.filter(
|
||||||
|
(b) => b.awardees.has(ev.pubkey) && b.accepted.has(ev.pubkey),
|
||||||
|
);
|
||||||
|
|
||||||
useOnClickOutside(ref, () => {
|
useOnClickOutside(ref, () => {
|
||||||
setShowZapDialog(false);
|
setShowZapDialog(false);
|
||||||
@ -141,13 +142,20 @@ export function ChatMessage({
|
|||||||
>
|
>
|
||||||
<Profile
|
<Profile
|
||||||
icon={
|
icon={
|
||||||
ev.pubkey === streamer && <Icon name="signal" size={16} />
|
ev.pubkey === streamer ? (
|
||||||
// todo: styling is ready if we want to add stream badges
|
<Icon name="signal" size={16} />
|
||||||
// <img
|
) : (
|
||||||
// className="badge-icon"
|
awardedBadges.map((badge) => {
|
||||||
// src="https://nostr.build/i/nostr.build_4b0d4f7293eb0f2bacb5b232a8d2ef3fe7648192d636e152a3c18b9fc06142d7.png"
|
return (
|
||||||
// alt="TODO"
|
<img
|
||||||
// />
|
key={badge.name}
|
||||||
|
className="badge-icon"
|
||||||
|
src={badge.thumb || badge.image}
|
||||||
|
alt={badge.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
pubkey={ev.pubkey}
|
pubkey={ev.pubkey}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
@ -170,7 +178,7 @@ export function ChatMessage({
|
|||||||
<div className="message-reaction-container">
|
<div className="message-reaction-container">
|
||||||
{isCustomEmojiReaction && emoji ? (
|
{isCustomEmojiReaction && emoji ? (
|
||||||
<span className="message-reaction">
|
<span className="message-reaction">
|
||||||
<Emoji name={emoji.at(1)!} url={emoji.at(2)!} />
|
<EmojiComponent name={emoji.at(1)!} url={emoji.at(2)!} />
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="message-reaction">{e}</span>
|
<span className="message-reaction">{e}</span>
|
||||||
|
@ -357,3 +357,20 @@
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
border-radius: unset;
|
border-radius: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-award {
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #303030;
|
||||||
|
background: #111;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-award .title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-awardees {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
@ -11,28 +11,49 @@ import {
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import uniqBy from "lodash.uniqby";
|
import uniqBy from "lodash.uniqby";
|
||||||
|
|
||||||
import { System } from "../index";
|
import { Icon } from "element/icon";
|
||||||
import useEmoji, { packId } from "../hooks/emoji";
|
import Spinner from "element/spinner";
|
||||||
import { useLiveChatFeed } from "../hooks/live-chat";
|
import { Text } from "element/text";
|
||||||
import { Profile } from "./profile";
|
import { Profile } from "element/profile";
|
||||||
import { Icon } from "./icon";
|
import { ChatMessage } from "element/chat-message";
|
||||||
import Spinner from "./spinner";
|
import { Goal } from "element/goal";
|
||||||
import { Text } from "./text";
|
import { Badge } from "element/badge";
|
||||||
import { useLogin } from "../hooks/login";
|
import { NewGoalDialog } from "element/new-goal";
|
||||||
import { formatSats } from "../number";
|
import { WriteMessage } from "element/write-message";
|
||||||
import useTopZappers from "../hooks/top-zappers";
|
import useEmoji, { packId } from "hooks/emoji";
|
||||||
import { LIVE_STREAM_CHAT } from "../const";
|
import { useLiveChatFeed } from "hooks/live-chat";
|
||||||
import { ChatMessage } from "./chat-message";
|
import { useBadges } from "hooks/badges";
|
||||||
import { Goal } from "./goal";
|
import { useLogin } from "hooks/login";
|
||||||
import { NewGoalDialog } from "./new-goal";
|
import useTopZappers from "hooks/top-zappers";
|
||||||
import { WriteMessage } from "./write-message";
|
import { useAddress } from "hooks/event";
|
||||||
|
import { formatSats } from "number";
|
||||||
|
import { LIVE_STREAM_CHAT } from "const";
|
||||||
import { findTag, getTagValues, getHost } from "utils";
|
import { findTag, getTagValues, getHost } from "utils";
|
||||||
|
import { System } from "index";
|
||||||
|
|
||||||
export interface LiveChatOptions {
|
export interface LiveChatOptions {
|
||||||
canWrite?: boolean;
|
canWrite?: boolean;
|
||||||
showHeader?: boolean;
|
showHeader?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BadgeAward({ ev }: { ev: NostrEvent }) {
|
||||||
|
const badge = findTag(ev, "a");
|
||||||
|
const [k, pubkey, d] = badge.split(":");
|
||||||
|
const awardees = getTagValues(ev.tags, "p");
|
||||||
|
const event = useAddress(Number(k), pubkey, d);
|
||||||
|
return (
|
||||||
|
<div className="badge-award">
|
||||||
|
{event && <Badge ev={event} />}
|
||||||
|
<p>awarded to</p>
|
||||||
|
<div className="badge-awardees">
|
||||||
|
{awardees.map((pk) => (
|
||||||
|
<Profile key={pk} pubkey={pk} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
|
function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
|
||||||
const zappers = useTopZappers(zaps);
|
const zappers = useTopZappers(zaps);
|
||||||
|
|
||||||
@ -69,6 +90,7 @@ export function LiveChat({
|
|||||||
height?: number;
|
height?: number;
|
||||||
}) {
|
}) {
|
||||||
const host = getHost(ev);
|
const host = getHost(ev);
|
||||||
|
const { badges, awards } = useBadges(host);
|
||||||
const feed = useLiveChatFeed(link, goal ? [goal.id] : undefined);
|
const feed = useLiveChatFeed(link, goal ? [goal.id] : undefined);
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -92,7 +114,7 @@ export function LiveChat({
|
|||||||
.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
|
.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
|
||||||
.filter((z) => z && z.valid);
|
.filter((z) => z && z.valid);
|
||||||
const events = useMemo(() => {
|
const events = useMemo(() => {
|
||||||
return [...feed.messages, ...feed.zaps].sort(
|
return [...feed.messages, ...feed.zaps, ...awards].sort(
|
||||||
(a, b) => b.created_at - a.created_at,
|
(a, b) => b.created_at - a.created_at,
|
||||||
);
|
);
|
||||||
}, [feed.messages, feed.zaps]);
|
}, [feed.messages, feed.zaps]);
|
||||||
@ -143,9 +165,13 @@ export function LiveChat({
|
|||||||
<div className="messages">
|
<div className="messages">
|
||||||
{filteredEvents.map((a) => {
|
{filteredEvents.map((a) => {
|
||||||
switch (a.kind) {
|
switch (a.kind) {
|
||||||
|
case EventKind.BadgeAward: {
|
||||||
|
return <BadgeAward ev={a} />;
|
||||||
|
}
|
||||||
case LIVE_STREAM_CHAT: {
|
case LIVE_STREAM_CHAT: {
|
||||||
return (
|
return (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
|
badges={badges}
|
||||||
emojiPacks={allEmojiPacks}
|
emojiPacks={allEmojiPacks}
|
||||||
streamer={streamer}
|
streamer={streamer}
|
||||||
ev={a}
|
ev={a}
|
||||||
|
93
src/hooks/badges.ts
Normal file
93
src/hooks/badges.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { EventKind, NoteCollection, RequestBuilder } from "@snort/system";
|
||||||
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
import { unixNow } from "@snort/shared";
|
||||||
|
|
||||||
|
import { findTag, toAddress, getTagValues } from "utils";
|
||||||
|
import { WEEK } from "const";
|
||||||
|
import { System } from "index";
|
||||||
|
import type { Badge } from "types";
|
||||||
|
|
||||||
|
export function useBadges(pubkey: string, leaveOpen = true): Array<Badge> {
|
||||||
|
const since = useMemo(() => unixNow() - WEEK, [pubkey]);
|
||||||
|
const rb = useMemo(() => {
|
||||||
|
const rb = new RequestBuilder(`badges:${pubkey.slice(0, 12)}`);
|
||||||
|
rb.withOptions({ leaveOpen });
|
||||||
|
rb.withFilter()
|
||||||
|
.authors([pubkey])
|
||||||
|
.kinds([EventKind.Badge, EventKind.BadgeAward]);
|
||||||
|
return rb;
|
||||||
|
}, [pubkey]);
|
||||||
|
|
||||||
|
const { data: badgeEvents } = useRequestBuilder<NoteCollection>(
|
||||||
|
System,
|
||||||
|
NoteCollection,
|
||||||
|
rb,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rawBadges = useMemo(() => {
|
||||||
|
if (badgeEvents) {
|
||||||
|
return badgeEvents
|
||||||
|
.filter((e) => e.kind === EventKind.Badge)
|
||||||
|
.sort((a, b) => b.created_at - a.created_at);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [badgeEvents]);
|
||||||
|
const badgeAwards = useMemo(() => {
|
||||||
|
if (badgeEvents) {
|
||||||
|
return badgeEvents.filter((e) => e.kind === EventKind.BadgeAward);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [badgeEvents]);
|
||||||
|
|
||||||
|
const acceptedSub = useMemo(() => {
|
||||||
|
if (rawBadges.length === 0) return null;
|
||||||
|
const rb = new RequestBuilder(
|
||||||
|
`accepted-badges:${pubkey.slice(0, 12)}:${rawBadges.length}`,
|
||||||
Kieran marked this conversation as resolved
Outdated
|
|||||||
|
);
|
||||||
|
rb.withFilter()
|
||||||
|
.kinds([EventKind.ProfileBadges])
|
||||||
|
.tag("d", ["profile_badges"])
|
||||||
|
.tag("a", rawBadges.map(toAddress));
|
||||||
|
return rb;
|
||||||
|
}, [rawBadges]);
|
||||||
|
|
||||||
|
const acceptedStream = useRequestBuilder<NoteCollection>(
|
||||||
|
System,
|
||||||
|
NoteCollection,
|
||||||
|
acceptedSub,
|
||||||
|
);
|
||||||
|
const acceptedEvents = acceptedStream.data ?? [];
|
||||||
|
|
||||||
|
const badges = useMemo(() => {
|
||||||
|
return rawBadges.map((e) => {
|
||||||
|
const name = findTag(e, "d");
|
||||||
|
const address = toAddress(e);
|
||||||
|
const awardEvents = badgeAwards.filter(
|
||||||
|
(b) => findTag(b, "a") === address,
|
||||||
|
);
|
||||||
|
const awardees = new Set(
|
||||||
|
awardEvents.map((e) => getTagValues(e.tags, "p")).flat(),
|
||||||
|
);
|
||||||
|
const accepted = new Set(
|
||||||
|
acceptedEvents
|
||||||
|
.filter((pb) => awardees.has(pb.pubkey))
|
||||||
|
.filter((pb) =>
|
||||||
|
pb.tags.find((t) => t.at(0) === "a" && t.at(1) === address),
|
||||||
|
)
|
||||||
|
.map((pb) => pb.pubkey),
|
||||||
|
);
|
||||||
|
const thumb = findTag(e, "thumb");
|
||||||
|
const image = findTag(e, "image");
|
||||||
|
return { name, thumb, image, awardees, accepted };
|
||||||
|
});
|
||||||
|
return [];
|
||||||
|
}, [rawBadges]);
|
||||||
|
|
||||||
|
const awards = useMemo(() => {
|
||||||
|
return badgeAwards.filter((e) => e.created_at > since);
|
||||||
|
}, [badgeAwards]);
|
||||||
|
|
||||||
|
return { badges, awards };
|
||||||
|
}
|
@ -10,6 +10,22 @@ import { useRequestBuilder } from "@snort/system-react";
|
|||||||
|
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
|
|
||||||
|
export function useAddress(kind: number, pubkey: string, identifier: string) {
|
||||||
|
const sub = useMemo(() => {
|
||||||
|
const b = new RequestBuilder(`event:${kind}:${identifier}`);
|
||||||
|
b.withFilter().kinds([kind]).authors([pubkey]).tag("d", [identifier]);
|
||||||
|
return b;
|
||||||
|
}, [kind, pubkey, identifier]);
|
||||||
|
|
||||||
|
const { data } = useRequestBuilder<ReplaceableNoteStore>(
|
||||||
|
System,
|
||||||
|
ReplaceableNoteStore,
|
||||||
|
sub,
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
export function useEvent(link: NostrLink) {
|
export function useEvent(link: NostrLink) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const b = new RequestBuilder(`event:${link.id.slice(0, 12)}`);
|
const b = new RequestBuilder(`event:${link.id.slice(0, 12)}`);
|
||||||
|
@ -8,13 +8,10 @@ import { useRequestBuilder } from "@snort/system-react";
|
|||||||
import { unixNow } from "@snort/shared";
|
import { unixNow } from "@snort/shared";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { LIVE_STREAM_CHAT } from "const";
|
import { LIVE_STREAM_CHAT, WEEK } from "const";
|
||||||
|
|
||||||
export function useLiveChatFeed(link: NostrLink, eZaps?: Array<string>) {
|
export function useLiveChatFeed(link: NostrLink, eZaps?: Array<string>) {
|
||||||
const since = useMemo(
|
const since = useMemo(() => unixNow() - WEEK, [link.id]);
|
||||||
() => unixNow() - 60 * 60 * 24 * 7, // 7-days of zaps
|
|
||||||
[link.id],
|
|
||||||
);
|
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const rb = new RequestBuilder(`live:${link.id}:${link.author}`);
|
const rb = new RequestBuilder(`live:${link.id}:${link.author}`);
|
||||||
rb.withOptions({
|
rb.withOptions({
|
||||||
@ -45,7 +42,9 @@ export function useLiveChatFeed(link: NostrLink, eZaps?: Array<string>) {
|
|||||||
|
|
||||||
const esub = useMemo(() => {
|
const esub = useMemo(() => {
|
||||||
if (etags.length === 0) return null;
|
if (etags.length === 0) return null;
|
||||||
const rb = new RequestBuilder(`reactions:${link.id}:${link.author}`);
|
const rb = new RequestBuilder(
|
||||||
|
`reactions:${link.id}:${link.author}:${etags.length}`,
|
||||||
Kieran marked this conversation as resolved
Outdated
Kieran
commented
Length should not be needed Length should not be needed
|
|||||||
|
);
|
||||||
rb.withOptions({
|
rb.withOptions({
|
||||||
leaveOpen: true,
|
leaveOpen: true,
|
||||||
});
|
});
|
||||||
|
@ -3,12 +3,12 @@ import { parseNostrLink, TaggedRawEvent } from "@snort/system";
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
import useEventFeed from "hooks/event-feed";
|
|
||||||
import { LiveVideoPlayer } from "element/live-video-player";
|
import { LiveVideoPlayer } from "element/live-video-player";
|
||||||
import { findTag, getHost } from "utils";
|
import { findTag, getHost } from "utils";
|
||||||
import { Profile, getName } from "element/profile";
|
import { Profile, getName } from "element/profile";
|
||||||
import { LiveChat } from "element/live-chat";
|
import { LiveChat } from "element/live-chat";
|
||||||
import AsyncButton from "element/async-button";
|
import AsyncButton from "element/async-button";
|
||||||
|
import useEventFeed from "hooks/event-feed";
|
||||||
import { useLogin } from "hooks/login";
|
import { useLogin } from "hooks/login";
|
||||||
import { useZapGoal } from "hooks/goals";
|
import { useZapGoal } from "hooks/goals";
|
||||||
import { StreamState, System } from "index";
|
import { StreamState, System } from "index";
|
||||||
@ -22,7 +22,10 @@ import { StreamCards } from "element/stream-cards";
|
|||||||
import { formatSats } from "number";
|
import { formatSats } from "number";
|
||||||
import { StreamTimer } from "element/stream-time";
|
import { StreamTimer } from "element/stream-time";
|
||||||
import { ShareMenu } from "element/share-menu";
|
import { ShareMenu } from "element/share-menu";
|
||||||
import { ContentWarningOverlay, isContentWarningAccepted } from "element/content-warning";
|
import {
|
||||||
|
ContentWarningOverlay,
|
||||||
|
isContentWarningAccepted,
|
||||||
|
} from "element/content-warning";
|
||||||
|
|
||||||
function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedRawEvent }) {
|
function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedRawEvent }) {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
@ -117,7 +120,7 @@ export function StreamPage() {
|
|||||||
const tags = ev?.tags.filter((a) => a[0] === "t").map((a) => a[1]) ?? [];
|
const tags = ev?.tags.filter((a) => a[0] === "t").map((a) => a[1]) ?? [];
|
||||||
|
|
||||||
if (contentWarning && !isContentWarningAccepted()) {
|
if (contentWarning && !isContentWarningAccepted()) {
|
||||||
return <ContentWarningOverlay />
|
return <ContentWarningOverlay />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const descriptionContent = [
|
const descriptionContent = [
|
||||||
|
@ -20,3 +20,11 @@ export interface EmojiPack {
|
|||||||
author: string;
|
author: string;
|
||||||
emojis: EmojiTag[];
|
emojis: EmojiTag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Badge {
|
||||||
|
name: string;
|
||||||
|
thumb: string;
|
||||||
|
image: string;
|
||||||
|
awardees: Set<string>;
|
||||||
|
accepted: Set<string>;
|
||||||
|
}
|
||||||
|
14
src/utils.ts
14
src/utils.ts
@ -2,6 +2,20 @@ import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
|
|||||||
import * as utils from "@noble/curves/abstract/utils";
|
import * as utils from "@noble/curves/abstract/utils";
|
||||||
import { bech32 } from "@scure/base";
|
import { bech32 } from "@scure/base";
|
||||||
|
|
||||||
|
export function toAddress(e: NostrEvent): string {
|
||||||
|
if (e.kind && e.kind >= 30000 && e.kind <= 40000) {
|
||||||
|
const dTag = findTag(e, "d");
|
||||||
|
|
||||||
|
return `${e.kind}:${e.pubkey}:${dTag}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.kind === 0 || e.kind === 3) {
|
||||||
|
return e.pubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.id;
|
||||||
|
}
|
||||||
|
|
||||||
export function toTag(e: NostrEvent): string[] {
|
export function toTag(e: NostrEvent): string[] {
|
||||||
if (e.kind && e.kind >= 30000 && e.kind <= 40000) {
|
if (e.kind && e.kind >= 30000 && e.kind <= 40000) {
|
||||||
const dTag = findTag(e, "d");
|
const dTag = findTag(e, "d");
|
||||||
|
Loading…
Reference in New Issue
Block a user
shouldn't need the
rawBadges.length
if thea
tags array gets longer it should send only the extra tags that are added for the sameRequestBuilder(id)
mb, removed this and the other.