refactor: optimize bundle size

This commit is contained in:
Kieran 2023-12-05 12:58:17 +00:00
parent 6905fb63fd
commit 13edd58987
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
21 changed files with 501 additions and 293 deletions

View File

@ -5,15 +5,14 @@
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
"@getalby/bitcoin-connect-react": "^1.1.0",
"@noble/curves": "^1.1.0",
"@noble/hashes": "^1.3.1",
"@noble/curves": "^1.2.0",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@react-hook/resize-observer": "^1.2.6",
"@scure/base": "^1.1.1",
"@scure/base": "^1.1.3",
"@snort/shared": "^1.0.10",
"@snort/system": "^1.1.5",
"@snort/system-react": "^1.1.5",
@ -28,7 +27,6 @@
"flag-icons": "^6.11.0",
"hls.js": "^1.4.6",
"lodash": "^4.17.21",
"lodash.uniqby": "^4.7.0",
"marked": "^9.1.2",
"qr-code-styling": "^1.6.0-rc.1",
"react": "^18.2.0",
@ -99,6 +97,7 @@
"postcss": "^8.4.32",
"prettier": "^2.8.8",
"prop-types": "^15.8.1",
"rollup-plugin-visualizer": "^5.10.0",
"tailwindcss": "^3.3.5",
"typescript": "^5.2.2",
"vite": "^5.0.5",

View File

@ -0,0 +1,5 @@
import { Button as AlbyZapsButton } from "@getalby/bitcoin-connect-react";
export default function AlbyButton() {
return <AlbyZapsButton />
}

View File

@ -1,10 +1,10 @@
import { SnortContext, useEventReactions, useUserProfile } from "@snort/system-react";
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
import React, { useContext, useMemo, useRef, useState } from "react";
import React, { Suspense, lazy, useContext, useMemo, useRef, useState } from "react";
import { useHover, useIntersectionObserver, useMediaQuery, useOnClickOutside } from "usehooks-ts";
import { dedupe } from "@snort/shared";
import { EmojiPicker } from "./emoji-picker";
const EmojiPicker = lazy(() => import("./emoji-picker"));
import { Icon } from "./icon";
import { Emoji as EmojiComponent } from "./emoji";
import { Profile } from "./profile";
@ -176,15 +176,15 @@ export function ChatMessage({
style={
isTablet
? {
display: showZapDialog || isHovering ? "flex" : "none",
}
display: showZapDialog || isHovering ? "flex" : "none",
}
: {
position: "fixed",
top: topOffset ? topOffset - 12 : 0,
left: leftOffset ? leftOffset - 32 : 0,
opacity: showZapDialog || isHovering ? 1 : 0,
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
}
position: "fixed",
top: topOffset ? topOffset - 12 : 0,
left: leftOffset ? leftOffset - 32 : 0,
opacity: showZapDialog || isHovering ? 1 : 0,
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
}
}>
{zapTarget && (
<SendZapsDialog
@ -211,14 +211,16 @@ export function ChatMessage({
)}
</div>
{showEmojiPicker && (
<EmojiPicker
topOffset={topOffset ?? 0}
leftOffset={leftOffset ?? 0}
emojiPacks={emojiPacks}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
<Suspense>
<EmojiPicker
topOffset={topOffset ?? 0}
leftOffset={leftOffset ?? 0}
emojiPacks={emojiPacks}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
</Suspense>
)}
</>
);

View File

@ -13,7 +13,7 @@ interface EmojiPickerProps {
ref: RefObject<HTMLDivElement>;
}
export function EmojiPicker({
export default function EmojiPicker({
topOffset,
leftOffset,
onEmojiSelect,

View File

@ -4,7 +4,6 @@ import { EventKind, NostrEvent, NostrLink, ParsedZap } from "@snort/system";
import { useEventReactions } from "@snort/system-react";
import { unixNow } from "@snort/shared";
import { useMemo } from "react";
import uniqBy from "lodash.uniqby";
import { Icon } from "./icon";
import Spinner from "./spinner";
@ -22,7 +21,7 @@ import { useLogin } from "@/hooks/login";
import { useAddress } from "@/hooks/event";
import { formatSats } from "@/number";
import { LIVE_STREAM_CHAT, WEEK } from "@/const";
import { findTag, getHost, getTagValues } from "@/utils";
import { findTag, getHost, getTagValues, uniqBy } from "@/utils";
import { TopZappers } from "./top-zappers";
export interface LiveChatOptions {

View File

@ -17,7 +17,7 @@ export interface VideoPlayerProps {
poster?: string;
}
export function LiveVideoPlayer(props: VideoPlayerProps) {
export default function LiveVideoPlayer(props: VideoPlayerProps) {
const video = useRef<HTMLVideoElement>(null);
const hlsObj = useRef<Hls>(null);
const streamCached = useMemo(() => props.stream, [props.stream]);

View File

@ -82,7 +82,7 @@ function renderToken(t: Token): ReactNode {
}
}
export const Markdown = forwardRef<HTMLDivElement, MarkdownProps>((props: MarkdownProps, ref) => {
const Markdown = forwardRef<HTMLDivElement, MarkdownProps>((props: MarkdownProps, ref) => {
const parsed = useMemo(() => {
return marked.lexer(props.content);
}, [props.content, props.tags]);
@ -93,3 +93,5 @@ export const Markdown = forwardRef<HTMLDivElement, MarkdownProps>((props: Markdo
</div>
);
});
export default Markdown;

View File

@ -1,8 +1,9 @@
import "./note.css";
import { lazy } from "react";
import { type NostrEvent, NostrPrefix } from "@snort/system";
import { hexToBech32 } from "@snort/shared";
import { Markdown } from "./markdown";
const Markdown = lazy(() => import("./markdown"));
import { ExternalIconLink } from "./external-link";
import { Profile } from "./profile";

View File

@ -1,6 +1,6 @@
import "./stream-cards.css";
import { forwardRef, useContext, useState } from "react";
import { Suspense, forwardRef, lazy, useContext, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import * as Dialog from "@radix-ui/react-dialog";
import { DndProvider, useDrag, useDrop } from "react-dnd";
@ -10,11 +10,11 @@ import { removeUndefined, unwrap } from "@snort/shared";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { SnortContext } from "@snort/system-react";
const Markdown = lazy(() => import("./markdown"));
import { Toggle } from "./toggle";
import { Icon } from "./icon";
import { ExternalLink } from "./external-link";
import { FileUploader } from "./file-uploader";
import { Markdown } from "./markdown";
import { useLogin } from "@/hooks/login";
import { useCards, useUserCards } from "@/hooks/cards";
import { CARD, USER_CARDS } from "@/const";
@ -57,7 +57,9 @@ const CardPreview = forwardRef(({ style, title, link, image, content }: CardPrev
) : (
<img className="card-image" src={image} alt={title} />
))}
<Markdown content={content} />
<Suspense>
<Markdown content={content} />
</Suspense>
</div>
);
});

View File

@ -0,0 +1,227 @@
import { LIVE_STREAM_CHAT } from "@/const";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { useLiveChatFeed } from "@/hooks/live-chat";
import { formatSats } from "@/number";
import { findTag } from "@/utils";
import { unixNow } from "@snort/shared";
import { NostrLink, NostrEvent, ParsedZap, EventKind } from "@snort/system";
import { useEventReactions } from "@snort/system-react";
import { useMemo } from "react";
import { FormattedMessage, FormattedNumber, FormattedDate } from "react-intl";
import { ResponsiveContainer, BarChart, XAxis, YAxis, Bar, Tooltip } from "recharts";
import { StreamState } from "..";
import { Profile } from "./profile";
import { StatePill } from "./state-pill";
interface StatSlot {
time: number;
zaps: number;
messages: number;
reactions: number;
}
export default function StreamSummary({ link, preload }: { link: NostrLink; preload?: NostrEvent }) {
const ev = useCurrentStreamFeed(link, true, preload);
const thisLink = ev ? NostrLink.fromEvent(ev) : undefined;
const data = useLiveChatFeed(thisLink, undefined, 5_000);
const reactions = useEventReactions(thisLink ?? link, data.reactions);
const chatSummary = useMemo(() => {
return Object.entries(
data.messages.reduce((acc, v) => {
acc[v.pubkey] ??= [];
acc[v.pubkey].push(v);
return acc;
}, {} as Record<string, Array<NostrEvent>>)
)
.map(([k, v]) => ({
pubkey: k,
messages: v,
}))
.sort((a, b) => (a.messages.length > b.messages.length ? -1 : 1));
}, [data.messages]);
const zapsSummary = useMemo(() => {
return Object.entries(
reactions.zaps.reduce((acc, v) => {
if (!v.sender) return acc;
acc[v.sender] ??= [];
acc[v.sender].push(v);
return acc;
}, {} as Record<string, Array<ParsedZap>>)
)
.map(([k, v]) => ({
pubkey: k,
zaps: v,
total: v.reduce((acc, vv) => acc + vv.amount, 0),
}))
.sort((a, b) => (a.total > b.total ? -1 : 1));
}, [reactions.zaps]);
const title = findTag(ev, "title");
const summary = findTag(ev, "summary");
const status = findTag(ev, "status");
const starts = findTag(ev, "starts");
const Day = 60 * 60 * 24;
const startTime = starts ? Number(starts) : ev?.created_at ?? unixNow();
const endTime = status === StreamState.Live ? unixNow() : ev?.created_at ?? unixNow();
const streamLength = endTime - startTime;
const windowSize = streamLength > Day ? Day : 60 * 10;
const stats = useMemo(() => {
let min = unixNow();
let max = 0;
const ret = [...data.messages, ...data.reactions]
.sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
.reduce((acc, v) => {
const time = Math.floor(v.created_at - (v.created_at % windowSize));
if (time < min) {
min = time;
}
if (time > max) {
max = time;
}
const key = time.toString();
acc[key] ??= {
time,
zaps: 0,
messages: 0,
reactions: 0,
};
if (v.kind === LIVE_STREAM_CHAT) {
acc[key].messages++;
} else if (v.kind === EventKind.ZapReceipt) {
acc[key].zaps++;
} else if (v.kind === EventKind.Reaction) {
acc[key].reactions++;
} else {
console.debug("Uncounted stat", v);
}
return acc;
}, {} as Record<string, StatSlot>);
// fill empty time slots
for (let x = min; x < max; x += windowSize) {
ret[x.toString()] ??= {
time: x,
zaps: 0,
messages: 0,
reactions: 0,
};
}
return ret;
}, [data]);
return (
<div className="stream-summary">
<h1>{title}</h1>
<p>{summary}</p>
<div className="flex gap-1">
<StatePill state={status as StreamState} />
{streamLength > 0 && (
<FormattedMessage
defaultMessage="Stream Duration {duration} mins"
id="J/+m9y"
values={{
duration: <FormattedNumber value={streamLength / 60} maximumFractionDigits={2} />,
}}
/>
)}
</div>
<h2>
<FormattedMessage defaultMessage="Summary" id="RrCui3" />
</h2>
<ResponsiveContainer height={200}>
<BarChart data={Object.values(stats)} margin={{ left: 0, right: 0 }} style={{ userSelect: "none" }}>
<XAxis tick={false} />
<YAxis />
<Bar dataKey="messages" fill="green" stackId="" />
<Bar dataKey="zaps" fill="yellow" stackId="" />
<Bar dataKey="reactions" fill="red" stackId="" />
<Tooltip
cursor={{ fill: "rgba(255,255,255,0.2)" }}
content={({ active, payload }) => {
if (active && payload && payload.length) {
const data = payload[0].payload as StatSlot;
return (
<div className="plain-paper flex flex-col gap-2">
<div>
<FormattedDate value={data.time * 1000} timeStyle="short" dateStyle="short" />
</div>
<div className="flex justify-between">
<div>
<FormattedMessage defaultMessage="Messages" id="hMzcSq" />
</div>
<div>{data.messages}</div>
</div>
<div className="flex justify-between">
<div>
<FormattedMessage defaultMessage="Reactions" id="XgWvGA" />
</div>
<div>{data.reactions}</div>
</div>
<div className="flex justify-between">
<div>
<FormattedMessage defaultMessage="Zaps" id="OEW7yJ" />
</div>
<div>{data.zaps}</div>
</div>
</div>
);
}
return null;
}}
/>
</BarChart>
</ResponsiveContainer>
<div className="flex gap-1">
<div className="plain-paper flex-1">
<h3>
<FormattedMessage defaultMessage="Top Chatters" id="GGaJMU" />
</h3>
<div className="flex flex-col gap-2">
{chatSummary.slice(0, 5).map(a => (
<div className="flex justify-between items-center" key={a.pubkey}>
<Profile pubkey={a.pubkey} />
<div>
<FormattedMessage
defaultMessage="{n} messages"
id="gzsn7k"
values={{
n: <FormattedNumber value={a.messages.length} />,
}}
/>
</div>
</div>
))}
</div>
</div>
<div className="plain-paper flex-1">
<h3>
<FormattedMessage defaultMessage="Top Zappers" id="dVD/AR" />
</h3>
<div className="flex flex-col gap-2">
{zapsSummary.slice(0, 5).map(a => (
<div className="flex justify-between items-center" key={a.pubkey}>
<Profile pubkey={a.pubkey} />
<div>
<FormattedMessage
defaultMessage="{n} sats"
id="CsCUYo"
values={{
n: formatSats(a.total),
}}
/>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -1,14 +1,14 @@
import { EventKind, NostrLink } from "@snort/system";
import React, { useContext, useRef, useState } from "react";
import React, { Suspense, lazy, useContext, useRef, useState } from "react";
import { FormattedMessage } from "react-intl";
import { SnortContext } from "@snort/system-react";
import { unixNowMs } from "@snort/shared";
const EmojiPicker = lazy(() => import("./emoji-picker"));
import { useLogin } from "@/hooks/login";
import AsyncButton from "./async-button";
import { Icon } from "./icon";
import { Textarea } from "./textarea";
import { EmojiPicker } from "./emoji-picker";
import type { Emoji, EmojiPack } from "@/types";
import { LIVE_STREAM_CHAT } from "@/const";
import { TimeSync } from "@/index";
@ -88,14 +88,16 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
<Icon name="face" className="write-emoji-button" />
</div>
{showEmojiPicker && (
<EmojiPicker
topOffset={topOffset ?? 0}
leftOffset={leftOffset ?? 0}
emojiPacks={emojiPacks}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
<Suspense>
<EmojiPicker
topOffset={topOffset ?? 0}
leftOffset={leftOffset ?? 0}
emojiPacks={emojiPacks}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
</Suspense>
)}
</div>
<AsyncButton onClick={sendChatMessage} className="btn btn-border">

View File

@ -1,9 +1,8 @@
import { useMemo } from "react";
import uniqBy from "lodash.uniqby";
import { NostrEvent, NoteCollection, ReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { findTag } from "@/utils";
import { findTag, uniqBy } from "@/utils";
import { EMOJI_PACK, USER_EMOJIS } from "@/const";
import type { EmojiPack, EmojiTag, Tags } from "@/types";

View File

@ -1,6 +1,6 @@
import "./layout.css";
import { useState } from "react";
import { useState, useSyncExternalStore } from "react";
import * as Dialog from "@radix-ui/react-dialog";
import { Outlet, useNavigate } from "react-router-dom";
import { Helmet } from "react-helmet";
@ -16,6 +16,8 @@ import { LoginSignup } from "@/element/login-signup";
import { Login } from "@/index";
import { useLang } from "@/hooks/lang";
import { AllLocales } from "@/intl";
import { NewVersion } from "@/serviceWorker";
import AsyncButton from "@/element/async-button";
export function LayoutPage() {
const navigate = useNavigate();
@ -137,6 +139,26 @@ export function LayoutPage() {
</div>
</header>
<Outlet />
{NewVersion && <NewVersionBanner />}
</div>
);
}
function NewVersionBanner() {
const newVersion = useSyncExternalStore(c => NewVersion.hook(c), () => NewVersion.snapshot());
if (!newVersion) return;
return <div className="fixed top-0 left-0 w-max flex bg-slate-800 py-2 px-4 opacity-95">
<div className="grow">
<h1>
<FormattedMessage defaultMessage="A new version has been detected" id="RJ2VxG" />
</h1>
<p>
<FormattedMessage defaultMessage="Refresh the page to use the latest version" id="Gmiwnd" />
</p>
</div>
<AsyncButton onClick={() => window.location.reload()} className="btn">
<FormattedMessage defaultMessage="Refresh" id="rELDbB" />
</AsyncButton>
</div>
}

View File

@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { Suspense, lazy, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { Button as AlbyZapsButton } from "@getalby/bitcoin-connect-react";
import { hexToBech32, unwrap } from "@snort/shared";
const AlbyButton = lazy(() => import("@/element/alby-button"));
import { useLogin } from "@/hooks/login";
import Copy from "@/element/copy";
import { NostrProviderDialog } from "@/element/nostr-provider-dialog";
@ -53,7 +53,9 @@ export function SettingsPage() {
<h1>
<FormattedMessage defaultMessage="Zaps" id="OEW7yJ" />
</h1>
<AlbyZapsButton />
<Suspense>
<AlbyButton />
</Suspense>
<h1>
<FormattedMessage defaultMessage="Stream Key" id="LknBsU" />
</h1>

View File

@ -5,9 +5,9 @@ import { Helmet } from "react-helmet";
import { NostrEvent } from "@snort/system";
import { SnortContext, useUserProfile } from "@snort/system-react";
import { FormattedMessage } from "react-intl";
import { useContext } from "react";
import { Suspense, lazy, useContext } from "react";
import { LiveVideoPlayer } from "@/element/live-video-player";
const LiveVideoPlayer = lazy(() => import("@/element/live-video-player"));
import { findTag, getEventFromLocationState, getHost } from "@/utils";
import { Profile, getName } from "@/element/profile";
import { LiveChat } from "@/element/live-chat";
@ -147,7 +147,9 @@ export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link:
<meta property="og:image" content={image ?? ""} />
</Helmet>
<div className="video-content">
<LiveVideoPlayer stream={stream} poster={image} status={status} />
<Suspense>
<LiveVideoPlayer stream={stream} poster={image} status={status} />
</Suspense>
<ProfileInfo ev={ev} goal={goal} />
<StreamCards host={host} />
</div>

View File

@ -1,20 +1,9 @@
import { useMemo } from "react";
import { unixNow } from "@snort/shared";
import { useLocation } from "react-router-dom";
import { EventKind, NostrEvent, NostrLink, ParsedZap } from "@snort/system";
import { useEventReactions } from "@snort/system-react";
import { FormattedDate, FormattedMessage, FormattedNumber } from "react-intl";
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { LIVE_STREAM_CHAT } from "@/const";
import { Profile } from "@/element/profile";
import { StatePill } from "@/element/state-pill";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { useLiveChatFeed } from "@/hooks/live-chat";
import { useStreamLink } from "@/hooks/stream-link";
import { StreamState } from "@/index";
import { formatSats } from "@/number";
import { findTag, getEventFromLocationState } from "@/utils";
import { getEventFromLocationState } from "@/utils";
import { lazy } from "react";
const StreamSummary = lazy(() => import("@/element/summary-chart"));
export function StreamSummaryPage() {
const location = useLocation();
@ -24,216 +13,3 @@ export function StreamSummaryPage() {
return <StreamSummary link={link} preload={evPreload} />;
}
}
interface StatSlot {
time: number;
zaps: number;
messages: number;
reactions: number;
}
export function StreamSummary({ link, preload }: { link: NostrLink; preload?: NostrEvent }) {
const ev = useCurrentStreamFeed(link, true, preload);
const thisLink = ev ? NostrLink.fromEvent(ev) : undefined;
const data = useLiveChatFeed(thisLink, undefined, 5_000);
const reactions = useEventReactions(thisLink ?? link, data.reactions);
const chatSummary = useMemo(() => {
return Object.entries(
data.messages.reduce((acc, v) => {
acc[v.pubkey] ??= [];
acc[v.pubkey].push(v);
return acc;
}, {} as Record<string, Array<NostrEvent>>)
)
.map(([k, v]) => ({
pubkey: k,
messages: v,
}))
.sort((a, b) => (a.messages.length > b.messages.length ? -1 : 1));
}, [data.messages]);
const zapsSummary = useMemo(() => {
return Object.entries(
reactions.zaps.reduce((acc, v) => {
if (!v.sender) return acc;
acc[v.sender] ??= [];
acc[v.sender].push(v);
return acc;
}, {} as Record<string, Array<ParsedZap>>)
)
.map(([k, v]) => ({
pubkey: k,
zaps: v,
total: v.reduce((acc, vv) => acc + vv.amount, 0),
}))
.sort((a, b) => (a.total > b.total ? -1 : 1));
}, [reactions.zaps]);
const title = findTag(ev, "title");
const summary = findTag(ev, "summary");
const status = findTag(ev, "status");
const starts = findTag(ev, "starts");
const Day = 60 * 60 * 24;
const startTime = starts ? Number(starts) : ev?.created_at ?? unixNow();
const endTime = status === StreamState.Live ? unixNow() : ev?.created_at ?? unixNow();
const streamLength = endTime - startTime;
const windowSize = streamLength > Day ? Day : 60 * 10;
const stats = useMemo(() => {
let min = unixNow();
let max = 0;
const ret = [...data.messages, ...data.reactions]
.sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
.reduce((acc, v) => {
const time = Math.floor(v.created_at - (v.created_at % windowSize));
if (time < min) {
min = time;
}
if (time > max) {
max = time;
}
const key = time.toString();
acc[key] ??= {
time,
zaps: 0,
messages: 0,
reactions: 0,
};
if (v.kind === LIVE_STREAM_CHAT) {
acc[key].messages++;
} else if (v.kind === EventKind.ZapReceipt) {
acc[key].zaps++;
} else if (v.kind === EventKind.Reaction) {
acc[key].reactions++;
} else {
console.debug("Uncounted stat", v);
}
return acc;
}, {} as Record<string, StatSlot>);
// fill empty time slots
for (let x = min; x < max; x += windowSize) {
ret[x.toString()] ??= {
time: x,
zaps: 0,
messages: 0,
reactions: 0,
};
}
return ret;
}, [data]);
return (
<div className="stream-summary">
<h1>{title}</h1>
<p>{summary}</p>
<div className="flex gap-1">
<StatePill state={status as StreamState} />
{streamLength > 0 && (
<FormattedMessage
defaultMessage="Stream Duration {duration} mins"
id="J/+m9y"
values={{
duration: <FormattedNumber value={streamLength / 60} maximumFractionDigits={2} />,
}}
/>
)}
</div>
<h2>
<FormattedMessage defaultMessage="Summary" id="RrCui3" />
</h2>
<ResponsiveContainer height={200}>
<BarChart data={Object.values(stats)} margin={{ left: 0, right: 0 }} style={{ userSelect: "none" }}>
<XAxis tick={false} />
<YAxis />
<Bar dataKey="messages" fill="green" stackId="" />
<Bar dataKey="zaps" fill="yellow" stackId="" />
<Bar dataKey="reactions" fill="red" stackId="" />
<Tooltip
cursor={{ fill: "rgba(255,255,255,0.2)" }}
content={({ active, payload }) => {
if (active && payload && payload.length) {
const data = payload[0].payload as StatSlot;
return (
<div className="plain-paper flex flex-col gap-2">
<div>
<FormattedDate value={data.time * 1000} timeStyle="short" dateStyle="short" />
</div>
<div className="flex justify-between">
<div>
<FormattedMessage defaultMessage="Messages" id="hMzcSq" />
</div>
<div>{data.messages}</div>
</div>
<div className="flex justify-between">
<div>
<FormattedMessage defaultMessage="Reactions" id="XgWvGA" />
</div>
<div>{data.reactions}</div>
</div>
<div className="flex justify-between">
<div>
<FormattedMessage defaultMessage="Zaps" id="OEW7yJ" />
</div>
<div>{data.zaps}</div>
</div>
</div>
);
}
return null;
}}
/>
</BarChart>
</ResponsiveContainer>
<div className="flex gap-1">
<div className="plain-paper flex-1">
<h3>
<FormattedMessage defaultMessage="Top Chatters" id="GGaJMU" />
</h3>
<div className="flex flex-col gap-2">
{chatSummary.slice(0, 5).map(a => (
<div className="flex justify-between items-center" key={a.pubkey}>
<Profile pubkey={a.pubkey} />
<div>
<FormattedMessage
defaultMessage="{n} messages"
id="gzsn7k"
values={{
n: <FormattedNumber value={a.messages.length} />,
}}
/>
</div>
</div>
))}
</div>
</div>
<div className="plain-paper flex-1">
<h3>
<FormattedMessage defaultMessage="Top Zappers" id="dVD/AR" />
</h3>
<div className="flex flex-col gap-2">
{zapsSummary.slice(0, 5).map(a => (
<div className="flex justify-between items-center" key={a.pubkey}>
<Profile pubkey={a.pubkey} />
<div>
<FormattedMessage
defaultMessage="{n} sats"
id="CsCUYo"
values={{
n: formatSats(a.total),
}}
/>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -14,3 +14,18 @@ self.addEventListener("message", event => {
self.skipWaiting();
}
});
self.addEventListener("install", event => {
// delete all cache on install
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
console.debug("Deleting cache: ", cacheName);
return caches.delete(cacheName);
})
);
})
);
// always skip waiting
self.skipWaiting();
});

View File

@ -1,3 +1,5 @@
import { ExternalStore } from "@snort/shared";
export function register() {
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
@ -6,6 +8,25 @@ export function register() {
}
}
class BoolStore extends ExternalStore<boolean> {
#value = false;
set value(v: boolean) {
this.#value = v;
this.notifyChange();
}
get value() {
return this.#value;
}
takeSnapshot(): boolean {
return this.#value;
}
}
export const NewVersion = new BoolStore();
async function registerValidSW(swUrl: string) {
try {
const registration = await navigator.serviceWorker.register(swUrl);
@ -18,6 +39,7 @@ async function registerValidSW(swUrl: string) {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
console.log("Service worker updated, pending reload");
NewVersion.value = true;
} else {
console.log("Content is cached for offline use.");
}

View File

@ -68,3 +68,13 @@ export function getEventFromLocationState(state: unknown | undefined | null) {
? (state as NostrEvent)
: undefined;
}
export function uniqBy<T>(vals: Array<T>, key: (x: T) => string) {
return Object.values(
vals.reduce((acc, v) => {
const k = key(v);
acc[k] ??= v;
return acc;
}, {} as Record<string, T>)
);
}

View File

@ -1,6 +1,7 @@
import react from "@vitejs/plugin-react";
import { VitePWA } from "vite-plugin-pwa";
import { defineConfig } from "vite";
import { visualizer } from "rollup-plugin-visualizer";
import { vitePluginVersionMark } from "vite-plugin-version-mark";
export default defineConfig({
@ -21,6 +22,11 @@ export default defineConfig({
command: "git describe --always --tags",
ifMeta: false,
}),
visualizer({
open: true,
gzipSize: true,
filename: "build/stats.html",
}),
],
build: {
outDir: "build",

147
yarn.lock
View File

@ -2110,7 +2110,7 @@ __metadata:
languageName: node
linkType: hard
"@noble/curves@npm:^1.1.0, @noble/curves@npm:^1.2.0":
"@noble/curves@npm:^1.2.0":
version: 1.2.0
resolution: "@noble/curves@npm:1.2.0"
dependencies:
@ -2126,7 +2126,7 @@ __metadata:
languageName: node
linkType: hard
"@noble/hashes@npm:1.3.2, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1":
"@noble/hashes@npm:1.3.2, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1":
version: 1.3.2
resolution: "@noble/hashes@npm:1.3.2"
checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474
@ -2835,13 +2835,20 @@ __metadata:
languageName: node
linkType: hard
"@scure/base@npm:^1.1.1, @scure/base@npm:^1.1.2, @scure/base@npm:~1.1.0":
"@scure/base@npm:^1.1.2, @scure/base@npm:~1.1.0":
version: 1.1.2
resolution: "@scure/base@npm:1.1.2"
checksum: f666b09dbd62ecb5fe6d0e7a629c8a86a972a47dc4f4555ebbbd7b09782b10a5f894fed9c3b8c74fd683b1588c064df079a44e9f695c075ccd98c30a8d3e91f7
languageName: node
linkType: hard
"@scure/base@npm:^1.1.3":
version: 1.1.3
resolution: "@scure/base@npm:1.1.3"
checksum: 1606ab8a4db898cb3a1ada16c15437c3bce4e25854fadc8eb03ae93cbbbac1ed90655af4b0be3da37e12056fef11c0374499f69b9e658c9e5b7b3e06353c630c
languageName: node
linkType: hard
"@scure/bip32@npm:1.3.1":
version: 1.3.1
resolution: "@scure/bip32@npm:1.3.1"
@ -4069,6 +4076,17 @@ __metadata:
languageName: node
linkType: hard
"cliui@npm:^8.0.1":
version: 8.0.1
resolution: "cliui@npm:8.0.1"
dependencies:
string-width: ^4.2.0
strip-ansi: ^6.0.1
wrap-ansi: ^7.0.0
checksum: 79648b3b0045f2e285b76fb2e24e207c6db44323581e421c3acbd0e86454cba1b37aea976ab50195a49e7384b871e6dfb2247ad7dec53c02454ac6497394cb56
languageName: node
linkType: hard
"color-convert@npm:^1.9.0":
version: 1.9.3
resolution: "color-convert@npm:1.9.3"
@ -4368,6 +4386,13 @@ __metadata:
languageName: node
linkType: hard
"define-lazy-prop@npm:^2.0.0":
version: 2.0.0
resolution: "define-lazy-prop@npm:2.0.0"
checksum: 0115fdb065e0490918ba271d7339c42453d209d4cb619dfe635870d906731eff3e1ade8028bb461ea27ce8264ec5e22c6980612d332895977e89c1bbc80fcee2
languageName: node
linkType: hard
"define-properties@npm:^1.1.3, define-properties@npm:^1.1.4, define-properties@npm:^1.2.0":
version: 1.2.0
resolution: "define-properties@npm:1.2.0"
@ -5144,6 +5169,13 @@ __metadata:
languageName: node
linkType: hard
"get-caller-file@npm:^2.0.5":
version: 2.0.5
resolution: "get-caller-file@npm:2.0.5"
checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9
languageName: node
linkType: hard
"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1":
version: 1.2.1
resolution: "get-intrinsic@npm:1.2.1"
@ -5616,6 +5648,15 @@ __metadata:
languageName: node
linkType: hard
"is-docker@npm:^2.0.0, is-docker@npm:^2.1.1":
version: 2.2.1
resolution: "is-docker@npm:2.2.1"
bin:
is-docker: cli.js
checksum: 3fef7ddbf0be25958e8991ad941901bf5922ab2753c46980b60b05c1bf9c9c2402d35e6dc32e4380b980ef5e1970a5d9d5e5aa2e02d77727c3b6b5e918474c56
languageName: node
linkType: hard
"is-extglob@npm:^2.1.1":
version: 2.1.1
resolution: "is-extglob@npm:2.1.1"
@ -5790,6 +5831,15 @@ __metadata:
languageName: node
linkType: hard
"is-wsl@npm:^2.2.0":
version: 2.2.0
resolution: "is-wsl@npm:2.2.0"
dependencies:
is-docker: ^2.0.0
checksum: 20849846ae414997d290b75e16868e5261e86ff5047f104027026fd61d8b5a9b0b3ade16239f35e1a067b3c7cc02f70183cb661010ed16f4b6c7c93dad1b19d8
languageName: node
linkType: hard
"isarray@npm:^2.0.5":
version: 2.0.5
resolution: "isarray@npm:2.0.5"
@ -6093,13 +6143,6 @@ __metadata:
languageName: node
linkType: hard
"lodash.uniqby@npm:^4.7.0":
version: 4.7.0
resolution: "lodash.uniqby@npm:4.7.0"
checksum: 659264545a95726d1493123345aad8cbf56e17810fa9a0b029852c6d42bc80517696af09d99b23bef1845d10d95e01b8b4a1da578f22aeba7a30d3e0022a4938
languageName: node
linkType: hard
"lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21":
version: 4.17.21
resolution: "lodash@npm:4.17.21"
@ -6538,6 +6581,17 @@ __metadata:
languageName: node
linkType: hard
"open@npm:^8.4.0":
version: 8.4.2
resolution: "open@npm:8.4.2"
dependencies:
define-lazy-prop: ^2.0.0
is-docker: ^2.1.1
is-wsl: ^2.2.0
checksum: 6388bfff21b40cb9bd8f913f9130d107f2ed4724ea81a8fd29798ee322b361ca31fa2cdfb491a5c31e43a3996cfe9566741238c7a741ada8d7af1cb78d85cf26
languageName: node
linkType: hard
"optionator@npm:^0.9.3":
version: 0.9.3
resolution: "optionator@npm:0.9.3"
@ -7273,6 +7327,13 @@ __metadata:
languageName: node
linkType: hard
"require-directory@npm:^2.1.1":
version: 2.1.1
resolution: "require-directory@npm:2.1.1"
checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80
languageName: node
linkType: hard
"require-from-string@npm:^2.0.2":
version: 2.0.2
resolution: "require-from-string@npm:2.0.2"
@ -7378,6 +7439,25 @@ __metadata:
languageName: node
linkType: hard
"rollup-plugin-visualizer@npm:^5.10.0":
version: 5.10.0
resolution: "rollup-plugin-visualizer@npm:5.10.0"
dependencies:
open: ^8.4.0
picomatch: ^2.3.1
source-map: ^0.7.4
yargs: ^17.5.1
peerDependencies:
rollup: 2.x || 3.x || 4.x
peerDependenciesMeta:
rollup:
optional: true
bin:
rollup-plugin-visualizer: dist/bin/cli.js
checksum: b60d50bd3d69fadcba2536bcd0f1926bc26f23ad8872108aad005f050f4d379969bfe09c658f9ae81efcf4329aedf3b0b7fcd80d9a650401b065cf514c8ca78b
languageName: node
linkType: hard
"rollup@npm:^2.43.1":
version: 2.79.1
resolution: "rollup@npm:2.79.1"
@ -7667,6 +7747,13 @@ __metadata:
languageName: node
linkType: hard
"source-map@npm:^0.7.4":
version: 0.7.4
resolution: "source-map@npm:0.7.4"
checksum: 01cc5a74b1f0e1d626a58d36ad6898ea820567e87f18dfc9d24a9843a351aaa2ec09b87422589906d6ff1deed29693e176194dc88bcae7c9a852dc74b311dbf5
languageName: node
linkType: hard
"source-map@npm:^0.8.0-beta.0":
version: 0.8.0-beta.0
resolution: "source-map@npm:0.8.0-beta.0"
@ -7710,15 +7797,14 @@ __metadata:
"@formatjs/cli": ^6.1.3
"@formatjs/ts-transformer": ^3.13.3
"@getalby/bitcoin-connect-react": ^1.1.0
"@noble/curves": ^1.1.0
"@noble/hashes": ^1.3.1
"@noble/curves": ^1.2.0
"@radix-ui/react-collapsible": ^1.0.3
"@radix-ui/react-dialog": ^1.0.4
"@radix-ui/react-progress": ^1.0.3
"@radix-ui/react-tabs": ^1.0.4
"@radix-ui/react-toggle": ^1.0.3
"@react-hook/resize-observer": ^1.2.6
"@scure/base": ^1.1.1
"@scure/base": ^1.1.3
"@snort/shared": ^1.0.10
"@snort/system": ^1.1.5
"@snort/system-react": ^1.1.5
@ -7747,7 +7833,6 @@ __metadata:
flag-icons: ^6.11.0
hls.js: ^1.4.6
lodash: ^4.17.21
lodash.uniqby: ^4.7.0
marked: ^9.1.2
postcss: ^8.4.32
prettier: ^2.8.8
@ -7764,6 +7849,7 @@ __metadata:
react-router-dom: ^6.13.0
react-tag-input-component: ^2.0.2
recharts: ^2.9.3
rollup-plugin-visualizer: ^5.10.0
semantic-sdp: ^3.26.3
tailwindcss: ^3.3.5
typescript: ^5.2.2
@ -7780,7 +7866,7 @@ __metadata:
languageName: unknown
linkType: soft
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.3":
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
version: 4.2.3
resolution: "string-width@npm:4.2.3"
dependencies:
@ -8787,7 +8873,7 @@ __metadata:
languageName: node
linkType: hard
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0":
version: 7.0.0
resolution: "wrap-ansi@npm:7.0.0"
dependencies:
@ -8831,6 +8917,13 @@ __metadata:
languageName: node
linkType: hard
"y18n@npm:^5.0.5":
version: 5.0.8
resolution: "y18n@npm:5.0.8"
checksum: 54f0fb95621ee60898a38c572c515659e51cc9d9f787fb109cef6fde4befbe1c4602dc999d30110feee37456ad0f1660fa2edcfde6a9a740f86a290999550d30
languageName: node
linkType: hard
"yallist@npm:^3.0.2":
version: 3.1.1
resolution: "yallist@npm:3.1.1"
@ -8852,6 +8945,28 @@ __metadata:
languageName: node
linkType: hard
"yargs-parser@npm:^21.1.1":
version: 21.1.1
resolution: "yargs-parser@npm:21.1.1"
checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c
languageName: node
linkType: hard
"yargs@npm:^17.5.1":
version: 17.7.2
resolution: "yargs@npm:17.7.2"
dependencies:
cliui: ^8.0.1
escalade: ^3.1.1
get-caller-file: ^2.0.5
require-directory: ^2.1.1
string-width: ^4.2.3
y18n: ^5.0.5
yargs-parser: ^21.1.1
checksum: 73b572e863aa4a8cbef323dd911d79d193b772defd5a51aab0aca2d446655216f5002c42c5306033968193bdbf892a7a4c110b0d77954a7fdf563e653967b56a
languageName: node
linkType: hard
"yocto-queue@npm:^0.1.0":
version: 0.1.0
resolution: "yocto-queue@npm:0.1.0"