parent
f23c97fa0e
commit
c6d109f182
@ -2,7 +2,7 @@ import "./badge.css";
|
|||||||
import type { NostrEvent } from "@snort/system";
|
import type { NostrEvent } from "@snort/system";
|
||||||
import { findTag } from "@/utils";
|
import { findTag } from "@/utils";
|
||||||
|
|
||||||
export function Badge({ ev }: { ev: NostrEvent }) {
|
export function BadgeInfo({ ev }: { ev: NostrEvent }) {
|
||||||
const name = findTag(ev, "name") || findTag(ev, "d");
|
const name = findTag(ev, "name") || findTag(ev, "d");
|
||||||
const description = findTag(ev, "description") ?? "";
|
const description = findTag(ev, "description") ?? "";
|
||||||
const thumb = findTag(ev, "thumb");
|
const thumb = findTag(ev, "thumb");
|
||||||
|
15
src/element/chat/chat-badge.tsx
Normal file
15
src/element/chat/chat-badge.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useProfileBadges } from "@/hooks/badges";
|
||||||
|
import { findTag } from "@/utils";
|
||||||
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
|
import { useEventFeed } from "@snort/system-react";
|
||||||
|
|
||||||
|
export default function AwardedChatBadge({ ev, pubkey }: { ev: TaggedNostrEvent; pubkey: string }) {
|
||||||
|
const badgeLink = NostrLink.fromTag(ev.tags.find(a => a[0] === "a")!);
|
||||||
|
const badge = useEventFeed(badgeLink);
|
||||||
|
const image = findTag(badge, "image");
|
||||||
|
const name = findTag(badge, "name");
|
||||||
|
|
||||||
|
const profileBadges = useProfileBadges(pubkey);
|
||||||
|
|
||||||
|
return badge && profileBadges.isAccepted(badgeLink) && <img src={image} className="h-4" title={name} />;
|
||||||
|
}
|
@ -16,11 +16,13 @@ import { CollapsibleEvent } from "../collapsible";
|
|||||||
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { formatSats } from "@/number";
|
import { formatSats } from "@/number";
|
||||||
import type { Badge, Emoji, EmojiPack } from "@/types";
|
import type { Emoji, EmojiPack } from "@/types";
|
||||||
import Pill from "../pill";
|
import Pill from "../pill";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import Modal from "../modal";
|
import Modal from "../modal";
|
||||||
import { ChatMenu } from "./chat-menu";
|
import { ChatMenu } from "./chat-menu";
|
||||||
|
import { BadgeAward } from "@/hooks/badges";
|
||||||
|
import AwardedChatBadge from "./chat-badge";
|
||||||
|
|
||||||
function emojifyReaction(reaction: string) {
|
function emojifyReaction(reaction: string) {
|
||||||
if (reaction === "+") {
|
if (reaction === "+") {
|
||||||
@ -41,7 +43,7 @@ export function ChatMessage({
|
|||||||
ev: TaggedNostrEvent;
|
ev: TaggedNostrEvent;
|
||||||
streamer: string;
|
streamer: string;
|
||||||
emojiPacks: EmojiPack[];
|
emojiPacks: EmojiPack[];
|
||||||
badges: Badge[];
|
badges: BadgeAward[];
|
||||||
}) {
|
}) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
@ -76,7 +78,7 @@ export function ChatMessage({
|
|||||||
return zaps.filter(a => a.event?.id === ev.id).reduce((acc, z) => acc + z.amount, 0);
|
return zaps.filter(a => a.event?.id === ev.id).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));
|
const awardedBadges = badges.filter(b => b.awardees.has(ev.pubkey));
|
||||||
|
|
||||||
useOnClickOutside(ref, () => {
|
useOnClickOutside(ref, () => {
|
||||||
setZapping(false);
|
setZapping(false);
|
||||||
@ -142,9 +144,7 @@ export function ChatMessage({
|
|||||||
ev.pubkey === streamer ? (
|
ev.pubkey === streamer ? (
|
||||||
<Icon name="signal" size={16} />
|
<Icon name="signal" size={16} />
|
||||||
) : (
|
) : (
|
||||||
awardedBadges.map(badge => {
|
awardedBadges.map(a => <AwardedChatBadge ev={a.event} pubkey={ev.pubkey} />)
|
||||||
return <img key={badge.name} className="h-4" src={badge.thumb || badge.image} alt={badge.name} />;
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pubkey={ev.pubkey}
|
pubkey={ev.pubkey}
|
||||||
|
@ -11,11 +11,11 @@ import { Text } from "../text";
|
|||||||
import { Profile } from "../profile";
|
import { Profile } from "../profile";
|
||||||
import { ChatMessage } from "./chat-message";
|
import { ChatMessage } from "./chat-message";
|
||||||
import { Goal } from "../goal";
|
import { Goal } from "../goal";
|
||||||
import { Badge } from "../badge";
|
import { BadgeInfo } from "../badge";
|
||||||
import { WriteMessage } from "./write-message";
|
import { WriteMessage } from "./write-message";
|
||||||
import useEmoji, { packId } from "@/hooks/emoji";
|
import useEmoji, { packId } from "@/hooks/emoji";
|
||||||
import { useMutedPubkeys } from "@/hooks/lists";
|
import { useMutedPubkeys } from "@/hooks/lists";
|
||||||
import { useBadges } from "@/hooks/badges";
|
import { useBadgeAwards } from "@/hooks/badges";
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { formatSats } from "@/number";
|
import { formatSats } from "@/number";
|
||||||
import { LIVE_STREAM_CHAT, LIVE_STREAM_CLIP, LIVE_STREAM_RAID, WEEK } from "@/const";
|
import { LIVE_STREAM_CHAT, LIVE_STREAM_CLIP, LIVE_STREAM_RAID, WEEK } from "@/const";
|
||||||
@ -33,7 +33,7 @@ function BadgeAward({ ev }: { ev: NostrEvent }) {
|
|||||||
const event = useEventFeed(new NostrLink(NostrPrefix.Address, d, Number(k), pubkey));
|
const event = useEventFeed(new NostrLink(NostrPrefix.Address, d, Number(k), pubkey));
|
||||||
return (
|
return (
|
||||||
<div className="badge-award">
|
<div className="badge-award">
|
||||||
{event && <Badge ev={event} />}
|
{event && <BadgeInfo ev={event} />}
|
||||||
<p>awarded to</p>
|
<p>awarded to</p>
|
||||||
<div className="badge-awardees">
|
<div className="badge-awardees">
|
||||||
{awardees.map(pk => (
|
{awardees.map(pk => (
|
||||||
@ -86,7 +86,7 @@ export function LiveChat({
|
|||||||
const starts = findTag(ev, "starts");
|
const starts = findTag(ev, "starts");
|
||||||
return starts ? Number(starts) : unixNow() - WEEK;
|
return starts ? Number(starts) : unixNow() - WEEK;
|
||||||
}, [ev]);
|
}, [ev]);
|
||||||
const { badges, awards } = useBadges(host, started);
|
const { awards } = useBadgeAwards(host);
|
||||||
|
|
||||||
const hostMutedPubkeys = useMutedPubkeys(host, true);
|
const hostMutedPubkeys = useMutedPubkeys(host, true);
|
||||||
const userEmojiPacks = useEmoji(login?.pubkey);
|
const userEmojiPacks = useEmoji(login?.pubkey);
|
||||||
@ -108,7 +108,7 @@ export function LiveChat({
|
|||||||
if (ends) {
|
if (ends) {
|
||||||
extra.push({ kind: -2, created_at: Number(ends) } as TaggedNostrEvent);
|
extra.push({ kind: -2, created_at: Number(ends) } as TaggedNostrEvent);
|
||||||
}
|
}
|
||||||
return removeUndefined([...feed, ...awards, ...extra])
|
return removeUndefined([...feed, ...awards.map(a => a.event), ...extra])
|
||||||
.filter(a => a.created_at >= started)
|
.filter(a => a.created_at >= started)
|
||||||
.sort((a, b) => b.created_at - a.created_at);
|
.sort((a, b) => b.created_at - a.created_at);
|
||||||
}, [feed, awards]);
|
}, [feed, awards]);
|
||||||
@ -202,7 +202,7 @@ export function LiveChat({
|
|||||||
return <BadgeAward ev={a} key={a.id} />;
|
return <BadgeAward ev={a} key={a.id} />;
|
||||||
}
|
}
|
||||||
case LIVE_STREAM_CHAT: {
|
case LIVE_STREAM_CHAT: {
|
||||||
return <ChatMessage badges={badges} emojiPacks={allEmojiPacks} streamer={host} ev={a} key={a.id} />;
|
return <ChatMessage badges={awards} emojiPacks={allEmojiPacks} streamer={host} ev={a} key={a.id} />;
|
||||||
}
|
}
|
||||||
case LIVE_STREAM_RAID: {
|
case LIVE_STREAM_RAID: {
|
||||||
return <ChatRaid ev={a} link={link} key={a.id} autoRaid={autoRaid} />;
|
return <ChatRaid ev={a} link={link} key={a.id} autoRaid={autoRaid} />;
|
||||||
|
@ -4,7 +4,7 @@ import { Icon } from "./icon";
|
|||||||
import { Goal } from "./goal";
|
import { Goal } from "./goal";
|
||||||
import { Note } from "./note";
|
import { Note } from "./note";
|
||||||
import { EmojiPack } from "./emoji-pack";
|
import { EmojiPack } from "./emoji-pack";
|
||||||
import { Badge } from "./badge";
|
import { BadgeInfo } from "./badge";
|
||||||
import { GOAL, LIVE_STREAM_CLIP, StreamState } from "@/const";
|
import { GOAL, LIVE_STREAM_CLIP, StreamState } from "@/const";
|
||||||
import { useEventFeed } from "@snort/system-react";
|
import { useEventFeed } from "@snort/system-react";
|
||||||
import LiveStreamClip from "./stream/clip";
|
import LiveStreamClip from "./stream/clip";
|
||||||
@ -39,7 +39,7 @@ export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
return <EmojiPack ev={ev} />;
|
return <EmojiPack ev={ev} />;
|
||||||
}
|
}
|
||||||
case EventKind.Badge: {
|
case EventKind.Badge: {
|
||||||
return <Badge ev={ev} />;
|
return <BadgeInfo ev={ev} />;
|
||||||
}
|
}
|
||||||
case EventKind.TextNote: {
|
case EventKind.TextNote: {
|
||||||
return <Note ev={ev} />;
|
return <Note ev={ev} />;
|
||||||
|
@ -1,69 +1,45 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { EventKind, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
import { EventKind, NostrLink, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
|
||||||
import { findTag, getTagValues, toAddress } from "@/utils";
|
export interface BadgeAward {
|
||||||
import type { Badge } from "@/types";
|
event: TaggedNostrEvent;
|
||||||
|
awardees: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
export function useBadges(
|
export function useBadgeAwards(pubkey: string, leaveOpen = true) {
|
||||||
pubkey: string,
|
const subBadgeAwards = useMemo(() => {
|
||||||
since: number,
|
|
||||||
leaveOpen = true,
|
|
||||||
): { badges: Badge[]; awards: TaggedNostrEvent[] } {
|
|
||||||
const rb = useMemo(() => {
|
|
||||||
const rb = new RequestBuilder(`badges:${pubkey}`);
|
const rb = new RequestBuilder(`badges:${pubkey}`);
|
||||||
rb.withOptions({ leaveOpen });
|
rb.withOptions({ leaveOpen });
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
rb.withFilter().authors([pubkey]).kinds([EventKind.Badge, EventKind.BadgeAward]);
|
rb.withFilter().authors([pubkey]).kinds([EventKind.BadgeAward]);
|
||||||
}
|
}
|
||||||
return rb;
|
return rb;
|
||||||
}, [pubkey, since]);
|
}, [pubkey]);
|
||||||
|
|
||||||
const badgeEvents = useRequestBuilder(rb);
|
const awards = useRequestBuilder(subBadgeAwards);
|
||||||
|
return {
|
||||||
const rawBadges = useMemo(() => {
|
awards: awards.map(
|
||||||
if (badgeEvents) {
|
a =>
|
||||||
return badgeEvents.filter(e => e.kind === EventKind.Badge).sort((a, b) => b.created_at - a.created_at);
|
({
|
||||||
}
|
event: a,
|
||||||
return [];
|
awardees: new Set(a.tags.filter(b => b[0] === "p").map(b => b[1])),
|
||||||
}, [badgeEvents]);
|
}) as BadgeAward,
|
||||||
const badgeAwards = useMemo(() => {
|
),
|
||||||
if (badgeEvents) {
|
};
|
||||||
return badgeEvents.filter(e => e.kind === EventKind.BadgeAward);
|
}
|
||||||
}
|
|
||||||
return [];
|
export function useProfileBadges(pubkey: string) {
|
||||||
}, [badgeEvents]);
|
const sub = new RequestBuilder(`profile-badges:${pubkey}`);
|
||||||
|
sub.withFilter().kinds([EventKind.ProfileBadges]).authors([pubkey]).tag("d", ["profile_badges"]);
|
||||||
const acceptedSub = useMemo(() => {
|
const data = useRequestBuilder(sub).at(0);
|
||||||
const rb = new RequestBuilder(`accepted-badges:${pubkey}`);
|
return {
|
||||||
if (rawBadges.length > 0) {
|
event: data,
|
||||||
rb.withFilter().kinds([EventKind.ProfileBadges]).tag("d", ["profile_badges"]).tag("a", rawBadges.map(toAddress));
|
isAccepted: (link: NostrLink) => {
|
||||||
}
|
if (!data) return false;
|
||||||
return rb;
|
const links = NostrLink.fromAllTags(data.tags);
|
||||||
}, [rawBadges]);
|
return links.some(a => a.equals(link));
|
||||||
|
},
|
||||||
const acceptedStream = useRequestBuilder(acceptedSub);
|
};
|
||||||
const acceptedEvents = acceptedStream ?? [];
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
return { badges, awards: badgeAwards };
|
|
||||||
}
|
}
|
||||||
|
@ -24,11 +24,3 @@ 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>;
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user