From fb5d7f2d2b6c3767cd7a66bc62829ba8c2abf413 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Wed, 27 Nov 2024 10:09:39 -0600 Subject: [PATCH] add noStrudel users view --- src/app.tsx | 2 + .../charts/event-kinds-pie-chart.tsx | 44 +----- src/components/charts/simple-pie-chart.tsx | 44 ++++++ src/helpers/app-settings.ts | 21 +-- src/helpers/parse.ts | 4 +- src/hooks/use-app-settings.ts | 3 +- src/queries/app-settings.ts | 5 +- src/services/user-app-settings.ts | 2 - src/services/user-event-sync.ts | 2 +- .../dvm-feed/components/dvm-card.tsx | 4 +- src/views/discovery/dvm-feed/feed.tsx | 26 ++-- src/views/other-stuff/apps.ts | 8 ++ src/views/tools/nostrudel-users/index.tsx | 136 ++++++++++++++++++ 13 files changed, 217 insertions(+), 84 deletions(-) create mode 100644 src/components/charts/simple-pie-chart.tsx delete mode 100644 src/services/user-app-settings.ts create mode 100644 src/views/tools/nostrudel-users/index.tsx diff --git a/src/app.tsx b/src/app.tsx index a0e75f747..5e0ca33a5 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -119,6 +119,7 @@ const DMTimelineView = lazy(() => import("./views/tools/dm-timeline")); const TransformNoteView = lazy(() => import("./views/tools/transform-note")); const SatelliteCDNView = lazy(() => import("./views/tools/satellite-cdn")); const CorrectionsFeedView = lazy(() => import("./views/tools/corrections")); +const NoStrudelUsersView = lazy(() => import("./views/tools/nostrudel-users/index")); const UserStreamsTab = lazy(() => import("./views/user/streams")); const StreamsView = lazy(() => import("./views/streams")); @@ -413,6 +414,7 @@ const router = createHashRouter([ { path: "unknown", element: }, { path: "console", element: }, { path: "corrections", element: }, + { path: "nostrudel-users", element: }, ], }, { diff --git a/src/components/charts/event-kinds-pie-chart.tsx b/src/components/charts/event-kinds-pie-chart.tsx index b8d0b13ec..5e115b2ab 100644 --- a/src/components/charts/event-kinds-pie-chart.tsx +++ b/src/components/charts/event-kinds-pie-chart.tsx @@ -1,32 +1,6 @@ import { useMemo } from "react"; -import { useColorModeValue, useTheme } from "@chakra-ui/react"; -import { - Chart as ChartJS, - ArcElement, - CategoryScale, - ChartData, - Colors, - Legend, - LineElement, - LinearScale, - PointElement, - Title, - Tooltip, -} from "chart.js"; -import { Pie } from "react-chartjs-2"; - -ChartJS.register( - ArcElement, - Tooltip, - Legend, - Colors, - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, -); +import { ChartData } from "chart.js"; +import SimplePieChart from "./simple-pie-chart"; function createChartData(kinds: Record) { const sortedKinds = Object.entries(kinds) @@ -42,19 +16,7 @@ function createChartData(kinds: Record) { } export default function EventKindsPieChart({ kinds }: { kinds: Record }) { - const theme = useTheme(); - const token = theme.semanticTokens.colors["chakra-body-text"]; - const color = useColorModeValue(token._light, token._dark) as string; - const chartData = useMemo(() => createChartData(kinds), [kinds]); - return ( - - ); + return ; } diff --git a/src/components/charts/simple-pie-chart.tsx b/src/components/charts/simple-pie-chart.tsx new file mode 100644 index 000000000..988debbc0 --- /dev/null +++ b/src/components/charts/simple-pie-chart.tsx @@ -0,0 +1,44 @@ +import { useColorModeValue, useTheme } from "@chakra-ui/react"; +import { + Chart as ChartJS, + ArcElement, + CategoryScale, + ChartData, + Colors, + Legend, + LineElement, + LinearScale, + PointElement, + Title, + Tooltip, +} from "chart.js"; +import { Pie } from "react-chartjs-2"; + +ChartJS.register( + ArcElement, + Tooltip, + Legend, + Colors, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, +); + +export default function SimplePieChart({ data }: { data: ChartData<"pie", number[], string> }) { + const theme = useTheme(); + const token = theme.semanticTokens.colors["chakra-body-text"]; + const color = useColorModeValue(token._light, token._dark) as string; + + return ( + + ); +} diff --git a/src/helpers/app-settings.ts b/src/helpers/app-settings.ts index aa2f2bf28..d3ecdfaed 100644 --- a/src/helpers/app-settings.ts +++ b/src/helpers/app-settings.ts @@ -1,6 +1,7 @@ import { ColorModeWithSystem } from "@chakra-ui/react"; -import { NostrEvent } from "../types/nostr-event"; -import { safeJson } from "./parse"; + +export const APP_SETTINGS_KIND = 30078; +export const APP_SETTING_IDENTIFIER = "nostrudel-settings"; export type AppSettingsV0 = { version: 0; @@ -88,19 +89,3 @@ export const defaultSettings: AppSettings = { redditRedirect: undefined, youtubeRedirect: undefined, }; - -export function upgradeSettings(settings: { version: number }): AppSettings | null { - return { ...defaultSettings, ...settings, version: 10 }; -} - -export function parseAppSettings(event: NostrEvent): AppSettings { - const json = safeJson(event.content, {}); - const upgraded = upgradeSettings(json); - - return upgraded - ? { - ...defaultSettings, - ...upgraded, - } - : defaultSettings; -} diff --git a/src/helpers/parse.ts b/src/helpers/parse.ts index e4114fb58..5a5ed839b 100644 --- a/src/helpers/parse.ts +++ b/src/helpers/parse.ts @@ -4,7 +4,9 @@ export function safeUrl(url: string) { } catch (e) {} } -export function safeJson(json: string, fallback: T) { +export function safeJson(json: string): T | undefined; +export function safeJson(json: string, fallback: T): T; +export function safeJson(json: string, fallback?: T): T | undefined { try { return JSON.parse(json); } catch (e) { diff --git a/src/hooks/use-app-settings.ts b/src/hooks/use-app-settings.ts index eca1d523e..6e34a129c 100644 --- a/src/hooks/use-app-settings.ts +++ b/src/hooks/use-app-settings.ts @@ -3,10 +3,9 @@ import { useStoreQuery } from "applesauce-react/hooks"; import { EventTemplate } from "nostr-tools"; import dayjs from "dayjs"; -import { AppSettings, defaultSettings } from "../helpers/app-settings"; +import { APP_SETTING_IDENTIFIER, APP_SETTINGS_KIND, AppSettings, defaultSettings } from "../helpers/app-settings"; import useCurrentAccount from "./use-current-account"; import accountService from "../services/account"; -import { APP_SETTING_IDENTIFIER, APP_SETTINGS_KIND } from "../services/user-app-settings"; import { usePublishEvent } from "../providers/global/publish-provider"; import AppSettingsQuery from "../queries/app-settings"; import useReplaceableEvent from "./use-replaceable-event"; diff --git a/src/queries/app-settings.ts b/src/queries/app-settings.ts index c59cc1114..19a0b6d99 100644 --- a/src/queries/app-settings.ts +++ b/src/queries/app-settings.ts @@ -1,8 +1,7 @@ import { Query } from "applesauce-core"; import { map } from "rxjs"; -import { AppSettings, defaultSettings } from "../helpers/app-settings"; -import { APP_SETTING_IDENTIFIER, APP_SETTINGS_KIND } from "../services/user-app-settings"; +import { APP_SETTING_IDENTIFIER, APP_SETTINGS_KIND, AppSettings, defaultSettings } from "../helpers/app-settings"; import { safeJson } from "../helpers/parse"; export default function AppSettingsQuery(pubkey: string): Query { @@ -12,7 +11,7 @@ export default function AppSettingsQuery(pubkey: string): Query { events.replaceable(APP_SETTINGS_KIND, pubkey, APP_SETTING_IDENTIFIER).pipe( map((event) => { if (!event) return defaultSettings; - const parsed = safeJson(event.content, defaultSettings) as Partial; + const parsed = safeJson>(event.content, defaultSettings); return { ...defaultSettings, ...parsed }; }), ), diff --git a/src/services/user-app-settings.ts b/src/services/user-app-settings.ts deleted file mode 100644 index 1b73737c7..000000000 --- a/src/services/user-app-settings.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const APP_SETTINGS_KIND = 30078; -export const APP_SETTING_IDENTIFIER = "nostrudel-settings"; diff --git a/src/services/user-event-sync.ts b/src/services/user-event-sync.ts index 14004565e..8ea5eec66 100644 --- a/src/services/user-event-sync.ts +++ b/src/services/user-event-sync.ts @@ -11,12 +11,12 @@ import accountService from "./account"; import clientRelaysService from "./client-relays"; import { offlineMode } from "./offline-mode"; import replaceableEventsService from "./replaceable-events"; -import { APP_SETTING_IDENTIFIER, APP_SETTINGS_KIND } from "./user-app-settings"; import { eventStore, queryStore } from "./event-store"; import { Account } from "../classes/accounts/account"; import { MultiSubscription } from "applesauce-net/subscription"; import relayPoolService from "./relay-pool"; import { localRelay } from "./local-relay"; +import { APP_SETTING_IDENTIFIER, APP_SETTINGS_KIND } from "../helpers/app-settings"; const log = logger.extend("UserEventSync"); function downloadEvents(account: Account) { diff --git a/src/views/discovery/dvm-feed/components/dvm-card.tsx b/src/views/discovery/dvm-feed/components/dvm-card.tsx index c7ac420dd..f3dd32c4e 100644 --- a/src/views/discovery/dvm-feed/components/dvm-card.tsx +++ b/src/views/discovery/dvm-feed/components/dvm-card.tsx @@ -5,7 +5,7 @@ import { AddressPointer } from "nostr-tools/nip19"; import { NostrEvent } from "../../../../types/nostr-event"; import HoverLinkOverlay from "../../../../components/hover-link-overlay"; -import { DVMAvatarLink } from "./dvm-avatar"; +import { DVMAvatar } from "./dvm-avatar"; import { getEventAddressPointer } from "../../../../helpers/nostr/event"; import { DVMName } from "./dvm-name"; import DebugEventButton from "../../../../components/debug-modal/debug-event-button"; @@ -26,7 +26,7 @@ export default function DVMCard({ <> - + diff --git a/src/views/discovery/dvm-feed/feed.tsx b/src/views/discovery/dvm-feed/feed.tsx index 3a4cf84c8..38dc55f56 100644 --- a/src/views/discovery/dvm-feed/feed.tsx +++ b/src/views/discovery/dvm-feed/feed.tsx @@ -35,32 +35,30 @@ import Feed from "./components/feed"; import { AddressPointer } from "nostr-tools/nip19"; import useParamsAddressPointer from "../../../hooks/use-params-address-pointer"; import DVMParams from "./components/dvm-params"; -import useUserMailboxes from "../../../hooks/use-user-mailboxes"; +import { useUserOutbox } from "../../../hooks/use-user-mailboxes"; import { usePublishEvent } from "../../../providers/global/publish-provider"; import { getHumanReadableCoordinate } from "../../../services/replaceable-events"; function DVMFeedPage({ pointer }: { pointer: AddressPointer }) { - const [since] = useState(() => dayjs().subtract(1, "hour").unix()); + const [since] = useState(() => dayjs().subtract(1, "day").unix()); const publish = usePublishEvent(); const navigate = useNavigate(); const account = useCurrentAccount()!; const debugModal = useDisclosure(); - const dvmRelays = useUserMailboxes(pointer.pubkey)?.outboxes; + const dvmRelays = useUserOutbox(pointer.pubkey); const readRelays = useReadRelays(dvmRelays); - const { loader, timeline: events } = useTimelineLoader( + const { loader, timeline } = useTimelineLoader( `${getHumanReadableCoordinate(pointer.kind, pointer.pubkey, pointer.identifier)}-jobs`, readRelays, - [ - { - authors: [account.pubkey, pointer.pubkey], - "#p": [account.pubkey, pointer.pubkey], - kinds: [DVM_CONTENT_DISCOVERY_JOB_KIND, DVM_CONTENT_DISCOVERY_RESULT_KIND, DVM_STATUS_KIND], - since, - }, - ], + { + authors: [account.pubkey, pointer.pubkey], + "#p": [account.pubkey, pointer.pubkey], + kinds: [DVM_CONTENT_DISCOVERY_JOB_KIND, DVM_CONTENT_DISCOVERY_RESULT_KIND, DVM_STATUS_KIND], + since, + }, ); - const jobs = groupEventsIntoJobs(events); + const jobs = groupEventsIntoJobs(timeline); const pages = chainJobs(Array.from(Object.values(jobs))); const jobChains = flattenJobChain(pages); @@ -87,7 +85,7 @@ function DVMFeedPage({ pointer }: { pointer: AddressPointer }) { useEffect(() => { setRequesting(false); - }, [events.length]); + }, [timeline.length]); return ( diff --git a/src/views/other-stuff/apps.ts b/src/views/other-stuff/apps.ts index ca79ac024..42a5f3366 100644 --- a/src/views/other-stuff/apps.ts +++ b/src/views/other-stuff/apps.ts @@ -22,6 +22,7 @@ import ShieldOff from "../../components/icons/shield-off"; import MessageQuestionSquare from "../../components/icons/message-question-square"; import UploadCloud01 from "../../components/icons/upload-cloud-01"; import Edit04 from "../../components/icons/edit-04"; +import Users03 from "../../components/icons/users-03"; export const internalApps: App[] = [ { @@ -122,6 +123,13 @@ export const internalTools: App[] = [ id: "corrections", to: "/tools/corrections ", }, + { + title: "noStrudel Users", + description: "Discover other users using noStrudel", + icon: Users03, + id: "nostrudel-users", + to: "/tools/nostrudel-users", + }, ]; export const externalTools: App[] = [ diff --git a/src/views/tools/nostrudel-users/index.tsx b/src/views/tools/nostrudel-users/index.tsx new file mode 100644 index 000000000..5c31cfcde --- /dev/null +++ b/src/views/tools/nostrudel-users/index.tsx @@ -0,0 +1,136 @@ +import { + Box, + Flex, + IconButton, + Stat, + StatLabel, + StatNumber, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, +} from "@chakra-ui/react"; +import { NostrEvent } from "nostr-tools"; + +import VerticalPageLayout from "../../../components/vertical-page-layout"; +import { APP_SETTING_IDENTIFIER, APP_SETTINGS_KIND, AppSettings } from "../../../helpers/app-settings"; +import { useReadRelays } from "../../../hooks/use-client-relays"; +import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback"; +import useTimelineLoader from "../../../hooks/use-timeline-loader"; +import IntersectionObserverProvider from "../../../providers/local/intersection-observer"; +import { safeJson } from "../../../helpers/parse"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; +import UserLink from "../../../components/user/user-link"; +import Timestamp from "../../../components/timestamp"; +import useCurrentAccount from "../../../hooks/use-current-account"; +import { GhostIcon } from "../../../components/icons"; +import accountService from "../../../services/account"; +import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref"; + +function UserRow({ event, settings }: { event: NostrEvent; settings: Partial }) { + const account = useCurrentAccount(); + const isSelf = event.pubkey === account?.pubkey; + const ref = useEventIntersectionRef(event); + + return ( + + + + + + + + {settings.primaryColor && } + + + + + {!isSelf && ( + } + size="sm" + aria-label="ghost user" + title="ghost user" + onClick={() => accountService.startGhost(event.pubkey)} + /> + )} + + + ); +} + +export default function NoStrudelUsersView() { + const readRelays = useReadRelays(); + const { loader, timeline } = useTimelineLoader("nostrudel-users", readRelays, { + kinds: [APP_SETTINGS_KIND], + "#d": [APP_SETTING_IDENTIFIER], + }); + const callback = useTimelineCurserIntersectionCallback(loader); + + const users = timeline + .map((event) => ({ event, settings: safeJson>(event.content) })) + .filter((s) => !!s.settings) as { event: NostrEvent; settings: Partial }[]; + + const colors = new Set(users.map((u) => u.settings.primaryColor).filter((c) => !!c) as string[]); + const colorModes = users.reduce>((dir, u) => { + if (u.settings.colorMode) dir[u.settings.colorMode] = (dir[u.settings.colorMode] ?? 0) + 1; + return dir; + }, {}); + + return ( + + + + + Users + {timeline.length} + + + + Unique Colors + {colors.size} + + + + + + + + + + + + {Object.entries(colorModes).map(([mode, count]) => ( + + + + + ))} + +
Color ModeCount
{mode}{count}
+
+
+ + + + + + + + + + + + {users.map((user) => ( + + ))} + +
UserColorLast updated
+
+
+
+ ); +}