forked from Kieran/snort
feat: render zap goals
This commit is contained in:
parent
d06d6afbf7
commit
35423cc91b
@ -34,6 +34,7 @@ import PubkeyList from "Element/PubkeyList";
|
|||||||
import { LiveEvent } from "Element/LiveEvent";
|
import { LiveEvent } from "Element/LiveEvent";
|
||||||
import { NoteContextMenu, NoteTranslation } from "Element/NoteContextMenu";
|
import { NoteContextMenu, NoteTranslation } from "Element/NoteContextMenu";
|
||||||
import Reactions from "Element/Reactions";
|
import Reactions from "Element/Reactions";
|
||||||
|
import { ZapGoal } from "Element/ZapGoal";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
@ -91,6 +92,9 @@ export default function Note(props: NoteProps) {
|
|||||||
if (ev.kind === EventKind.LiveEvent) {
|
if (ev.kind === EventKind.LiveEvent) {
|
||||||
return <LiveEvent ev={ev} />;
|
return <LiveEvent ev={ev} />;
|
||||||
}
|
}
|
||||||
|
if (ev.kind === (9041 as EventKind)) {
|
||||||
|
return <ZapGoal ev={ev} />;
|
||||||
|
}
|
||||||
|
|
||||||
const baseClassName = `note card${className ? ` ${className}` : ""}`;
|
const baseClassName = `note card${className ? ` ${className}` : ""}`;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -7,7 +7,17 @@ import SendSats from "Element/SendSats";
|
|||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
|
|
||||||
const ZapButton = ({ pubkey, lnurl, children }: { pubkey: HexKey; lnurl?: string; children?: React.ReactNode }) => {
|
const ZapButton = ({
|
||||||
|
pubkey,
|
||||||
|
lnurl,
|
||||||
|
children,
|
||||||
|
event,
|
||||||
|
}: {
|
||||||
|
pubkey: HexKey;
|
||||||
|
lnurl?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
event?: string;
|
||||||
|
}) => {
|
||||||
const profile = useUserProfile(System, pubkey);
|
const profile = useUserProfile(System, pubkey);
|
||||||
const [zap, setZap] = useState(false);
|
const [zap, setZap] = useState(false);
|
||||||
const service = lnurl ?? (profile?.lud16 || profile?.lud06);
|
const service = lnurl ?? (profile?.lud16 || profile?.lud06);
|
||||||
@ -25,6 +35,7 @@ const ZapButton = ({ pubkey, lnurl, children }: { pubkey: HexKey; lnurl?: string
|
|||||||
show={zap}
|
show={zap}
|
||||||
onClose={() => setZap(false)}
|
onClose={() => setZap(false)}
|
||||||
author={pubkey}
|
author={pubkey}
|
||||||
|
note={event}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
21
packages/app/src/Element/ZapGoal.css
Normal file
21
packages/app/src/Element/ZapGoal.css
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.zap-goal {
|
||||||
|
}
|
||||||
|
|
||||||
|
.zap-goal h1 {
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zap-goal .progress {
|
||||||
|
position: relative;
|
||||||
|
height: 1em;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zap-goal .progress > div {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--success);
|
||||||
|
width: var(--progress);
|
||||||
|
height: 100%;
|
||||||
|
}
|
38
packages/app/src/Element/ZapGoal.tsx
Normal file
38
packages/app/src/Element/ZapGoal.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import "./ZapGoal.css";
|
||||||
|
import { NostrEvent, NostrPrefix, createNostrLink } from "@snort/system";
|
||||||
|
import useZapsFeed from "Feed/ZapsFeed";
|
||||||
|
import { formatShort } from "Number";
|
||||||
|
import { findTag } from "SnortUtils";
|
||||||
|
import { CSSProperties } from "react";
|
||||||
|
import ZapButton from "./ZapButton";
|
||||||
|
|
||||||
|
export function ZapGoal({ ev }: { ev: NostrEvent }) {
|
||||||
|
const zaps = useZapsFeed(createNostrLink(NostrPrefix.Note, ev.id));
|
||||||
|
const target = Number(findTag(ev, "amount"));
|
||||||
|
const amount = zaps.reduce((acc, v) => (acc += v.amount * 1000), 0);
|
||||||
|
const progress = Math.min(100, 100 * (amount / target));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="zap-goal card">
|
||||||
|
<div className="flex f-space">
|
||||||
|
<h2>{ev.content}</h2>
|
||||||
|
<ZapButton pubkey={ev.pubkey} event={ev.id} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex f-space">
|
||||||
|
<div>{progress.toFixed(1)}%</div>
|
||||||
|
<div>
|
||||||
|
{formatShort(amount / 1000)}/{formatShort(target / 1000)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="progress">
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--progress": `${progress}%`,
|
||||||
|
} as CSSProperties
|
||||||
|
}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,25 +1,27 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { HexKey, EventKind, FlatNoteStore, RequestBuilder, parseZap } from "@snort/system";
|
import { EventKind, RequestBuilder, parseZap, NostrLink, NostrPrefix, NoteCollection } from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import { UserCache } from "Cache";
|
import { UserCache } from "Cache";
|
||||||
|
|
||||||
export default function useZapsFeed(pubkey?: HexKey) {
|
export default function useZapsFeed(link?: NostrLink) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
if (!pubkey) return null;
|
if (!link) return null;
|
||||||
const b = new RequestBuilder(`zaps:${pubkey.slice(0, 12)}`);
|
const b = new RequestBuilder(`zaps:${link.encode()}`);
|
||||||
b.withFilter().tag("p", [pubkey]).kinds([EventKind.ZapReceipt]);
|
if (link.type === NostrPrefix.PublicKey) {
|
||||||
|
b.withFilter().tag("p", [link.id]).kinds([EventKind.ZapReceipt]);
|
||||||
|
} else if (link.type === NostrPrefix.Event || link.type === NostrPrefix.Note) {
|
||||||
|
b.withFilter().tag("e", [link.id]).kinds([EventKind.ZapReceipt]);
|
||||||
|
}
|
||||||
return b;
|
return b;
|
||||||
}, [pubkey]);
|
}, [link]);
|
||||||
|
|
||||||
const zapsFeed = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub);
|
const zapsFeed = useRequestBuilder(System, NoteCollection, sub);
|
||||||
|
|
||||||
const zaps = useMemo(() => {
|
const zaps = useMemo(() => {
|
||||||
if (zapsFeed.data) {
|
if (zapsFeed.data) {
|
||||||
const profileZaps = zapsFeed.data
|
const profileZaps = zapsFeed.data.map(a => parseZap(a, UserCache)).filter(z => z.valid);
|
||||||
.map(a => parseZap(a, UserCache))
|
|
||||||
.filter(z => z.valid && z.receiver === pubkey && z.sender !== pubkey && !z.event);
|
|
||||||
profileZaps.sort((a, b) => b.amount - a.amount);
|
profileZaps.sort((a, b) => b.amount - a.amount);
|
||||||
return profileZaps;
|
return profileZaps;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
|
createNostrLink,
|
||||||
encodeTLV,
|
encodeTLV,
|
||||||
encodeTLVEntries,
|
encodeTLVEntries,
|
||||||
EventKind,
|
EventKind,
|
||||||
@ -68,7 +69,7 @@ const RELAYS = 7;
|
|||||||
const BOOKMARKS = 8;
|
const BOOKMARKS = 8;
|
||||||
|
|
||||||
function ZapsProfileTab({ id }: { id: HexKey }) {
|
function ZapsProfileTab({ id }: { id: HexKey }) {
|
||||||
const zaps = useZapsFeed(id);
|
const zaps = useZapsFeed(createNostrLink(NostrPrefix.PublicKey, id));
|
||||||
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||||
return (
|
return (
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
|
@ -64,7 +64,7 @@ export class EventPublisher {
|
|||||||
/**
|
/**
|
||||||
* Apply POW to every event
|
* Apply POW to every event
|
||||||
*/
|
*/
|
||||||
pow(target:number, miner?: PowMiner) {
|
pow(target: number, miner?: PowMiner) {
|
||||||
this.#pow = target;
|
this.#pow = target;
|
||||||
this.#miner = miner;
|
this.#miner = miner;
|
||||||
}
|
}
|
||||||
|
@ -180,7 +180,7 @@ export class Query implements QueryBase {
|
|||||||
onEvent(sub: string, e: TaggedNostrEvent) {
|
onEvent(sub: string, e: TaggedNostrEvent) {
|
||||||
for (const t of this.#tracing) {
|
for (const t of this.#tracing) {
|
||||||
if (t.id === sub) {
|
if (t.id === sub) {
|
||||||
if(t.filters.some(v => eventMatchesFilter(e, v))) {
|
if (t.filters.some(v => eventMatchesFilter(e, v))) {
|
||||||
this.feed.add(e);
|
this.feed.add(e);
|
||||||
} else {
|
} else {
|
||||||
this.#log("Event did not match filter, rejecting %O", e);
|
this.#log("Event did not match filter, rejecting %O", e);
|
||||||
|
@ -49,4 +49,4 @@ export function splitByUrl(str: string) {
|
|||||||
/((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i;
|
/((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i;
|
||||||
|
|
||||||
return str.split(urlRegex);
|
return str.split(urlRegex);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user