Compare commits
18 Commits
feature/re
...
refactor/h
Author | SHA1 | Date | |
---|---|---|---|
41f390cf59 | |||
8c0b5f379e | |||
905dc7ac1b | |||
ca25712d20 | |||
6e334c5078 | |||
051eaf001f | |||
4a2362f6b9 | |||
da6f68e00a | |||
c08e26629a | |||
7aba51b103 | |||
c61869b2a2 | |||
47dc8e20fe | |||
f55ba7e6f2 | |||
05bd08e86d | |||
4fa5c57a69 | |||
3de4a508be | |||
ecf27d8d23 | |||
7379d75002 |
@ -3,37 +3,37 @@ const path = require('path')
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
|
||||
|
||||
module.exports = function override(config) {
|
||||
const fallback = config.resolve.fallback || {}
|
||||
Object.assign(fallback, {
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
assert: require.resolve('assert'),
|
||||
http: require.resolve('stream-http'),
|
||||
https: require.resolve('https-browserify'),
|
||||
os: require.resolve('os-browserify'),
|
||||
url: require.resolve('url'),
|
||||
})
|
||||
config.resolve.fallback = fallback
|
||||
config.plugins = (config.plugins || []).concat([
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
])
|
||||
config.module.rules.unshift({
|
||||
test: /\.m?js$/,
|
||||
resolve: {
|
||||
fullySpecified: false, // disable the behavior
|
||||
},
|
||||
})
|
||||
// turns off the plugin that forbids importing from node_modules for the above-mentioned stuff
|
||||
config.resolve.plugins = config.resolve.plugins.filter((plugin) => {
|
||||
return !(plugin instanceof ModuleScopePlugin)
|
||||
})
|
||||
const fallback = config.resolve.fallback || {}
|
||||
Object.assign(fallback, {
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
assert: require.resolve('assert'),
|
||||
http: require.resolve('stream-http'),
|
||||
https: require.resolve('https-browserify'),
|
||||
os: require.resolve('os-browserify'),
|
||||
url: require.resolve('url'),
|
||||
})
|
||||
config.resolve.fallback = fallback
|
||||
config.plugins = (config.plugins || []).concat([
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
])
|
||||
config.module.rules.unshift({
|
||||
test: /\.m?js$/,
|
||||
resolve: {
|
||||
fullySpecified: false, // disable the behavior
|
||||
},
|
||||
})
|
||||
// turns off the plugin that forbids importing from node_modules for the above-mentioned stuff
|
||||
config.resolve.plugins = config.resolve.plugins.filter((plugin) => {
|
||||
return !(plugin instanceof ModuleScopePlugin)
|
||||
})
|
||||
|
||||
config.resolve.alias = {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
}
|
||||
config.resolve.alias = {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
}
|
||||
|
||||
return config
|
||||
return config
|
||||
}
|
||||
|
23
package-lock.json
generated
23
package-lock.json
generated
@ -41,6 +41,7 @@
|
||||
"redux-persist": "^6.0.0",
|
||||
"typescript": "^5.3.2",
|
||||
"use-debounce": "^10.0.0",
|
||||
"usehooks-ts": "^2.14.0",
|
||||
"web-vitals": "^2.1.4",
|
||||
"workbox-background-sync": "^6.6.0",
|
||||
"workbox-broadcast-update": "^6.6.0",
|
||||
@ -17214,6 +17215,20 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/usehooks-ts": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.14.0.tgz",
|
||||
"integrity": "sha512-jnhrjTRJoJS7cFxz63tRYc5mzTKf/h+Ii8P0PDHymT9qDe4ZA2/gzDRmDR4WGausg5X8wMIdghwi3BBCN9JKow==",
|
||||
"dependencies": {
|
||||
"lodash.debounce": "^4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.15.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/utf-8-validate": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
||||
@ -30591,6 +30606,14 @@
|
||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||
"requires": {}
|
||||
},
|
||||
"usehooks-ts": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.14.0.tgz",
|
||||
"integrity": "sha512-jnhrjTRJoJS7cFxz63tRYc5mzTKf/h+Ii8P0PDHymT9qDe4ZA2/gzDRmDR4WGausg5X8wMIdghwi3BBCN9JKow==",
|
||||
"requires": {
|
||||
"lodash.debounce": "^4.0.8"
|
||||
}
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
||||
|
@ -36,6 +36,7 @@
|
||||
"redux-persist": "^6.0.0",
|
||||
"typescript": "^5.3.2",
|
||||
"use-debounce": "^10.0.0",
|
||||
"usehooks-ts": "^2.14.0",
|
||||
"web-vitals": "^2.1.4",
|
||||
"workbox-background-sync": "^6.6.0",
|
||||
"workbox-broadcast-update": "^6.6.0",
|
||||
|
@ -10,12 +10,6 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
|
23
src/App.tsx
23
src/App.tsx
@ -9,12 +9,15 @@ import { ModalInitial } from './components/Modal/ModalInitial/ModalInitial'
|
||||
import { ModalImportKeys } from './components/Modal/ModalImportKeys/ModalImportKeys'
|
||||
import { ModalSignUp } from './components/Modal/ModalSignUp/ModalSignUp'
|
||||
import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { useSessionStorage } from 'usehooks-ts'
|
||||
import { RELOAD_STORAGE_KEY } from './utils/consts'
|
||||
|
||||
function App() {
|
||||
const [render, setRender] = useState(0)
|
||||
const dispatch = useAppDispatch()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
|
||||
// eslint-disable-next-line
|
||||
const [_, setNeedReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
|
||||
@ -80,14 +83,24 @@ function App() {
|
||||
// subscribe to service worker updates
|
||||
swicOnReload(() => {
|
||||
console.log('reload')
|
||||
searchParams.set('reload', 'true')
|
||||
setSearchParams(searchParams)
|
||||
setNeedReload(true)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = () => {
|
||||
setNeedReload(false)
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppRoutes />
|
||||
|
||||
<ModalInitial />
|
||||
<ModalImportKeys />
|
||||
<ModalSignUp />
|
||||
|
BIN
src/assets/fonts/Inter/Inter-Bold.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Inter/Inter-ExtraLight.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Inter/Inter-Light.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-Light.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Inter/Inter-Medium.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-Medium.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Inter/Inter-Regular.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Inter/Inter-SemiBold.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-SemiBold.ttf
Normal file
Binary file not shown.
@ -1,4 +1,5 @@
|
||||
import { AppInputProps, Input } from '@/shared/Input/Input'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { AppInputProps } from '@/shared/Input/types'
|
||||
import { styled } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
|
@ -95,8 +95,9 @@ export const ModalConfirmConnect = () => {
|
||||
const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId)
|
||||
// console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending});
|
||||
if (isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) {
|
||||
if (isPopup) window.close()
|
||||
else closeModalAfterRequest()
|
||||
// if (isPopup) window.close()
|
||||
// else closeModalAfterRequest()
|
||||
if (!isPopup) closeModalAfterRequest()
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -169,7 +170,9 @@ export const ModalConfirmConnect = () => {
|
||||
if (isPopup) {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
disallow()
|
||||
// FIXME it should be 'ignore once',
|
||||
// not 'disallow & remember' - this is too strict
|
||||
// disallow()
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -194,11 +197,11 @@ export const ModalConfirmConnect = () => {
|
||||
>
|
||||
{appAvatarTitle}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h5" fontWeight={600}>
|
||||
<Box overflow={'auto'}>
|
||||
<Typography variant="h5" fontWeight={600} noWrap>
|
||||
{appName}
|
||||
</Typography>
|
||||
<Typography variant="body2" color={'GrayText'}>
|
||||
<Typography variant="body2" color={'GrayText'} noWrap>
|
||||
New app would like to connect
|
||||
</Typography>
|
||||
</Box>
|
||||
|
@ -21,5 +21,8 @@ export const StyledToggleButtonsGroup = styled((props: ToggleButtonGroupProps) =
|
||||
border: 'initial',
|
||||
borderRadius: '1rem',
|
||||
},
|
||||
'@media screen and (max-width: 320px)': {
|
||||
marginBottom: '0.25rem',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
@ -4,22 +4,18 @@ import { StyledToggleButton } from './styled'
|
||||
|
||||
type ActionToggleButtonProps = ToggleButtonProps & {
|
||||
description?: string
|
||||
hasinfo?: boolean
|
||||
}
|
||||
|
||||
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({ hasinfo = false, ...props }) => {
|
||||
export const ActionToggleButton: FC<ActionToggleButtonProps> = (props) => {
|
||||
const { title, description = '' } = props
|
||||
return (
|
||||
<StyledToggleButton {...props}>
|
||||
<Typography variant="body2">{title}</Typography>
|
||||
<Typography variant="body2" noWrap className="title">
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography className="description" variant="caption" color={'GrayText'}>
|
||||
{description}
|
||||
</Typography>
|
||||
{hasinfo && (
|
||||
<Typography className="info" color={'GrayText'}>
|
||||
Info
|
||||
</Typography>
|
||||
)}
|
||||
</StyledToggleButton>
|
||||
)
|
||||
}
|
||||
|
@ -29,4 +29,17 @@ export const StyledToggleButton = styled((props: ToggleButtonProps) => (
|
||||
fontSize: '10px',
|
||||
fontWeight: 500,
|
||||
},
|
||||
'@media screen and (max-width: 320px)': {
|
||||
'& .title': {
|
||||
fontSize: '14px',
|
||||
},
|
||||
'& .description': {
|
||||
margin: '0.25rem 0',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
@ -83,8 +83,10 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
||||
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||
// console.log("confirm event", { confirmEventReqs, isModalOpened, isNpubExists, isAppNpubExists });
|
||||
if (isModalOpened && (!currentAppPendingReqs.length || !isNpubExists || !isAppNpubExists)) {
|
||||
if (isPopup) window.close()
|
||||
else closeModalAfterRequest()
|
||||
// if (isPopup) window.close()
|
||||
// else closeModalAfterRequest()
|
||||
if (!isPopup)
|
||||
closeModalAfterRequest()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import { DOMAIN } from '@/utils/consts'
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
import { getPublicKey, nip19 } from 'nostr-tools'
|
||||
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||
import { HeadingContainer } from './styled'
|
||||
|
||||
const FORM_DEFAULT_VALUES = {
|
||||
username: '',
|
||||
@ -150,14 +151,14 @@ export const ModalImportKeys = () => {
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
|
||||
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
||||
<Stack gap={'0.2rem'} padding={'0 1rem'} alignSelf={'flex-start'}>
|
||||
<HeadingContainer>
|
||||
<Typography fontWeight={600} variant="h5">
|
||||
Import key
|
||||
</Typography>
|
||||
<Typography noWrap variant="body2" color={'GrayText'}>
|
||||
Bring your existing Nostr keys to Nsec.app
|
||||
</Typography>
|
||||
</Stack>
|
||||
</HeadingContainer>
|
||||
<Input
|
||||
label="Choose a username"
|
||||
fullWidth
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AppLogo } from '@/assets'
|
||||
import { Box, styled } from '@mui/material'
|
||||
import { Box, Stack, StackProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledAppLogo = styled((props) => (
|
||||
<Box {...props}>
|
||||
@ -12,3 +12,14 @@ export const StyledAppLogo = styled((props) => (
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}))
|
||||
|
||||
export const HeadingContainer = styled((props: StackProps) => <Stack {...props} />)(() => ({
|
||||
gap: '0.2rem',
|
||||
padding: '0 1rem',
|
||||
alignSelf: 'flex-start',
|
||||
overflow: 'auto',
|
||||
width: '100%',
|
||||
'@media screen and (max-width: 320px)': {
|
||||
padding: '0 0.75rem',
|
||||
},
|
||||
}))
|
||||
|
@ -1,33 +1,33 @@
|
||||
import { FC } from 'react'
|
||||
import { Collapse, Stack, Typography } from '@mui/material'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { FC, memo, useCallback } from 'react'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import { StyledAlert, StyledReloadButton } from './styled'
|
||||
import { useSessionStorage } from 'usehooks-ts'
|
||||
import { RELOAD_STORAGE_KEY } from '@/utils/consts'
|
||||
|
||||
const ReloadBadgeContent: FC = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
|
||||
const handleReload = () => {
|
||||
searchParams.delete('reload')
|
||||
setSearchParams(searchParams)
|
||||
window.location.reload()
|
||||
}
|
||||
return (
|
||||
<Collapse in>
|
||||
<StyledAlert>
|
||||
<Stack direction={'row'} className="content">
|
||||
<Typography flex={1} className="title">
|
||||
New version available, please reload the page!
|
||||
</Typography>
|
||||
<StyledReloadButton onClick={handleReload}>Reload</StyledReloadButton>
|
||||
</Stack>
|
||||
</StyledAlert>
|
||||
</Collapse>
|
||||
)
|
||||
type ReloadBadgeContentProps = {
|
||||
onReload: () => void
|
||||
}
|
||||
|
||||
const ReloadBadgeContent: FC<ReloadBadgeContentProps> = memo(({ onReload }) => {
|
||||
return (
|
||||
<StyledAlert>
|
||||
<Stack direction={'row'} className="content">
|
||||
<Typography flex={1} className="title">
|
||||
New version available!
|
||||
</Typography>
|
||||
<StyledReloadButton onClick={onReload}>Reload</StyledReloadButton>
|
||||
</Stack>
|
||||
</StyledAlert>
|
||||
)
|
||||
})
|
||||
|
||||
export const ReloadBadge = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
const open = searchParams.get('reload') === 'true'
|
||||
const [needReload, setNeedReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||
|
||||
return <>{open && <ReloadBadgeContent />}</>
|
||||
const handleReload = useCallback(() => {
|
||||
setNeedReload(false)
|
||||
window.location.reload()
|
||||
}, [setNeedReload])
|
||||
|
||||
return <>{needReload && <ReloadBadgeContent onReload={handleReload} />}</>
|
||||
}
|
||||
|
@ -1,21 +1,27 @@
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { IconContainer, StyledContainer } from './styled'
|
||||
import { BoxProps, Stack, Typography } from '@mui/material'
|
||||
import { BoxProps, Typography } from '@mui/material'
|
||||
|
||||
type WarningProps = {
|
||||
message?: string | ReactNode
|
||||
hint?: string | ReactNode
|
||||
icon?: ReactNode
|
||||
} & BoxProps
|
||||
|
||||
export const Warning: FC<WarningProps> = ({ hint, message, icon, ...restProps }) => {
|
||||
export const Warning: FC<WarningProps> = ({ message, icon, ...restProps }) => {
|
||||
const renderMessage = () => {
|
||||
if (typeof message === 'string') {
|
||||
return (
|
||||
<Typography noWrap width={'100%'}>
|
||||
{message}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
return message
|
||||
}
|
||||
return (
|
||||
<StyledContainer {...restProps}>
|
||||
{icon && <IconContainer>{icon}</IconContainer>}
|
||||
<Stack flex={1} direction={'column'} gap={'0.2rem'}>
|
||||
<Typography noWrap>{message}</Typography>
|
||||
{hint && <Typography>{hint}</Typography>}
|
||||
</Stack>
|
||||
{renderMessage()}
|
||||
</StyledContainer>
|
||||
)
|
||||
}
|
||||
|
@ -7,13 +7,14 @@ export const StyledContainer = styled((props: BoxProps) => <Box {...props} />)((
|
||||
padding: '0.5rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
gap: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
}
|
||||
})
|
||||
|
||||
export const IconContainer = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
||||
width: '40px',
|
||||
minWidth: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
background: 'grey',
|
||||
|
@ -14,7 +14,7 @@ export const useProfile = (npub: string) => {
|
||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||
const currentKey = useAppSelector((state) => selectKeyByNpub(state, npub))
|
||||
|
||||
const userName = getProfileUsername(profile) || currentKey?.name
|
||||
const userName = currentKey?.name || getProfileUsername(profile)
|
||||
const userAvatar = profile?.info?.picture || ''
|
||||
const avatarTitle = getFirstLetter(userName)
|
||||
|
||||
|
@ -1,17 +1,63 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src:
|
||||
local('Inter ExtraLight'),
|
||||
local('Inter-ExtraLight'),
|
||||
url('./assets/fonts/Inter/Inter-ExtraLight.ttf') format('truetype');
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src:
|
||||
local('Inter Light'),
|
||||
local('Inter-Light'),
|
||||
url('./assets/fonts/Inter/Inter-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src:
|
||||
local('Inter Regular'),
|
||||
local('Inter-Regular'),
|
||||
url('./assets/fonts/Inter/Inter-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src:
|
||||
local('Inter Medium'),
|
||||
local('Inter-Medium'),
|
||||
url('./assets/fonts/Inter/Inter-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src:
|
||||
local('Inter SemiBold'),
|
||||
local('Inter-SemiBold'),
|
||||
url('./assets/fonts/Inter/Inter-SemiBold.ttf') format('truetype');
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src:
|
||||
local('Inter Bold'),
|
||||
local('Inter-Bold'),
|
||||
url('./assets/fonts/Inter/Inter-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
|
||||
'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
html,
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import './index.css'
|
||||
import ThemeProvider from './modules/theme/ThemeProvider'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
import reportWebVitals from './reportWebVitals'
|
||||
import { swicRegister } from './modules/swic'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import { persistor, store } from './store'
|
||||
import ThemeProvider from './modules/theme/ThemeProvider'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
import { SnackbarProvider } from 'notistack'
|
||||
|
||||
|
@ -8,17 +8,22 @@ import DarkModeIcon from '@mui/icons-material/DarkMode'
|
||||
import LightModeIcon from '@mui/icons-material/LightMode'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||
import { setThemeMode } from '@/store/reducers/ui.slice'
|
||||
import { useSessionStorage } from 'usehooks-ts'
|
||||
import { RELOAD_STORAGE_KEY } from '@/utils/consts'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export const Header = () => {
|
||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useAppDispatch()
|
||||
const [needReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||
|
||||
const [searchParams] = useSearchParams()
|
||||
const needReload = searchParams.get('reload') === 'true'
|
||||
const isPopupMode = searchParams.get('popup') === 'true'
|
||||
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||
const showProfile = Boolean(npub)
|
||||
const isKeyPage = Boolean(npub)
|
||||
|
||||
const handleNavigate = () => {
|
||||
navigate(`/key/${npub}`)
|
||||
@ -31,12 +36,17 @@ export const Header = () => {
|
||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||
}
|
||||
|
||||
const renderMenus = useCallback(() => {
|
||||
if (isPopupMode && isKeyPage) return null
|
||||
return isKeyPage ? <ProfileMenu /> : <Menu />
|
||||
}, [isPopupMode, isKeyPage])
|
||||
|
||||
return (
|
||||
<StyledAppBar position={needReload ? 'relative' : 'fixed'}>
|
||||
<Toolbar sx={{ padding: '12px' }}>
|
||||
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}>
|
||||
{showProfile && (
|
||||
<StyledProfileContainer>
|
||||
{isKeyPage && (
|
||||
<StyledProfileContainer nonclickable={isPopupMode}>
|
||||
<Avatar src={userAvatar} alt={userName} onClick={handleNavigate} className="avatar">
|
||||
{avatarTitle}
|
||||
</Avatar>
|
||||
@ -46,7 +56,7 @@ export const Header = () => {
|
||||
</StyledProfileContainer>
|
||||
)}
|
||||
|
||||
{!showProfile && (
|
||||
{!isKeyPage && (
|
||||
<StyledAppName>
|
||||
<StyledAppLogo />
|
||||
<span>Nsec.app</span>
|
||||
@ -55,7 +65,7 @@ export const Header = () => {
|
||||
|
||||
<StyledThemeButton onClick={handleChangeMode}>{themeIcon}</StyledThemeButton>
|
||||
|
||||
{showProfile ? <ProfileMenu /> : <Menu />}
|
||||
{renderMenus()}
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
<StyledDivider />
|
||||
|
@ -32,18 +32,23 @@ export const StyledAppName = styled((props: TypographyProps) => (
|
||||
marginLeft: '0.5rem',
|
||||
}))
|
||||
|
||||
export const StyledProfileContainer = styled((props: StackProps) => <Stack {...props} />)(() => ({
|
||||
gap: '1rem',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
'& .avatar': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'& .username': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}))
|
||||
export const StyledProfileContainer = styled((props: StackProps & { nonclickable: boolean }) => <Stack {...props} />)(
|
||||
({ nonclickable = false }) => ({
|
||||
gap: '1rem',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
'& .avatar': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'& .username': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'& > *': {
|
||||
pointerEvents: nonclickable ? 'none' : 'initial',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
export const StyledThemeButton = styled(IconButton)({
|
||||
margin: '0 0.5rem',
|
||||
|
@ -1,15 +1,17 @@
|
||||
import { FC } from 'react'
|
||||
import { Outlet, useSearchParams } from 'react-router-dom'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { Header } from './Header/Header'
|
||||
import { Container, ContainerProps, styled } from '@mui/material'
|
||||
import { ReloadBadge } from '@/components/ReloadBadge/ReloadBadge'
|
||||
import { useSessionStorage } from 'usehooks-ts'
|
||||
import { RELOAD_STORAGE_KEY } from '@/utils/consts'
|
||||
|
||||
export const Layout: FC = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
const needReload = searchParams.get('reload') === 'true'
|
||||
const [needReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||
const containerClassName = needReload ? 'reload' : ''
|
||||
|
||||
return (
|
||||
<StyledContainer maxWidth="md" className={needReload ? 'reload' : ''}>
|
||||
<StyledContainer maxWidth="md" className={containerClassName}>
|
||||
<ReloadBadge />
|
||||
<Header />
|
||||
<main>
|
||||
@ -25,11 +27,17 @@ const StyledContainer = styled((props: ContainerProps) => <Container maxWidth="s
|
||||
flexDirection: 'column',
|
||||
paddingBottom: '1rem',
|
||||
position: 'relative',
|
||||
'&': {
|
||||
'& > main': {
|
||||
flex: 1,
|
||||
maxHeight: '100%',
|
||||
},
|
||||
'&:not(.reload) > main': {
|
||||
paddingTop: 'calc(66px + 1rem)',
|
||||
},
|
||||
'@media screen and (max-width: 320px)': {
|
||||
marginBottom: '0.25rem',
|
||||
paddingLeft: '0.75rem',
|
||||
paddingBottom: '0.75rem',
|
||||
paddingRight: '0.75rem',
|
||||
},
|
||||
})
|
||||
|
@ -10,7 +10,7 @@ import NDK, {
|
||||
NDKSubscriptionCacheUsage,
|
||||
NDKUser,
|
||||
} from '@nostr-dev-kit/ndk'
|
||||
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN } from '../utils/consts'
|
||||
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN, REQ_TTL } from '../utils/consts'
|
||||
// import { Nip04 } from './nip04'
|
||||
import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
|
||||
import { NostrPowEvent, minePow } from './pow'
|
||||
@ -310,6 +310,13 @@ export class NoauthBackend {
|
||||
this.apps = await dbi.listApps()
|
||||
console.log('started apps', this.apps)
|
||||
|
||||
// drop old pending reqs
|
||||
const pending = await dbi.listPending()
|
||||
for (const p of pending) {
|
||||
if (p.timestamp < Date.now() - REQ_TTL)
|
||||
await dbi.removePending(p.id)
|
||||
}
|
||||
|
||||
const sub = await this.swg.registration.pushManager.getSubscription()
|
||||
|
||||
for (const k of this.enckeys) {
|
||||
@ -913,12 +920,22 @@ export class NoauthBackend {
|
||||
const confirmMethod = isConnect ? 'confirm-connect' : 'confirm-event'
|
||||
const authUrl = `${self.swg.location.origin}/key/${npub}?${confirmMethod}=true&appNpub=${appNpub}&reqId=${id}&popup=true`
|
||||
console.log('sending authUrl', authUrl, 'for', req)
|
||||
// NOTE: if you set 'Update on reload' in the Chrome SW console
|
||||
// then this message will cause a new tab opened by the peer,
|
||||
// which will cause SW (this code) to reload, to fetch
|
||||
// the pending requests and to re-send this event,
|
||||
// looping for 10 seconds (our request age threshold)
|
||||
backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl)
|
||||
|
||||
// NOTE: don't send auth_url immediately, wait some time
|
||||
// to make sure other bunkers aren't replying
|
||||
setTimeout(() => {
|
||||
// request still there? (not dropped by the watcher)
|
||||
if (self.confirmBuffer.find((r) => r.req.id === id)) {
|
||||
// NOTE: if you set 'Update on reload' in the Chrome SW console
|
||||
// then this message will cause a new tab opened by the peer,
|
||||
// which will cause SW (this code) to reload, to fetch
|
||||
// the pending requests and to re-send this event,
|
||||
// looping for 10 seconds (our request age threshold)
|
||||
backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl)
|
||||
} else {
|
||||
console.log("skip sending auth_url")
|
||||
}
|
||||
}, 500)
|
||||
|
||||
// show notifs
|
||||
// this.notify()
|
||||
@ -941,6 +958,8 @@ export class NoauthBackend {
|
||||
const backend = new Nip46Backend(ndk, signer, this.allowPermitCallback.bind(this)) // , () => Promise.resolve(true)
|
||||
const watcher = new Watcher(ndk, signer, (id) => {
|
||||
// drop pending request
|
||||
const index = self.confirmBuffer.findIndex((r) => r.req.id === id)
|
||||
if (index >= 0) self.confirmBuffer.splice(index, 1)
|
||||
dbi.removePending(id).then(() => this.updateUI())
|
||||
})
|
||||
this.keys.push({ npub, backend, signer, ndk, backoff, watcher })
|
||||
@ -1156,7 +1175,7 @@ export class NoauthBackend {
|
||||
}
|
||||
|
||||
private async editName(npub: string, name: string) {
|
||||
const key = this.enckeys.find((k) => k.npub == npub)
|
||||
const key = this.enckeys.find((k) => k.npub === npub)
|
||||
if (!key) throw new Error('Npub not found')
|
||||
if (key.name) {
|
||||
try {
|
||||
|
@ -21,6 +21,7 @@ const commonTheme: Theme = createTheme({
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'initial',
|
||||
color: 'red',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -96,4 +97,6 @@ const darkTheme: Theme = createTheme({
|
||||
},
|
||||
})
|
||||
|
||||
console.log(darkTheme)
|
||||
|
||||
export { lightTheme, darkTheme }
|
||||
|
@ -9,7 +9,6 @@ import { getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/help
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { ACTION_TYPE } from '@/utils/consts'
|
||||
import { Permissions } from './components/Permissions/Permissions'
|
||||
import { StyledAppIcon } from './styled'
|
||||
import { useToggleConfirm } from '@/hooks/useToggleConfirm'
|
||||
import { ConfirmModal } from '@/shared/ConfirmModal/ConfirmModal'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
@ -20,6 +19,8 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import MoreIcon from '@mui/icons-material/MoreVertRounded'
|
||||
import { ModalAppDetails } from '@/components/Modal/ModalAppDetails/ModalAppDetails'
|
||||
import { IconApp } from '@/shared/IconApp/IconApp'
|
||||
import { HeadingContainer, AppInfoContainer, AppNameContainer } from './styled'
|
||||
|
||||
const AppPage = () => {
|
||||
const keys = useAppSelector(selectKeys)
|
||||
@ -67,12 +68,13 @@ const AppPage = () => {
|
||||
<>
|
||||
<Stack maxHeight={'100%'} overflow={'auto'} alignItems={'flex-start'} height={'100%'}>
|
||||
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
|
||||
<Stack marginBottom={'1rem'} direction={'row'} gap={'1rem'} width={'100%'} alignItems={'center'}>
|
||||
<StyledAppIcon src={icon}>{appAvatarTitle}</StyledAppIcon>
|
||||
<Box flex={'1'} overflow={'hidden'}>
|
||||
<Stack direction={'row'} alignItems={'flex-start'} gap={'0.5rem'} marginBottom={'0.5rem'}>
|
||||
<Box display={'flex'} flexDirection={'column'} flex={1}>
|
||||
<Typography variant="h4" noWrap>
|
||||
|
||||
<HeadingContainer>
|
||||
<IconApp size="big" picture={icon} alt={appAvatarTitle} />
|
||||
<Box flex={'1'} overflow={'auto'} alignSelf={'flex-start'} width={'100%'}>
|
||||
<AppInfoContainer>
|
||||
<AppNameContainer>
|
||||
<Typography className="app_name" variant="h4" noWrap>
|
||||
{appName}
|
||||
</Typography>
|
||||
{isAppNameExists && (
|
||||
@ -80,16 +82,19 @@ const AppPage = () => {
|
||||
{shortAppNpub}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</AppNameContainer>
|
||||
|
||||
<IconButton onClick={handleShowAppDetailsModal}>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</AppInfoContainer>
|
||||
|
||||
<Typography variant="body2" noWrap>
|
||||
{connectedOn}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</HeadingContainer>
|
||||
|
||||
<Box marginBottom={'1rem'}>
|
||||
<SectionTitle marginBottom={'0.5rem'}>Disconnect</SectionTitle>
|
||||
<Button fullWidth onClick={handleShow}>
|
||||
|
@ -1,6 +1,32 @@
|
||||
import { Avatar, AvatarProps, styled } from '@mui/material'
|
||||
import { Box, BoxProps, Stack, StackProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledAppIcon = styled((props: AvatarProps) => <Avatar {...props} variant="rounded" />)(() => ({
|
||||
width: 70,
|
||||
height: 70,
|
||||
export const HeadingContainer = styled((props: StackProps) => <Stack {...props} />)(() => ({
|
||||
width: '100%',
|
||||
marginBottom: '1rem',
|
||||
flexDirection: 'row',
|
||||
gap: '1rem',
|
||||
alignItems: 'center',
|
||||
'@media screen and (max-width: 320px)': {
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
},
|
||||
}))
|
||||
|
||||
export const AppInfoContainer = styled((props: StackProps) => <Stack {...props} direction={'row'} />)(() => ({
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
alignItems: 'flex-start',
|
||||
gap: '0.5rem',
|
||||
marginBottom: '0.5rem',
|
||||
overflow: 'hidden',
|
||||
'@media screen and (max-width: 320px)': {
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
}))
|
||||
|
||||
export const AppNameContainer = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
}))
|
||||
|
@ -5,9 +5,7 @@ import { Box, Stack, Typography } from '@mui/material'
|
||||
import { FC } from 'react'
|
||||
import { StyledEmptyAppsBox } from '../styled'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { call } from '@/utils/helpers/helpers'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
|
||||
import { ItemApp } from './ItemApp'
|
||||
|
||||
type AppsProps = {
|
||||
@ -16,22 +14,12 @@ type AppsProps = {
|
||||
}
|
||||
|
||||
export const Apps: FC<AppsProps> = ({ apps = [] }) => {
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
// eslint-disable-next-line
|
||||
async function deletePerm(id: string) {
|
||||
call(async () => {
|
||||
await swicCall('deletePerm', id)
|
||||
notify('Perm deleted!', 'success')
|
||||
})
|
||||
}
|
||||
|
||||
const openAppStore = () => {
|
||||
window.open('https://nostrapp.link', '_blank')
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flex={1} marginBottom={'1rem'} display={'flex'} flexDirection={'column'} overflow={'auto'}>
|
||||
<Box marginBottom={'1rem'} display={'flex'} flexDirection={'column'}>
|
||||
<Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
|
||||
<SectionTitle>Connected apps</SectionTitle>
|
||||
<AppLink title="Discover Apps" onClick={openAppStore} />
|
||||
@ -45,7 +33,7 @@ export const Apps: FC<AppsProps> = ({ apps = [] }) => {
|
||||
</StyledEmptyAppsBox>
|
||||
)}
|
||||
|
||||
<Stack gap={'0.5rem'} overflow={'auto'} flex={1}>
|
||||
<Stack gap={'0.5rem'} overflow={'auto'} flex={1} paddingBottom={'0.75rem'}>
|
||||
{apps.map((a) => (
|
||||
<ItemApp {...a} key={a.appNpub} />
|
||||
))}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { FC } from 'react'
|
||||
import { Warning } from '@/components/Warning/Warning'
|
||||
import { CircularProgress, Stack, Typography } from '@mui/material'
|
||||
import { CircularProgress, Stack, Typography, TypographyProps, styled } from '@mui/material'
|
||||
import AutoModeOutlinedIcon from '@mui/icons-material/AutoModeOutlined'
|
||||
|
||||
type BackgroundSigningWarningProps = {
|
||||
@ -12,13 +12,29 @@ export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({ is
|
||||
return (
|
||||
<Warning
|
||||
message={
|
||||
<Stack direction={'row'} alignItems={'center'} gap={'1rem'}>
|
||||
Enable background service {isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
|
||||
<Stack gap={'0.25rem'} overflow={'auto'} width={'100%'}>
|
||||
<Typography variant="body1" noWrap fontWeight={'500'}>
|
||||
Enable background service
|
||||
</Typography>
|
||||
<StyledHint>Please allow notifications for background operation.</StyledHint>
|
||||
</Stack>
|
||||
}
|
||||
hint={<Typography variant="body2">Please allow notifications for background operation.</Typography>}
|
||||
icon={<AutoModeOutlinedIcon htmlColor="white" />}
|
||||
icon={
|
||||
isEnabling ? (
|
||||
<CircularProgress size={'1.5rem'} sx={{ color: '#fff' }} />
|
||||
) : (
|
||||
<AutoModeOutlinedIcon htmlColor="white" />
|
||||
)
|
||||
}
|
||||
onClick={isEnabling ? undefined : onEnableBackSigning}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledHint = styled((props: TypographyProps) => <Typography variant="body2" {...props} />)(() => ({
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}))
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Input, AppInputProps } from '@/shared/Input/Input'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { AppInputProps } from '@/shared/Input/types'
|
||||
import { Stack, StackProps, styled } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { DbPending, DbPerm } from '@/modules/db'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { ACTION_TYPE } from '@/utils/consts'
|
||||
import { ACTION_TYPE, REQ_TTL } from '@/utils/consts'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
|
||||
@ -25,7 +25,7 @@ export const useTriggerConfirmModal = (npub: string, pending: DbPending[], perms
|
||||
const isConfirmConnectModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||||
const isConfirmEventModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
||||
|
||||
const filteredPendingReqs = pending.filter((p) => p.npub === npub)
|
||||
const filteredPendingReqs = pending.filter((p) => p.npub === npub && p.timestamp > Date.now() - REQ_TTL)
|
||||
const filteredPerms = perms.filter((p) => p.npub === npub)
|
||||
|
||||
const npubConnectPerms = filteredPerms.filter((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC)
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Input, AppInputProps } from '@/shared/Input/Input'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { AppInputProps } from '@/shared/Input/types'
|
||||
import { Box, Button, ButtonProps, styled, Badge } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { Suspense, lazy } from 'react'
|
||||
import { Route, Routes, Navigate } from 'react-router-dom'
|
||||
import HomePage from '../pages/HomePage/Home.Page'
|
||||
import { Layout } from '../layout/Layout'
|
||||
import { CircularProgress, Stack } from '@mui/material'
|
||||
import CreatePage from '@/pages/CreatePage/Create.Page'
|
||||
|
||||
const KeyPage = lazy(() => import('../pages/KeyPage/Key.Page'))
|
||||
const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
|
||||
const AppPage = lazy(() => import('../pages/AppPage/App.Page'))
|
||||
// Pages
|
||||
import CreatePage from '@/pages/CreatePage/Create.Page'
|
||||
import HomePage from '../pages/HomePage/Home.Page'
|
||||
import KeyPage from '../pages/KeyPage/Key.Page'
|
||||
|
||||
const ConfirmPage = lazy(() => import('@/pages/Confirm.Page'))
|
||||
const AppPage = lazy(() => import('@/pages/AppPage/App.Page'))
|
||||
|
||||
const LoadingSpinner = () => (
|
||||
<Stack height={'100%'} justifyContent={'center'} alignItems={'center'}>
|
||||
|
@ -19,6 +19,9 @@ const StyledButton = styled(
|
||||
const commonStyles = {
|
||||
fontWeight: 500,
|
||||
borderRadius: '1rem',
|
||||
'@media screen and (max-width: 320px)': {
|
||||
padding: '0.25rem 0.75rem',
|
||||
},
|
||||
}
|
||||
if (varianttype === 'secondary') {
|
||||
return {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useRef } from 'react'
|
||||
import { Input, AppInputProps } from '../Input/Input'
|
||||
import { Input } from '../Input/Input'
|
||||
import { AppInputProps } from '../Input/types'
|
||||
|
||||
export type DebounceProps = {
|
||||
handleDebounce: (value: string) => void
|
||||
|
@ -9,7 +9,8 @@ export const StyledButton = styled((props: ButtonProps) => (
|
||||
startIcon: 'icon',
|
||||
}}
|
||||
/>
|
||||
))(() => ({
|
||||
))(({ theme }) => ({
|
||||
color: theme.palette.primary.main,
|
||||
marginBottom: '0.5rem',
|
||||
borderRadius: '8px',
|
||||
'&:is(:hover,:active)': {
|
||||
@ -18,4 +19,7 @@ export const StyledButton = styled((props: ButtonProps) => (
|
||||
'& .icon': {
|
||||
marginRight: '5px',
|
||||
},
|
||||
'@media screen and (max-width: 320px)': {
|
||||
marginBottom: '0.25rem',
|
||||
},
|
||||
}))
|
||||
|
@ -1,8 +1,11 @@
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { StyledAppIcon, StyledAppImg } from './styled'
|
||||
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'
|
||||
import { IIconApp } from './types'
|
||||
|
||||
const failedCache = new Map<string, boolean>()
|
||||
|
||||
export const IconApp: FC<{ picture: string }> = ({ picture }) => {
|
||||
export const IconApp: FC<IIconApp> = ({ picture = '', alt, isRounded, isSmall, onClick, size, ...rest }) => {
|
||||
const c = failedCache.get(picture)
|
||||
const [isFailed, setIsFailed] = useState(c !== undefined ? c : true)
|
||||
|
||||
@ -26,5 +29,21 @@ export const IconApp: FC<{ picture: string }> = ({ picture }) => {
|
||||
}
|
||||
}, [picture])
|
||||
|
||||
return <div>IconApp</div>
|
||||
return (
|
||||
<StyledAppIcon isNotLoaded={isFailed} size={size} onClick={onClick} {...rest}>
|
||||
{alt ? (
|
||||
<StyledAppImg size={size} alt={alt} isSmall={isSmall} src={isFailed ? '' : picture}>
|
||||
{isFailed && (
|
||||
<div className="MuiAvatar-root MuiAvatar-square MuiAvatar-colorDefault">
|
||||
{alt.substring(0, 1).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</StyledAppImg>
|
||||
) : (
|
||||
<StyledAppImg size={size} alt={alt} isSmall={isSmall} src={isFailed ? '/' : picture}>
|
||||
<ImageOutlinedIcon fontSize="inherit" />
|
||||
</StyledAppImg>
|
||||
)}
|
||||
</StyledAppIcon>
|
||||
)
|
||||
}
|
||||
|
53
src/shared/IconApp/const.ts
Normal file
53
src/shared/IconApp/const.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { APP_NSEC_SIZE } from '@/utils/consts'
|
||||
|
||||
const SIZE_VALUE = {
|
||||
[APP_NSEC_SIZE.BIG]: 70,
|
||||
[APP_NSEC_SIZE.LARGE]: 56,
|
||||
[APP_NSEC_SIZE.MEDIUM]: 40,
|
||||
[APP_NSEC_SIZE.SMALL]: 36,
|
||||
[APP_NSEC_SIZE.EXTRA_SMALL]: 30,
|
||||
}
|
||||
|
||||
export const APP_SIZE_VALUE = {
|
||||
[APP_NSEC_SIZE.BIG]: {
|
||||
height: SIZE_VALUE[APP_NSEC_SIZE.BIG],
|
||||
minWidth: SIZE_VALUE[APP_NSEC_SIZE.BIG],
|
||||
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.BIG],
|
||||
},
|
||||
[APP_NSEC_SIZE.LARGE]: {
|
||||
height: SIZE_VALUE[APP_NSEC_SIZE.LARGE],
|
||||
minWidth: SIZE_VALUE[APP_NSEC_SIZE.LARGE],
|
||||
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.LARGE],
|
||||
},
|
||||
[APP_NSEC_SIZE.MEDIUM]: {
|
||||
height: SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
|
||||
minWidth: SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
|
||||
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
|
||||
},
|
||||
[APP_NSEC_SIZE.SMALL]: {
|
||||
height: SIZE_VALUE[APP_NSEC_SIZE.SMALL],
|
||||
minWidth: SIZE_VALUE[APP_NSEC_SIZE.SMALL],
|
||||
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.SMALL],
|
||||
},
|
||||
[APP_NSEC_SIZE.EXTRA_SMALL]: {
|
||||
height: SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
|
||||
minWidth: SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
|
||||
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
|
||||
},
|
||||
}
|
||||
|
||||
const FONT_SIZE_VALUE = {
|
||||
[APP_NSEC_SIZE.BIG]: 24,
|
||||
[APP_NSEC_SIZE.LARGE]: 20,
|
||||
[APP_NSEC_SIZE.MEDIUM]: 16,
|
||||
[APP_NSEC_SIZE.SMALL]: 12,
|
||||
[APP_NSEC_SIZE.EXTRA_SMALL]: 10,
|
||||
}
|
||||
|
||||
export const APP_NAME_FONT_SIZE_VALUE = {
|
||||
[APP_NSEC_SIZE.LARGE]: FONT_SIZE_VALUE[APP_NSEC_SIZE.LARGE],
|
||||
[APP_NSEC_SIZE.BIG]: FONT_SIZE_VALUE[APP_NSEC_SIZE.BIG],
|
||||
[APP_NSEC_SIZE.MEDIUM]: FONT_SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
|
||||
[APP_NSEC_SIZE.SMALL]: FONT_SIZE_VALUE[APP_NSEC_SIZE.SMALL],
|
||||
[APP_NSEC_SIZE.EXTRA_SMALL]: FONT_SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
|
||||
}
|
54
src/shared/IconApp/styled.tsx
Normal file
54
src/shared/IconApp/styled.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { Avatar, Box, styled } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
import { IAvatarStyled, IBoxStyled } from './types'
|
||||
import { grey } from '@mui/material/colors'
|
||||
import { AppNostrSize } from '@/types/app-nsec'
|
||||
import { APP_NAME_FONT_SIZE_VALUE, APP_SIZE_VALUE } from './const'
|
||||
import { APP_NSEC_SIZE } from '@/utils/consts'
|
||||
|
||||
const color = grey[500]
|
||||
|
||||
const getVariantApp = (isRounded: boolean, size: AppNostrSize) => {
|
||||
if (isRounded) {
|
||||
return {
|
||||
height: 34,
|
||||
minWidth: 34,
|
||||
maxWidth: 34,
|
||||
borderRadius: '7px',
|
||||
}
|
||||
}
|
||||
|
||||
return APP_SIZE_VALUE[size]
|
||||
}
|
||||
|
||||
export const StyledAppIcon = styled(
|
||||
forwardRef<HTMLAnchorElement, IBoxStyled>(function BoxDisplayName(props, ref) {
|
||||
return <Box ref={ref} {...props} />
|
||||
})
|
||||
)(({ theme, isNotLoaded, isRounded = false, size = APP_NSEC_SIZE.MEDIUM }) => ({
|
||||
position: 'relative',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
overflow: 'hidden',
|
||||
...getVariantApp(isRounded, size),
|
||||
transition: theme.transitions.create(['border-color', 'transition']),
|
||||
backgroundColor: isNotLoaded ? color : theme.palette.background.default,
|
||||
boxSizing: 'border-box',
|
||||
':active': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
},
|
||||
}))
|
||||
|
||||
export const StyledAppImg = styled(function BoxDisplayName(props: IAvatarStyled) {
|
||||
return <Avatar variant="square" {...props} />
|
||||
})(({ isSmall = false, size = APP_NSEC_SIZE.MEDIUM }) => ({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
fontWeight: '500',
|
||||
fontSize: APP_NAME_FONT_SIZE_VALUE[size],
|
||||
'.MuiAvatar-img': {
|
||||
objectFit: isSmall ? 'scale-down' : 'cover',
|
||||
},
|
||||
}))
|
24
src/shared/IconApp/types.ts
Normal file
24
src/shared/IconApp/types.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { AppNostrSize } from '@/types/app-nsec'
|
||||
import { AvatarProps, BoxProps } from '@mui/material'
|
||||
|
||||
export type IconAppProps = {
|
||||
size?: AppNostrSize
|
||||
isRounded?: boolean
|
||||
isSmall?: boolean
|
||||
onClick?: () => void
|
||||
isNotLoaded?: boolean
|
||||
}
|
||||
|
||||
export type IconAppBase = {
|
||||
picture: string
|
||||
alt?: string
|
||||
}
|
||||
|
||||
export type IIconApp = Omit<IconAppProps, 'isNotLoaded'> & IconAppBase
|
||||
|
||||
export type IBoxStyled = IconAppProps & BoxProps
|
||||
|
||||
export type IAvatarStyled = {
|
||||
size?: AppNostrSize
|
||||
isSmall?: boolean
|
||||
} & AvatarProps
|
@ -1,74 +1,34 @@
|
||||
import { ReactNode, forwardRef } from 'react'
|
||||
import {
|
||||
Box,
|
||||
BoxProps,
|
||||
FormHelperText,
|
||||
FormHelperTextProps,
|
||||
FormLabel,
|
||||
InputBase,
|
||||
InputBaseProps,
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
import { FormHelperText, FormLabel, InputBase } from '@mui/material'
|
||||
import { StyledInputContainer } from './styled'
|
||||
import { AppInputProps } from './types'
|
||||
|
||||
export type AppInputProps = InputBaseProps & {
|
||||
helperText?: string | ReactNode
|
||||
helperTextProps?: FormHelperTextProps
|
||||
containerProps?: BoxProps
|
||||
label?: string
|
||||
}
|
||||
const renderItem = <T,>(item: T, value: ReactNode) => (item ? value : null)
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, AppInputProps>(
|
||||
({ helperText, containerProps, helperTextProps, label, ...props }, ref) => {
|
||||
return (
|
||||
<StyledInputContainer {...containerProps}>
|
||||
{label ? (
|
||||
{renderItem(
|
||||
label,
|
||||
<FormLabel className="label" htmlFor={props.id}>
|
||||
{label}
|
||||
</FormLabel>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
<InputBase
|
||||
autoComplete="off"
|
||||
{...props}
|
||||
classes={{ error: 'error', root: 'input_root', input: 'input', disabled: 'disabled' }}
|
||||
ref={ref}
|
||||
/>
|
||||
{helperText ? (
|
||||
{renderItem(
|
||||
helperText,
|
||||
<FormHelperText {...helperTextProps} className="helper_text">
|
||||
{helperText}
|
||||
</FormHelperText>
|
||||
) : null}
|
||||
)}
|
||||
</StyledInputContainer>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({ theme }) => {
|
||||
const isDark = theme.palette.mode === 'dark'
|
||||
return {
|
||||
width: '100%',
|
||||
'& > .input_root': {
|
||||
background: isDark ? '#000000A8' : '#000',
|
||||
color: theme.palette.common.white,
|
||||
padding: '0.75rem 1rem',
|
||||
borderRadius: '1rem',
|
||||
border: '0.3px solid #FFFFFF54',
|
||||
fontSize: '0.875rem',
|
||||
'&.error': {
|
||||
border: '0.3px solid ' + theme.palette.error.main,
|
||||
},
|
||||
},
|
||||
'& .input:is(.disabled, &)': {
|
||||
WebkitTextFillColor: '#ffffff80',
|
||||
},
|
||||
'& > .helper_text': {
|
||||
margin: '0.5rem 0.5rem 0',
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'& > .label': {
|
||||
margin: '0 1rem 0.5rem',
|
||||
display: 'block',
|
||||
color: theme.palette.primary.main,
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
45
src/shared/Input/styled.tsx
Normal file
45
src/shared/Input/styled.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Box, BoxProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({ theme }) => {
|
||||
const isDark = theme.palette.mode === 'dark'
|
||||
return {
|
||||
width: '100%',
|
||||
'& > .input_root': {
|
||||
background: isDark ? '#000000A8' : '#000',
|
||||
color: theme.palette.common.white,
|
||||
padding: '0.75rem 1rem',
|
||||
borderRadius: '1rem',
|
||||
border: '0.3px solid #FFFFFF54',
|
||||
fontSize: '0.875rem',
|
||||
'&.error': {
|
||||
border: '0.3px solid ' + theme.palette.error.main,
|
||||
},
|
||||
},
|
||||
'& .input:is(.disabled, &)': {
|
||||
WebkitTextFillColor: '#ffffff80',
|
||||
},
|
||||
'& > .helper_text': {
|
||||
margin: '0.5rem 0.5rem 0',
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'& > .label': {
|
||||
margin: '0 1rem 0.5rem',
|
||||
display: 'block',
|
||||
color: theme.palette.primary.main,
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
'@media screen and (max-width: 320px)': {
|
||||
'& > .input_root': {
|
||||
padding: '0.5rem 0.75rem',
|
||||
borderRadius: '0.75rem',
|
||||
},
|
||||
'& > .label': {
|
||||
margin: '0 0.25rem 0.25rem',
|
||||
fontSize: '0.75rem',
|
||||
},
|
||||
'& > .helper_text': {
|
||||
fontSize: '0.75rem',
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
9
src/shared/Input/types.ts
Normal file
9
src/shared/Input/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { BoxProps, FormHelperTextProps, InputBaseProps } from '@mui/material'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
export type AppInputProps = InputBaseProps & {
|
||||
helperText?: string | ReactNode
|
||||
helperTextProps?: FormHelperTextProps
|
||||
containerProps?: BoxProps
|
||||
label?: string
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { Fade, IconButton, Typography } from '@mui/material'
|
||||
import { Fade, Typography } from '@mui/material'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import { CopyIcon } from '@/assets'
|
||||
import { StyledContainer } from './styled'
|
||||
import { StyledContainer, StyledCopyButton } from './styled'
|
||||
|
||||
type InputCopyButtonProps = {
|
||||
value: string
|
||||
@ -40,9 +39,7 @@ export const InputCopyButton: FC<InputCopyButtonProps> = ({ value, onCopy = () =
|
||||
</Fade>
|
||||
)}
|
||||
<CopyToClipboard text={value} onCopy={handleCopy}>
|
||||
<IconButton color="inherit">
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
<StyledCopyButton />
|
||||
</CopyToClipboard>
|
||||
</StyledContainer>
|
||||
)
|
||||
|
@ -1,7 +1,17 @@
|
||||
import { Stack, StackProps, styled } from '@mui/material'
|
||||
import { IconButton, Stack, StackProps, styled } from '@mui/material'
|
||||
import { CopyIcon } from '@/assets'
|
||||
|
||||
export const StyledContainer = styled((props: StackProps & { copied: number }) => (
|
||||
<Stack {...props} direction={'row'} alignItems={'center'} />
|
||||
))(({ theme, copied }) => ({
|
||||
color: copied ? theme.palette.success.main : theme.palette.textSecondaryDecorate.main,
|
||||
}))
|
||||
|
||||
export const StyledCopyButton = styled((props) => (
|
||||
<IconButton color="inherit" {...props}>
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
))(() => ({
|
||||
width: 40,
|
||||
height: 40,
|
||||
}))
|
||||
|
6
src/types/app-nsec.ts
Normal file
6
src/types/app-nsec.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { APP_NSEC_SIZE } from '@/utils/consts'
|
||||
import { OverridableStringUnion } from './utils'
|
||||
|
||||
export type AppNostrSizeUnion = (typeof APP_NSEC_SIZE)[keyof typeof APP_NSEC_SIZE]
|
||||
|
||||
export type AppNostrSize = OverridableStringUnion<AppNostrSizeUnion>
|
10
src/types/utils.ts
Normal file
10
src/types/utils.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export type OverridableStringUnion<T extends string> = GenerateStringUnion<DistributiveOmit<Record<T, true>>>
|
||||
|
||||
type GenerateStringUnion<T> = Extract<
|
||||
{
|
||||
[Key in keyof T]: true extends T[Key] ? Key : never
|
||||
}[keyof T],
|
||||
string
|
||||
>
|
||||
|
||||
type DistributiveOmit<T> = T extends T ? T : never
|
@ -9,6 +9,10 @@ export const MAX_POW = 19
|
||||
|
||||
export const KIND_RPC = 24133
|
||||
|
||||
export const RELOAD_STORAGE_KEY = 'reload'
|
||||
|
||||
export const REQ_TTL = 60000 // 1 min
|
||||
|
||||
export enum ACTION_TYPE {
|
||||
BASIC = 'basic',
|
||||
ADVANCED = 'advanced',
|
||||
@ -23,3 +27,11 @@ export const ACTIONS: { [type: string]: string } = {
|
||||
nip04_encrypt: 'Encrypt message',
|
||||
nip04_decrypt: 'Decrypt message',
|
||||
}
|
||||
|
||||
export const APP_NSEC_SIZE = {
|
||||
BIG: 'big',
|
||||
LARGE: 'large',
|
||||
MEDIUM: 'medium',
|
||||
SMALL: 'small',
|
||||
EXTRA_SMALL: 'extra-small',
|
||||
} as const
|
||||
|
@ -16,7 +16,7 @@ export const getShortenNpub = (npub = '') => {
|
||||
}
|
||||
|
||||
export const getProfileUsername = (profile: MetaEvent | null) => {
|
||||
if (!profile) return null
|
||||
if (!profile) return undefined
|
||||
return profile?.info?.name || profile?.info?.display_name
|
||||
}
|
||||
|
||||
@ -72,6 +72,7 @@ export function isPackagePerm(perm: string, reqPerm: string) {
|
||||
case 'sign_event:10002':
|
||||
case 'sign_event:30023':
|
||||
case 'sign_event:10000':
|
||||
case 'sign_event:27235':
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user