show recent event kinds on profile

This commit is contained in:
hzrd149 2024-11-19 13:45:41 -06:00
parent bf6d243d61
commit c9daeb96ae
6 changed files with 205 additions and 10 deletions

8
pnpm-lock.yaml generated
View File

@ -1368,8 +1368,8 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@kurkle/color@0.3.2':
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
'@lezer/common@1.2.3':
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
@ -5664,7 +5664,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@kurkle/color@0.3.2': {}
'@kurkle/color@0.3.4': {}
'@lezer/common@1.2.3': {}
@ -6424,7 +6424,7 @@ snapshots:
chart.js@4.4.6:
dependencies:
'@kurkle/color': 0.3.2
'@kurkle/color': 0.3.4
cheerio-select@2.1.0:
dependencies:

View File

@ -68,6 +68,8 @@ import Recording02 from "./icons/recording-02";
import Upload01 from "./icons/upload-01";
import Modem02 from "./icons/modem-02";
import BookOpen01 from "./icons/book-open-01";
import Edit04 from "./icons/edit-04";
import Film02 from "./icons/film-02";
const defaultProps: IconProps = { boxSize: 4 };
@ -248,3 +250,6 @@ export const InboxIcon = Download01;
export const OutboxIcon = Upload01;
export const WikiIcon = BookOpen01;
export const ArticleIcon = Edit04;
export const VideoIcon = Film02;

View File

@ -1,4 +1,5 @@
import {
ArticleIcon,
BadgeIcon,
BookmarkIcon,
ChannelsIcon,
@ -13,11 +14,11 @@ import {
SearchIcon,
TorrentIcon,
TrackIcon,
VideoIcon,
WikiIcon,
} from "../../components/icons";
import { App } from "./component/app-card";
import ShieldOff from "../../components/icons/shield-off";
import Film02 from "../../components/icons/film-02";
import MessageQuestionSquare from "../../components/icons/message-question-square";
import UploadCloud01 from "../../components/icons/upload-cloud-01";
import Edit04 from "../../components/icons/edit-04";
@ -52,8 +53,8 @@ export const internalApps: App[] = [
{ title: "Bookmarks", description: "Manage your bookmarks", icon: BookmarkIcon, id: "bookmarks", to: "/bookmarks" },
{ title: "Lists", description: "Browse and create lists", icon: ListsIcon, id: "lists", to: "/lists" },
{ title: "Tracks", description: "Browse stemstr tracks", icon: TrackIcon, id: "tracks", to: "/tracks" },
{ title: "Videos", description: "Browse flare videos", icon: Film02, id: "videos", to: "/videos" },
{ title: "Articles", description: "Browse articles", icon: Edit04, id: "articles", to: "/articles" },
{ title: "Videos", description: "Browse videos", icon: VideoIcon, id: "videos", to: "/videos" },
{ title: "Articles", description: "Browse articles", icon: ArticleIcon, id: "articles", to: "/articles" },
];
export const internalTools: App[] = [

View File

@ -21,7 +21,7 @@ import { useLocalStorage } from "react-use";
import { Subscription as IDBSubscription } from "nostr-idb";
import _throttle from "lodash.throttle";
import stringify from "json-stringify-deterministic";
import { useSearchParams } from "react-router-dom";
import { useLocation, useSearchParams } from "react-router-dom";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import BackButton from "../../../components/router/back-button";
@ -52,6 +52,7 @@ const EventTimeline = memo(({ events }: { events: NostrEvent[] }) => {
export default function EventConsoleView() {
const [params, setParams] = useSearchParams();
const location = useLocation();
const historyDrawer = useDisclosure();
const [history, setHistory] = useLocalStorage<string[]>("console-history", []);
const helpModal = useDisclosure();
@ -62,11 +63,13 @@ export default function EventConsoleView() {
const [sub, setSub] = useState<Subscription | IDBSubscription | null>(null);
const [query, setQuery] = useState(() => {
if (params.has("filter")) {
if (params.has("filter") || location.state.filter) {
const str = params.get("filter");
if (str) {
const f = safeJson(str, null);
if (f) return JSON.stringify(f, null, 2);
} else if (typeof location.state.filter === "object") {
return JSON.stringify(location.state.filter, null, 2);
}
}
if (history?.[0]) return history?.[0];

View File

@ -17,7 +17,7 @@ import {
Text,
useDisclosure,
} from "@chakra-ui/react";
import { nip19 } from "nostr-tools";
import { nip19, NostrEvent } from "nostr-tools";
import { ChatIcon } from "@chakra-ui/icons";
import { getLudEndpoint } from "../../../helpers/lnurl";
@ -51,6 +51,9 @@ import UserName from "../../../components/user/user-name";
import { useUserDNSIdentity } from "../../../hooks/use-user-dns-identity";
import { renderGenericUrl } from "../../../components/content/links/common";
import UserAboutContent from "../../../components/user/user-about";
import { useStoreQuery } from "applesauce-react/hooks";
import { TimelineQuery } from "applesauce-core/queries";
import UserRecentEvents from "./user-recent-events";
function DNSIdentityWarning({ pubkey }: { pubkey: string }) {
const metadata = useUserProfile(pubkey);
@ -218,6 +221,10 @@ export default function UserAboutTab() {
</Flex>
<UserProfileBadges pubkey={pubkey} px="2" />
<Box px="2">
<Heading size="md">Recent activity:</Heading>
<UserRecentEvents pubkey={pubkey} />
</Box>
<UserStatsAccordion pubkey={pubkey} />
<Flex gap="2" wrap="wrap">

View File

@ -0,0 +1,179 @@
import { Badge, Button, ButtonProps, ComponentWithAs, Flex, IconProps, useDisclosure } from "@chakra-ui/react";
import { Filter, kinds, nip19, NostrEvent } from "nostr-tools";
import { Link as RouteLink, To } from "react-router-dom";
import { ArticleIcon, DirectMessagesIcon, ListsIcon, NotesIcon, RepostIcon } from "../../../components/icons";
import AnnotationQuestion from "../../../components/icons/annotation-question";
import { getSharableEventAddress } from "../../../services/event-relay-hint";
import { npubEncode } from "nostr-tools/nip19";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import { useUserOutbox } from "../../../hooks/use-user-mailboxes";
import { useReadRelays } from "../../../hooks/use-client-relays";
import AlertTriangle from "../../../components/icons/alert-triangle";
type KnownKind = {
kind: number;
name?: string;
hidden?: boolean;
icon?: ComponentWithAs<"svg", IconProps>;
link?: (events: NostrEvent[], pubkey: string) => LinkNav | undefined;
single?: (event: NostrEvent, pubkey: string) => LinkNav | undefined;
multiple?: (events: NostrEvent[], pubkey: string) => LinkNav | undefined;
};
type LinkNav = string | { to: To; state: any };
function singleLink(event: NostrEvent, _pubkey: string) {
const address = getSharableEventAddress(event);
return address ? `/l/${address}` : undefined;
}
function consoleLink(events: NostrEvent[], pubkey: string) {
const kinds = new Set(events.map((e) => e.kind));
return {
to: "/tools/console",
state: { filter: { kinds: Array.from(kinds), authors: [pubkey] } satisfies Filter },
};
}
const KnownKinds: KnownKind[] = [
{
kind: kinds.ShortTextNote,
name: "Notes",
icon: NotesIcon,
link: (_, p) => `/u/${npubEncode(p)}/notes`,
},
{
kind: kinds.Repost,
name: "Repost",
icon: RepostIcon,
link: (_e, p) => `/u/${npubEncode(p)}/notes`,
},
{
kind: kinds.GenericRepost,
name: "Generic Repost",
icon: RepostIcon,
hidden: true,
link: (_e, p) => `/u/${npubEncode(p)}/notes`,
},
{
kind: kinds.LongFormArticle,
name: "Articles",
icon: ArticleIcon,
link: (_, p) => `/u/${npubEncode(p)}/articles`,
},
{
kind: kinds.EncryptedDirectMessage,
name: "Legacy DMs",
icon: DirectMessagesIcon,
link: (_e, p) => `/u/${nip19.npubEncode(p)}/dms`,
},
{ kind: kinds.Followsets, name: "Lists", icon: ListsIcon, link: (_e, p) => `/u/${npubEncode(p)}/lists` },
{ kind: kinds.Report, name: "Report", icon: AlertTriangle, link: (_e, p) => `/u/${npubEncode(p)}/reports` },
{ kind: kinds.Handlerinformation, name: "Application" },
{ kind: kinds.Handlerrecommendation, name: "App recommendation" },
{ kind: kinds.BadgeAward, name: "Badge Award" },
// common kinds
{ kind: kinds.Metadata, hidden: true },
{ kind: kinds.Contacts, hidden: true },
{ kind: kinds.EventDeletion, hidden: true },
{ kind: kinds.Reaction, hidden: true },
// NIP-51 lists
{ kind: kinds.RelayList, hidden: true },
{ kind: kinds.BookmarkList, hidden: true },
{ kind: kinds.InterestsList, hidden: true },
{ kind: kinds.Pinlist, hidden: true },
{ kind: kinds.UserEmojiList, hidden: true },
{ kind: kinds.Mutelist, hidden: true },
{ kind: kinds.CommunitiesList, hidden: true },
{ kind: kinds.SearchRelaysList, hidden: true },
{ kind: kinds.BlockedRelaysList, hidden: true },
{ kind: 30008, hidden: true, name: "Badges" }, // profile badges
{ kind: kinds.Application, name: "App data", hidden: true },
];
function EventKindButton({
kind,
events,
pubkey,
known,
}: { kind: number; events: NostrEvent[]; pubkey: string; known?: KnownKind } & Omit<ButtonProps, "icon">) {
const Icon = known?.icon;
const icon = Icon ? <Icon boxSize={10} mb="4" /> : <AnnotationQuestion boxSize={10} mb="4" />;
let nav = known?.link?.(events, pubkey);
if (!nav) {
if (events.length === 1) {
nav = known?.single?.(events[0], pubkey) || singleLink(events[0], pubkey);
} else {
nav = known?.multiple?.(events, pubkey) || consoleLink(events, pubkey);
}
}
const linkProps = typeof nav === "string" ? { to: nav } : nav;
return (
<Button
as={RouteLink}
{...linkProps}
variant="outline"
leftIcon={icon}
h="36"
w="36"
flexDirection="column"
position="relative"
>
<Badge position="absolute" top="2" right="2" fontSize="md">
{events.length}
</Badge>
{known?.name || kind}
</Button>
);
}
export default function UserRecentEvents({ pubkey }: { pubkey: string }) {
const outbox = useUserOutbox(pubkey);
const readRelays = useReadRelays();
const { timeline: recent } = useTimelineLoader(`${pubkey}-recent-events`, outbox || readRelays, {
authors: [pubkey],
limit: 100,
});
// const recent = useStoreQuery(TimelineQuery, [{ authors: [pubkey], limit: 100 }]);
const all = useDisclosure();
const byKind = recent?.reduce(
(dir, event) => {
if (dir[event.kind]) dir[event.kind].events.push(event);
else
dir[event.kind] = {
known: KnownKinds.find((k) => k.kind === event.kind),
events: [event],
};
return dir;
},
{} as Record<number, { events: NostrEvent[]; known?: KnownKind }>,
);
return (
<Flex gap="2" wrap="wrap">
{byKind &&
Object.entries(byKind)
.filter(([_, { known }]) => (known ? known.hidden !== true : true))
.sort((a, b) => parseInt(a[0]) - parseInt(b[0]))
.map(([kind, { events, known }]) => (
<EventKindButton key={kind} kind={parseInt(kind)} events={events} pubkey={pubkey} known={known} />
))}
</Flex>
);
}