mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-28 20:43:33 +02:00
add event signature verification icon
improve QrCode on user profile
This commit is contained in:
@@ -11,10 +11,13 @@ import { UserLink } from "./user-link";
|
|||||||
import { UserDnsIdentityIcon } from "./user-dns-identity";
|
import { UserDnsIdentityIcon } from "./user-dns-identity";
|
||||||
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip19";
|
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip19";
|
||||||
import { convertTimestampToDate } from "../helpers/date";
|
import { convertTimestampToDate } from "../helpers/date";
|
||||||
|
import useSubject from "../hooks/use-subject";
|
||||||
|
import settings from "../services/settings";
|
||||||
|
import EventVerificationIcon from "./event-verification-icon";
|
||||||
|
|
||||||
const EmbeddedNote = ({ note }: { note: NostrEvent }) => {
|
const EmbeddedNote = ({ note }: { note: NostrEvent }) => {
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
|
const showSignatureVerification = useSubject(settings.showSignatureVerification);
|
||||||
|
|
||||||
const contacts = useUserContacts(account.pubkey);
|
const contacts = useUserContacts(account.pubkey);
|
||||||
const following = contacts?.contacts || [];
|
const following = contacts?.contacts || [];
|
||||||
@@ -29,7 +32,8 @@ const EmbeddedNote = ({ note }: { note: NostrEvent }) => {
|
|||||||
<UserLink pubkey={note.pubkey} />
|
<UserLink pubkey={note.pubkey} />
|
||||||
</Heading>
|
</Heading>
|
||||||
<UserDnsIdentityIcon pubkey={note.pubkey} onlyIcon />
|
<UserDnsIdentityIcon pubkey={note.pubkey} onlyIcon />
|
||||||
{!isMobile && <Flex grow={1} />}
|
<Flex grow={1} />
|
||||||
|
{showSignatureVerification && <EventVerificationIcon event={note} />}
|
||||||
<Link as={RouterLink} to={`/n/${normalizeToBech32(note.id, Bech32Prefix.Note)}`} whiteSpace="nowrap">
|
<Link as={RouterLink} to={`/n/${normalizeToBech32(note.id, Bech32Prefix.Note)}`} whiteSpace="nowrap">
|
||||||
{moment(convertTimestampToDate(note.created_at)).fromNow()}
|
{moment(convertTimestampToDate(note.created_at)).fromNow()}
|
||||||
</Link>
|
</Link>
|
||||||
|
13
src/components/event-verification-icon.tsx
Normal file
13
src/components/event-verification-icon.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
|
import { verifySignature } from "nostr-tools";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { CheckIcon, VerificationFailed } from "./icons";
|
||||||
|
|
||||||
|
export default function EventVerificationIcon({ event }: { event: NostrEvent }) {
|
||||||
|
const valid = useMemo(() => verifySignature(event), [event]);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
return <VerificationFailed color="red.500" />;
|
||||||
|
}
|
||||||
|
return <CheckIcon color="green.500" />;
|
||||||
|
}
|
@@ -3,7 +3,6 @@ import { Link as RouterLink } from "react-router-dom";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
@@ -36,6 +35,7 @@ import NoteZapButton from "./note-zap-button";
|
|||||||
import { ExpandProvider } from "./expanded";
|
import { ExpandProvider } from "./expanded";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import settings from "../../services/settings";
|
import settings from "../../services/settings";
|
||||||
|
import EventVerificationIcon from "../event-verification-icon";
|
||||||
|
|
||||||
export type NoteProps = {
|
export type NoteProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
@@ -47,6 +47,7 @@ export const Note = React.memo(({ event, maxHeight, variant = "outline" }: NoteP
|
|||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const { openModal } = useContext(PostModalContext);
|
const { openModal } = useContext(PostModalContext);
|
||||||
const showReactions = useSubject(settings.showReactions);
|
const showReactions = useSubject(settings.showReactions);
|
||||||
|
const showSignatureVerification = useSubject(settings.showSignatureVerification);
|
||||||
|
|
||||||
const contacts = useUserContacts(account.pubkey);
|
const contacts = useUserContacts(account.pubkey);
|
||||||
const following = contacts?.contacts || [];
|
const following = contacts?.contacts || [];
|
||||||
@@ -65,7 +66,8 @@ export const Note = React.memo(({ event, maxHeight, variant = "outline" }: NoteP
|
|||||||
<UserLink pubkey={event.pubkey} />
|
<UserLink pubkey={event.pubkey} />
|
||||||
</Heading>
|
</Heading>
|
||||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||||
{!isMobile && <Flex grow={1} />}
|
<Flex grow={1} />
|
||||||
|
{showSignatureVerification && <EventVerificationIcon event={event} />}
|
||||||
<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(convertTimestampToDate(event.created_at)).fromNow()}
|
{moment(convertTimestampToDate(event.created_at)).fromNow()}
|
||||||
</Link>
|
</Link>
|
||||||
|
@@ -1,31 +0,0 @@
|
|||||||
import {
|
|
||||||
IconButton,
|
|
||||||
IconButtonProps,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalHeader,
|
|
||||||
useDisclosure,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { QrCodeIcon } from "./icons";
|
|
||||||
import QrCodeSvg from "./qr-code-svg";
|
|
||||||
|
|
||||||
export const QrIconButton = ({ content, ...props }: { content: string } & Omit<IconButtonProps, "icon">) => {
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IconButton icon={<QrCodeIcon />} onClick={onOpen} {...props} />
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalBody>
|
|
||||||
<QrCodeSvg content={content} border={2} />
|
|
||||||
</ModalBody>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -7,6 +7,7 @@ const settings = {
|
|||||||
autoShowMedia: new PersistentSubject(true),
|
autoShowMedia: new PersistentSubject(true),
|
||||||
proxyUserMedia: new PersistentSubject(false),
|
proxyUserMedia: new PersistentSubject(false),
|
||||||
showReactions: new PersistentSubject(true),
|
showReactions: new PersistentSubject(true),
|
||||||
|
showSignatureVerification: new PersistentSubject(false),
|
||||||
accounts: new PersistentSubject<Account[]>([]),
|
accounts: new PersistentSubject<Account[]>([]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -26,6 +26,7 @@ export default function SettingsView() {
|
|||||||
const autoShowMedia = useSubject(settings.autoShowMedia);
|
const autoShowMedia = useSubject(settings.autoShowMedia);
|
||||||
const proxyUserMedia = useSubject(settings.proxyUserMedia);
|
const proxyUserMedia = useSubject(settings.proxyUserMedia);
|
||||||
const showReactions = useSubject(settings.showReactions);
|
const showReactions = useSubject(settings.showReactions);
|
||||||
|
const showSignatureVerification = useSubject(settings.showSignatureVerification);
|
||||||
|
|
||||||
const { colorMode, setColorMode } = useColorMode();
|
const { colorMode, setColorMode } = useColorMode();
|
||||||
|
|
||||||
@@ -129,9 +130,9 @@ export default function SettingsView() {
|
|||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<FormHelperText>
|
<FormHelperText>
|
||||||
<span>Enabled: media.nostr.band is used to get smaller of profile images (saves ~50Mb of data)</span>
|
<span>Enabled: Use media.nostr.band to get smaller profile pictures (saves ~50Mb of data)</span>
|
||||||
<br />
|
<br />
|
||||||
<span>Side Effect: some user pictures may not load or may be outdated</span>
|
<span>Side Effect: Some user pictures may not load or may be outdated</span>
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -145,12 +146,12 @@ export default function SettingsView() {
|
|||||||
onChange={(v) => settings.autoShowMedia.next(v.target.checked)}
|
onChange={(v) => settings.autoShowMedia.next(v.target.checked)}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<FormHelperText>Disabled: images and videos will show expandable buttons.</FormHelperText>
|
<FormHelperText>Disabled: Images and videos will show expandable buttons</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Flex alignItems="center">
|
<Flex alignItems="center">
|
||||||
<FormLabel htmlFor="show-reactions" mb="0">
|
<FormLabel htmlFor="show-reactions" mb="0">
|
||||||
Show Reactions
|
Show reactions
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Switch
|
<Switch
|
||||||
id="show-reactions"
|
id="show-reactions"
|
||||||
@@ -158,7 +159,20 @@ export default function SettingsView() {
|
|||||||
onChange={(v) => settings.showReactions.next(v.target.checked)}
|
onChange={(v) => settings.showReactions.next(v.target.checked)}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<FormHelperText>Enabled: show reactions on notes.</FormHelperText>
|
<FormHelperText>Enabled: Show reactions on notes</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<FormLabel htmlFor="show-sig-verify" mb="0">
|
||||||
|
Show signature verification
|
||||||
|
</FormLabel>
|
||||||
|
<Switch
|
||||||
|
id="show-sig-verify"
|
||||||
|
isChecked={showSignatureVerification}
|
||||||
|
onChange={(v) => settings.showSignatureVerification.next(v.target.checked)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<FormHelperText>Enabled: show signature verification on notes</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Flex>
|
</Flex>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
|
@@ -5,7 +5,7 @@ import { useNavigate, Link as RouterLink } from "react-router-dom";
|
|||||||
import { RelayMode } from "../../../classes/relay";
|
import { RelayMode } from "../../../classes/relay";
|
||||||
import { CopyIconButton } from "../../../components/copy-icon-button";
|
import { CopyIconButton } from "../../../components/copy-icon-button";
|
||||||
import { ChatIcon, ExternalLinkIcon, KeyIcon, SettingsIcon } from "../../../components/icons";
|
import { ChatIcon, ExternalLinkIcon, KeyIcon, SettingsIcon } from "../../../components/icons";
|
||||||
import { QrIconButton } from "../../../components/qr-icon-button";
|
import { QrIconButton } from "./share-qr-button";
|
||||||
import { UserAvatar } from "../../../components/user-avatar";
|
import { UserAvatar } from "../../../components/user-avatar";
|
||||||
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity";
|
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity";
|
||||||
import { UserFollowButton } from "../../../components/user-follow-button";
|
import { UserFollowButton } from "../../../components/user-follow-button";
|
||||||
@@ -75,7 +75,7 @@ export default function Header({ pubkey }: { pubkey: string }) {
|
|||||||
<KeyIcon />
|
<KeyIcon />
|
||||||
<Text>{truncatedId(npub, 10)}</Text>
|
<Text>{truncatedId(npub, 10)}</Text>
|
||||||
<CopyIconButton text={npub} title="Copy npub" aria-label="Copy npub" size="xs" />
|
<CopyIconButton text={npub} title="Copy npub" aria-label="Copy npub" size="xs" />
|
||||||
<QrIconButton content={"nostr:" + shareLink} title="Show QrCode" aria-label="Show QrCode" size="xs" />
|
<QrIconButton pubkey={pubkey} title="Show QrCode" aria-label="Show QrCode" size="xs" />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
<Flex gap="2" ml="auto">
|
<Flex gap="2" ml="auto">
|
||||||
|
71
src/views/user/components/share-qr-button.tsx
Normal file
71
src/views/user/components/share-qr-button.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
IconButtonProps,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalOverlay,
|
||||||
|
useDisclosure,
|
||||||
|
Tabs,
|
||||||
|
TabList,
|
||||||
|
Tab,
|
||||||
|
TabPanels,
|
||||||
|
TabPanel,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { RelayMode } from "../../../classes/relay";
|
||||||
|
import { QrCodeIcon } from "../../../components/icons";
|
||||||
|
import QrCodeSvg from "../../../components/qr-code-svg";
|
||||||
|
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
|
||||||
|
import useFallbackUserRelays from "../../../hooks/use-fallback-user-relays";
|
||||||
|
import relayScoreboardService from "../../../services/relay-scoreboard";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
|
||||||
|
function useUserShareLink(pubkey: string) {
|
||||||
|
const userRelays = useFallbackUserRelays(pubkey);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const writeUrls = userRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
|
||||||
|
const ranked = relayScoreboardService.getRankedRelays(writeUrls);
|
||||||
|
const onlyTwo = ranked.slice(0, 2);
|
||||||
|
|
||||||
|
return onlyTwo.length > 0 ? nip19.nprofileEncode({ pubkey, relays: onlyTwo }) : nip19.npubEncode(pubkey);
|
||||||
|
}, [userRelays]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QrIconButton = ({ pubkey, ...props }: { pubkey: string } & Omit<IconButtonProps, "icon">) => {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
|
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey) || pubkey;
|
||||||
|
const nprofile = useUserShareLink(pubkey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton icon={<QrCodeIcon />} onClick={onOpen} {...props} />
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody p="2">
|
||||||
|
<Tabs>
|
||||||
|
<TabList>
|
||||||
|
<Tab>npub</Tab>
|
||||||
|
<Tab>nprofile</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel>
|
||||||
|
<QrCodeSvg content={"nostr:" + npub} border={2} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<QrCodeSvg content={"nostr:" + nprofile} border={2} />
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
Reference in New Issue
Block a user