diff --git a/src/helpers/nostr/events.ts b/src/helpers/nostr/events.ts index e1f7a8b45..64a1db03a 100644 --- a/src/helpers/nostr/events.ts +++ b/src/helpers/nostr/events.ts @@ -187,16 +187,18 @@ export function pointerToATag(pointer: AddressPointer): ATag { return relay ? ["a", coordinate, relay] : ["a", coordinate]; } -export type CustomEventPointer = Omit & { +export type CustomAddressPointer = Omit & { identifier?: string; }; -export function parseCoordinate(a: string): CustomEventPointer | null; -export function parseCoordinate(a: string, requireD: false): CustomEventPointer | null; +export function parseCoordinate(a: string): CustomAddressPointer | null; +export function parseCoordinate(a: string, requireD: false): CustomAddressPointer | null; export function parseCoordinate(a: string, requireD: true): AddressPointer | null; -export function parseCoordinate(a: string, requireD: false, silent: false): CustomEventPointer; +export function parseCoordinate(a: string, requireD: false, silent: false): CustomAddressPointer; export function parseCoordinate(a: string, requireD: true, silent: false): AddressPointer; -export function parseCoordinate(a: string, requireD = false, silent = true): CustomEventPointer | null { +export function parseCoordinate(a: string, requireD: true, silent: true): AddressPointer | null; +export function parseCoordinate(a: string, requireD: false, silent: true): CustomAddressPointer | null; +export function parseCoordinate(a: string, requireD = false, silent = true): CustomAddressPointer | null { const parts = a.split(":") as (string | undefined)[]; const kind = parts[0] && parseInt(parts[0]); const pubkey = parts[1]; diff --git a/src/hooks/use-params-address-pointer.ts b/src/hooks/use-params-address-pointer.ts new file mode 100644 index 000000000..d2f3ee165 --- /dev/null +++ b/src/hooks/use-params-address-pointer.ts @@ -0,0 +1,33 @@ +import { useParams } from "react-router-dom"; +import { nip19 } from "nostr-tools"; +import type { AddressPointer } from "nostr-tools/lib/types/nip19"; + +import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/events"; + +export default function useParamsAddressPointer(key: string): AddressPointer; +export default function useParamsAddressPointer(key: string, requireD: true): AddressPointer; +export default function useParamsAddressPointer(key: string, requireD: false): CustomAddressPointer; +export default function useParamsAddressPointer(key: string, requireD: boolean = true): AddressPointer { + const params = useParams(); + const value = params[key] as string; + if (!value) throw new Error(`Missing ${key} in route`); + + if (value.includes(":")) { + if (requireD) { + return parseCoordinate(value, true, false); + } else { + // @ts-ignore + return parseCoordinate(value, false, false); + } + } + + // its not a coordinate, try to parse the nip19 + const pointer = nip19.decode(value as string); + + switch (pointer.type) { + case "naddr": + return pointer.data; + default: + throw new Error(`Unknown type ${pointer.type}`); + } +} diff --git a/src/hooks/use-params-event-pointer.ts b/src/hooks/use-params-event-pointer.ts new file mode 100644 index 000000000..1d648ed7b --- /dev/null +++ b/src/hooks/use-params-event-pointer.ts @@ -0,0 +1,23 @@ +import { useParams } from "react-router-dom"; +import { nip19 } from "nostr-tools"; +import type { EventPointer } from "nostr-tools/lib/types/nip19"; + +import { isHexKey } from "../helpers/nip19"; + +export default function useParamsEventPointer(key: string): EventPointer { + const params = useParams(); + const value = params[key] as string; + if (!value) throw new Error(`Missing ${key} in route`); + + if (isHexKey(value)) return { id: value, relays: [] }; + const pointer = nip19.decode(value as string); + + switch (pointer.type) { + case "note": + return { id: pointer.data as string, relays: [] }; + case "nevent": + return pointer.data; + default: + throw new Error(`Unknown type ${pointer.type}`); + } +} diff --git a/src/hooks/use-params-pubkey-pointer.ts b/src/hooks/use-params-pubkey-pointer.ts new file mode 100644 index 000000000..774ef5f18 --- /dev/null +++ b/src/hooks/use-params-pubkey-pointer.ts @@ -0,0 +1,22 @@ +import { useParams } from "react-router-dom"; +import { nip19 } from "nostr-tools"; +import type { ProfilePointer } from "nostr-tools/lib/types/nip19"; +import { isHexKey } from "../helpers/nip19"; + +export default function useParamsProfilePointer(key: string = "pubkey"): ProfilePointer { + const params = useParams(); + const value = params[key] as string; + if (!value) throw new Error(`Missing ${key} in route`); + + if (isHexKey(value)) return { pubkey: value, relays: [] }; + const pointer = nip19.decode(value); + + switch (pointer.type) { + case "npub": + return { pubkey: pointer.data as string, relays: [] }; + case "nprofile": + return pointer.data; + default: + throw new Error(`Unknown type ${pointer.type}`); + } +} diff --git a/src/hooks/use-replaceable-event.ts b/src/hooks/use-replaceable-event.ts index a8acbf369..75dca6630 100644 --- a/src/hooks/use-replaceable-event.ts +++ b/src/hooks/use-replaceable-event.ts @@ -2,11 +2,11 @@ import { useMemo } from "react"; import { useReadRelayUrls } from "./use-client-relays"; import replaceableEventLoaderService, { RequestOptions } from "../services/replaceable-event-requester"; -import { CustomEventPointer, parseCoordinate } from "../helpers/nostr/events"; +import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/events"; import useSubject from "./use-subject"; export default function useReplaceableEvent( - cord: string | CustomEventPointer | undefined, + cord: string | CustomAddressPointer | undefined, additionalRelays: string[] = [], opts: RequestOptions = {}, ) { diff --git a/src/hooks/use-replaceable-events.ts b/src/hooks/use-replaceable-events.ts index b79eb2d01..5c13777ae 100644 --- a/src/hooks/use-replaceable-events.ts +++ b/src/hooks/use-replaceable-events.ts @@ -2,13 +2,13 @@ import { useMemo } from "react"; import { useReadRelayUrls } from "./use-client-relays"; import replaceableEventLoaderService, { RequestOptions } from "../services/replaceable-event-requester"; -import { CustomEventPointer, parseCoordinate } from "../helpers/nostr/events"; +import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/events"; import Subject from "../classes/subject"; import { NostrEvent } from "../types/nostr-event"; import useSubjects from "./use-subjects"; export default function useReplaceableEvents( - coordinates: string[] | CustomEventPointer[] | undefined, + coordinates: string[] | CustomAddressPointer[] | undefined, additionalRelays: string[] = [], opts: RequestOptions = {}, ) { diff --git a/src/views/badges/badge-details.tsx b/src/views/badges/badge-details.tsx index 48d9e3b15..45ebb9662 100644 --- a/src/views/badges/badge-details.tsx +++ b/src/views/badges/badge-details.tsx @@ -1,5 +1,5 @@ -import { useNavigate, useParams } from "react-router-dom"; -import { Kind, nip19 } from "nostr-tools"; +import { useNavigate } from "react-router-dom"; +import { Kind } from "nostr-tools"; import { Button, Flex, @@ -35,6 +35,7 @@ import VerticalPageLayout from "../../components/vertical-page-layout"; import BadgeAwardCard from "./components/badge-award-card"; import TimelineLoader from "../../classes/timeline-loader"; import { ErrorBoundary } from "../../components/error-boundary"; +import useParamsAddressPointer from "../../hooks/use-params-address-pointer"; function BadgeActivityTab({ timeline }: { timeline: TimelineLoader }) { const awards = useSubject(timeline.timeline); @@ -154,15 +155,8 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) { ); } -function useBadgeCoordinate() { - const { naddr } = useParams() as { naddr: string }; - const parsed = nip19.decode(naddr); - if (parsed.type !== "naddr") throw new Error(`Unknown type ${parsed.type}`); - return parsed.data; -} - export default function BadgeDetailsView() { - const pointer = useBadgeCoordinate(); + const pointer = useParamsAddressPointer("naddr"); const badge = useReplaceableEvent(pointer); if (!badge) return ; diff --git a/src/views/channels/channel.tsx b/src/views/channels/channel.tsx index da6e0b023..742c34787 100644 --- a/src/views/channels/channel.tsx +++ b/src/views/channels/channel.tsx @@ -1,9 +1,8 @@ import { memo, useCallback, useMemo } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { Button, Flex, Heading, Spacer, Spinner, useDisclosure } from "@chakra-ui/react"; import { Kind } from "nostr-tools"; -import { safeDecode } from "../../helpers/nip19"; import useSingleEvent from "../../hooks/use-single-event"; import { ErrorBoundary } from "../../components/error-boundary"; import { NostrEvent } from "../../types/nostr-event"; @@ -25,6 +24,7 @@ import { groupMessages } from "../../helpers/nostr/dms"; import ChannelMessageBlock from "./components/channel-message-block"; import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; import ChannelMessageForm from "./components/send-message-form"; +import useParamsEventPointer from "../../hooks/use-params-event-pointer"; const ChannelChatLog = memo(({ timeline, channel }: { timeline: TimelineLoader; channel: NostrEvent }) => { const messages = useSubject(timeline.timeline); @@ -109,14 +109,8 @@ function ChannelPage({ channel }: { channel: NostrEvent }) { } export default function ChannelView() { - const { id } = useParams() as { id: string }; - const parsed = useMemo(() => { - const result = safeDecode(id); - if (!result) return; - if (result.type === "note") return { id: result.data }; - if (result.type === "nevent") return result.data; - }, [id]); - const channel = useSingleEvent(parsed?.id, parsed?.relays ?? []); + const pointer = useParamsEventPointer("id"); + const channel = useSingleEvent(pointer?.id, pointer?.relays); if (!channel) return ; diff --git a/src/views/dms/chat.tsx b/src/views/dms/chat.tsx index 94c30b9ad..c49de36b5 100644 --- a/src/views/dms/chat.tsx +++ b/src/views/dms/chat.tsx @@ -24,6 +24,7 @@ import ThreadsProvider from "../../providers/thread-provider"; import { useRouterMarker } from "../../providers/drawer-sub-view-provider"; import TimelineLoader from "../../classes/timeline-loader"; import DirectMessageBlock from "./components/direct-message-block"; +import useParamsProfilePointer from "../../hooks/use-params-pubkey-pointer"; /** This is broken out from DirectMessageChatPage for performance reasons. Don't use outside of file */ const ChatLog = memo(({ timeline }: { timeline: TimelineLoader }) => { @@ -142,25 +143,8 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) { ); } -function useUserPointer() { - const { pubkey } = useParams() as { pubkey: string }; - - if (isHexKey(pubkey)) return { pubkey, relays: [] }; - const pointer = nip19.decode(pubkey); - - switch (pointer.type) { - case "npub": - return { pubkey: pointer.data as string, relays: [] }; - case "nprofile": - const d = pointer.data as nip19.ProfilePointer; - return { pubkey: d.pubkey, relays: d.relays ?? [] }; - default: - throw new Error(`Unknown type ${pointer.type}`); - } -} - export default function DirectMessageChatView() { - const { pubkey } = useUserPointer(); + const { pubkey } = useParamsProfilePointer(); return ( diff --git a/src/views/dvm-feed/feed.tsx b/src/views/dvm-feed/feed.tsx index 5ed55a8ca..8c587aa0f 100644 --- a/src/views/dvm-feed/feed.tsx +++ b/src/views/dvm-feed/feed.tsx @@ -13,8 +13,8 @@ import { useToast, } from "@chakra-ui/react"; import { ChevronLeftIcon } from "@chakra-ui/icons"; -import { nip19 } from "nostr-tools"; import dayjs from "dayjs"; +import { useNavigate } from "react-router-dom"; import { DMV_CONTENT_DISCOVERY_JOB_KIND, @@ -32,7 +32,6 @@ import useSubject from "../../hooks/use-subject"; import useTimelineLoader from "../../hooks/use-timeline-loader"; import { useReadRelayUrls } from "../../hooks/use-client-relays"; import { useUserRelays } from "../../hooks/use-user-relays"; -import { useNavigate, useParams } from "react-router-dom"; import { useSigningContext } from "../../providers/signing-provider"; import useCurrentAccount from "../../hooks/use-current-account"; import RequireCurrentAccount from "../../providers/require-current-account"; @@ -40,8 +39,8 @@ import { CodeIcon } from "../../components/icons"; import { unique } from "../../helpers/array"; import DebugChains from "./components/debug-chains"; import Feed from "./components/feed"; -import { parseCoordinate } from "../../helpers/nostr/events"; import { AddressPointer } from "nostr-tools/lib/types/nip19"; +import useParamsAddressPointer from "../../hooks/use-params-address-pointer"; function DVMFeedPage({ pointer }: { pointer: AddressPointer }) { const [since] = useState(() => dayjs().subtract(1, "hour").unix()); @@ -124,20 +123,8 @@ function DVMFeedPage({ pointer }: { pointer: AddressPointer }) { ); } -function useDVMCoordinate() { - const { addr } = useParams() as { addr: string }; - if (addr.includes(":")) { - const parsed = parseCoordinate(addr, true); - if (!parsed) throw new Error("Bad coordinate"); - return parsed; - } - - const parsed = nip19.decode(addr); - if (parsed.type !== "naddr") throw new Error(`Unknown type ${parsed.type}`); - return parsed.data; -} export default function DVMFeedView() { - const pointer = useDVMCoordinate(); + const pointer = useParamsAddressPointer("addr"); return ( diff --git a/src/views/emoji-packs/emoji-pack.tsx b/src/views/emoji-packs/emoji-pack.tsx index bce8268fb..7a4f92bbe 100644 --- a/src/views/emoji-packs/emoji-pack.tsx +++ b/src/views/emoji-packs/emoji-pack.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { nip19 } from "nostr-tools"; +import { useNavigate } from "react-router-dom"; import { useForm } from "react-hook-form"; import { useThrottle } from "react-use"; import dayjs from "dayjs"; @@ -8,7 +7,6 @@ import dayjs from "dayjs"; import { Button, ButtonGroup, - Divider, Flex, Heading, Image, @@ -39,6 +37,7 @@ import UserAvatarLink from "../../components/user-avatar-link"; import NoteZapButton from "../../components/note/note-zap-button"; import { QuoteRepostButton } from "../../components/note/components/quote-repost-button"; import Timestamp from "../../components/timestamp"; +import useParamsAddressPointer from "../../hooks/use-params-address-pointer"; function AddEmojiForm({ onAdd }: { onAdd: (values: { name: string; url: string }) => void }) { const { register, handleSubmit, watch, getValues, reset } = useForm({ @@ -220,15 +219,8 @@ function EmojiPackPage({ pack }: { pack: NostrEvent }) { ); } -function useListCoordinate() { - const { addr } = useParams() as { addr: string }; - const parsed = nip19.decode(addr); - if (parsed.type !== "naddr") throw new Error(`Unknown type ${parsed.type}`); - return parsed.data; -} - export default function EmojiPackView() { - const coordinate = useListCoordinate(); + const coordinate = useParamsAddressPointer("addr"); const pack = useReplaceableEvent(coordinate); if (!pack) { diff --git a/src/views/goals/goal-details.tsx b/src/views/goals/goal-details.tsx index 13b68a5bb..c24dc0058 100644 --- a/src/views/goals/goal-details.tsx +++ b/src/views/goals/goal-details.tsx @@ -1,5 +1,4 @@ -import { useNavigate, useParams } from "react-router-dom"; -import { nip19 } from "nostr-tools"; +import { useNavigate } from "react-router-dom"; import { Button, ButtonGroup, Divider, Flex, Heading, Spacer, Spinner } from "@chakra-ui/react"; import { ChevronLeftIcon } from "../../components/icons"; @@ -7,8 +6,6 @@ import GoalMenu from "./components/goal-menu"; import { getGoalAmount, getGoalName } from "../../helpers/nostr/goal"; import GoalProgress from "./components/goal-progress"; import useSingleEvent from "../../hooks/use-single-event"; -import { isHexKey } from "../../helpers/nip19"; -import { EventPointer } from "nostr-tools/lib/types/nip19"; import UserAvatar from "../../components/user-avatar"; import UserLink from "../../components/user-link"; import GoalContents from "./components/goal-contents"; @@ -16,19 +13,11 @@ import GoalZapList from "./components/goal-zap-list"; import { readablizeSats } from "../../helpers/bolt11"; import GoalZapButton from "./components/goal-zap-button"; import VerticalPageLayout from "../../components/vertical-page-layout"; - -function useGoalPointerFromParams(): EventPointer { - const { id } = useParams() as { id: string }; - if (isHexKey(id)) return { id }; - const parsed = nip19.decode(id); - if (parsed.type === "nevent") return parsed.data; - if (parsed.type === "note") return { id: parsed.data }; - throw new Error("bad goal id"); -} +import useParamsEventPointer from "../../hooks/use-params-event-pointer"; export default function GoalDetailsView() { const navigate = useNavigate(); - const pointer = useGoalPointerFromParams(); + const pointer = useParamsEventPointer("id"); const goal = useSingleEvent(pointer.id, pointer.relays); if (!goal) return ; diff --git a/src/views/lists/list-details.tsx b/src/views/lists/list-details.tsx index adf7047b9..9a964671b 100644 --- a/src/views/lists/list-details.tsx +++ b/src/views/lists/list-details.tsx @@ -1,12 +1,12 @@ -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { Kind, nip19 } from "nostr-tools"; +import type { DecodeResult } from "nostr-tools/lib/types/nip19"; +import { Box, Button, Flex, Heading, SimpleGrid, Spacer, Text } from "@chakra-ui/react"; import UserLink from "../../components/user-link"; -import { Box, Button, Flex, Heading, SimpleGrid, Spacer, Text } from "@chakra-ui/react"; import { ChevronLeftIcon } from "../../components/icons"; import useCurrentAccount from "../../hooks/use-current-account"; import { useDeleteEventContext } from "../../providers/delete-event-provider"; -import { parseCoordinate } from "../../helpers/nostr/events"; import { getEventsFromList, getListDescription, @@ -27,23 +27,9 @@ import VerticalPageLayout from "../../components/vertical-page-layout"; import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities"; import { EmbedEvent, EmbedEventPointer } from "../../components/embed-event"; import { encodePointer } from "../../helpers/nip19"; -import { DecodeResult } from "nostr-tools/lib/types/nip19"; import useSingleEvent from "../../hooks/use-single-event"; import UserAvatarLink from "../../components/user-avatar-link"; - -function useListCoordinate() { - const { addr } = useParams() as { addr: string }; - - if (addr.includes(":")) { - const parsed = parseCoordinate(addr); - if (!parsed) throw new Error("Bad coordinate"); - return parsed; - } - - const parsed = nip19.decode(addr); - if (parsed.type !== "naddr") throw new Error(`Unknown type ${parsed.type}`); - return parsed.data; -} +import useParamsAddressPointer from "../../hooks/use-params-address-pointer"; function BookmarkedEvent({ id, relays }: { id: string; relays?: string[] }) { const event = useSingleEvent(id, relays); @@ -53,16 +39,16 @@ function BookmarkedEvent({ id, relays }: { id: string; relays?: string[] }) { export default function ListDetailsView() { const navigate = useNavigate(); - const coordinate = useListCoordinate(); + const pointer = useParamsAddressPointer("addr"); const { deleteEvent } = useDeleteEventContext(); const account = useCurrentAccount(); - const list = useReplaceableEvent(coordinate, [], { alwaysRequest: true }); + const list = useReplaceableEvent(pointer, [], { alwaysRequest: true }); if (!list) return ( <> - Looking for list "{coordinate.identifier}" created by + Looking for list "{pointer.identifier}" created by ); diff --git a/src/views/note-transform/index.tsx b/src/views/note-transform/index.tsx new file mode 100644 index 000000000..edace0ec0 --- /dev/null +++ b/src/views/note-transform/index.tsx @@ -0,0 +1,5 @@ +import useParamsEventPointer from "../../hooks/use-params-event-pointer"; + +export default function NoteTransformView() { + const pointer = useParamsEventPointer("id"); +} diff --git a/src/views/streams/stream/index.tsx b/src/views/streams/stream/index.tsx index 6a02eda73..0f6fdcaa3 100644 --- a/src/views/streams/stream/index.tsx +++ b/src/views/streams/stream/index.tsx @@ -42,8 +42,6 @@ import StreamSatsPerMinute from "../components/stream-sats-per-minute"; import { UserEmojiProvider } from "../../../providers/emoji-provider"; import StreamStatusBadge from "../components/status-badge"; import ChatMessageForm from "./stream-chat/stream-chat-form"; -import useStreamChatTimeline from "./stream-chat/use-stream-chat-timeline"; -import UserSearchDirectoryProvider from "../../../providers/user-directory-provider"; import StreamChatLog from "./stream-chat/chat-log"; import TopZappers from "../components/top-zappers"; import StreamHashtags from "../components/stream-hashtags"; diff --git a/src/views/thread/index.tsx b/src/views/thread/index.tsx index 1e135fdb5..1fe844e72 100644 --- a/src/views/thread/index.tsx +++ b/src/views/thread/index.tsx @@ -13,6 +13,7 @@ import IntersectionObserverProvider from "../../providers/intersection-observer" import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import useThreadTimelineLoader from "../../hooks/use-thread-timeline-loader"; import useSingleEvent from "../../hooks/use-single-event"; +import useParamsEventPointer from "../../hooks/use-params-event-pointer"; function ThreadPage({ thread, rootId, focusId }: { thread: Map; rootId: string; focusId: string }) { const isRoot = rootId === focusId; @@ -61,23 +62,8 @@ function ThreadPage({ thread, rootId, focusId }: { thread: Map { - const result = safeDecode(id); - if (!result) return; - if (result.type === "note") return { id: result.data }; - if (result.type === "nevent") return result.data; - }, [id]); - const torrent = useSingleEvent(parsed?.id, parsed?.relays ?? []); + const pointer = useParamsEventPointer("id"); + const torrent = useSingleEvent(pointer?.id, pointer?.relays ?? []); if (!torrent) return ; diff --git a/src/views/user/index.tsx b/src/views/user/index.tsx index b5e29f77f..c2528053e 100644 --- a/src/views/user/index.tsx +++ b/src/views/user/index.tsx @@ -46,6 +46,7 @@ import { STEMSTR_TRACK_KIND } from "../../helpers/nostr/stemstr"; import { STREAM_KIND } from "../../helpers/nostr/stream"; import { TORRENT_KIND } from "../../helpers/nostr/torrents"; import { GOAL_KIND } from "../../helpers/nostr/goal"; +import useParamsProfilePointer from "../../hooks/use-params-pubkey-pointer"; const tabs = [ { label: "About", path: "about" }, @@ -66,22 +67,6 @@ const tabs = [ { label: "Muted by", path: "muted-by" }, ]; -function useUserPointer() { - const { pubkey } = useParams() as { pubkey: string }; - if (isHexKey(pubkey)) return { pubkey, relays: [] }; - const pointer = nip19.decode(pubkey); - - switch (pointer.type) { - case "npub": - return { pubkey: pointer.data as string, relays: [] }; - case "nprofile": - const d = pointer.data as nip19.ProfilePointer; - return { pubkey: d.pubkey, relays: d.relays ?? [] }; - default: - throw new Error(`Unknown type ${pointer.type}`); - } -} - function useUserTopRelays(pubkey: string, count: number = 4) { const readRelays = useReadRelayUrls(); // get user relays @@ -96,7 +81,7 @@ function useUserTopRelays(pubkey: string, count: number = 4) { } const UserView = () => { - const { pubkey, relays: pointerRelays } = useUserPointer(); + const { pubkey, relays: pointerRelays = [] } = useParamsProfilePointer(); const navigate = useNavigate(); const [relayCount, setRelayCount] = useState(4); const userTopRelays = useUserTopRelays(pubkey, relayCount);