mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-10 12:53:14 +02:00
add nip-05 support
This commit is contained in:
@@ -9,13 +9,13 @@
|
|||||||
- [x] Lighting invoices
|
- [x] Lighting invoices
|
||||||
- [x] Blurred or hidden images and embeds for people you dont follow
|
- [x] Blurred or hidden images and embeds for people you dont follow
|
||||||
- [x] Thread view
|
- [x] Thread view
|
||||||
|
- [x] NIP-05 support
|
||||||
|
- [x] Broadcast events
|
||||||
|
- [x] User tipping
|
||||||
- [ ] Manage followers ( Contact List )
|
- [ ] Manage followers ( Contact List )
|
||||||
- [ ] Profile management
|
- [ ] Profile management
|
||||||
- [ ] Relay management
|
- [ ] Relay management
|
||||||
- [ ] NIP-05 support
|
|
||||||
- [x] Broadcast events
|
|
||||||
- [ ] Image upload
|
- [ ] Image upload
|
||||||
- [x] User tipping
|
|
||||||
- [ ] Reactions
|
- [ ] Reactions
|
||||||
- [ ] Dynamically connect to relays (start with one relay then connect to others as required)
|
- [ ] Dynamically connect to relays (start with one relay then connect to others as required)
|
||||||
- [ ] Reporting users and events
|
- [ ] Reporting users and events
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
- [ ] [NIP-02](https://github.com/nostr-protocol/nips/blob/master/02.md): Contact List and Petnames
|
- [ ] [NIP-02](https://github.com/nostr-protocol/nips/blob/master/02.md): Contact List and Petnames
|
||||||
- [ ] [NIP-03](https://github.com/nostr-protocol/nips/blob/master/03.md): OpenTimestamps Attestations for Events
|
- [ ] [NIP-03](https://github.com/nostr-protocol/nips/blob/master/03.md): OpenTimestamps Attestations for Events
|
||||||
- [ ] [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md): Encrypted Direct Message
|
- [ ] [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md): Encrypted Direct Message
|
||||||
- [ ] [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md): Mapping Nostr keys to DNS-based internet identifiers
|
- [x] [NIP-05](https://github.com/nostr-protocol/nips/blob/master/05.md): Mapping Nostr keys to DNS-based internet identifiers
|
||||||
- [ ] [NIP-06](https://github.com/nostr-protocol/nips/blob/master/06.md): Basic key derivation from mnemonic seed phrase
|
- [ ] [NIP-06](https://github.com/nostr-protocol/nips/blob/master/06.md): Basic key derivation from mnemonic seed phrase
|
||||||
- [ ] [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md): `window.nostr` capability for web browsers
|
- [ ] [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md): `window.nostr` capability for web browsers
|
||||||
- [ ] [NIP-08](https://github.com/nostr-protocol/nips/blob/master/08.md): Handling Mentions
|
- [ ] [NIP-08](https://github.com/nostr-protocol/nips/blob/master/08.md): Handling Mentions
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
- massive thread note1dapvuu8fl09yjtg2gyr2h6nypaffl2sq0aj5raz86463qk5kpyzqlxvtc3
|
- massive thread note1dapvuu8fl09yjtg2gyr2h6nypaffl2sq0aj5raz86463qk5kpyzqlxvtc3
|
||||||
- sort replies by date
|
- sort replies by date
|
||||||
- filter list of followers by users the user has blocked/reported (stops bots/spammers from showing up at followers)
|
- filter list of followers by users the user has blocked/reported (stops bots/spammers from showing up at followers)
|
||||||
|
- Add client side relay groups
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
@@ -15,7 +15,7 @@ export const IMAGE_ICONS = {
|
|||||||
const defaultProps: IconProps = { fontSize: "1.2em" };
|
const defaultProps: IconProps = { fontSize: "1.2em" };
|
||||||
|
|
||||||
export const GlobalIcon = createIcon({
|
export const GlobalIcon = createIcon({
|
||||||
displayName: "global-line",
|
displayName: "global-icon",
|
||||||
d: "M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-2.29-2.333A17.9 17.9 0 0 1 8.027 13H4.062a8.008 8.008 0 0 0 5.648 6.667zM10.03 13c.151 2.439.848 4.73 1.97 6.752A15.905 15.905 0 0 0 13.97 13h-3.94zm9.908 0h-3.965a17.9 17.9 0 0 1-1.683 6.667A8.008 8.008 0 0 0 19.938 13zM4.062 11h3.965A17.9 17.9 0 0 1 9.71 4.333 8.008 8.008 0 0 0 4.062 11zm5.969 0h3.938A15.905 15.905 0 0 0 12 4.248 15.905 15.905 0 0 0 10.03 11zm4.259-6.667A17.9 17.9 0 0 1 15.973 11h3.965a8.008 8.008 0 0 0-5.648-6.667z",
|
d: "M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-2.29-2.333A17.9 17.9 0 0 1 8.027 13H4.062a8.008 8.008 0 0 0 5.648 6.667zM10.03 13c.151 2.439.848 4.73 1.97 6.752A15.905 15.905 0 0 0 13.97 13h-3.94zm9.908 0h-3.965a17.9 17.9 0 0 1-1.683 6.667A8.008 8.008 0 0 0 19.938 13zM4.062 11h3.965A17.9 17.9 0 0 1 9.71 4.333 8.008 8.008 0 0 0 4.062 11zm5.969 0h3.938A15.905 15.905 0 0 0 12 4.248 15.905 15.905 0 0 0 10.03 11zm4.259-6.667A17.9 17.9 0 0 1 15.973 11h3.965a8.008 8.008 0 0 0-5.648-6.667z",
|
||||||
defaultProps,
|
defaultProps,
|
||||||
});
|
});
|
||||||
@@ -27,13 +27,13 @@ export const FeedIcon = createIcon({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const MoreIcon = createIcon({
|
export const MoreIcon = createIcon({
|
||||||
displayName: "more-line",
|
displayName: "more-icon",
|
||||||
d: "M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z",
|
d: "M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z",
|
||||||
defaultProps,
|
defaultProps,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CodeIcon = createIcon({
|
export const CodeIcon = createIcon({
|
||||||
displayName: "code-line",
|
displayName: "code-icon",
|
||||||
d: `M23 12l-7.071 7.071-1.414-1.414L20.172 12l-5.657-5.657 1.414-1.414L23 12zM3.828 12l5.657 5.657-1.414 1.414L1 12l7.071-7.071 1.414 1.414L3.828 12z`,
|
d: `M23 12l-7.071 7.071-1.414-1.414L20.172 12l-5.657-5.657 1.414-1.414L23 12zM3.828 12l5.657 5.657-1.414 1.414L1 12l7.071-7.071 1.414 1.414L3.828 12z`,
|
||||||
defaultProps,
|
defaultProps,
|
||||||
});
|
});
|
||||||
@@ -139,3 +139,15 @@ export const ReplyIcon = createIcon({
|
|||||||
d: "M11 20L1 12l10-8v5c5.523 0 10 4.477 10 10 0 .273-.01.543-.032.81-1.463-2.774-4.33-4.691-7.655-4.805L13 15h-2v5zm-2-7h4.034l.347.007c1.285.043 2.524.31 3.676.766C15.59 12.075 13.42 11 11 11H9V8.161L4.202 12 9 15.839V13z",
|
d: "M11 20L1 12l10-8v5c5.523 0 10 4.477 10 10 0 .273-.01.543-.032.81-1.463-2.774-4.33-4.691-7.655-4.805L13 15h-2v5zm-2-7h4.034l.347.007c1.285.043 2.524.31 3.676.766C15.59 12.075 13.42 11 11 11H9V8.161L4.202 12 9 15.839V13z",
|
||||||
defaultProps,
|
defaultProps,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const VerifiedIcon = createIcon({
|
||||||
|
displayName: "VerifiedIcon",
|
||||||
|
d: "M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-.997-6l7.07-7.071-1.414-1.414-5.656 5.657-2.829-2.829-1.414 1.414L11.003 16z",
|
||||||
|
defaultProps,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const VerificationFailed = createIcon({
|
||||||
|
displayName: "VerificationFailed",
|
||||||
|
d: "M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z",
|
||||||
|
defaultProps,
|
||||||
|
});
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Box, Card, CardBody, CardFooter, CardHeader, Flex, Heading, IconButton, Link } from "@chakra-ui/react";
|
import { Box, Card, CardBody, CardFooter, CardHeader, Flex, Heading, IconButton, Link, Text } from "@chakra-ui/react";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { UserAvatarLink } from "../user-avatar-link";
|
import { UserAvatarLink } from "../user-avatar-link";
|
||||||
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
||||||
@@ -18,6 +18,7 @@ import { UserLink } from "../user-link";
|
|||||||
import { ReplyIcon } from "../icons";
|
import { ReplyIcon } from "../icons";
|
||||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||||
import { buildReply } from "../../helpers/nostr-event";
|
import { buildReply } from "../../helpers/nostr-event";
|
||||||
|
import { UserDnsIdentityIcon } from "../user-dns-identity";
|
||||||
|
|
||||||
export type NoteProps = {
|
export type NoteProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
@@ -34,13 +35,15 @@ export const Note = React.memo(({ event }: NoteProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="outline">
|
<Card variant="outline">
|
||||||
<CardHeader padding="2" mb="2">
|
<CardHeader padding="2">
|
||||||
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
|
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
|
||||||
<UserAvatarLink pubkey={event.pubkey} size={isMobile ? "xs" : "sm"} />
|
<UserAvatarLink pubkey={event.pubkey} size={isMobile ? "xs" : "sm"} />
|
||||||
|
|
||||||
<Heading size="sm" display="inline">
|
<Heading size="sm" display="inline">
|
||||||
<UserLink pubkey={event.pubkey} />
|
<UserLink pubkey={event.pubkey} />
|
||||||
</Heading>
|
</Heading>
|
||||||
|
<UserDnsIdentityIcon pubkey={event.pubkey} />
|
||||||
|
{!isMobile && <Flex grow={1} />}
|
||||||
<Link as={RouterLink} to={`/n/${normalizeToBech32(event.id, Bech32Prefix.Note)}`} whiteSpace="nowrap">
|
<Link as={RouterLink} to={`/n/${normalizeToBech32(event.id, Bech32Prefix.Note)}`} whiteSpace="nowrap">
|
||||||
{moment(event.created_at * 1000).fromNow()}
|
{moment(event.created_at * 1000).fromNow()}
|
||||||
</Link>
|
</Link>
|
||||||
|
28
src/components/user-dns-identity.tsx
Normal file
28
src/components/user-dns-identity.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Spinner, Tooltip } from "@chakra-ui/react";
|
||||||
|
import { useDnsIdentity } from "../hooks/use-dns-identity";
|
||||||
|
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||||
|
import { VerificationFailed, VerifiedIcon } from "./icons";
|
||||||
|
|
||||||
|
export const UserDnsIdentityIcon = ({ pubkey }: { pubkey: string }) => {
|
||||||
|
const metadata = useUserMetadata(pubkey);
|
||||||
|
const { identity, loading, error } = useDnsIdentity(metadata?.nip05);
|
||||||
|
|
||||||
|
if (!metadata?.nip05) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = metadata.nip05;
|
||||||
|
|
||||||
|
const renderIcon = () => {
|
||||||
|
if (loading) {
|
||||||
|
return <Spinner size="xs" ml="1" />;
|
||||||
|
} else if (error) {
|
||||||
|
return <VerificationFailed color="yellow.500" />;
|
||||||
|
} else {
|
||||||
|
const isValid = !!identity && pubkey === identity.pubkey;
|
||||||
|
return isValid ? <VerifiedIcon color="purple.500" /> : <VerificationFailed color="red.500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Tooltip label={title}>{renderIcon()}</Tooltip>;
|
||||||
|
};
|
15
src/hooks/use-dns-identity.ts
Normal file
15
src/hooks/use-dns-identity.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useAsync } from "react-use";
|
||||||
|
import dnsIdentityService from "../services/dns-identity";
|
||||||
|
|
||||||
|
export function useDnsIdentity(address: string | undefined) {
|
||||||
|
const { value, loading, error } = useAsync(async () => {
|
||||||
|
if (!address) return;
|
||||||
|
return dnsIdentityService.getIdentity(address);
|
||||||
|
}, [address]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
identity: value,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
@@ -22,6 +22,12 @@ const MIGRATIONS: MigrationFunction[] = [
|
|||||||
contacts.createIndex("created_at", "created_at");
|
contacts.createIndex("created_at", "created_at");
|
||||||
contacts.createIndex("contacts", "contacts", { multiEntry: true });
|
contacts.createIndex("contacts", "contacts", { multiEntry: true });
|
||||||
|
|
||||||
|
const dnsIdentifiers = db.createObjectStore("dns-identifiers");
|
||||||
|
dnsIdentifiers.createIndex("pubkey", "pubkey", { unique: false });
|
||||||
|
dnsIdentifiers.createIndex("name", "name", { unique: false });
|
||||||
|
dnsIdentifiers.createIndex("domain", "domain", { unique: false });
|
||||||
|
dnsIdentifiers.createIndex("updated", "updated", { unique: false });
|
||||||
|
|
||||||
// setup data
|
// setup data
|
||||||
const settings = db.createObjectStore("settings");
|
const settings = db.createObjectStore("settings");
|
||||||
},
|
},
|
||||||
@@ -44,7 +50,7 @@ const db = await openDB<CustomSchema>("storage", version, {
|
|||||||
export async function clearData() {
|
export async function clearData() {
|
||||||
await db.clear("user-metadata");
|
await db.clear("user-metadata");
|
||||||
await db.clear("user-contacts");
|
await db.clear("user-contacts");
|
||||||
await db.clear("settings");
|
await db.clear("dns-identifiers");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,6 +20,11 @@ export interface CustomSchema extends DBSchema {
|
|||||||
};
|
};
|
||||||
indexes: { created_at: number; contacts: string };
|
indexes: { created_at: number; contacts: string };
|
||||||
};
|
};
|
||||||
|
"dns-identifiers": {
|
||||||
|
key: string;
|
||||||
|
value: { name: string; domain: string; pubkey: string; relays: string[]; updated: number };
|
||||||
|
indexes: { name: string; domain: string; pubkey: string; updated: number };
|
||||||
|
};
|
||||||
settings: {
|
settings: {
|
||||||
key: string;
|
key: string;
|
||||||
value: any;
|
value: any;
|
||||||
|
96
src/services/dns-identity.ts
Normal file
96
src/services/dns-identity.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
import db from "./db";
|
||||||
|
|
||||||
|
function parseAddress(address: string) {
|
||||||
|
const parts = address.split("@");
|
||||||
|
return { name: parts[0], domain: parts[1] };
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityJson = {
|
||||||
|
names: Record<string, string | undefined>;
|
||||||
|
relays?: Record<string, string[]>;
|
||||||
|
};
|
||||||
|
export type DnsIdentity = {
|
||||||
|
name: string;
|
||||||
|
domain: string;
|
||||||
|
pubkey: string;
|
||||||
|
relays: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function getIdentityFromJson(name: string, domain: string, json: IdentityJson): DnsIdentity | undefined {
|
||||||
|
const relays: string[] = json.relays?.[name] ?? [];
|
||||||
|
const pubkey = json.names[name];
|
||||||
|
|
||||||
|
if (pubkey) return { name, domain, pubkey, relays };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAllIdentities(domain: string) {
|
||||||
|
const json = await fetch(`//${domain}/.well-known/nostr.json`).then((res) => res.json() as Promise<IdentityJson>);
|
||||||
|
|
||||||
|
await addToCache(domain, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchIdentity(address: string) {
|
||||||
|
const { name, domain } = parseAddress(address);
|
||||||
|
const json = await fetch(`https://${domain}/.well-known/nostr.json?name=${name}`).then(
|
||||||
|
(res) => res.json() as Promise<IdentityJson>
|
||||||
|
);
|
||||||
|
|
||||||
|
await addToCache(domain, json);
|
||||||
|
|
||||||
|
return getIdentityFromJson(name, domain, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addToCache(domain: string, json: IdentityJson) {
|
||||||
|
const now = moment().unix();
|
||||||
|
const wait = [];
|
||||||
|
for (const name of Object.keys(json.names)) {
|
||||||
|
const identity = getIdentityFromJson(name, domain, json);
|
||||||
|
if (identity) {
|
||||||
|
wait.push(db.put("dns-identifiers", { ...identity, updated: now }, `${name}@${domain}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(wait);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getIdentity(address: string) {
|
||||||
|
const cached = await db.get("dns-identifiers", address);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
// TODO: if it fails, maybe cache a failure message
|
||||||
|
return fetchIdentity(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pruneCache() {
|
||||||
|
const keys = await db.getAllKeysFromIndex(
|
||||||
|
"dns-identifiers",
|
||||||
|
"updated",
|
||||||
|
IDBKeyRange.upperBound(moment().subtract(1, "hour").unix())
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const pubkey of keys) {
|
||||||
|
db.delete("dns-identifiers", pubkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending: Record<string, ReturnType<typeof getIdentity> | undefined> = {};
|
||||||
|
function dedupedGetIdentity(address: string) {
|
||||||
|
if (pending[address]) return pending[address];
|
||||||
|
return (pending[address] = getIdentity(address));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dnsIdentityService = {
|
||||||
|
fetchAllIdentities,
|
||||||
|
fetchIdentity,
|
||||||
|
getIdentity: dedupedGetIdentity,
|
||||||
|
pruneCache,
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => pruneCache(), 1000 * 60 * 20);
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.dnsIdentityService = dnsIdentityService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default dnsIdentityService;
|
@@ -35,6 +35,7 @@ export type Kind0ParsedContent = {
|
|||||||
website?: string;
|
website?: string;
|
||||||
lud16?: string;
|
lud16?: string;
|
||||||
lud06?: string;
|
lud06?: string;
|
||||||
|
nip05?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isETag(tag: Tag): tag is ETag {
|
export function isETag(tag: Tag): tag is ETag {
|
||||||
|
@@ -20,6 +20,7 @@ import { useIsMobile } from "../../hooks/use-is-mobile";
|
|||||||
import { UserProfileMenu } from "./components/user-profile-menu";
|
import { UserProfileMenu } from "./components/user-profile-menu";
|
||||||
import { LinkIcon } from "@chakra-ui/icons";
|
import { LinkIcon } from "@chakra-ui/icons";
|
||||||
import { UserTipButton } from "../../components/user-tip-button";
|
import { UserTipButton } from "../../components/user-tip-button";
|
||||||
|
import { UserDnsIdentityIcon } from "../../components/user-dns-identity";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ label: "Notes", path: "notes" },
|
{ label: "Notes", path: "notes" },
|
||||||
@@ -45,7 +46,10 @@ const UserView = () => {
|
|||||||
<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}>
|
||||||
<Heading size={isMobile ? "md" : "lg"}>{getUserDisplayName(metadata, pubkey)}</Heading>
|
<Heading size={isMobile ? "md" : "lg"}>
|
||||||
|
{getUserDisplayName(metadata, pubkey)}
|
||||||
|
<UserDnsIdentityIcon pubkey={pubkey} />
|
||||||
|
</Heading>
|
||||||
{!metadata ? <SkeletonText /> : <Text>{metadata?.about}</Text>}
|
{!metadata ? <SkeletonText /> : <Text>{metadata?.about}</Text>}
|
||||||
{metadata?.website && (
|
{metadata?.website && (
|
||||||
<Text>
|
<Text>
|
||||||
|
Reference in New Issue
Block a user