mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
add noStrudel users view
This commit is contained in:
parent
5403d37a49
commit
fb5d7f2d2b
@ -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: <UnknownTimelineView /> },
|
||||
{ path: "console", element: <EventConsoleView /> },
|
||||
{ path: "corrections", element: <CorrectionsFeedView /> },
|
||||
{ path: "nostrudel-users", element: <NoStrudelUsersView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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<string, number>) {
|
||||
const sortedKinds = Object.entries(kinds)
|
||||
@ -42,19 +16,7 @@ function createChartData(kinds: Record<string, number>) {
|
||||
}
|
||||
|
||||
export default function EventKindsPieChart({ kinds }: { kinds: Record<string, number> }) {
|
||||
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 (
|
||||
<Pie
|
||||
data={chartData}
|
||||
options={{
|
||||
color,
|
||||
plugins: { colors: { forceOverride: true } },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <SimplePieChart data={chartData} />;
|
||||
}
|
||||
|
44
src/components/charts/simple-pie-chart.tsx
Normal file
44
src/components/charts/simple-pie-chart.tsx
Normal file
@ -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 (
|
||||
<Pie
|
||||
data={data}
|
||||
options={{
|
||||
color,
|
||||
plugins: { colors: { forceOverride: true } },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ export function safeUrl(url: string) {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function safeJson<T>(json: string, fallback: T) {
|
||||
export function safeJson<T>(json: string): T | undefined;
|
||||
export function safeJson<T>(json: string, fallback: T): T;
|
||||
export function safeJson<T>(json: string, fallback?: T): T | undefined {
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch (e) {
|
||||
|
@ -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";
|
||||
|
@ -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<AppSettings> {
|
||||
@ -12,7 +11,7 @@ export default function AppSettingsQuery(pubkey: string): Query<AppSettings> {
|
||||
events.replaceable(APP_SETTINGS_KIND, pubkey, APP_SETTING_IDENTIFIER).pipe(
|
||||
map((event) => {
|
||||
if (!event) return defaultSettings;
|
||||
const parsed = safeJson(event.content, defaultSettings) as Partial<AppSettings>;
|
||||
const parsed = safeJson<Partial<AppSettings>>(event.content, defaultSettings);
|
||||
return { ...defaultSettings, ...parsed };
|
||||
}),
|
||||
),
|
||||
|
@ -1,2 +0,0 @@
|
||||
export const APP_SETTINGS_KIND = 30078;
|
||||
export const APP_SETTING_IDENTIFIER = "nostrudel-settings";
|
@ -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) {
|
||||
|
@ -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({
|
||||
<>
|
||||
<Card as={LinkBox} display="block" p="4" ref={ref} {...props}>
|
||||
<DebugEventButton size="sm" float="right" zIndex={1} event={appData} />
|
||||
<DVMAvatarLink pointer={pointer} w="24" float="left" mr="4" mb="2" />
|
||||
<DVMAvatar pointer={pointer} w="24" float="left" mr="4" mb="2" />
|
||||
<Heading size="md">
|
||||
<HoverLinkOverlay as={RouterLink} to={to} onClick={onClick}>
|
||||
<DVMName pointer={pointer} />
|
||||
|
@ -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 (
|
||||
<VerticalPageLayout>
|
||||
|
@ -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[] = [
|
||||
|
136
src/views/tools/nostrudel-users/index.tsx
Normal file
136
src/views/tools/nostrudel-users/index.tsx
Normal file
@ -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<AppSettings> }) {
|
||||
const account = useCurrentAccount();
|
||||
const isSelf = event.pubkey === account?.pubkey;
|
||||
const ref = useEventIntersectionRef<HTMLTableRowElement>(event);
|
||||
|
||||
return (
|
||||
<Tr ref={ref}>
|
||||
<Td>
|
||||
<Flex alignItems="center" gap="2">
|
||||
<UserAvatarLink pubkey={event.pubkey} size="sm" />
|
||||
<UserLink pubkey={event.pubkey} />
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>{settings.primaryColor && <Box w="6" h="6" rounded="md" bg={settings.primaryColor} />}</Td>
|
||||
<Td isNumeric>
|
||||
<Timestamp timestamp={event.created_at} />
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
{!isSelf && (
|
||||
<IconButton
|
||||
icon={<GhostIcon />}
|
||||
size="sm"
|
||||
aria-label="ghost user"
|
||||
title="ghost user"
|
||||
onClick={() => accountService.startGhost(event.pubkey)}
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
|
||||
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<Partial<AppSettings>>(event.content) }))
|
||||
.filter((s) => !!s.settings) as { event: NostrEvent; settings: Partial<AppSettings> }[];
|
||||
|
||||
const colors = new Set(users.map((u) => u.settings.primaryColor).filter((c) => !!c) as string[]);
|
||||
const colorModes = users.reduce<Record<string, number>>((dir, u) => {
|
||||
if (u.settings.colorMode) dir[u.settings.colorMode] = (dir[u.settings.colorMode] ?? 0) + 1;
|
||||
return dir;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Flex gap="4" wrap="wrap">
|
||||
<Stat maxW={{ base: "50%", md: "xs" }}>
|
||||
<StatLabel>Users</StatLabel>
|
||||
<StatNumber>{timeline.length}</StatNumber>
|
||||
</Stat>
|
||||
|
||||
<Stat maxW={{ base: "50%", md: "xs" }}>
|
||||
<StatLabel>Unique Colors</StatLabel>
|
||||
<StatNumber>{colors.size}</StatNumber>
|
||||
</Stat>
|
||||
|
||||
<TableContainer w={{ base: "full", sm: "xs" }}>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Color Mode</Th>
|
||||
<Th isNumeric>Count</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{Object.entries(colorModes).map(([mode, count]) => (
|
||||
<Tr key={mode}>
|
||||
<Td>{mode}</Td>
|
||||
<Td isNumeric>{count}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Flex>
|
||||
<TableContainer>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>User</Th>
|
||||
<Th>Color</Th>
|
||||
<Th isNumeric>Last updated</Th>
|
||||
<Th isNumeric></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{users.map((user) => (
|
||||
<UserRow key={user.event.pubkey} event={user.event} settings={user.settings} />
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</IntersectionObserverProvider>
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user