This commit is contained in:
Kieran 2023-08-28 15:07:59 +01:00
parent 9bbbd513c2
commit b5033798c4
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
21 changed files with 427 additions and 103 deletions

View File

@ -6,19 +6,20 @@ import Confetti from "react-confetti";
import { type NostrEvent } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { findTag } from "utils";
import { eventToLink, findTag } from "utils";
import { formatSats } from "number";
import usePreviousValue from "hooks/usePreviousValue";
import { SendZapsDialog } from "element/send-zap";
import { useZaps } from "hooks/goals";
import { getName } from "element/profile";
import { Icon } from "./icon";
import { FormattedMessage } from "react-intl";
import { useZaps } from "hooks/zaps";
export function Goal({ ev }: { ev: NostrEvent }) {
const profile = useUserProfile(ev.pubkey);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const zaps = useZaps(ev, true);
const link = eventToLink(ev);
const zaps = useZaps(link, true);
const goalAmount = useMemo(() => {
const amount = findTag(ev, "amount");
return amount ? Number(amount) / 1000 : null;

View File

@ -1,7 +1,8 @@
import "./live-chat.css";
import { FormattedMessage } from "react-intl";
import { EventKind, NostrPrefix, NostrLink, ParsedZap, NostrEvent, parseZap, encodeTLV } from "@snort/system";
import { unixNow, unwrap } from "@snort/shared";
import { useEffect, useMemo } from "react";
import { unixNow } from "@snort/shared";
import { useMemo } from "react";
import uniqBy from "lodash.uniqby";
import { Icon } from "element/icon";
@ -18,13 +19,12 @@ import { useLiveChatFeed } from "hooks/live-chat";
import { useMutedPubkeys } from "hooks/lists";
import { useBadges } from "hooks/badges";
import { useLogin } from "hooks/login";
import useTopZappers from "hooks/top-zappers";
import { useAddress } from "hooks/event";
import { formatSats } from "number";
import { WEEK, LIVE_STREAM_CHAT } from "const";
import { findTag, getTagValues, getHost } from "utils";
import { System } from "index";
import { FormattedMessage } from "react-intl";
import { TopZappers } from "element/top-zappers";
export interface LiveChatOptions {
canWrite?: boolean;
@ -49,28 +49,6 @@ function BadgeAward({ ev }: { ev: NostrEvent }) {
);
}
function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
const zappers = useTopZappers(zaps);
return (
<>
{zappers.map(({ pubkey, total }) => {
return (
<div className="top-zapper" key={pubkey}>
{pubkey === "anon" ? (
<p className="top-zapper-name">Anon</p>
) : (
<Profile pubkey={pubkey} options={{ showName: false }} />
)}
<Icon name="zap-filled" className="zap-icon" />
<p className="top-zapper-amount">{formatSats(total)}</p>
</div>
);
})}
</>
);
}
export function LiveChat({
link,
ev,
@ -87,11 +65,6 @@ export function LiveChat({
const host = getHost(ev);
const feed = useLiveChatFeed(link, goal ? [goal.id] : undefined);
const login = useLogin();
useEffect(() => {
const pubkeys = [...new Set(feed.zaps.flatMap(a => [a.pubkey, unwrap(findTag(a, "p"))]))];
System.ProfileLoader.TrackMetadata(pubkeys);
return () => System.ProfileLoader.UntrackMetadata(pubkeys);
}, [feed.zaps]);
const started = useMemo(() => {
const starts = findTag(ev, "starts");
return starts ? Number(starts) : unixNow() - WEEK;
@ -111,7 +84,6 @@ export function LiveChat({
const events = useMemo(() => {
return [...feed.messages, ...feed.zaps, ...awards].sort((a, b) => b.created_at - a.created_at);
}, [feed.messages, feed.zaps, awards]);
const streamer = getHost(ev);
const naddr = useMemo(() => {
if (ev) {
return encodeTLV(NostrPrefix.Address, findTag(ev, "d") ?? "", undefined, ev.kind, ev.pubkey);
@ -145,7 +117,7 @@ export function LiveChat({
<TopZappers zaps={zaps} />
</div>
{goal && <Goal ev={goal} />}
{login?.pubkey === streamer && <NewGoalDialog link={link} />}
{login?.pubkey === host && <NewGoalDialog link={link} />}
</div>
)}
<div className="messages">
@ -159,7 +131,7 @@ export function LiveChat({
<ChatMessage
badges={badges}
emojiPacks={allEmojiPacks}
streamer={streamer}
streamer={host}
ev={a}
key={a.id}
reactions={feed.reactions}
@ -167,7 +139,7 @@ export function LiveChat({
);
}
case EventKind.ZapReceipt: {
const zap = zaps.find(b => b.id === a.id && b.receiver === streamer);
const zap = zaps.find(b => b.id === a.id && b.receiver === host);
if (zap) {
return <ChatZap zap={zap} key={a.id} />;
}

View File

@ -0,0 +1,27 @@
import { ParsedZap } from "@snort/system";
import useTopZappers from "hooks/top-zappers";
import { formatSats } from "number";
import { Icon } from "./icon";
import { Profile } from "./profile";
export function TopZappers({ zaps, limit }: { zaps: ParsedZap[], limit?: number }) {
const zappers = useTopZappers(zaps);
return (
<>
{zappers.slice(0, limit ?? 10).map(({ pubkey, total }) => {
return (
<div className="top-zapper" key={pubkey}>
{pubkey === "anon" ? (
<p className="top-zapper-name">Anon</p>
) : (
<Profile pubkey={pubkey} options={{ showName: false }} />
)}
<Icon name="zap-filled" className="zap-icon" />
<p className="top-zapper-amount">{formatSats(total)}</p>
</div>
);
})}
</>
);
}

View File

@ -1,30 +1,8 @@
import { useMemo } from "react";
import {
EventKind,
NostrEvent,
RequestBuilder,
NoteCollection,
ReplaceableNoteStore,
NostrLink,
parseZap,
} from "@snort/system";
import { RequestBuilder, ReplaceableNoteStore, NostrLink } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { unwrap } from "@snort/shared";
import { GOAL } from "const";
import { System } from "index";
export function useZaps(goal: NostrEvent, leaveOpen = false) {
const sub = useMemo(() => {
const b = new RequestBuilder(`goal-zaps:${goal.id.slice(0, 12)}`);
b.withOptions({ leaveOpen });
b.withFilter().kinds([EventKind.ZapReceipt]).tag("e", [goal.id]).since(goal.created_at);
return b;
}, [goal, leaveOpen]);
const { data } = useRequestBuilder(NoteCollection, sub);
return data?.map(ev => parseZap(ev, System.ProfileLoader.Cache)).filter(z => z && z.valid) ?? [];
}
export function useZapGoal(host: string, link?: NostrLink, leaveOpen = false) {
const sub = useMemo(() => {

View File

@ -1,10 +1,12 @@
import { NostrLink, RequestBuilder, EventKind, NoteCollection } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { unixNow } from "@snort/shared";
import { useMemo } from "react";
import { SnortContext, useRequestBuilder } from "@snort/system-react";
import { unixNow, unwrap } from "@snort/shared";
import { useContext, useEffect, useMemo } from "react";
import { LIVE_STREAM_CHAT, WEEK } from "const";
import { findTag } from "utils";
export function useLiveChatFeed(link: NostrLink, eZaps?: Array<string>) {
const system = useContext(SnortContext);
const since = useMemo(() => unixNow() - WEEK, [link.id]);
const sub = useMemo(() => {
const rb = new RequestBuilder(`live:${link.id}:${link.author}`);
@ -44,6 +46,12 @@ export function useLiveChatFeed(link: NostrLink, eZaps?: Array<string>) {
return rb;
}, [etags]);
useEffect(() => {
const pubkeys = [...new Set(zaps.flatMap(a => [a.pubkey, unwrap(findTag(a, "p"))]))];
system.ProfileLoader.TrackMetadata(pubkeys);
return () => system.ProfileLoader.UntrackMetadata(pubkeys);
}, [zaps]);
const reactionsSub = useRequestBuilder(NoteCollection, esub);
const reactions = reactionsSub.data ?? [];

30
src/hooks/stream-link.ts Normal file
View File

@ -0,0 +1,30 @@
import { fetchNip05Pubkey, hexToBech32 } from "@snort/shared";
import { NostrLink, tryParseNostrLink, NostrPrefix } from "@snort/system";
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
export function useStreamLink() {
const params = useParams();
const [link, setLink] = useState<NostrLink>();
useEffect(() => {
if (params.id) {
const parsedLink = tryParseNostrLink(params.id);
if (parsedLink) {
setLink(parsedLink);
} else {
const [handle, domain] = (params.id.includes("@") ? params.id : `${params.id}@zap.stream`).split("@");
fetchNip05Pubkey(handle, domain).then(d => {
if (d) {
setLink({
id: d,
type: NostrPrefix.PublicKey,
encode: () => hexToBech32(NostrPrefix.PublicKey, d),
} as NostrLink);
}
});
}
}
}, [params.id]);
return link;
}

40
src/hooks/zaps.ts Normal file
View File

@ -0,0 +1,40 @@
import { unwrap } from "@snort/shared";
import { NostrLink, RequestBuilder, NostrPrefix, EventKind, NoteCollection, parseZap } from "@snort/system";
import { SnortContext, useRequestBuilder } from "@snort/system-react";
import { System } from "index";
import { useContext, useMemo, useEffect } from "react";
import { findTag } from "utils";
export function useZaps(link?: NostrLink, leaveOpen = false) {
const system = useContext(SnortContext);
const sub = useMemo(() => {
if (link) {
const b = new RequestBuilder(`zaps:${link.id}`);
b.withOptions({ leaveOpen });
if (link.type === NostrPrefix.Event || link.type === NostrPrefix.Note) {
b.withFilter().kinds([EventKind.ZapReceipt]).tag("e", [link.id]);
} else if (link.type === NostrPrefix.Address) {
b.withFilter()
.kinds([EventKind.ZapReceipt])
.tag("a", [`${link.kind}:${link.author}:${link.id}`]);
}
return b;
}
return null;
}, [link, leaveOpen]);
const { data: zaps } = useRequestBuilder(NoteCollection, sub);
useEffect(() => {
const pubkeys = zaps ? [...new Set(zaps.flatMap(a => [a.pubkey, unwrap(findTag(a, "p"))]))] : [];
system.ProfileLoader.TrackMetadata(pubkeys);
return () => system.ProfileLoader.UntrackMetadata(pubkeys);
}, [zaps]);
return (
[...(zaps ?? [])]
.sort((a, b) => (b.created_at > a.created_at ? 1 : -1))
.map(ev => parseZap(ev, System.ProfileLoader.Cache))
.filter(z => z && z.valid) ?? []
);
}

View File

@ -76,6 +76,10 @@ a {
color: white;
}
.g8 {
gap: 8px;
}
.g24 {
gap: 24px;
}

View File

@ -20,6 +20,8 @@ import { defaultRelays } from "const";
import { CatchAllRoutePage } from "pages/catch-all";
import { register } from "serviceWorker";
import { IntlProvider } from "intl";
import { WidgetsPage } from "pages/widgets";
import { AlertsPage } from "pages/alerts";
export enum StreamState {
Live = "live",
@ -65,6 +67,10 @@ const router = createBrowserRouter([
path: "/providers/:id?",
element: <StreamProvidersPage />,
},
{
path: "/widgets",
element: <WidgetsPage />
},
{
path: "*",
element: <CatchAllRoutePage />,
@ -74,7 +80,19 @@ const router = createBrowserRouter([
{
path: "/chat/:id",
element: <ChatPopout />,
loader: async () => {
await System.Init();
return null;
},
},
{
path: "/alert/:id/:type",
element: <AlertsPage />,
loader: async () => {
await System.Init();
return null;
},
}
]);
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLDivElement);
root.render(

View File

@ -143,6 +143,9 @@
"QceMQZ": {
"defaultMessage": "Goal: {amount}"
},
"Qe1MJu": {
"defaultMessage": "{name} with {amount}"
},
"RJOmzk": {
"defaultMessage": "I have read and agree with {provider}''s {terms}."
},
@ -194,6 +197,9 @@
"hGQqkW": {
"defaultMessage": "Schedule"
},
"hpl4BP": {
"defaultMessage": "Chat Widget"
},
"ieGrWo": {
"defaultMessage": "Follow"
},
@ -245,12 +251,18 @@
"rfC1Zq": {
"defaultMessage": "Save card"
},
"rgsbu9": {
"defaultMessage": "Current Viewers"
},
"s5ksS7": {
"defaultMessage": "Image Link"
},
"s7V+5p": {
"defaultMessage": "Confirm your age"
},
"tG1ST3": {
"defaultMessage": "Incoming Zap"
},
"thsiMl": {
"defaultMessage": "terms and conditions"
},
@ -277,5 +289,8 @@
},
"x82IOl": {
"defaultMessage": "Mute"
},
"zVDHAu": {
"defaultMessage": "Zap Alert"
}
}

78
src/pages/alerts.css Normal file
View File

@ -0,0 +1,78 @@
.zap-alert-widgets .zap-alert {
animation: cssAnimation 0s ease-in 5s forwards;
animation-fill-mode: forwards;
}
.zap-alert {
display: inline-flex;
flex-direction: column;
align-items: center;
}
.zap-alert > div:nth-of-type(1) {
width: fit-content;
font-size: 21px;
font-weight: 600;
border-radius: 33px;
background: linear-gradient(135deg, #882bff 0%, #f83838 100%);
margin: 0;
padding: 8px 24px;
margin-bottom: -11px;
z-index: 2;
}
.zap-alert > div:nth-of-type(2) {
z-index: 1;
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 17px;
background: #2d2d2d;
padding: 27px 90px;
font-size: 29px;
font-weight: 600;
}
.zap-alert .highlight {
color: #ff4468;
}
@keyframes cssAnimation {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.views {
display: inline-block;
border-radius: 16px;
background: #222;
padding: 16px 24px;
font-size: 21px;
font-weight: 600;
line-height: 32px;
}
.top-zappers-widget {
display: inline-flex;
border-radius: 16px;
background: #222;
padding: 16px 24px;
font-size: 21px;
font-weight: 600;
align-items: center;
gap: 16px;
}
.top-zappers-widget .top-zapper {
background-color: #3f3f3f;
border: unset;
}
.top-zappers-widget .profile > img {
width: 24px;
height: 24px;
}

29
src/pages/alerts.tsx Normal file
View File

@ -0,0 +1,29 @@
import "./alerts.css";
import Spinner from "element/spinner";
import { useStreamLink } from "hooks/stream-link";
import { useParams } from "react-router-dom";
import { ZapAlerts } from "./widgets/zaps";
import { Views } from "./widgets/views";
import { TopZappersWidget } from "./widgets/top-zappers";
export function AlertsPage() {
const params = useParams();
const link = useStreamLink();
if (!link) {
return <Spinner />
}
switch (params.type) {
case "zaps": {
return <ZapAlerts link={link} />
}
case "views": {
return <Views link={link} />
}
case "top-zappers": {
return <TopZappersWidget link={link} />
}
}
return null;
}

View File

@ -1,11 +1,10 @@
import "./stream-page.css";
import { NostrLink, NostrPrefix, TaggedNostrEvent, tryParseNostrLink } from "@snort/system";
import { fetchNip05Pubkey } from "@snort/shared";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { useLocation, useNavigate } from "react-router-dom";
import { Helmet } from "react-helmet";
import { LiveVideoPlayer } from "element/live-video-player";
import { createNostrLink, findTag, getEventFromLocationState, getHost, hexToBech32 } from "utils";
import { eventToLink, findTag, getEventFromLocationState, getHost } from "utils";
import { Profile, getName } from "element/profile";
import { LiveChat } from "element/live-chat";
import AsyncButton from "element/async-button";
@ -24,7 +23,7 @@ import { StreamTimer } from "element/stream-time";
import { ShareMenu } from "element/share-menu";
import { ContentWarningOverlay, isContentWarningAccepted } from "element/content-warning";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { useEffect, useState } from "react";
import { useStreamLink } from "hooks/stream-link";
function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedNostrEvent }) {
const login = useLogin();
@ -97,30 +96,9 @@ function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedNostrEvent })
}
export function StreamPageHandler() {
const params = useParams();
const location = useLocation();
const evPreload = getEventFromLocationState(location.state);
const [link, setLink] = useState<NostrLink>();
useEffect(() => {
if (params.id) {
const parsedLink = tryParseNostrLink(params.id);
if (parsedLink) {
setLink(parsedLink);
} else {
const [handle, domain] = (params.id.includes("@") ? params.id : `${params.id}@zap.stream`).split("@");
fetchNip05Pubkey(handle, domain).then(d => {
if (d) {
setLink({
id: d,
type: NostrPrefix.PublicKey,
encode: () => hexToBech32(NostrPrefix.PublicKey, d),
} as NostrLink);
}
});
}
}
}, [params.id]);
const link = useStreamLink();
if (link) {
return <StreamPage link={link} evPreload={evPreload} />;
@ -130,7 +108,8 @@ export function StreamPageHandler() {
export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link: NostrLink }) {
const ev = useCurrentStreamFeed(link, true, evPreload);
const host = getHost(ev);
const goal = useZapGoal(host, createNostrLink(ev), true);
const evLink = ev ? eventToLink(ev) : undefined;
const goal = useZapGoal(host, evLink, true);
const title = findTag(ev, "title");
const summary = findTag(ev, "summary");
@ -161,7 +140,7 @@ export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link:
<ProfileInfo ev={ev} goal={goal} />
<StreamCards host={host} />
</div>
<LiveChat link={createNostrLink(ev) ?? link} ev={ev} goal={goal} />
<LiveChat link={evLink ?? link} ev={ev} goal={goal} />
</div>
);
}

10
src/pages/widgets.css Normal file
View File

@ -0,0 +1,10 @@
.widgets {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
.widgets > div {
background-color: #3f3f3f;
border-radius: 16px;
padding: 8px 12px;
}

58
src/pages/widgets.tsx Normal file
View File

@ -0,0 +1,58 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import "./widgets.css";
import { NostrPrefix, createNostrLink } from "@snort/system";
import Copy from "element/copy";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { useLogin } from "hooks/login";
import { FormattedMessage } from "react-intl";
import { eventToLink, hexToBech32 } from "utils";
import { ZapAlertItem } from "./widgets/zaps";
import { TopZappersWidget } from "./widgets/top-zappers";
import { Views } from "./widgets/views";
export function WidgetsPage() {
const login = useLogin();
const profileLink = createNostrLink(NostrPrefix.PublicKey, login?.pubkey ?? "");
const current = useCurrentStreamFeed(profileLink);
const currentLink = current ? eventToLink(current) : undefined;
const npub = hexToBech32("npub", login?.pubkey);
const baseUrl = `${window.location.protocol}//${window.location.host}`;
return <div className="widgets g8">
<div className="flex f-col g8">
<h3>
<FormattedMessage defaultMessage="Chat Widget" />
</h3>
<Copy text={`${baseUrl}/chat/${npub}`} />
</div>
<div className="flex f-col g8">
<h3>
<FormattedMessage defaultMessage="Zap Alert" />
</h3>
<Copy text={`${baseUrl}/alert/${npub}/zaps`} />
<ZapAlertItem item={{
id: "",
valid: true,
zapService: "",
anonZap: false,
errors: [],
sender: login?.pubkey,
amount: 1_000_000
}} />
</div>
<div className="flex f-col g8">
<h3>
<FormattedMessage defaultMessage="Top Zappers" />
</h3>
<Copy text={`${baseUrl}/alert/${npub}/top-zappers`} />
{currentLink && <TopZappersWidget link={currentLink} />}
</div>
<div className="flex f-col g8">
<h3>
<FormattedMessage defaultMessage="Current Viewers" />
</h3>
<Copy text={`${baseUrl}/alert/${npub}/views`} />
{currentLink && <Views link={currentLink} />}
</div>
</div>
}

View File

@ -0,0 +1,19 @@
import { NostrLink } from "@snort/system";
import { TopZappers } from "element/top-zappers";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { useZaps } from "hooks/zaps";
import { FormattedMessage } from "react-intl";
import { eventToLink } from "utils";
export function TopZappersWidget({ link }: { link: NostrLink }) {
const currentEvent = useCurrentStreamFeed(link, true);
const zaps = useZaps(currentEvent ? eventToLink(currentEvent) : undefined, true);
return <div className="top-zappers-widget">
<div>
<FormattedMessage defaultMessage="Top Zappers" />
</div>
<div className="flex g8">
<TopZappers zaps={zaps} limit={3} />
</div>
</div>;
}

View File

@ -0,0 +1,13 @@
import { NostrLink } from "@snort/system";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { FormattedMessage } from "react-intl";
import { findTag } from "utils";
export function Views({ link }: { link: NostrLink }) {
const current = useCurrentStreamFeed(link);
const viewers = findTag(current, "current_participants");
return <div className="views">
<FormattedMessage defaultMessage="{n} viewers" values={{ n: Number(viewers) }} />
</div>
}

View File

@ -0,0 +1,34 @@
import { hexToBech32 } from "@snort/shared";
import { NostrLink, ParsedZap } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
import { useZaps } from "hooks/zaps";
import { formatSats } from "number";
import { FormattedMessage } from "react-intl";
import { eventToLink } from "utils";
export function ZapAlerts({ link }: { link: NostrLink }) {
const currentEvent = useCurrentStreamFeed(link, true);
const currentLink = currentEvent ? eventToLink(currentEvent) : undefined;
const zaps = useZaps(currentLink, true);
return <div className="flex f-center f-col zap-alert-widgets">
{zaps.slice(0, 5).map(v => <ZapAlertItem key={v.id} item={v} />)}
</div>
}
export function ZapAlertItem({ item }: { item: ParsedZap }) {
const profile = useUserProfile(item.sender);
if (!profile) return;
return <div className="zap-alert">
<div>
<FormattedMessage defaultMessage="Incoming Zap" />
</div>
<div>
<FormattedMessage defaultMessage="{name} with {amount}" values={{
name: <span className="highlight">{profile?.name ?? hexToBech32("npub", item?.sender ?? "").slice(0, 12)}&nbsp;</span>,
amount: <span className="highlight">&nbsp;{formatSats(item.amount)}</span>
}} />
</div>
</div>
}

View File

@ -47,6 +47,7 @@
"QRHNuF": "What are we steaming today?",
"QRRCp0": "Stream URL",
"QceMQZ": "Goal: {amount}",
"Qe1MJu": "{name} with {amount}",
"RJOmzk": "I have read and agree with {provider}''s {terms}.",
"RXQdxR": "Please login to write messages!",
"RrCui3": "Summary",
@ -64,6 +65,7 @@
"ebmhes": "Nostr Extension",
"fBI91o": "Zap",
"hGQqkW": "Schedule",
"hpl4BP": "Chat Widget",
"ieGrWo": "Follow",
"itPgxd": "Profile",
"izWS4J": "Unfollow",
@ -81,8 +83,10 @@
"rWBFZA": "Sexually explicit material ahead!",
"rbrahO": "Close",
"rfC1Zq": "Save card",
"rgsbu9": "Current Viewers",
"s5ksS7": "Image Link",
"s7V+5p": "Confirm your age",
"tG1ST3": "Incoming Zap",
"thsiMl": "terms and conditions",
"tzMNF3": "Status",
"uYw2LD": "Stream",
@ -91,5 +95,6 @@
"wEQDC6": "Edit",
"wOy57k": "Add stream goal",
"wzWWzV": "Top zappers",
"x82IOl": "Mute"
"x82IOl": "Mute",
"zVDHAu": "Zap Alert"
}

View File

@ -1,8 +1,9 @@
import { NostrEvent, NostrPrefix, TaggedNostrEvent, encodeTLV, parseNostrLink } from "@snort/system";
import { NostrEvent, NostrPrefix, TaggedNostrEvent, createNostrLink, encodeTLV } from "@snort/system";
import * as utils from "@noble/curves/abstract/utils";
import { bech32 } from "@scure/base";
import type { Tag, Tags } from "types";
import { LIVE_STREAM } from "const";
import { unwrap } from "@snort/shared";
export function toAddress(e: NostrEvent): string {
if (e.kind && e.kind >= 30000 && e.kind <= 40000) {
@ -76,11 +77,6 @@ export function eventLink(ev: NostrEvent | TaggedNostrEvent) {
}
}
export function createNostrLink(ev?: NostrEvent) {
if (!ev) return;
return parseNostrLink(eventLink(ev));
}
export function getHost(ev?: NostrEvent) {
return ev?.tags.find(a => a[0] === "p" && a[3] === "host")?.[1] ?? ev?.pubkey ?? "";
}
@ -114,3 +110,11 @@ export function getEventFromLocationState(state: unknown | undefined | null) {
? (state as NostrEvent)
: undefined;
}
export function eventToLink(ev: NostrEvent) {
if (ev.kind >= 30_000 && ev.kind < 40_000) {
const dTag = unwrap(findTag(ev, "d"));
return createNostrLink(NostrPrefix.Address, dTag, undefined, ev.kind, ev.pubkey);
}
return createNostrLink(NostrPrefix.Event, ev.id, undefined, ev.kind, ev.pubkey);
}

View File

@ -62,9 +62,11 @@ const config = {
__XXX: process.env["__XXX"] || JSON.stringify(false),
__XXX_HOST: JSON.stringify("https://xxzap.com"),
}),
new WorkboxPlugin.InjectManifest({
swSrc: "./src/service-worker.ts",
}),
isProduction
? new WorkboxPlugin.InjectManifest({
swSrc: "./src/service-worker.ts",
})
: false,
],
module: {
rules: [