feat: improve user npub QR code card

This commit is contained in:
codytseng
2025-07-04 21:38:32 +08:00
parent b470ef4857
commit e53d74edd1
10 changed files with 169 additions and 92 deletions

21
package-lock.json generated
View File

@@ -51,7 +51,7 @@
"nostr-tools": "^2.13.0",
"nstart-modal": "^2.0.0",
"path-to-regexp": "^8.2.0",
"qrcode.react": "^4.2.0",
"qr-code-styling": "^1.9.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.2.0",
@@ -8300,19 +8300,22 @@
"node": ">=6"
}
},
"node_modules/qr-code-styling": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz",
"integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==",
"dependencies": {
"qrcode-generator": "^1.4.4"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/qrcode-generator": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz",
"integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw=="
},
"node_modules/qrcode.react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",

View File

@@ -61,7 +61,7 @@
"nostr-tools": "^2.13.0",
"nstart-modal": "^2.0.0",
"path-to-regexp": "^8.2.0",
"qrcode.react": "^4.2.0",
"qr-code-styling": "^1.9.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.2.0",

View File

@@ -1,13 +1,13 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { useNostr } from '@/providers/NostrProvider'
import { Loader, Copy, Check } from 'lucide-react'
import { createNostrConnectURI, NostrConnectParams } from '@/providers/NostrProvider/nip46'
import { DEFAULT_NOSTRCONNECT_RELAY } from '@/constants'
import { useNostr } from '@/providers/NostrProvider'
import { createNostrConnectURI, NostrConnectParams } from '@/providers/NostrProvider/nip46'
import { Check, Copy, Loader } from 'lucide-react'
import { generateSecretKey, getPublicKey } from 'nostr-tools'
import { QRCodeSVG } from 'qrcode.react'
import { useState, useEffect, useRef, useLayoutEffect } from 'react'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import QrCode from '../QrCode'
export default function NostrConnectLogin({
back,
@@ -107,12 +107,7 @@ export default function NostrConnectLogin({
<>
<div ref={qrContainerRef} className="flex flex-col items-center w-full space-y-3 mb-3">
<a href={loginDetails.connectionString} aria-label="Open with Nostr signer app">
<QRCodeSVG
size={qrCodeSize}
value={loginDetails.connectionString}
bgColor="hsl(var(--background))"
fgColor="hsl(var(--foreground))"
/>
<QrCode size={qrCodeSize} value={loginDetails.connectionString} />
</a>
{nostrConnectionErrMsg && (
<div className="text-xs text-destructive text-center pt-1">{nostrConnectionErrMsg}</div>

View File

@@ -0,0 +1,57 @@
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { QrCodeIcon } from 'lucide-react'
import { nip19 } from 'nostr-tools'
import { useMemo } from 'react'
import Nip05 from '../Nip05'
import PubkeyCopy from '../PubkeyCopy'
import QrCode from '../QrCode'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
export default function NpubQrCode({ pubkey }: { pubkey: string }) {
const { isSmallScreen } = useScreenSize()
const npub = useMemo(() => (pubkey ? nip19.npubEncode(pubkey) : ''), [pubkey])
if (!npub) return null
const trigger = (
<div className="bg-muted rounded-full h-5 w-5 flex flex-col items-center justify-center text-muted-foreground hover:text-foreground">
<QrCodeIcon size={14} />
</div>
)
const content = (
<div className="w-full flex flex-col items-center gap-4 p-8">
<div className="flex items-center w-full gap-2 pointer-events-none px-1">
<UserAvatar size="semiBig" userId={pubkey} />
<div className="flex-1 w-0">
<Username userId={pubkey} className="text-xl font-semibold truncate" />
<Nip05 pubkey={pubkey} />
</div>
</div>
<QrCode size={512} value={`nostr:${npub}`} />
<div className="flex flex-col items-center">
<PubkeyCopy pubkey={pubkey} />
</div>
</div>
)
if (isSmallScreen) {
return (
<Drawer>
<DrawerTrigger>{trigger}</DrawerTrigger>
<DrawerContent>{content}</DrawerContent>
</Drawer>
)
}
return (
<Dialog>
<DialogTrigger>{trigger}</DialogTrigger>
<DialogContent className="w-80 p-0 m-0" onOpenAutoFocus={(e) => e.preventDefault()}>
{content}
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,70 @@
import { useTheme } from '@/providers/ThemeProvider'
import QRCodeStyling from 'qr-code-styling'
import { useEffect, useRef, useState } from 'react'
export default function QrCode({ value, size = 180 }: { value: string; size?: number }) {
const { theme } = useTheme()
const ref = useRef<HTMLDivElement>(null)
const [foregroundColor, setForegroundColor] = useState<string | undefined>()
const [backgroundColor, setBackgroundColor] = useState<string | undefined>()
useEffect(() => {
setTimeout(() => {
const fgColor = `hsl(${getColor('foreground')})`
const bgColor = `hsl(${getColor('background')})`
setForegroundColor(fgColor)
setBackgroundColor(bgColor)
}, 0)
}, [theme])
useEffect(() => {
setTimeout(() => {
const pixelRatio = window.devicePixelRatio || 2
const qrCode = new QRCodeStyling({
width: size * pixelRatio,
height: size * pixelRatio,
data: value,
dotsOptions: {
type: 'dots',
color: foregroundColor
},
cornersDotOptions: {
type: 'extra-rounded',
color: foregroundColor
},
cornersSquareOptions: {
type: 'extra-rounded',
color: foregroundColor
},
backgroundOptions: {
color: backgroundColor
}
})
if (ref.current) {
ref.current.innerHTML = ''
qrCode.append(ref.current)
const canvas = ref.current.querySelector('canvas')
if (canvas) {
canvas.style.width = `${size}px`
canvas.style.height = `${size}px`
canvas.style.maxWidth = '100%'
canvas.style.height = 'auto'
}
}
}, 0)
return () => {
if (ref.current) ref.current.innerHTML = ''
}
}, [value, size, foregroundColor, backgroundColor])
return <div ref={ref} />
}
function getColor(name: string) {
if (typeof window !== 'undefined') {
return getComputedStyle(document.documentElement).getPropertyValue(`--${name}`).trim()
}
}

View File

@@ -1,52 +0,0 @@
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { QrCode } from 'lucide-react'
import { nip19 } from 'nostr-tools'
import { QRCodeSVG } from 'qrcode.react'
import { useMemo } from 'react'
export default function QrCodePopover({ pubkey }: { pubkey: string }) {
const { isSmallScreen } = useScreenSize()
const npub = useMemo(() => (pubkey ? nip19.npubEncode(pubkey) : ''), [pubkey])
if (!npub) return null
if (isSmallScreen) {
return (
<Drawer>
<DrawerTrigger>
<div className="bg-muted rounded-full h-5 w-5 flex flex-col items-center justify-center text-muted-foreground hover:text-foreground">
<QrCode size={14} />
</div>
</DrawerTrigger>
<DrawerContent className="h-1/2">
<div className="flex justify-center items-center h-full">
<QRCodeSVG
size={300}
value={`nostr:${npub}`}
bgColor="hsl(var(--background))"
fgColor="hsl(var(--foreground))"
/>
</div>
</DrawerContent>
</Drawer>
)
}
return (
<Popover>
<PopoverTrigger>
<div className="bg-muted rounded-full h-5 w-5 flex flex-col items-center justify-center text-muted-foreground hover:text-foreground">
<QrCode size={14} />
</div>
</PopoverTrigger>
<PopoverContent className="w-fit h-fit">
<QRCodeSVG
value={`nostr:${npub}`}
bgColor="hsl(var(--background))"
fgColor="hsl(var(--foreground))"
/>
</PopoverContent>
</Popover>
)
}

View File

@@ -12,6 +12,7 @@ import { useMemo } from 'react'
const UserAvatarSizeCnMap = {
large: 'w-24 h-24',
big: 'w-16 h-16',
semiBig: 'w-12 h-12',
normal: 'w-10 h-10',
medium: 'w-8 h-8',
small: 'w-7 h-7',
@@ -26,7 +27,7 @@ export default function UserAvatar({
}: {
userId: string
className?: string
size?: 'large' | 'big' | 'normal' | 'medium' | 'small' | 'xSmall' | 'tiny'
size?: 'large' | 'big' | 'semiBig' | 'normal' | 'medium' | 'small' | 'xSmall' | 'tiny'
}) {
const { profile } = useFetchProfile(userId)
const defaultAvatar = useMemo(

View File

@@ -2,7 +2,7 @@ import AccountManager from '@/components/AccountManager'
import LoginDialog from '@/components/LoginDialog'
import LogoutDialog from '@/components/LogoutDialog'
import PubkeyCopy from '@/components/PubkeyCopy'
import QrCodePopover from '@/components/QrCodePopover'
import NpubQrCode from '@/components/NpubQrCode'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { SimpleUserAvatar } from '@/components/UserAvatar'
@@ -53,7 +53,7 @@ const MePage = forwardRef((_, ref) => {
/>
<div className="flex gap-1 mt-1">
<PubkeyCopy pubkey={pubkey} />
<QrCodePopover pubkey={pubkey} />
<NpubQrCode pubkey={pubkey} />
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@ import ProfileBanner from '@/components/ProfileBanner'
import ProfileOptions from '@/components/ProfileOptions'
import ProfileZapButton from '@/components/ProfileZapButton'
import PubkeyCopy from '@/components/PubkeyCopy'
import QrCodePopover from '@/components/QrCodePopover'
import NpubQrCode from '@/components/NpubQrCode'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
@@ -157,7 +157,7 @@ const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number },
)}
<div className="flex gap-1 mt-1">
<PubkeyCopy pubkey={pubkey} />
<QrCodePopover pubkey={pubkey} />
<NpubQrCode pubkey={pubkey} />
</div>
<Collapsible>
<ProfileAbout

View File

@@ -9,6 +9,7 @@ type ThemeProviderProps = {
type ThemeProviderState = {
themeSetting: TThemeSetting
theme: TTheme
setThemeSetting: (themeSetting: TThemeSetting) => Promise<void>
}
@@ -62,8 +63,12 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
updateTheme()
}, [theme])
const value = {
return (
<ThemeProviderContext.Provider
{...props}
value={{
themeSetting: themeSetting,
theme: theme,
setThemeSetting: async (themeSetting: TThemeSetting) => {
storage.setThemeSetting(themeSetting)
setThemeSetting(themeSetting)
@@ -73,10 +78,8 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
}
setTheme(themeSetting)
}
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
}}
>
{children}
</ThemeProviderContext.Provider>
)