mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-12 13:49:33 +02:00
lots of improvements
This commit is contained in:
parent
d610c838fc
commit
fe108683c8
@ -14,7 +14,3 @@
|
||||
- add emoji reaction button
|
||||
- save relay list as 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
|
||||
|
@ -3,6 +3,7 @@ import { BehaviorSubject } from "rxjs";
|
||||
export class PubkeySubjectCache<T> {
|
||||
subjects = new Map<string, BehaviorSubject<T | null>>();
|
||||
relays = new Map<string, Set<string>>();
|
||||
dirty = false;
|
||||
|
||||
hasSubject(pubkey: string) {
|
||||
return this.subjects.has(pubkey);
|
||||
@ -12,6 +13,7 @@ export class PubkeySubjectCache<T> {
|
||||
if (!subject) {
|
||||
subject = new BehaviorSubject<T | null>(null);
|
||||
this.subjects.set(pubkey, subject);
|
||||
this.dirty = true;
|
||||
}
|
||||
return subject;
|
||||
}
|
||||
@ -19,6 +21,7 @@ export class PubkeySubjectCache<T> {
|
||||
const set = this.relays.get(pubkey) ?? new Set();
|
||||
for (const url of relays) set.add(url);
|
||||
this.relays.set(pubkey, set);
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
getAllPubkeysMissingData(include: string[] = []) {
|
||||
@ -44,6 +47,7 @@ export class PubkeySubjectCache<T> {
|
||||
this.subjects.delete(key);
|
||||
this.relays.delete(key);
|
||||
prunedKeys.push(key);
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
return prunedKeys;
|
||||
|
@ -3,11 +3,13 @@ import { createIcon, IconProps } from "@chakra-ui/icons";
|
||||
import astralIcon from "./icons/astral.png";
|
||||
import nostrGuruIcon from "./icons/nostr-guru.jpg";
|
||||
import brbIcon from "./icons/brb.png";
|
||||
import snortSocialIcon from "./icons/snort-social.png";
|
||||
|
||||
export const IMAGE_ICONS = {
|
||||
astralIcon,
|
||||
nostrGuruIcon,
|
||||
brbIcon,
|
||||
snortSocialIcon,
|
||||
};
|
||||
|
||||
const defaultProps: IconProps = { fontSize: "1.2em" };
|
||||
|
BIN
src/components/icons/snort-social.png
Normal file
BIN
src/components/icons/snort-social.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 171 KiB |
@ -23,6 +23,7 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
||||
import { PostContents } from "./post-contents";
|
||||
import { PostMenu } from "./post-menu";
|
||||
import { PostCC } from "./post-cc";
|
||||
import { isReply } from "../../helpers/nostr-event";
|
||||
|
||||
export type PostProps = {
|
||||
event: NostrEvent;
|
||||
@ -40,19 +41,14 @@ export const Post = React.memo(({ event }: PostProps) => {
|
||||
|
||||
<Box>
|
||||
<Heading size="sm" display="inline">
|
||||
<Link
|
||||
to={`/u/${normalizeToBech32(
|
||||
event.pubkey,
|
||||
Bech32Prefix.Pubkey
|
||||
)}`}
|
||||
>
|
||||
<Link to={`/u/${normalizeToBech32(event.pubkey, Bech32Prefix.Pubkey)}`}>
|
||||
{getUserDisplayName(metadata, event.pubkey)}
|
||||
</Link>
|
||||
</Heading>
|
||||
<Text display="inline" ml="2">
|
||||
{moment(event.created_at * 1000).fromNow()}
|
||||
</Text>
|
||||
<PostCC event={event} />
|
||||
{isReply(event) && <PostCC event={event} />}
|
||||
</Box>
|
||||
</Flex>
|
||||
<PostMenu event={event} />
|
||||
@ -70,9 +66,7 @@ export const Post = React.memo(({ event }: PostProps) => {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
onClick={() =>
|
||||
navigate(`/e/${normalizeToBech32(event.id, Bech32Prefix.Note)}`)
|
||||
}
|
||||
onClick={() => navigate(`/e/${normalizeToBech32(event.id, Bech32Prefix.Note)}`)}
|
||||
>
|
||||
Replies
|
||||
</Button>
|
||||
|
@ -49,7 +49,12 @@ export const PostMenu = ({ event }: { event: NostrEvent }) => {
|
||||
>
|
||||
Open in BRB
|
||||
</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
|
||||
</MenuItem>
|
||||
{noteId && (
|
||||
|
@ -1,20 +1,27 @@
|
||||
import { useRef } from "react";
|
||||
import { useMount, useUnmount } from "react-use";
|
||||
import { useDeepCompareEffect, useUnmount } from "react-use";
|
||||
import { NostrSubscription } from "../classes/nostr-subscription";
|
||||
import settings from "../services/settings";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
/** @deprecated */
|
||||
export function useSubscription(
|
||||
urls: string[],
|
||||
query: NostrQuery,
|
||||
name?: string
|
||||
) {
|
||||
type Options = {
|
||||
name?: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
export function useSubscription(query: NostrQuery, opts?: Options) {
|
||||
const relays = useSubject(settings.relays);
|
||||
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(() => {
|
||||
if (sub.current) sub.current.open();
|
||||
});
|
||||
useDeepCompareEffect(() => {
|
||||
if (sub.current) {
|
||||
sub.current.update(query);
|
||||
if (opts?.enabled ?? true) sub.current.open();
|
||||
else sub.current.close();
|
||||
}
|
||||
}, [query]);
|
||||
useUnmount(() => {
|
||||
if (sub.current) {
|
||||
sub.current.close();
|
||||
|
39
src/hooks/use-user-timeline.ts
Normal file
39
src/hooks/use-user-timeline.ts
Normal 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,
|
||||
};
|
||||
}
|
@ -54,6 +54,8 @@ function requestContacts(pubkey: string, relays: string[] = [], alwaysRequest =
|
||||
}
|
||||
|
||||
function flushRequests() {
|
||||
if (!subjects.dirty) return;
|
||||
|
||||
const pubkeys = new Set<string>();
|
||||
const relays = new Set<string>();
|
||||
|
||||
@ -73,6 +75,7 @@ function flushRequests() {
|
||||
if (subscription.state !== NostrSubscription.OPEN) {
|
||||
subscription.open();
|
||||
}
|
||||
subjects.dirty = false;
|
||||
}
|
||||
|
||||
function receiveEvent(event: NostrEvent) {
|
||||
|
@ -28,16 +28,18 @@ function requestFollowers(pubkey: string, relays: string[] = [], alwaysRequest =
|
||||
|
||||
if (relays.length) subjects.addRelays(pubkey, relays);
|
||||
|
||||
if (alwaysRequest) forceRequestedKeys.add(pubkey);
|
||||
|
||||
db.getAllKeysFromIndex("user-contacts", "contacts", pubkey).then((cached) => {
|
||||
mergeNext(subject, cached);
|
||||
});
|
||||
|
||||
if (alwaysRequest) forceRequestedKeys.add(pubkey);
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
function flushRequests() {
|
||||
if (!subjects.dirty) return;
|
||||
|
||||
const pubkeys = new Set<string>();
|
||||
const relays = new Set<string>();
|
||||
|
||||
@ -57,6 +59,7 @@ function flushRequests() {
|
||||
if (subscription.state !== NostrSubscription.OPEN) {
|
||||
subscription.open();
|
||||
}
|
||||
subjects.dirty = false;
|
||||
}
|
||||
|
||||
function receiveEvent(event: NostrEvent) {
|
||||
|
@ -39,6 +39,8 @@ function parseMetadata(event: NostrEvent): Metadata | undefined {
|
||||
}
|
||||
|
||||
function flushRequests() {
|
||||
if (!subjects.dirty) return;
|
||||
|
||||
const pubkeys = new Set<string>();
|
||||
const relays = new Set<string>();
|
||||
|
||||
@ -58,6 +60,7 @@ function flushRequests() {
|
||||
if (subscription.state !== NostrSubscription.OPEN) {
|
||||
subscription.open();
|
||||
}
|
||||
subjects.dirty = false;
|
||||
}
|
||||
|
||||
subscription.onEvent.subscribe((event) => {
|
||||
|
@ -25,6 +25,7 @@ export type Kind0ParsedContent = {
|
||||
display_name?: string;
|
||||
about?: string;
|
||||
picture?: string;
|
||||
banner?: string;
|
||||
};
|
||||
|
||||
export function isETag(tag: Tag): tag is ETag {
|
||||
|
@ -1,59 +1,33 @@
|
||||
import {
|
||||
Flex,
|
||||
SkeletonText,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
} from "@chakra-ui/react";
|
||||
import { Flex, SkeletonText, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
|
||||
import { useSubscription } from "../../hooks/use-subscription";
|
||||
import { Post } from "../../components/post";
|
||||
import moment from "moment/moment";
|
||||
import settings from "../../services/settings";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useEventDir } from "../../hooks/use-event-dir";
|
||||
import { NostrSubscription } from "../../classes/nostr-subscription";
|
||||
import { isPost, isReply } from "../../helpers/nostr-event";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
const PostsTimeline = ({ sub }: { sub: NostrSubscription }) => {
|
||||
const { events } = useEventDir(sub, isPost);
|
||||
|
||||
const timeline = Object.values(events).sort(
|
||||
(a, b) => b.created_at - a.created_at
|
||||
);
|
||||
|
||||
const PostsTimeline = ({ timeline }: { timeline: NostrEvent[] }) => {
|
||||
if (timeline.length === 0) {
|
||||
return <SkeletonText />;
|
||||
}
|
||||
|
||||
if (timeline.length > 20) timeline.length = 20;
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
{timeline.map((event) => (
|
||||
{timeline.filter(isPost).map((event) => (
|
||||
<Post key={event.id} event={event} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const RepliesTimeline = ({ sub }: { sub: NostrSubscription }) => {
|
||||
const { events } = useEventDir(sub, isReply);
|
||||
|
||||
const timeline = Object.values(events).sort(
|
||||
(a, b) => b.created_at - a.created_at
|
||||
);
|
||||
|
||||
const RepliesTimeline = ({ timeline }: { timeline: NostrEvent[] }) => {
|
||||
if (timeline.length === 0) {
|
||||
return <SkeletonText />;
|
||||
}
|
||||
|
||||
if (timeline.length > 20) timeline.length = 20;
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
{timeline.map((event) => (
|
||||
{timeline.filter(isReply).map((event) => (
|
||||
<Post key={event.id} event={event} />
|
||||
))}
|
||||
</Flex>
|
||||
@ -61,32 +35,26 @@ const RepliesTimeline = ({ sub }: { sub: NostrSubscription }) => {
|
||||
};
|
||||
|
||||
export const GlobalView = () => {
|
||||
const relays = useSubject(settings.relays);
|
||||
|
||||
const sub = useSubscription(
|
||||
relays,
|
||||
{ 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 (
|
||||
<Tabs
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
flexGrow="1"
|
||||
overflow="hidden"
|
||||
isLazy
|
||||
>
|
||||
<Tabs display="flex" flexDirection="column" flexGrow="1" overflow="hidden" isLazy>
|
||||
<TabList>
|
||||
<Tab>Notes</Tab>
|
||||
<Tab>Posts</Tab>
|
||||
<Tab>Replies</Tab>
|
||||
</TabList>
|
||||
<TabPanels overflow="auto" height="100%">
|
||||
<TabPanel pr={0} pl={0}>
|
||||
<PostsTimeline sub={sub} />
|
||||
<PostsTimeline timeline={timeline} />
|
||||
</TabPanel>
|
||||
<TabPanel pr={0} pl={0}>
|
||||
<RepliesTimeline sub={sub} />
|
||||
<RepliesTimeline timeline={timeline} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
@ -39,7 +39,6 @@ function useExtendedContacts(pubkey: string) {
|
||||
}
|
||||
|
||||
export const DiscoverTab = () => {
|
||||
const relays = useSubject(settings.relays);
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
|
||||
const contactsOfContacts = useExtendedContacts(pubkey);
|
||||
@ -48,13 +47,12 @@ export const DiscoverTab = () => {
|
||||
const [after, setAfter] = useState(moment());
|
||||
|
||||
const sub = useSubscription(
|
||||
relays,
|
||||
{
|
||||
authors: contactsOfContacts,
|
||||
kinds: [1],
|
||||
since: since.unix(),
|
||||
},
|
||||
"home-discover"
|
||||
{ name: "home-discover", enabled: contactsOfContacts.length > 0 }
|
||||
);
|
||||
|
||||
const { events } = useEventDir(sub);
|
||||
|
@ -10,7 +10,6 @@ import identity from "../../services/identity";
|
||||
import settings from "../../services/settings";
|
||||
|
||||
export const FollowingTab = () => {
|
||||
const relays = useSubject(settings.relays);
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const contacts = useUserContacts(pubkey);
|
||||
|
||||
@ -19,13 +18,12 @@ export const FollowingTab = () => {
|
||||
|
||||
const following = contacts?.contacts || [];
|
||||
const sub = useSubscription(
|
||||
relays,
|
||||
{
|
||||
authors: following,
|
||||
kinds: [1],
|
||||
since: since.unix(),
|
||||
},
|
||||
"home-following"
|
||||
{ name: "home-following", enabled: following.length > 0 }
|
||||
);
|
||||
|
||||
const { events } = useEventDir(sub);
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
Tabs,
|
||||
Text,
|
||||
Box,
|
||||
Image,
|
||||
} from "@chakra-ui/react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { UserPostsTab } from "./posts";
|
||||
@ -27,6 +28,7 @@ import { Page } from "../../components/page";
|
||||
import { UserProfileMenu } from "./user-profile-menu";
|
||||
import { UserFollowersTab } from "./followers";
|
||||
import { useUserFollowers } from "../../hooks/use-user-followers";
|
||||
import { UserRepliesTab } from "./replies";
|
||||
|
||||
export const UserPage = () => {
|
||||
const params = useParams();
|
||||
@ -64,6 +66,7 @@ export const UserView = ({ pubkey }: UserViewProps) => {
|
||||
|
||||
return (
|
||||
<Flex direction="column" alignItems="stretch" gap="2" overflow="hidden" height="100%">
|
||||
{/* {metadata?.banner && <Image src={metadata.banner} />} */}
|
||||
<Flex gap="4" padding="2">
|
||||
<UserAvatar pubkey={pubkey} size={isMobile ? "md" : "xl"} />
|
||||
<Flex direction="column" gap={isMobile ? 0 : 2}>
|
||||
@ -76,7 +79,8 @@ export const UserView = ({ pubkey }: UserViewProps) => {
|
||||
</Flex>
|
||||
<Tabs display="flex" flexDirection="column" flexGrow="1" overflow="hidden" isLazy>
|
||||
<TabList>
|
||||
<Tab>Notes</Tab>
|
||||
<Tab>Posts</Tab>
|
||||
<Tab>Replies</Tab>
|
||||
<Tab>Followers ({followers?.length})</Tab>
|
||||
<Tab>Following</Tab>
|
||||
<Tab>Relays</Tab>
|
||||
@ -86,6 +90,9 @@ export const UserView = ({ pubkey }: UserViewProps) => {
|
||||
<TabPanel pr={0} pl={0}>
|
||||
<UserPostsTab pubkey={pubkey} />
|
||||
</TabPanel>
|
||||
<TabPanel pr={0} pl={0}>
|
||||
<UserRepliesTab pubkey={pubkey} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<UserFollowersTab pubkey={pubkey} />
|
||||
</TabPanel>
|
||||
|
@ -1,40 +1,19 @@
|
||||
import { useEffect } from "react";
|
||||
import { Flex, SkeletonText } from "@chakra-ui/react";
|
||||
import { useSubscription } from "../../hooks/use-subscription";
|
||||
import { Button, Flex, Spinner } from "@chakra-ui/react";
|
||||
import { Post } from "../../components/post";
|
||||
import settings from "../../services/settings";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useEventDir } from "../../hooks/use-event-dir";
|
||||
import { isPost } from "../../helpers/nostr-event";
|
||||
import { useUserTimeline } from "../../hooks/use-user-timeline";
|
||||
|
||||
export const UserPostsTab = ({ pubkey }: { pubkey: string }) => {
|
||||
const relays = useSubject(settings.relays);
|
||||
|
||||
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;
|
||||
const { timeline, more } = useUserTimeline(pubkey, isPost);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2" pr="2" pl="2">
|
||||
{timeline.map((event) => (
|
||||
<Post key={event.id} event={event} />
|
||||
))}
|
||||
{timeline.length > 0 ? (
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -1,43 +1,21 @@
|
||||
import { useEffect } from "react";
|
||||
import { Flex, SkeletonText } from "@chakra-ui/react";
|
||||
import { useSubscription } from "../../hooks/use-subscription";
|
||||
import { Button, Flex, SkeletonText } from "@chakra-ui/react";
|
||||
import { Post } from "../../components/post";
|
||||
import settings from "../../services/settings";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useEventDir } from "../../hooks/use-event-dir";
|
||||
import { isReply } from "../../helpers/nostr-event";
|
||||
import { useUserTimeline } from "../../hooks/use-user-timeline";
|
||||
|
||||
export const UserRepliesTab = ({ pubkey }: { pubkey: string }) => {
|
||||
const relays = useSubject(settings.relays);
|
||||
|
||||
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
|
||||
);
|
||||
const { timeline, more } = useUserTimeline(pubkey, isReply);
|
||||
|
||||
if (timeline.length === 0) {
|
||||
return <SkeletonText />;
|
||||
}
|
||||
|
||||
if (timeline.length > 30) timeline.length = 30;
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2" pr="2" pl="2">
|
||||
{timeline.map((event) => (
|
||||
<Post key={event.id} event={event} />
|
||||
))}
|
||||
<Button onClick={() => more(1)}>Load More</Button>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -1,18 +1,21 @@
|
||||
import { Avatar, MenuItem } from "@chakra-ui/react";
|
||||
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 { useCopyToClipboard } from "react-use";
|
||||
import { truncatedId } from "../../helpers/nostr-event";
|
||||
|
||||
export const UserProfileMenu = ({ pubkey }: { pubkey: string }) => {
|
||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
||||
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
||||
|
||||
return (
|
||||
<MenuIconButton>
|
||||
<MenuItem
|
||||
as="a"
|
||||
icon={<Avatar src={IMAGE_ICONS.nostrGuruIcon} size="xs" />}
|
||||
href={`https://www.nostr.guru/p/${pubkey}`}
|
||||
href={`https://www.nostr.guru/p/${npub}`}
|
||||
target="_blank"
|
||||
>
|
||||
Open in Nostr.guru
|
||||
@ -20,10 +23,7 @@ export const UserProfileMenu = ({ pubkey }: { pubkey: string }) => {
|
||||
<MenuItem
|
||||
as="a"
|
||||
icon={<Avatar src={IMAGE_ICONS.astralIcon} size="xs" />}
|
||||
href={`https://astral.ninja/${normalizeToBech32(
|
||||
pubkey,
|
||||
Bech32Prefix.Pubkey
|
||||
)}`}
|
||||
href={`https://astral.ninja/${npub}`}
|
||||
target="_blank"
|
||||
>
|
||||
Open in astral
|
||||
@ -31,11 +31,24 @@ export const UserProfileMenu = ({ pubkey }: { pubkey: string }) => {
|
||||
<MenuItem
|
||||
as="a"
|
||||
icon={<Avatar src={IMAGE_ICONS.brbIcon} size="xs" />}
|
||||
href={`https://brb.io/u/${pubkey}`}
|
||||
href={`https://brb.io/u/${npub}`}
|
||||
target="_blank"
|
||||
>
|
||||
Open in BRB
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user