diff --git a/package.json b/package.json index 7be2ece..b99547c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/element/alby-button.tsx b/src/element/alby-button.tsx new file mode 100644 index 0000000..7977db4 --- /dev/null +++ b/src/element/alby-button.tsx @@ -0,0 +1,5 @@ +import { Button as AlbyZapsButton } from "@getalby/bitcoin-connect-react"; + +export default function AlbyButton() { + return +} \ No newline at end of file diff --git a/src/element/chat-message.tsx b/src/element/chat-message.tsx index 06682ac..0f53e2d 100644 --- a/src/element/chat-message.tsx +++ b/src/element/chat-message.tsx @@ -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 && ( {showEmojiPicker && ( - setShowEmojiPicker(false)} - ref={emojiRef} - /> + + setShowEmojiPicker(false)} + ref={emojiRef} + /> + )} ); diff --git a/src/element/emoji-picker.tsx b/src/element/emoji-picker.tsx index cd365d3..f450f36 100644 --- a/src/element/emoji-picker.tsx +++ b/src/element/emoji-picker.tsx @@ -13,7 +13,7 @@ interface EmojiPickerProps { ref: RefObject; } -export function EmojiPicker({ +export default function EmojiPicker({ topOffset, leftOffset, onEmojiSelect, diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index 61968fd..9b83d69 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -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 { diff --git a/src/element/live-video-player.tsx b/src/element/live-video-player.tsx index 0c1e865..78a4c1a 100644 --- a/src/element/live-video-player.tsx +++ b/src/element/live-video-player.tsx @@ -17,7 +17,7 @@ export interface VideoPlayerProps { poster?: string; } -export function LiveVideoPlayer(props: VideoPlayerProps) { +export default function LiveVideoPlayer(props: VideoPlayerProps) { const video = useRef(null); const hlsObj = useRef(null); const streamCached = useMemo(() => props.stream, [props.stream]); diff --git a/src/element/markdown.tsx b/src/element/markdown.tsx index a84d930..c7d0045 100644 --- a/src/element/markdown.tsx +++ b/src/element/markdown.tsx @@ -82,7 +82,7 @@ function renderToken(t: Token): ReactNode { } } -export const Markdown = forwardRef((props: MarkdownProps, ref) => { +const Markdown = forwardRef((props: MarkdownProps, ref) => { const parsed = useMemo(() => { return marked.lexer(props.content); }, [props.content, props.tags]); @@ -93,3 +93,5 @@ export const Markdown = forwardRef((props: Markdo ); }); + +export default Markdown; diff --git a/src/element/note.tsx b/src/element/note.tsx index fa0332f..ea687ec 100644 --- a/src/element/note.tsx +++ b/src/element/note.tsx @@ -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"; diff --git a/src/element/stream-cards.tsx b/src/element/stream-cards.tsx index 755b9fa..e239972 100644 --- a/src/element/stream-cards.tsx +++ b/src/element/stream-cards.tsx @@ -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 ) : ( {title} ))} - + + + ); }); diff --git a/src/element/summary-chart.tsx b/src/element/summary-chart.tsx new file mode 100644 index 0000000..53a5737 --- /dev/null +++ b/src/element/summary-chart.tsx @@ -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>) + ) + .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>) + ) + .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); + + // 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 ( +
+

{title}

+

{summary}

+
+ + {streamLength > 0 && ( + , + }} + /> + )} +
+

+ +

+ + + + + + + + { + if (active && payload && payload.length) { + const data = payload[0].payload as StatSlot; + return ( +
+
+ +
+
+
+ +
+
{data.messages}
+
+
+
+ +
+
{data.reactions}
+
+
+
+ +
+
{data.zaps}
+
+
+ ); + } + return null; + }} + /> +
+
+ +
+
+

+ +

+
+ {chatSummary.slice(0, 5).map(a => ( +
+ +
+ , + }} + /> +
+
+ ))} +
+
+
+

+ +

+
+ {zapsSummary.slice(0, 5).map(a => ( +
+ +
+ +
+
+ ))} +
+
+
+
+ ); +} diff --git a/src/element/write-message.tsx b/src/element/write-message.tsx index d63f7de..58fd4d8 100644 --- a/src/element/write-message.tsx +++ b/src/element/write-message.tsx @@ -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 {showEmojiPicker && ( - setShowEmojiPicker(false)} - ref={emojiRef} - /> + + setShowEmojiPicker(false)} + ref={emojiRef} + /> + )} diff --git a/src/hooks/emoji.tsx b/src/hooks/emoji.tsx index fbfed31..5023614 100644 --- a/src/hooks/emoji.tsx +++ b/src/hooks/emoji.tsx @@ -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"; diff --git a/src/pages/layout.tsx b/src/pages/layout.tsx index 49b8522..66ea0e6 100644 --- a/src/pages/layout.tsx +++ b/src/pages/layout.tsx @@ -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() { + {NewVersion && } ); } + +function NewVersionBanner() { + const newVersion = useSyncExternalStore(c => NewVersion.hook(c), () => NewVersion.snapshot()); + if (!newVersion) return; + + return
+
+

+ +

+

+ +

+
+ window.location.reload()} className="btn"> + + +
+} \ No newline at end of file diff --git a/src/pages/settings-page.tsx b/src/pages/settings-page.tsx index 7444390..fcea4b5 100644 --- a/src/pages/settings-page.tsx +++ b/src/pages/settings-page.tsx @@ -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() {

- + + +

diff --git a/src/pages/stream-page.tsx b/src/pages/stream-page.tsx index bba28b8..af38c5d 100644 --- a/src/pages/stream-page.tsx +++ b/src/pages/stream-page.tsx @@ -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:
- + + +
diff --git a/src/pages/summary.tsx b/src/pages/summary.tsx index f753df2..db1b748 100644 --- a/src/pages/summary.tsx +++ b/src/pages/summary.tsx @@ -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 ; } } - -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>) - ) - .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>) - ) - .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); - - // 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 ( -
-

{title}

-

{summary}

-
- - {streamLength > 0 && ( - , - }} - /> - )} -
-

- -

- - - - - - - - { - if (active && payload && payload.length) { - const data = payload[0].payload as StatSlot; - return ( -
-
- -
-
-
- -
-
{data.messages}
-
-
-
- -
-
{data.reactions}
-
-
-
- -
-
{data.zaps}
-
-
- ); - } - return null; - }} - /> -
-
- -
-
-

- -

-
- {chatSummary.slice(0, 5).map(a => ( -
- -
- , - }} - /> -
-
- ))} -
-
-
-

- -

-
- {zapsSummary.slice(0, 5).map(a => ( -
- -
- -
-
- ))} -
-
-
-
- ); -} diff --git a/src/service-worker.ts b/src/service-worker.ts index cee0ca5..691d432 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -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(); +}); diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts index 8205761..bb69186 100644 --- a/src/serviceWorker.ts +++ b/src/serviceWorker.ts @@ -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 { + #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."); } diff --git a/src/utils.ts b/src/utils.ts index 1c7c194..8339baf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -68,3 +68,13 @@ export function getEventFromLocationState(state: unknown | undefined | null) { ? (state as NostrEvent) : undefined; } + +export function uniqBy(vals: Array, key: (x: T) => string) { + return Object.values( + vals.reduce((acc, v) => { + const k = key(v); + acc[k] ??= v; + return acc; + }, {} as Record) + ); +} diff --git a/vite.config.ts b/vite.config.ts index b098da6..abaf09e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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", diff --git a/yarn.lock b/yarn.lock index 9e2c378..0788f42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"