add DNS Identity settings view

This commit is contained in:
hzrd149 2025-02-14 13:21:39 -06:00
parent 7e388bd214
commit 723668b0fc
11 changed files with 229 additions and 57 deletions

View File

@ -6,7 +6,7 @@ import SuperMap from "../classes/super-map";
const parseCache = new SuperMap<string, { name: string; domain: string } | null>(parseNIP05Address);
export default function useDnsIdentity(address: string | undefined) {
export default function useDnsIdentity(address: string | undefined, force = false) {
const parsed = address ? parseCache.get(address) : null;
const { value: identity } = useAsync(async () => {
if (parsed) return await dnsIdentityLoader.requestIdentity(parsed.name, parsed.domain);

View File

@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { Button, ButtonGroup, Flex, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
import { Link as RouterLink, useLocation } from "react-router-dom";
import { kinds, nip19 } from "nostr-tools";
@ -21,20 +21,11 @@ import { useKind4Decrypt } from "../../hooks/use-kind4-decryption";
import { truncateId } from "../../helpers/string";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useUserMailboxes from "../../hooks/use-user-mailboxes";
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
import useUserContacts from "../../hooks/use-user-contacts";
import useUserMutes from "../../hooks/use-user-mutes";
import SimpleParentView from "../../components/layout/presets/simple-parent-view";
export function useDirectMessagesTimeline(pubkey?: string) {
const userMuteFilter = useClientSideMuteFilter();
const eventFilter = useCallback(
(event: NostrEvent) => {
if (userMuteFilter(event)) return false;
return true;
},
[userMuteFilter],
);
const mailboxes = useUserMailboxes(pubkey);
return useTimelineLoader(
@ -46,7 +37,6 @@ export function useDirectMessagesTimeline(pubkey?: string) {
{ "#p": [pubkey], kinds: [kinds.EncryptedDirectMessage] },
]
: undefined,
{ eventFilter },
);
}
@ -115,7 +105,7 @@ function MessagesHomePage() {
}
return filtered.sort((a, b) => b.messages[0].created_at - a.messages[0].created_at);
}, [messages, account.pubkey, contacts?.length, filter, mutes?.pubkeys]);
}, [messages, account.pubkey, contacts?.length, filter, mutes?.pubkeys.size]);
const callback = useTimelineCurserIntersectionCallback(loader);

View File

@ -1,4 +1,6 @@
import { useActiveAccount } from "applesauce-react/hooks";
import { IdentityStatus } from "applesauce-loaders/helpers/dns-identity";
import Database01 from "../../components/icons/database-01";
import { AtIcon, RelayIcon, SearchIcon } from "../../components/icons";
import Mail02 from "../../components/icons/mail-02";
@ -37,7 +39,7 @@ export default function RelaysView() {
<SimpleNavItem to="/relays/webrtc" leftIcon={<Server05 boxSize={6} />}>
WebRTC Relays
</SimpleNavItem>
{nip05?.exists && (
{nip05?.status === IdentityStatus.Found && (
<SimpleNavItem to="/relays/nip05" leftIcon={<AtIcon boxSize={6} />}>
NIP-05 Relays
</SimpleNavItem>

View File

@ -6,6 +6,7 @@ import { Link as RouterLink } from "react-router-dom";
import RelayFavicon from "../../../components/relay-favicon";
import SimpleView from "../../../components/layout/presets/simple-view";
import { IdentityStatus } from "applesauce-loaders/helpers/dns-identity";
function RelayItem({ url }: { url: string }) {
return (
@ -38,7 +39,7 @@ export default function NIP05RelaysView() {
</Link>
</Text>
{nip05?.relays?.map((url) => <RelayItem key={url} url={url} />)}
{nip05?.status === IdentityStatus.Found && nip05?.relays?.map((url) => <RelayItem key={url} url={url} />)}
</SimpleView>
);
}

View File

@ -0,0 +1,33 @@
import { Link, Text } from "@chakra-ui/react";
import { Identity, IdentityStatus } from "applesauce-loaders/helpers/dns-identity";
import { ExternalLinkIcon } from "../../../components/icons";
export default function DNSIdentityWarning({ identity, pubkey }: { pubkey: string; identity: Identity }) {
switch (identity?.status) {
case IdentityStatus.Missing:
return <Text color="red.500">Unable to find DNS Identity in nostr.json file</Text>;
case IdentityStatus.Error:
return (
<Text color="yellow.500">
Unable to check DNS identity due to CORS error{" "}
<Link
color="blue.500"
href={`https://cors-test.codehappy.dev/?url=${encodeURIComponent(`https://${identity.domain}/.well-known/nostr.json?name=${identity.name}`)}&method=get`}
isExternal
>
Test
<ExternalLinkIcon ml="1" />
</Link>
</Text>
);
case IdentityStatus.Found:
if (identity.pubkey !== pubkey)
return (
<Text color="red.500" fontWeight="bold">
Invalid DNS Identity!
</Text>
);
default:
return null;
}
}

View File

@ -0,0 +1,170 @@
import {
ButtonGroup,
Editable,
EditableInput,
EditablePreview,
EditableProps,
Heading,
IconButton,
Input,
Link,
Spinner,
Text,
useEditableControls,
useToast,
} from "@chakra-ui/react";
import { Navigate } from "react-router-dom";
import { getProfileContent, mergeRelaySets, parseNIP05Address, ProfileContent } from "applesauce-core/helpers";
import { CheckIcon, CloseIcon, EditIcon } from "@chakra-ui/icons";
import { kinds } from "nostr-tools";
import { setContent } from "applesauce-factory/operations";
import { IdentityStatus } from "applesauce-loaders/helpers/dns-identity";
import { useAsync } from "react-use";
import SimpleView from "../../../components/layout/presets/simple-view";
import { useActiveAccount, useEventFactory, useEventStore } from "applesauce-react/hooks";
import useUserProfile from "../../../hooks/use-user-profile";
import dnsIdentityLoader from "../../../services/dns-identity-loader";
import WikiLink from "../../../components/markdown/wiki-link";
import RawValue from "../../../components/debug-modal/raw-value";
import { ExternalLinkIcon } from "../../../components/icons";
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
import { useWriteRelays } from "../../../hooks/use-client-relays";
import { COMMON_CONTACT_RELAYS } from "../../../const";
import { usePublishEvent } from "../../../providers/global/publish-provider";
function EditableControls() {
const { isEditing, getSubmitButtonProps, getCancelButtonProps, getEditButtonProps } = useEditableControls();
return isEditing ? (
<ButtonGroup justifyContent="center" size="sm">
<IconButton icon={<CheckIcon />} {...getSubmitButtonProps()} aria-label="save" />
<IconButton icon={<CloseIcon />} {...getCancelButtonProps()} aria-label="Cancel" />
</ButtonGroup>
) : (
<IconButton size="sm" icon={<EditIcon />} {...getEditButtonProps()} aria-label="Edit" />
);
}
function EditableIdentity() {
const factory = useEventFactory();
const eventStore = useEventStore();
const account = useActiveAccount();
const toast = useToast();
const publish = usePublishEvent();
const profile = useUserProfile(account?.pubkey);
const mailboxes = useUserMailboxes();
const publishRelays = useWriteRelays();
const onSubmit: EditableProps["onSubmit"] = async (value) => {
if (!account) return;
try {
const metadata = eventStore.getReplaceable(kinds.Metadata, account.pubkey);
if (!metadata) throw new Error("Failed to find profile");
const profile = getProfileContent(metadata);
const newProfile = { ...profile, nip05: value };
const draft = await factory.modify(metadata, setContent(JSON.stringify(newProfile)));
const signed = await account.signEvent(draft);
await publish("Update NIP-05", signed, mergeRelaySets(publishRelays, mailboxes?.outboxes, COMMON_CONTACT_RELAYS));
} catch (error) {
if (error instanceof Error) toast({ status: "error", description: error.message });
}
};
return (
<Editable
alignItems="center"
defaultValue={profile?.nip05}
fontSize="2xl"
isPreviewFocusable={false}
onSubmit={onSubmit}
>
<EditablePreview mr="2" />
{/* Here is the custom input */}
<Input as={EditableInput} w="xs" mr="2" />
<EditableControls />
</Editable>
);
}
function IdentityDetails({ pubkey, profile }: { pubkey: string; profile: ProfileContent }) {
const { value: identity, loading } = useAsync(async () => {
if (!profile?.nip05) return null;
const parsed = parseNIP05Address(profile.nip05);
if (!parsed) return null;
return await dnsIdentityLoader.fetchIdentity(parsed.name, parsed.domain);
}, [profile?.nip05]);
const renderDetails = () => {
if (!identity) return null;
switch (identity.status) {
case IdentityStatus.Missing:
return <Text color="red.500">Unable to find DNS Identity in nostr.json file</Text>;
case IdentityStatus.Error:
return (
<Text color="yellow.500">
Unable to check DNS identity due to CORS error{" "}
<Link
color="blue.500"
href={`https://cors-test.codehappy.dev/?url=${encodeURIComponent(`https://${identity.domain}/.well-known/nostr.json?name=${identity.name}`)}&method=get`}
isExternal
>
Test
<ExternalLinkIcon ml="1" />
</Link>
</Text>
);
case IdentityStatus.Found:
if (identity.pubkey !== pubkey)
return (
<Text color="red.500" fontWeight="bold">
Invalid DNS Identity! <CloseIcon />
</Text>
);
else
return (
<Text color="green.500" fontWeight="bold">
DNS identity matches pubkey <CheckIcon />
</Text>
);
default:
return null;
}
};
return (
<>
<EditableIdentity />
{renderDetails()}
{loading && <Spinner />}
<RawValue heading="Your pubkey" value={pubkey} />
</>
);
}
export default function DnsIdentityView() {
const account = useActiveAccount();
if (!account) return <Navigate to="/" />;
const profile = useUserProfile(account.pubkey, undefined, true);
return (
<SimpleView title="DNS Identity">
{profile?.nip05 ? (
<IdentityDetails pubkey={account.pubkey} profile={profile} />
) : (
<>
<Heading>No DNS identity setup</Heading>
<RawValue heading="Your pubkey" value={account.pubkey} />
<Text>
or read the details on the wiki: <WikiLink topic="nip-05">NIP-05</WikiLink>
</Text>
</>
)}
</SimpleView>
);
}

View File

@ -10,6 +10,7 @@ import {
RelayIcon,
SearchIcon,
SpyIcon,
VerifiedIcon,
} from "../../components/icons";
import { useActiveAccount } from "applesauce-react/hooks";
import Image01 from "../../components/icons/image-01";
@ -56,6 +57,9 @@ export default function SettingsView() {
<SimpleNavItem to="/settings/search-relays" leftIcon={<SearchIcon boxSize={6} />}>
Search
</SimpleNavItem>
<SimpleNavItem to="/settings/identity" leftIcon={<VerifiedIcon boxSize={6} />}>
DNS Identity
</SimpleNavItem>
</>
)}

View File

@ -1,6 +1,8 @@
import { MouseEventHandler, useCallback, useMemo } from "react";
import { Button, Card, CardBody, CardHeader, Flex, Heading, SimpleGrid, Text } from "@chakra-ui/react";
import { WarningIcon } from "@chakra-ui/icons";
import { IdentityStatus } from "applesauce-loaders/helpers/dns-identity";
import { mergeRelaySets } from "applesauce-core/helpers";
import { RECOMMENDED_READ_RELAYS, RECOMMENDED_WRITE_RELAYS } from "../../../const";
import AddRelayForm from "./add-relay-form";
@ -10,7 +12,7 @@ import RelayControl from "./relay-control";
import { getRelaysFromExt } from "../../../helpers/nip07";
import { useUserDNSIdentity } from "../../../hooks/use-user-dns-identity";
import useUserContactRelays from "../../../hooks/use-user-contact-relays";
import { mergeRelaySets, safeRelayUrls } from "../../../helpers/relay";
import { safeRelayUrls } from "../../../helpers/relay";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import SimpleView from "../../../components/layout/presets/simple-view";
import localSettings from "../../../services/local-settings";
@ -108,7 +110,7 @@ export default function AppRelaysView() {
NIP-65 (Mailboxes)
</Button>
)}
{nip05?.relays && (
{nip05?.status === IdentityStatus.Found && (
<Button
onClick={() => {
if (!nip05.relays) return;

View File

@ -15,6 +15,7 @@ import PrivacySettings from "./privacy";
import LightningSettings from "./lightning";
import PerformanceSettings from "./performance";
import AuthenticationSettingsView from "./authentication";
import DnsIdentityView from "./dns-identity";
// bakery settings
const BakeryConnectView = lazy(() => import("./bakery/connect"));
@ -41,6 +42,7 @@ export default [
),
},
{ path: "mailboxes", Component: MailboxesView },
{ path: "identity", Component: DnsIdentityView },
{ path: "authentication", Component: AuthenticationSettingsView },
{ path: "media-servers", Component: MediaServersView },
{ path: "search-relays", Component: SearchRelaysView },

View File

@ -20,6 +20,7 @@ import {
import { nip19 } from "nostr-tools";
import { ChatIcon } from "@chakra-ui/icons";
import { parseLNURLOrAddress, parseNIP05Address } from "applesauce-core/helpers";
import { IdentityStatus } from "applesauce-loaders/helpers/dns-identity";
import { truncatedId } from "../../../helpers/nostr/event";
import { useAdditionalRelayContext } from "../../../providers/local/additional-relay-context";
@ -51,45 +52,7 @@ import UserAboutContent from "../../../components/user/user-about-content";
import UserRecentEvents from "./user-recent-events";
import { useUserAppSettings } from "../../../hooks/use-user-app-settings";
import UserJoinedGroups from "./user-joined-groups";
import { IdentityStatus } from "applesauce-loaders/helpers/dns-identity";
function DNSIdentityWarning({ pubkey }: { pubkey: string }) {
const metadata = useUserProfile(pubkey);
const identity = useUserDNSIdentity(pubkey);
const parsed = metadata?.nip05 ? parseNIP05Address(metadata.nip05) : undefined;
const nip05URL = parsed ? `https://${parsed.domain}/.well-known/nostr.json?name=${parsed.name}` : undefined;
switch (identity?.status) {
case IdentityStatus.Missing:
return <Text color="red.500">Unable to find DNS Identity in nostr.json file</Text>;
case IdentityStatus.Error:
return (
<Text color="yellow.500">
Unable to check DNS identity due to CORS error{" "}
{nip05URL && (
<Link
color="blue.500"
href={`https://cors-test.codehappy.dev/?url=${encodeURIComponent(nip05URL)}&method=get`}
isExternal
>
Test
<ExternalLinkIcon ml="1" />
</Link>
)}
</Text>
);
case IdentityStatus.Found:
if (identity.pubkey !== pubkey)
return (
<Text color="red.500" fontWeight="bold">
Invalid DNS Identity!
</Text>
);
default:
return null;
}
}
import DNSIdentityWarning from "../../settings/dns-identity/identity-warning";
export default function UserAboutTab() {
const expanded = useDisclosure();
@ -108,6 +71,8 @@ export default function UserAboutTab() {
? `https://${parsedNip05.domain}/.well-known/nostr.json?name=${parsedNip05.name}`
: undefined;
const identity = useUserDNSIdentity(pubkey);
return (
<Flex
overflowY="auto"
@ -202,7 +167,7 @@ export default function UserAboutTab() {
<UserDnsIdentity pubkey={pubkey} />
</Link>
</Flex>
<DNSIdentityWarning pubkey={pubkey} />
{identity && <DNSIdentityWarning identity={identity} pubkey={pubkey} />}
</Box>
)}
{metadata?.website && (

View File

@ -12,6 +12,7 @@ import { EditIcon } from "../../../components/icons";
import { getPageTopic } from "../../../helpers/nostr/wiki";
import GitBranch02 from "../../../components/icons/git-branch-02";
import useShareableEventAddress from "../../../hooks/use-shareable-event-address";
import DeleteEventMenuItem from "../../../components/common-menu-items/delete-event";
export default function WikiPageMenu({ page, ...props }: { page: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
const account = useActiveAccount();
@ -32,6 +33,8 @@ export default function WikiPageMenu({ page, ...props }: { page: NostrEvent } &
</MenuItem>
)}
<DeleteEventMenuItem event={page} label="Delete Page" />
<ShareLinkMenuItem event={page} />
<CopyEmbedCodeMenuItem event={page} />