feat: improve user npub QR code card
This commit is contained in:
21
package-lock.json
generated
21
package-lock.json
generated
@@ -51,7 +51,7 @@
|
|||||||
"nostr-tools": "^2.13.0",
|
"nostr-tools": "^2.13.0",
|
||||||
"nstart-modal": "^2.0.0",
|
"nstart-modal": "^2.0.0",
|
||||||
"path-to-regexp": "^8.2.0",
|
"path-to-regexp": "^8.2.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qr-code-styling": "^1.9.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-i18next": "^15.2.0",
|
"react-i18next": "^15.2.0",
|
||||||
@@ -8300,19 +8300,22 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/qrcode-generator": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz",
|
||||||
"integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw=="
|
"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": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
"nostr-tools": "^2.13.0",
|
"nostr-tools": "^2.13.0",
|
||||||
"nstart-modal": "^2.0.0",
|
"nstart-modal": "^2.0.0",
|
||||||
"path-to-regexp": "^8.2.0",
|
"path-to-regexp": "^8.2.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qr-code-styling": "^1.9.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-i18next": "^15.2.0",
|
"react-i18next": "^15.2.0",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
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 { 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 { generateSecretKey, getPublicKey } from 'nostr-tools'
|
||||||
import { QRCodeSVG } from 'qrcode.react'
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
import { useState, useEffect, useRef, useLayoutEffect } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import QrCode from '../QrCode'
|
||||||
|
|
||||||
export default function NostrConnectLogin({
|
export default function NostrConnectLogin({
|
||||||
back,
|
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">
|
<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">
|
<a href={loginDetails.connectionString} aria-label="Open with Nostr signer app">
|
||||||
<QRCodeSVG
|
<QrCode size={qrCodeSize} value={loginDetails.connectionString} />
|
||||||
size={qrCodeSize}
|
|
||||||
value={loginDetails.connectionString}
|
|
||||||
bgColor="hsl(var(--background))"
|
|
||||||
fgColor="hsl(var(--foreground))"
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
{nostrConnectionErrMsg && (
|
{nostrConnectionErrMsg && (
|
||||||
<div className="text-xs text-destructive text-center pt-1">{nostrConnectionErrMsg}</div>
|
<div className="text-xs text-destructive text-center pt-1">{nostrConnectionErrMsg}</div>
|
||||||
|
|||||||
57
src/components/NpubQrCode/index.tsx
Normal file
57
src/components/NpubQrCode/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
70
src/components/QrCode/index.tsx
Normal file
70
src/components/QrCode/index.tsx
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ import { useMemo } from 'react'
|
|||||||
const UserAvatarSizeCnMap = {
|
const UserAvatarSizeCnMap = {
|
||||||
large: 'w-24 h-24',
|
large: 'w-24 h-24',
|
||||||
big: 'w-16 h-16',
|
big: 'w-16 h-16',
|
||||||
|
semiBig: 'w-12 h-12',
|
||||||
normal: 'w-10 h-10',
|
normal: 'w-10 h-10',
|
||||||
medium: 'w-8 h-8',
|
medium: 'w-8 h-8',
|
||||||
small: 'w-7 h-7',
|
small: 'w-7 h-7',
|
||||||
@@ -26,7 +27,7 @@ export default function UserAvatar({
|
|||||||
}: {
|
}: {
|
||||||
userId: string
|
userId: string
|
||||||
className?: 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 { profile } = useFetchProfile(userId)
|
||||||
const defaultAvatar = useMemo(
|
const defaultAvatar = useMemo(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import AccountManager from '@/components/AccountManager'
|
|||||||
import LoginDialog from '@/components/LoginDialog'
|
import LoginDialog from '@/components/LoginDialog'
|
||||||
import LogoutDialog from '@/components/LogoutDialog'
|
import LogoutDialog from '@/components/LogoutDialog'
|
||||||
import PubkeyCopy from '@/components/PubkeyCopy'
|
import PubkeyCopy from '@/components/PubkeyCopy'
|
||||||
import QrCodePopover from '@/components/QrCodePopover'
|
import NpubQrCode from '@/components/NpubQrCode'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { SimpleUserAvatar } from '@/components/UserAvatar'
|
import { SimpleUserAvatar } from '@/components/UserAvatar'
|
||||||
@@ -53,7 +53,7 @@ const MePage = forwardRef((_, ref) => {
|
|||||||
/>
|
/>
|
||||||
<div className="flex gap-1 mt-1">
|
<div className="flex gap-1 mt-1">
|
||||||
<PubkeyCopy pubkey={pubkey} />
|
<PubkeyCopy pubkey={pubkey} />
|
||||||
<QrCodePopover pubkey={pubkey} />
|
<NpubQrCode pubkey={pubkey} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import ProfileBanner from '@/components/ProfileBanner'
|
|||||||
import ProfileOptions from '@/components/ProfileOptions'
|
import ProfileOptions from '@/components/ProfileOptions'
|
||||||
import ProfileZapButton from '@/components/ProfileZapButton'
|
import ProfileZapButton from '@/components/ProfileZapButton'
|
||||||
import PubkeyCopy from '@/components/PubkeyCopy'
|
import PubkeyCopy from '@/components/PubkeyCopy'
|
||||||
import QrCodePopover from '@/components/QrCodePopover'
|
import NpubQrCode from '@/components/NpubQrCode'
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
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">
|
<div className="flex gap-1 mt-1">
|
||||||
<PubkeyCopy pubkey={pubkey} />
|
<PubkeyCopy pubkey={pubkey} />
|
||||||
<QrCodePopover pubkey={pubkey} />
|
<NpubQrCode pubkey={pubkey} />
|
||||||
</div>
|
</div>
|
||||||
<Collapsible>
|
<Collapsible>
|
||||||
<ProfileAbout
|
<ProfileAbout
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type ThemeProviderProps = {
|
|||||||
|
|
||||||
type ThemeProviderState = {
|
type ThemeProviderState = {
|
||||||
themeSetting: TThemeSetting
|
themeSetting: TThemeSetting
|
||||||
|
theme: TTheme
|
||||||
setThemeSetting: (themeSetting: TThemeSetting) => Promise<void>
|
setThemeSetting: (themeSetting: TThemeSetting) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +63,12 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|||||||
updateTheme()
|
updateTheme()
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
const value = {
|
return (
|
||||||
|
<ThemeProviderContext.Provider
|
||||||
|
{...props}
|
||||||
|
value={{
|
||||||
themeSetting: themeSetting,
|
themeSetting: themeSetting,
|
||||||
|
theme: theme,
|
||||||
setThemeSetting: async (themeSetting: TThemeSetting) => {
|
setThemeSetting: async (themeSetting: TThemeSetting) => {
|
||||||
storage.setThemeSetting(themeSetting)
|
storage.setThemeSetting(themeSetting)
|
||||||
setThemeSetting(themeSetting)
|
setThemeSetting(themeSetting)
|
||||||
@@ -73,10 +78,8 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|||||||
}
|
}
|
||||||
setTheme(themeSetting)
|
setTheme(themeSetting)
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
|
>
|
||||||
return (
|
|
||||||
<ThemeProviderContext.Provider {...props} value={value}>
|
|
||||||
{children}
|
{children}
|
||||||
</ThemeProviderContext.Provider>
|
</ThemeProviderContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user