Merge branch 'next'

This commit is contained in:
hzrd149 2023-04-10 19:43:53 -05:00
commit b425631835
26 changed files with 1901 additions and 1707 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Remove brb.io link from user profiles

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add custom zap amounts to settings

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Increase min height of note before showing expandable overlay

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Support nostr: links in notes

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add "Download Backup" link to profile edit view

View File

@ -1,12 +1,10 @@
import { createIcon, IconProps } from "@chakra-ui/icons";
import nostrGuruIcon from "./icons/nostr-guru.jpg";
import brbIcon from "./icons/brb.png";
import snortSocialIcon from "./icons/snort-social.png";
export const IMAGE_ICONS = {
nostrGuruIcon,
brbIcon,
snortSocialIcon,
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -3,12 +3,13 @@ import { AspectRatio, Box, Button, ButtonGroup, Image, ImageProps, Link, useDisc
import { InlineInvoiceCard } from "../inline-invoice-card";
import { TweetEmbed } from "../tweet-embed";
import { UserLink } from "../user-link";
import { normalizeToHex } from "../../helpers/nip19";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import settings from "../../services/settings";
import styled from "@emotion/styled";
import QuoteNote from "./quote-note";
import { useExpand } from "./expanded";
import { nip19 } from "nostr-tools";
import { EventPointer, ProfilePointer } from "nostr-tools/lib/nip19";
const BlurredImage = (props: ImageProps) => {
const { isOpen, onToggle } = useDisclosure();
@ -191,6 +192,38 @@ const embeds: EmbedType[] = [
),
isMedia: false,
},
// nostr: links
// nostr:nevent1qqsthg2qlxp9l7egtwa92t8lusm7pjknmjwa75ctrrpcjyulr9754fqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq36amnwvaz7tmwdaehgu3dwp6kytnhv4kxcmmjv3jhytnwv46q2qg5q9
// nostr:nevent1qqsq3wc73lqxd70lg43m5rul57d4mhcanttjat56e30yx5zla48qzlspz9mhxue69uhkummnw3e82efwvdhk6qgdwaehxw309ahx7uewd3hkcq5hsum
{
regexp: /(nostr:)?((npub|note|nprofile|nevent)1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58,})/i,
render: (match, event) => {
try {
const decoded = nip19.decode(match[2]);
console.log(decoded);
switch (decoded.type) {
case "npub":
return <UserLink color="blue.500" pubkey={decoded.data as string} showAt />;
case "nprofile": {
const pointer = decoded.data as ProfilePointer;
return <UserLink color="blue.500" pubkey={pointer.pubkey} showAt />;
}
case "note":
return <QuoteNote noteId={decoded.data as string} />;
case "nevent": {
const pointer = decoded.data as EventPointer;
return <QuoteNote noteId={pointer.id} relay={pointer.relays?.[0]} />;
}
default:
return match[0];
}
} catch (e) {
return match[0];
}
},
isMedia: false,
},
// Nostr Mention Links
{
regexp: /#\[(\d+)\]/,

View File

@ -47,8 +47,12 @@ function finalizeNote(draft: DraftNostrEvent) {
const hex = normalizeToHex(match[1]);
if (!hex) continue;
const mentionType = match[2] === "npub1" ? "p" : "e";
// TODO: find the best relay for this user or note
const index = updatedDraft.tags.push([match[2] === "npub1" ? "p" : "e", hex, "", "mention"]) - 1;
const existingMention = updatedDraft.tags.find((t) => t[0] === mentionType && t[1] === hex);
const index = existingMention
? updatedDraft.tags.indexOf(existingMention)
: updatedDraft.tags.push([mentionType, hex, "", "mention"]) - 1;
// replace the npub1 or note1 with a mention tag #[0]
const c = updatedDraft.content;

View File

@ -1,7 +1,5 @@
import {
Button,
ButtonGroup,
DefaultIcon,
Flex,
IconButton,
Input,
@ -35,6 +33,7 @@ import QrCodeSvg from "./qr-code-svg";
import { CopyIconButton } from "./copy-icon-button";
import { useIsMobile } from "../hooks/use-is-mobile";
import settings from "../services/settings";
import useSubject from "../hooks/use-subject";
type FormValues = {
amount: number;
@ -64,6 +63,7 @@ export default function ZapModal({
const [promptInvoice, setPromptInvoice] = useState<string>();
const { isOpen: showQr, onToggle: toggleQr } = useDisclosure();
const isMobile = useIsMobile();
const zapAmounts = useSubject(settings.zapAmounts);
const {
register,
@ -239,17 +239,18 @@ export default function ZapModal({
<Text>{actionName}</Text>
<UserLink pubkey={pubkey} />
</Flex>
<Flex gap="2" alignItems="center">
<ButtonGroup>
<Button onClick={() => setValue("amount", 10)}>10</Button>
<Button onClick={() => setValue("amount", 100)}>100</Button>
<Button onClick={() => setValue("amount", 500)}>500</Button>
<Button onClick={() => setValue("amount", 1000)}>1K</Button>
</ButtonGroup>
<Flex gap="2" alignItems="center" flexWrap="wrap">
{zapAmounts.map((amount, i) => (
<Button key={amount + i} onClick={() => setValue("amount", amount)} size="sm" variant="outline">
{amount}
</Button>
))}
</Flex>
<Flex gap="2">
<InputGroup maxWidth={32}>
{!isMobile && (
<InputLeftElement pointerEvents="none" color="gray.300" fontSize="1.2em">
<LightningIcon fontSize="1rem" />
<LightningIcon fontSize="1rem" color="yellow.400" />
</InputLeftElement>
)}
<Input
@ -260,14 +261,14 @@ export default function ZapModal({
{...register("amount", { valueAsNumber: true, min: 1, required: true })}
/>
</InputGroup>
{(canZap || lnurlMetadata?.commentAllowed) && (
<Input
placeholder="Comment"
{...register("comment", { maxLength: lnurlMetadata?.commentAllowed ?? 150 })}
autoComplete="off"
/>
)}
</Flex>
{(canZap || lnurlMetadata?.commentAllowed) && (
<Input
placeholder="Comment"
{...register("comment", { maxLength: lnurlMetadata?.commentAllowed ?? 150 })}
autoComplete="off"
/>
)}
<Button leftIcon={<LightningIcon />} type="submit" isLoading={isSubmitting} variant="solid" size="md">
{actionName} {getUserDisplayName(metadata, pubkey)} {readablizeSats(watch("amount"))} sats
</Button>

View File

@ -6,7 +6,7 @@ export function encodeText(prefix: string, text: string) {
}
export function decodeText(encoded: string) {
const decoded = bech32.decode(encoded, 256);
const decoded = bech32.decode(encoded, Infinity);
const text = new TextDecoder().decode(new Uint8Array(bech32.fromWords(decoded.words)));
return {
text,

View File

@ -9,12 +9,13 @@ export enum Bech32Prefix {
Pubkey = "npub",
SecKey = "nsec",
Note = "note",
Profile = "nprofile",
}
export function isBech32Key(bech32String: string) {
try {
const { prefix } = bech32.decode(bech32String.toLowerCase());
if (!["npub", "nsec", "note"].includes(prefix)) return false;
if (!prefix) return false;
if (!isHex(bech32ToHex(bech32String))) return false;
} catch (error) {
return false;

View File

@ -16,6 +16,7 @@ const settings = {
showSignatureVerification: new PersistentSubject(false),
accounts: new PersistentSubject<Account[]>([]),
lightningPayMode: new PersistentSubject<LightningPayMode>(LightningPayMode.Prompt),
zapAmounts: new PersistentSubject<number[]>([50, 200, 500, 1000]),
};
async function loadSettings() {

View File

@ -80,7 +80,7 @@ export default function DiscoverTab() {
return (
<Flex direction="column" gap="2">
{timeline.map((event) => (
<Note key={event.id} event={event} maxHeight={300} />
<Note key={event.id} event={event} maxHeight={600} />
))}
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
</Flex>

View File

@ -44,7 +44,7 @@ export default function FollowingTab() {
<Switch id="show-replies" isChecked={showReplies} onChange={onToggle} />
</FormControl>
{timeline.map((event) => (
<Note key={event.id} event={event} maxHeight={300} />
<Note key={event.id} event={event} maxHeight={600} />
))}
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
</Flex>

View File

@ -57,7 +57,7 @@ export default function GlobalTab() {
</FormControl>
</Flex>
{timeline.map((event) => (
<Note key={event.id} event={event} maxHeight={300} />
<Note key={event.id} event={event} maxHeight={600} />
))}
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
</Flex>

View File

@ -6,6 +6,7 @@ import {
FormErrorMessage,
FormLabel,
Input,
Link,
Textarea,
useToast,
} from "@chakra-ui/react";
@ -13,6 +14,7 @@ import moment from "moment";
import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { nostrPostAction } from "../../classes/nostr-post-action";
import { ExternalLinkIcon } from "../../components/icons";
import { isLNURL } from "../../helpers/lnurl";
import { Kind0ParsedContent } from "../../helpers/user-metadata";
import { useReadRelayUrls, useWriteRelayUrls } from "../../hooks/use-client-relays";
@ -169,6 +171,9 @@ const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => {
<FormErrorMessage>{errors.lightningAddress?.message}</FormErrorMessage>
</FormControl>
<Flex alignSelf="flex-end" gap="2">
<Button as={Link} isExternal href="https://metadata.nostr.com/" rightIcon={<ExternalLinkIcon />}>
Download Backup
</Button>
<Button onClick={() => reset()}>Reset</Button>
<Button colorScheme="brand" isLoading={isSubmitting} type="submit">
Update

View File

@ -0,0 +1,50 @@
import {
Button,
AccordionItem,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
ButtonGroup,
} from "@chakra-ui/react";
import { useState } from "react";
import { clearCacheData, deleteDatabase } from "../../services/db";
export default function DatabaseSettings() {
const [clearing, setClearing] = useState(false);
const handleClearData = async () => {
setClearing(true);
await clearCacheData();
setClearing(false);
};
const [deleting, setDeleting] = useState(false);
const handleDeleteDatabase = async () => {
setDeleting(true);
await deleteDatabase();
setDeleting(false);
};
return (
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Database
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<ButtonGroup>
<Button onClick={handleClearData} isLoading={clearing} isDisabled={clearing}>
Clear cache data
</Button>
<Button colorScheme="red" onClick={handleDeleteDatabase} isLoading={deleting} isDisabled={deleting}>
Delete database
</Button>
</ButtonGroup>
</AccordionPanel>
</AccordionItem>
);
}

View File

@ -0,0 +1,83 @@
import {
Flex,
FormControl,
FormLabel,
Switch,
useColorMode,
AccordionItem,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
FormHelperText,
} from "@chakra-ui/react";
import settings from "../../services/settings";
import useSubject from "../../hooks/use-subject";
export default function DisplaySettings() {
const blurImages = useSubject(settings.blurImages);
const { colorMode, setColorMode } = useColorMode();
return (
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Display
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="use-dark-theme" mb="0">
Use dark theme
</FormLabel>
<Switch
id="use-dark-theme"
isChecked={colorMode === "dark"}
onChange={(v) => setColorMode(v.target.checked ? "dark" : "light")}
/>
</Flex>
<FormHelperText>
<span>Enabled: hacker mode</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="blur-images" mb="0">
Blur images from strangers
</FormLabel>
<Switch
id="blur-images"
isChecked={blurImages}
onChange={(v) => settings.blurImages.next(v.target.checked)}
/>
</Flex>
<FormHelperText>
<span>Enabled: blur images for people you aren't following</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="show-ads" mb="0">
Show Ads
</FormLabel>
<Switch
id="show-ads"
isChecked={false}
onChange={(v) => alert("Sorry, that feature will never be finished.")}
/>
</Flex>
<FormHelperText>
<span>Enabled: shows ads so I can steal your data</span>
</FormHelperText>
</FormControl>
</Flex>
</AccordionPanel>
</AccordionItem>
);
}

View File

@ -1,242 +1,22 @@
import {
Button,
Flex,
FormControl,
FormLabel,
Switch,
useColorMode,
AccordionItem,
Accordion,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
ButtonGroup,
FormHelperText,
Select,
Link,
} from "@chakra-ui/react";
import { useState } from "react";
import settings, { LightningPayMode } from "../../services/settings";
import { clearCacheData, deleteDatabase } from "../../services/db";
import { Button, Flex, Accordion, Link } from "@chakra-ui/react";
import accountService from "../../services/account";
import useSubject from "../../hooks/use-subject";
import { GithubIcon, LightningIcon, LogoutIcon } from "../../components/icons";
import { GithubIcon, LogoutIcon } from "../../components/icons";
import LightningSettings from "./lightning-settings";
import DatabaseSettings from "./database-settings";
import DisplaySettings from "./display-settings";
import PerformanceSettings from "./performance-settings";
export default function SettingsView() {
const blurImages = useSubject(settings.blurImages);
const autoShowMedia = useSubject(settings.autoShowMedia);
const proxyUserMedia = useSubject(settings.proxyUserMedia);
const showReactions = useSubject(settings.showReactions);
const showSignatureVerification = useSubject(settings.showSignatureVerification);
const lightningPayMode = useSubject(settings.lightningPayMode);
const { colorMode, setColorMode } = useColorMode();
const [clearing, setClearing] = useState(false);
const handleClearData = async () => {
setClearing(true);
await clearCacheData();
setClearing(false);
};
const [deleting, setDeleting] = useState(false);
const handleDeleteDatabase = async () => {
setDeleting(true);
await deleteDatabase();
setDeleting(false);
};
return (
<Flex direction="column" pt="2" pb="2" overflow="auto">
<Accordion defaultIndex={[0]} allowMultiple>
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Display
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="use-dark-theme" mb="0">
Use dark theme
</FormLabel>
<Switch
id="use-dark-theme"
isChecked={colorMode === "dark"}
onChange={(v) => setColorMode(v.target.checked ? "dark" : "light")}
/>
</Flex>
<FormHelperText>
<span>Enabled: hacker mode</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="blur-images" mb="0">
Blur images from strangers
</FormLabel>
<Switch
id="blur-images"
isChecked={blurImages}
onChange={(v) => settings.blurImages.next(v.target.checked)}
/>
</Flex>
<FormHelperText>
<span>Enabled: blur images for people you aren't following</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="show-ads" mb="0">
Show Ads
</FormLabel>
<Switch
id="show-ads"
isChecked={false}
onChange={(v) => alert("Sorry, that feature will never be finished.")}
/>
</Flex>
<FormHelperText>
<span>Enabled: shows ads so I can steal your data</span>
</FormHelperText>
</FormControl>
</Flex>
</AccordionPanel>
</AccordionItem>
<DisplaySettings />
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Performance
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="proxy-user-media" mb="0">
Proxy user media
</FormLabel>
<Switch
id="proxy-user-media"
isChecked={proxyUserMedia}
onChange={(v) => settings.proxyUserMedia.next(v.target.checked)}
/>
</Flex>
<FormHelperText>
<span>Enabled: Use media.nostr.band to get smaller profile pictures (saves ~50Mb of data)</span>
<br />
<span>Side Effect: Some user pictures may not load or may be outdated</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="auto-show-embeds" mb="0">
Automatically show media
</FormLabel>
<Switch
id="auto-show-embeds"
isChecked={autoShowMedia}
onChange={(v) => settings.autoShowMedia.next(v.target.checked)}
/>
</Flex>
<FormHelperText>Disabled: Images and videos will show expandable buttons</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="show-reactions" mb="0">
Show reactions
</FormLabel>
<Switch
id="show-reactions"
isChecked={showReactions}
onChange={(v) => settings.showReactions.next(v.target.checked)}
/>
</Flex>
<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>
</Flex>
</AccordionPanel>
</AccordionItem>
<PerformanceSettings />
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Lightning <LightningIcon color="yellow.400" />
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<FormLabel htmlFor="lightning-payment-mode" mb="0">
Payment mode
</FormLabel>
<Select
id="lightning-payment-mode"
value={lightningPayMode}
onChange={(e) => settings.lightningPayMode.next(e.target.value as LightningPayMode)}
>
<option value="prompt">Prompt</option>
<option value="webln">WebLN</option>
<option value="external">External</option>
</Select>
<FormHelperText>
<span>Prompt: Ask every time</span>
<br />
<span>WebLN: Use browser extension</span>
<br />
<span>External: Open an external app using "lightning:" link</span>
</FormHelperText>
</FormControl>
</Flex>
</AccordionPanel>
</AccordionItem>
<LightningSettings />
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Database
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<ButtonGroup>
<Button onClick={handleClearData} isLoading={clearing} isDisabled={clearing}>
Clear cache data
</Button>
<Button colorScheme="red" onClick={handleDeleteDatabase} isLoading={deleting} isDisabled={deleting}>
Delete database
</Button>
</ButtonGroup>
</AccordionPanel>
</AccordionItem>
<DatabaseSettings />
</Accordion>
<Flex gap="2" padding="4" alignItems="center" justifyContent="space-between">
<Button leftIcon={<LogoutIcon />} onClick={() => accountService.logout()}>

View File

@ -0,0 +1,86 @@
import {
Flex,
FormControl,
FormLabel,
AccordionItem,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
FormHelperText,
Input,
Select,
} from "@chakra-ui/react";
import { useState } from "react";
import settings, { LightningPayMode } from "../../services/settings";
import useSubject from "../../hooks/use-subject";
import { LightningIcon } from "../../components/icons";
export default function LightningSettings() {
const lightningPayMode = useSubject(settings.lightningPayMode);
const zapAmounts = useSubject(settings.zapAmounts);
const [zapInput, setZapInput] = useState(zapAmounts.join(","));
return (
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Lightning <LightningIcon color="yellow.400" />
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<FormLabel htmlFor="lightning-payment-mode" mb="0">
Payment mode
</FormLabel>
<Select
id="lightning-payment-mode"
value={lightningPayMode}
onChange={(e) => settings.lightningPayMode.next(e.target.value as LightningPayMode)}
>
<option value="prompt">Prompt</option>
<option value="webln">WebLN</option>
<option value="external">External</option>
</Select>
<FormHelperText>
<span>Prompt: Ask every time</span>
<br />
<span>WebLN: Use browser extension</span>
<br />
<span>External: Open an external app using "lightning:" link</span>
</FormHelperText>
</FormControl>
<FormControl>
<FormLabel htmlFor="zap-amounts" mb="0">
Zap Amounts
</FormLabel>
<Input
id="zap-amounts"
value={zapInput}
onChange={(e) => setZapInput(e.target.value)}
onBlur={() => {
const amounts = zapInput
.split(",")
.map((v) => parseInt(v))
.filter(Boolean)
.sort((a, b) => a - b);
settings.zapAmounts.next(amounts);
setZapInput(amounts.join(","));
}}
/>
<FormHelperText>
<span>Comma separated list of custom zap amounts</span>
</FormHelperText>
</FormControl>
</Flex>
</AccordionPanel>
</AccordionItem>
);
}

View File

@ -0,0 +1,94 @@
import {
Flex,
FormControl,
FormLabel,
Switch,
AccordionItem,
AccordionPanel,
AccordionButton,
Box,
AccordionIcon,
FormHelperText,
} from "@chakra-ui/react";
import settings from "../../services/settings";
import useSubject from "../../hooks/use-subject";
export default function PerformanceSettings() {
const autoShowMedia = useSubject(settings.autoShowMedia);
const proxyUserMedia = useSubject(settings.proxyUserMedia);
const showReactions = useSubject(settings.showReactions);
const showSignatureVerification = useSubject(settings.showSignatureVerification);
return (
<AccordionItem>
<h2>
<AccordionButton>
<Box as="span" flex="1" textAlign="left">
Performance
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="proxy-user-media" mb="0">
Proxy user media
</FormLabel>
<Switch
id="proxy-user-media"
isChecked={proxyUserMedia}
onChange={(v) => settings.proxyUserMedia.next(v.target.checked)}
/>
</Flex>
<FormHelperText>
<span>Enabled: Use media.nostr.band to get smaller profile pictures (saves ~50Mb of data)</span>
<br />
<span>Side Effect: Some user pictures may not load or may be outdated</span>
</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="auto-show-embeds" mb="0">
Automatically show media
</FormLabel>
<Switch
id="auto-show-embeds"
isChecked={autoShowMedia}
onChange={(v) => settings.autoShowMedia.next(v.target.checked)}
/>
</Flex>
<FormHelperText>Disabled: Images and videos will show expandable buttons</FormHelperText>
</FormControl>
<FormControl>
<Flex alignItems="center">
<FormLabel htmlFor="show-reactions" mb="0">
Show reactions
</FormLabel>
<Switch
id="show-reactions"
isChecked={showReactions}
onChange={(v) => settings.showReactions.next(v.target.checked)}
/>
</Flex>
<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>
</Flex>
</AccordionPanel>
</AccordionItem>
);
}

View File

@ -1,5 +1,8 @@
import {
Avatar,
Code,
Flex,
Heading,
MenuItem,
Modal,
ModalBody,
@ -17,6 +20,7 @@ import { useUserMetadata } from "../../../hooks/use-user-metadata";
import { getUserDisplayName } from "../../../helpers/user-metadata";
import { useUserRelays } from "../../../hooks/use-user-relays";
import { RelayMode } from "../../../classes/relay";
import { CopyIconButton } from "../../../components/copy-icon-button";
export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<MenuIconButtonProps, "children">) => {
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
@ -42,6 +46,9 @@ export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<
<MenuItem icon={<SpyIcon fontSize="1.5em" />} onClick={() => loginAsUser()}>
Login as {getUserDisplayName(metadata, pubkey)}
</MenuItem>
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
View Raw
</MenuItem>
<MenuItem
as="a"
icon={<Avatar src={IMAGE_ICONS.nostrGuruIcon} size="xs" />}
@ -50,14 +57,6 @@ export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<
>
Open in Nostr.guru
</MenuItem>
<MenuItem
as="a"
icon={<Avatar src={IMAGE_ICONS.brbIcon} size="xs" />}
href={`https://brb.io/u/${npub}`}
target="_blank"
>
Open in BRB
</MenuItem>
<MenuItem
as="a"
icon={<Avatar src={IMAGE_ICONS.snortSocialIcon} size="xs" />}
@ -66,9 +65,6 @@ export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<
>
Open in snort.social
</MenuItem>
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
View Raw
</MenuItem>
</MenuIconButton>
{infoModal.isOpen && (
<Modal isOpen={infoModal.isOpen} onClose={infoModal.onClose} size="6xl">
@ -76,7 +72,38 @@ export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<
<ModalContent>
<ModalCloseButton />
<ModalBody overflow="auto" fontSize="sm" padding="2">
<pre>{JSON.stringify(metadata, null, 2)}</pre>
<Flex gap="2" direction="column">
<Heading size="sm" mt="2">
Hex pubkey
</Heading>
<Flex gap="2">
<Code fontSize="md" wordBreak="break-all">
{pubkey}
</Code>
<CopyIconButton text={pubkey} size="xs" aria-label="copy hex" />
</Flex>
{npub && (
<>
<Heading size="sm" mt="2">
Encoded pubkey (NIP-19)
</Heading>
<Flex gap="2">
<Code fontSize="md" wordBreak="break-all">
{npub}
</Code>
<CopyIconButton text={npub} size="xs" aria-label="copy npub" />
</Flex>
</>
)}
<Heading size="sm" mt="2">
Metadata (kind 0)
</Heading>
<Code whiteSpace="pre" overflowX="auto">
{JSON.stringify(metadata, null, 2)}
</Code>
</Flex>
</ModalBody>
</ModalContent>
</Modal>

View File

@ -76,7 +76,7 @@ const UserNotesTab = () => {
</Popover>
</FormControl>
{timeline.map((event) => (
<Note key={event.id} event={event} maxHeight={300} />
<Note key={event.id} event={event} maxHeight={1200} />
))}
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
</Flex>

View File

@ -17,7 +17,9 @@ const UserRelaysTab = () => {
<Flex direction="column" gap="2">
{ranked.map((relayConfig) => (
<Box key={relayConfig.url} display="flex" gap="2" alignItems="center" pr="2" pl="2">
<Text flex={1}>{relayConfig.url}</Text>
<Text flex={1} isTruncated>
{relayConfig.url}
</Text>
<RelayScoreBreakdown relay={relayConfig.url} />
{relayConfig.mode & RelayMode.WRITE ? <Badge colorScheme="green">Write</Badge> : null}
{relayConfig.mode & RelayMode.READ ? <Badge>Read</Badge> : null}

2878
yarn.lock

File diff suppressed because it is too large Load Diff