lots of improvements

This commit is contained in:
hzrd149
2023-02-07 17:04:18 -06:00
parent d610c838fc
commit fe108683c8
19 changed files with 144 additions and 146 deletions

View File

@@ -14,7 +14,3 @@
- add emoji reaction button - add emoji reaction button
- save relay list as note - save relay list as note
- load relays from note - load relays from note
create a subscription manager that takes a "canMerge" function and batches requests
create a template for a cached subscription service
create a template for a cached request service

View File

@@ -3,6 +3,7 @@ import { BehaviorSubject } from "rxjs";
export class PubkeySubjectCache<T> { export class PubkeySubjectCache<T> {
subjects = new Map<string, BehaviorSubject<T | null>>(); subjects = new Map<string, BehaviorSubject<T | null>>();
relays = new Map<string, Set<string>>(); relays = new Map<string, Set<string>>();
dirty = false;
hasSubject(pubkey: string) { hasSubject(pubkey: string) {
return this.subjects.has(pubkey); return this.subjects.has(pubkey);
@@ -12,6 +13,7 @@ export class PubkeySubjectCache<T> {
if (!subject) { if (!subject) {
subject = new BehaviorSubject<T | null>(null); subject = new BehaviorSubject<T | null>(null);
this.subjects.set(pubkey, subject); this.subjects.set(pubkey, subject);
this.dirty = true;
} }
return subject; return subject;
} }
@@ -19,6 +21,7 @@ export class PubkeySubjectCache<T> {
const set = this.relays.get(pubkey) ?? new Set(); const set = this.relays.get(pubkey) ?? new Set();
for (const url of relays) set.add(url); for (const url of relays) set.add(url);
this.relays.set(pubkey, set); this.relays.set(pubkey, set);
this.dirty = true;
} }
getAllPubkeysMissingData(include: string[] = []) { getAllPubkeysMissingData(include: string[] = []) {
@@ -44,6 +47,7 @@ export class PubkeySubjectCache<T> {
this.subjects.delete(key); this.subjects.delete(key);
this.relays.delete(key); this.relays.delete(key);
prunedKeys.push(key); prunedKeys.push(key);
this.dirty = true;
} }
} }
return prunedKeys; return prunedKeys;

View File

@@ -3,11 +3,13 @@ import { createIcon, IconProps } from "@chakra-ui/icons";
import astralIcon from "./icons/astral.png"; import astralIcon from "./icons/astral.png";
import nostrGuruIcon from "./icons/nostr-guru.jpg"; import nostrGuruIcon from "./icons/nostr-guru.jpg";
import brbIcon from "./icons/brb.png"; import brbIcon from "./icons/brb.png";
import snortSocialIcon from "./icons/snort-social.png";
export const IMAGE_ICONS = { export const IMAGE_ICONS = {
astralIcon, astralIcon,
nostrGuruIcon, nostrGuruIcon,
brbIcon, brbIcon,
snortSocialIcon,
}; };
const defaultProps: IconProps = { fontSize: "1.2em" }; const defaultProps: IconProps = { fontSize: "1.2em" };

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -23,6 +23,7 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
import { PostContents } from "./post-contents"; import { PostContents } from "./post-contents";
import { PostMenu } from "./post-menu"; import { PostMenu } from "./post-menu";
import { PostCC } from "./post-cc"; import { PostCC } from "./post-cc";
import { isReply } from "../../helpers/nostr-event";
export type PostProps = { export type PostProps = {
event: NostrEvent; event: NostrEvent;
@@ -40,19 +41,14 @@ export const Post = React.memo(({ event }: PostProps) => {
<Box> <Box>
<Heading size="sm" display="inline"> <Heading size="sm" display="inline">
<Link <Link to={`/u/${normalizeToBech32(event.pubkey, Bech32Prefix.Pubkey)}`}>
to={`/u/${normalizeToBech32(
event.pubkey,
Bech32Prefix.Pubkey
)}`}
>
{getUserDisplayName(metadata, event.pubkey)} {getUserDisplayName(metadata, event.pubkey)}
</Link> </Link>
</Heading> </Heading>
<Text display="inline" ml="2"> <Text display="inline" ml="2">
{moment(event.created_at * 1000).fromNow()} {moment(event.created_at * 1000).fromNow()}
</Text> </Text>
<PostCC event={event} /> {isReply(event) && <PostCC event={event} />}
</Box> </Box>
</Flex> </Flex>
<PostMenu event={event} /> <PostMenu event={event} />
@@ -70,9 +66,7 @@ export const Post = React.memo(({ event }: PostProps) => {
<Button <Button
size="sm" size="sm"
variant="link" variant="link"
onClick={() => onClick={() => navigate(`/e/${normalizeToBech32(event.id, Bech32Prefix.Note)}`)}
navigate(`/e/${normalizeToBech32(event.id, Bech32Prefix.Note)}`)
}
> >
Replies Replies
</Button> </Button>

View File

@@ -49,7 +49,12 @@ export const PostMenu = ({ event }: { event: NostrEvent }) => {
> >
Open in BRB Open in BRB
</MenuItem> </MenuItem>
<MenuItem as="a" href={`https://snort.social/e/${noteId}`} target="_blank"> <MenuItem
as="a"
icon={<Avatar src={IMAGE_ICONS.snortSocialIcon} size="xs" />}
href={`https://snort.social/e/${noteId}`}
target="_blank"
>
Open in snort.social Open in snort.social
</MenuItem> </MenuItem>
{noteId && ( {noteId && (

View File

@@ -1,20 +1,27 @@
import { useRef } from "react"; import { useRef } from "react";
import { useMount, useUnmount } from "react-use"; import { useDeepCompareEffect, useUnmount } from "react-use";
import { NostrSubscription } from "../classes/nostr-subscription"; import { NostrSubscription } from "../classes/nostr-subscription";
import settings from "../services/settings";
import { NostrQuery } from "../types/nostr-query"; import { NostrQuery } from "../types/nostr-query";
import useSubject from "./use-subject";
/** @deprecated */ type Options = {
export function useSubscription( name?: string;
urls: string[], enabled?: boolean;
query: NostrQuery, };
name?: string
) { export function useSubscription(query: NostrQuery, opts?: Options) {
const relays = useSubject(settings.relays);
const sub = useRef<NostrSubscription | null>(null); const sub = useRef<NostrSubscription | null>(null);
sub.current = sub.current || new NostrSubscription(urls, query, name); sub.current = sub.current || new NostrSubscription(relays, undefined, opts?.name);
useMount(() => { useDeepCompareEffect(() => {
if (sub.current) sub.current.open(); if (sub.current) {
}); sub.current.update(query);
if (opts?.enabled ?? true) sub.current.open();
else sub.current.close();
}
}, [query]);
useUnmount(() => { useUnmount(() => {
if (sub.current) { if (sub.current) {
sub.current.close(); sub.current.close();

View File

@@ -0,0 +1,39 @@
import moment from "moment";
import { useCallback, useEffect, useState } from "react";
import { NostrEvent } from "../types/nostr-event";
import { useEventDir } from "./use-event-dir";
import { useSubscription } from "./use-subscription";
export function useUserTimeline(pubkey: string, filter?: (event: NostrEvent) => boolean) {
const [until, setUntil] = useState<number | undefined>(undefined);
const [since, setSince] = useState<number>(moment().subtract(1, "day").startOf("day").unix());
const sub = useSubscription({ authors: [pubkey], kinds: [1], since, until }, { name: `${pubkey} posts` });
const eventDir = useEventDir(sub, filter);
const reset = useCallback(() => {
setUntil(undefined);
setSince(moment().subtract(1, "day").startOf("day").unix());
eventDir.reset();
}, [eventDir.reset, setUntil, setSince]);
// clear events when pubkey changes
useEffect(() => reset(), [pubkey]);
const timeline = Object.values(eventDir.events).sort((a, b) => b.created_at - a.created_at);
const more = useCallback(
(days: number) => {
setUntil(since);
setSince(moment.unix(since).add(days, "days").unix());
},
[setSince, setUntil, since]
);
return {
timeline,
reset,
more,
};
}

View File

@@ -54,6 +54,8 @@ function requestContacts(pubkey: string, relays: string[] = [], alwaysRequest =
} }
function flushRequests() { function flushRequests() {
if (!subjects.dirty) return;
const pubkeys = new Set<string>(); const pubkeys = new Set<string>();
const relays = new Set<string>(); const relays = new Set<string>();
@@ -73,6 +75,7 @@ function flushRequests() {
if (subscription.state !== NostrSubscription.OPEN) { if (subscription.state !== NostrSubscription.OPEN) {
subscription.open(); subscription.open();
} }
subjects.dirty = false;
} }
function receiveEvent(event: NostrEvent) { function receiveEvent(event: NostrEvent) {

View File

@@ -28,16 +28,18 @@ function requestFollowers(pubkey: string, relays: string[] = [], alwaysRequest =
if (relays.length) subjects.addRelays(pubkey, relays); if (relays.length) subjects.addRelays(pubkey, relays);
if (alwaysRequest) forceRequestedKeys.add(pubkey);
db.getAllKeysFromIndex("user-contacts", "contacts", pubkey).then((cached) => { db.getAllKeysFromIndex("user-contacts", "contacts", pubkey).then((cached) => {
mergeNext(subject, cached); mergeNext(subject, cached);
}); });
if (alwaysRequest) forceRequestedKeys.add(pubkey);
return subject; return subject;
} }
function flushRequests() { function flushRequests() {
if (!subjects.dirty) return;
const pubkeys = new Set<string>(); const pubkeys = new Set<string>();
const relays = new Set<string>(); const relays = new Set<string>();
@@ -57,6 +59,7 @@ function flushRequests() {
if (subscription.state !== NostrSubscription.OPEN) { if (subscription.state !== NostrSubscription.OPEN) {
subscription.open(); subscription.open();
} }
subjects.dirty = false;
} }
function receiveEvent(event: NostrEvent) { function receiveEvent(event: NostrEvent) {

View File

@@ -39,6 +39,8 @@ function parseMetadata(event: NostrEvent): Metadata | undefined {
} }
function flushRequests() { function flushRequests() {
if (!subjects.dirty) return;
const pubkeys = new Set<string>(); const pubkeys = new Set<string>();
const relays = new Set<string>(); const relays = new Set<string>();
@@ -58,6 +60,7 @@ function flushRequests() {
if (subscription.state !== NostrSubscription.OPEN) { if (subscription.state !== NostrSubscription.OPEN) {
subscription.open(); subscription.open();
} }
subjects.dirty = false;
} }
subscription.onEvent.subscribe((event) => { subscription.onEvent.subscribe((event) => {

View File

@@ -25,6 +25,7 @@ export type Kind0ParsedContent = {
display_name?: string; display_name?: string;
about?: string; about?: string;
picture?: string; picture?: string;
banner?: string;
}; };
export function isETag(tag: Tag): tag is ETag { export function isETag(tag: Tag): tag is ETag {

View File

@@ -1,59 +1,33 @@
import { import { Flex, SkeletonText, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
Flex,
SkeletonText,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
} from "@chakra-ui/react";
import { useSubscription } from "../../hooks/use-subscription"; import { useSubscription } from "../../hooks/use-subscription";
import { Post } from "../../components/post"; import { Post } from "../../components/post";
import moment from "moment/moment"; import moment from "moment/moment";
import settings from "../../services/settings";
import useSubject from "../../hooks/use-subject";
import { useEventDir } from "../../hooks/use-event-dir"; import { useEventDir } from "../../hooks/use-event-dir";
import { NostrSubscription } from "../../classes/nostr-subscription";
import { isPost, isReply } from "../../helpers/nostr-event"; import { isPost, isReply } from "../../helpers/nostr-event";
import { NostrEvent } from "../../types/nostr-event";
const PostsTimeline = ({ sub }: { sub: NostrSubscription }) => { const PostsTimeline = ({ timeline }: { timeline: NostrEvent[] }) => {
const { events } = useEventDir(sub, isPost);
const timeline = Object.values(events).sort(
(a, b) => b.created_at - a.created_at
);
if (timeline.length === 0) { if (timeline.length === 0) {
return <SkeletonText />; return <SkeletonText />;
} }
if (timeline.length > 20) timeline.length = 20;
return ( return (
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
{timeline.map((event) => ( {timeline.filter(isPost).map((event) => (
<Post key={event.id} event={event} /> <Post key={event.id} event={event} />
))} ))}
</Flex> </Flex>
); );
}; };
const RepliesTimeline = ({ sub }: { sub: NostrSubscription }) => { const RepliesTimeline = ({ timeline }: { timeline: NostrEvent[] }) => {
const { events } = useEventDir(sub, isReply);
const timeline = Object.values(events).sort(
(a, b) => b.created_at - a.created_at
);
if (timeline.length === 0) { if (timeline.length === 0) {
return <SkeletonText />; return <SkeletonText />;
} }
if (timeline.length > 20) timeline.length = 20;
return ( return (
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
{timeline.map((event) => ( {timeline.filter(isReply).map((event) => (
<Post key={event.id} event={event} /> <Post key={event.id} event={event} />
))} ))}
</Flex> </Flex>
@@ -61,32 +35,26 @@ const RepliesTimeline = ({ sub }: { sub: NostrSubscription }) => {
}; };
export const GlobalView = () => { export const GlobalView = () => {
const relays = useSubject(settings.relays);
const sub = useSubscription( const sub = useSubscription(
relays,
{ kinds: [1], limit: 10, since: moment().startOf("day").valueOf() / 1000 }, { kinds: [1], limit: 10, since: moment().startOf("day").valueOf() / 1000 },
"global-events" { name: "global-events" }
); );
const { events } = useEventDir(sub);
const timeline = Object.values(events).sort((a, b) => b.created_at - a.created_at);
return ( return (
<Tabs <Tabs display="flex" flexDirection="column" flexGrow="1" overflow="hidden" isLazy>
display="flex"
flexDirection="column"
flexGrow="1"
overflow="hidden"
isLazy
>
<TabList> <TabList>
<Tab>Notes</Tab> <Tab>Posts</Tab>
<Tab>Replies</Tab> <Tab>Replies</Tab>
</TabList> </TabList>
<TabPanels overflow="auto" height="100%"> <TabPanels overflow="auto" height="100%">
<TabPanel pr={0} pl={0}> <TabPanel pr={0} pl={0}>
<PostsTimeline sub={sub} /> <PostsTimeline timeline={timeline} />
</TabPanel> </TabPanel>
<TabPanel pr={0} pl={0}> <TabPanel pr={0} pl={0}>
<RepliesTimeline sub={sub} /> <RepliesTimeline timeline={timeline} />
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
</Tabs> </Tabs>

View File

@@ -39,7 +39,6 @@ function useExtendedContacts(pubkey: string) {
} }
export const DiscoverTab = () => { export const DiscoverTab = () => {
const relays = useSubject(settings.relays);
const pubkey = useSubject(identity.pubkey); const pubkey = useSubject(identity.pubkey);
const contactsOfContacts = useExtendedContacts(pubkey); const contactsOfContacts = useExtendedContacts(pubkey);
@@ -48,13 +47,12 @@ export const DiscoverTab = () => {
const [after, setAfter] = useState(moment()); const [after, setAfter] = useState(moment());
const sub = useSubscription( const sub = useSubscription(
relays,
{ {
authors: contactsOfContacts, authors: contactsOfContacts,
kinds: [1], kinds: [1],
since: since.unix(), since: since.unix(),
}, },
"home-discover" { name: "home-discover", enabled: contactsOfContacts.length > 0 }
); );
const { events } = useEventDir(sub); const { events } = useEventDir(sub);

View File

@@ -10,7 +10,6 @@ import identity from "../../services/identity";
import settings from "../../services/settings"; import settings from "../../services/settings";
export const FollowingTab = () => { export const FollowingTab = () => {
const relays = useSubject(settings.relays);
const pubkey = useSubject(identity.pubkey); const pubkey = useSubject(identity.pubkey);
const contacts = useUserContacts(pubkey); const contacts = useUserContacts(pubkey);
@@ -19,13 +18,12 @@ export const FollowingTab = () => {
const following = contacts?.contacts || []; const following = contacts?.contacts || [];
const sub = useSubscription( const sub = useSubscription(
relays,
{ {
authors: following, authors: following,
kinds: [1], kinds: [1],
since: since.unix(), since: since.unix(),
}, },
"home-following" { name: "home-following", enabled: following.length > 0 }
); );
const { events } = useEventDir(sub); const { events } = useEventDir(sub);

View File

@@ -13,6 +13,7 @@ import {
Tabs, Tabs,
Text, Text,
Box, Box,
Image,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { UserPostsTab } from "./posts"; import { UserPostsTab } from "./posts";
@@ -27,6 +28,7 @@ import { Page } from "../../components/page";
import { UserProfileMenu } from "./user-profile-menu"; import { UserProfileMenu } from "./user-profile-menu";
import { UserFollowersTab } from "./followers"; import { UserFollowersTab } from "./followers";
import { useUserFollowers } from "../../hooks/use-user-followers"; import { useUserFollowers } from "../../hooks/use-user-followers";
import { UserRepliesTab } from "./replies";
export const UserPage = () => { export const UserPage = () => {
const params = useParams(); const params = useParams();
@@ -64,6 +66,7 @@ export const UserView = ({ pubkey }: UserViewProps) => {
return ( return (
<Flex direction="column" alignItems="stretch" gap="2" overflow="hidden" height="100%"> <Flex direction="column" alignItems="stretch" gap="2" overflow="hidden" height="100%">
{/* {metadata?.banner && <Image src={metadata.banner} />} */}
<Flex gap="4" padding="2"> <Flex gap="4" padding="2">
<UserAvatar pubkey={pubkey} size={isMobile ? "md" : "xl"} /> <UserAvatar pubkey={pubkey} size={isMobile ? "md" : "xl"} />
<Flex direction="column" gap={isMobile ? 0 : 2}> <Flex direction="column" gap={isMobile ? 0 : 2}>
@@ -76,7 +79,8 @@ export const UserView = ({ pubkey }: UserViewProps) => {
</Flex> </Flex>
<Tabs display="flex" flexDirection="column" flexGrow="1" overflow="hidden" isLazy> <Tabs display="flex" flexDirection="column" flexGrow="1" overflow="hidden" isLazy>
<TabList> <TabList>
<Tab>Notes</Tab> <Tab>Posts</Tab>
<Tab>Replies</Tab>
<Tab>Followers ({followers?.length})</Tab> <Tab>Followers ({followers?.length})</Tab>
<Tab>Following</Tab> <Tab>Following</Tab>
<Tab>Relays</Tab> <Tab>Relays</Tab>
@@ -86,6 +90,9 @@ export const UserView = ({ pubkey }: UserViewProps) => {
<TabPanel pr={0} pl={0}> <TabPanel pr={0} pl={0}>
<UserPostsTab pubkey={pubkey} /> <UserPostsTab pubkey={pubkey} />
</TabPanel> </TabPanel>
<TabPanel pr={0} pl={0}>
<UserRepliesTab pubkey={pubkey} />
</TabPanel>
<TabPanel> <TabPanel>
<UserFollowersTab pubkey={pubkey} /> <UserFollowersTab pubkey={pubkey} />
</TabPanel> </TabPanel>

View File

@@ -1,40 +1,19 @@
import { useEffect } from "react"; import { Button, Flex, Spinner } from "@chakra-ui/react";
import { Flex, SkeletonText } from "@chakra-ui/react";
import { useSubscription } from "../../hooks/use-subscription";
import { Post } from "../../components/post"; import { Post } from "../../components/post";
import settings from "../../services/settings"; import { isPost } from "../../helpers/nostr-event";
import useSubject from "../../hooks/use-subject"; import { useUserTimeline } from "../../hooks/use-user-timeline";
import { useEventDir } from "../../hooks/use-event-dir";
export const UserPostsTab = ({ pubkey }: { pubkey: string }) => { export const UserPostsTab = ({ pubkey }: { pubkey: string }) => {
const relays = useSubject(settings.relays); const { timeline, more } = useUserTimeline(pubkey, isPost);
const sub = useSubscription(
relays,
{ authors: [pubkey], kinds: [1] },
`${pubkey} posts`
);
const { events, reset } = useEventDir(sub);
// clear events when pubkey changes
useEffect(() => reset(), [pubkey]);
const timeline = Object.values(events).sort(
(a, b) => b.created_at - a.created_at
);
if (timeline.length === 0) {
return <SkeletonText />;
}
if (timeline.length > 30) timeline.length = 30;
return ( return (
<Flex direction="column" gap="2" pr="2" pl="2"> <Flex direction="column" gap="2" pr="2" pl="2">
{timeline.map((event) => ( {timeline.length > 0 ? (
<Post key={event.id} event={event} /> timeline.map((event) => <Post key={event.id} event={event} />)
))} ) : (
<Spinner ml="auto" mr="auto" mt="8" mb="8" />
)}
<Button onClick={() => more(1)}>Load More</Button>
</Flex> </Flex>
); );
}; };

View File

@@ -1,43 +1,21 @@
import { useEffect } from "react"; import { Button, Flex, SkeletonText } from "@chakra-ui/react";
import { Flex, SkeletonText } from "@chakra-ui/react";
import { useSubscription } from "../../hooks/use-subscription";
import { Post } from "../../components/post"; import { Post } from "../../components/post";
import settings from "../../services/settings"; import { isReply } from "../../helpers/nostr-event";
import useSubject from "../../hooks/use-subject"; import { useUserTimeline } from "../../hooks/use-user-timeline";
import { useEventDir } from "../../hooks/use-event-dir";
export const UserRepliesTab = ({ pubkey }: { pubkey: string }) => { export const UserRepliesTab = ({ pubkey }: { pubkey: string }) => {
const relays = useSubject(settings.relays); const { timeline, more } = useUserTimeline(pubkey, isReply);
const sub = useSubscription(
relays,
{ authors: [pubkey], kinds: [1] },
`${pubkey} posts`
);
const { events, reset } = useEventDir(
sub,
(event) => !!event.tags.find((t) => t[0] === "e")
);
// clear events when pubkey changes
useEffect(() => reset(), [pubkey]);
const timeline = Object.values(events).sort(
(a, b) => b.created_at - a.created_at
);
if (timeline.length === 0) { if (timeline.length === 0) {
return <SkeletonText />; return <SkeletonText />;
} }
if (timeline.length > 30) timeline.length = 30;
return ( return (
<Flex direction="column" gap="2" pr="2" pl="2"> <Flex direction="column" gap="2" pr="2" pl="2">
{timeline.map((event) => ( {timeline.map((event) => (
<Post key={event.id} event={event} /> <Post key={event.id} event={event} />
))} ))}
<Button onClick={() => more(1)}>Load More</Button>
</Flex> </Flex>
); );
}; };

View File

@@ -1,18 +1,21 @@
import { Avatar, MenuItem } from "@chakra-ui/react"; import { Avatar, MenuItem } from "@chakra-ui/react";
import { MenuIconButton } from "../../components/menu-icon-button"; import { MenuIconButton } from "../../components/menu-icon-button";
import { IMAGE_ICONS } from "../../components/icons"; import { ClipboardIcon, IMAGE_ICONS } from "../../components/icons";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19"; import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
import { useCopyToClipboard } from "react-use"; import { useCopyToClipboard } from "react-use";
import { truncatedId } from "../../helpers/nostr-event"; import { truncatedId } from "../../helpers/nostr-event";
export const UserProfileMenu = ({ pubkey }: { pubkey: string }) => { export const UserProfileMenu = ({ pubkey }: { pubkey: string }) => {
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
return ( return (
<MenuIconButton> <MenuIconButton>
<MenuItem <MenuItem
as="a" as="a"
icon={<Avatar src={IMAGE_ICONS.nostrGuruIcon} size="xs" />} icon={<Avatar src={IMAGE_ICONS.nostrGuruIcon} size="xs" />}
href={`https://www.nostr.guru/p/${pubkey}`} href={`https://www.nostr.guru/p/${npub}`}
target="_blank" target="_blank"
> >
Open in Nostr.guru Open in Nostr.guru
@@ -20,10 +23,7 @@ export const UserProfileMenu = ({ pubkey }: { pubkey: string }) => {
<MenuItem <MenuItem
as="a" as="a"
icon={<Avatar src={IMAGE_ICONS.astralIcon} size="xs" />} icon={<Avatar src={IMAGE_ICONS.astralIcon} size="xs" />}
href={`https://astral.ninja/${normalizeToBech32( href={`https://astral.ninja/${npub}`}
pubkey,
Bech32Prefix.Pubkey
)}`}
target="_blank" target="_blank"
> >
Open in astral Open in astral
@@ -31,11 +31,24 @@ export const UserProfileMenu = ({ pubkey }: { pubkey: string }) => {
<MenuItem <MenuItem
as="a" as="a"
icon={<Avatar src={IMAGE_ICONS.brbIcon} size="xs" />} icon={<Avatar src={IMAGE_ICONS.brbIcon} size="xs" />}
href={`https://brb.io/u/${pubkey}`} href={`https://brb.io/u/${npub}`}
target="_blank" target="_blank"
> >
Open in BRB Open in BRB
</MenuItem> </MenuItem>
<MenuItem
as="a"
icon={<Avatar src={IMAGE_ICONS.snortSocialIcon} size="xs" />}
href={`https://snort.social/p/${npub}`}
target="_blank"
>
Open in snort.social
</MenuItem>
{npub && (
<MenuItem onClick={() => copyToClipboard(npub)} icon={<ClipboardIcon />}>
Copy {truncatedId(npub)}
</MenuItem>
)}
</MenuIconButton> </MenuIconButton>
); );
}; };