diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d92f093c..e5139f39a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/components/icons.tsx b/src/components/icons.tsx index a314bad6c..cb5e852b2 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -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; diff --git a/src/views/other-stuff/apps.ts b/src/views/other-stuff/apps.ts index 5a476c762..ca79ac024 100644 --- a/src/views/other-stuff/apps.ts +++ b/src/views/other-stuff/apps.ts @@ -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[] = [ diff --git a/src/views/tools/event-console/index.tsx b/src/views/tools/event-console/index.tsx index d0ab20e95..160ae209b 100644 --- a/src/views/tools/event-console/index.tsx +++ b/src/views/tools/event-console/index.tsx @@ -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("console-history", []); const helpModal = useDisclosure(); @@ -62,11 +63,13 @@ export default function EventConsoleView() { const [sub, setSub] = useState(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]; diff --git a/src/views/user/about/index.tsx b/src/views/user/about/index.tsx index 7521b19a5..885f3eede 100644 --- a/src/views/user/about/index.tsx +++ b/src/views/user/about/index.tsx @@ -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() { + + Recent activity: + + diff --git a/src/views/user/about/user-recent-events.tsx b/src/views/user/about/user-recent-events.tsx new file mode 100644 index 000000000..ef1191604 --- /dev/null +++ b/src/views/user/about/user-recent-events.tsx @@ -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) { + const Icon = known?.icon; + const icon = Icon ? : ; + + 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 ( + + ); +} + +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, + ); + + return ( + + {byKind && + Object.entries(byKind) + .filter(([_, { known }]) => (known ? known.hidden !== true : true)) + .sort((a, b) => parseInt(a[0]) - parseInt(b[0])) + .map(([kind, { events, known }]) => ( + + ))} + + ); +}