Compare commits
20 Commits
feature/hi
...
refactor/r
Author | SHA1 | Date | |
---|---|---|---|
f55ba7e6f2 | |||
05bd08e86d | |||
4fa5c57a69 | |||
3de4a508be | |||
ecf27d8d23 | |||
59c03d16eb | |||
adbc7d455d | |||
13f9bb13fd | |||
b1dd6a5424 | |||
e3feb8b5a2 | |||
06fa8ffbd7 | |||
22753a8d89 | |||
425f7277fc | |||
ba3775e6c6 | |||
8c77b10b60 | |||
4ad66c8711 | |||
6a04c3ec4b | |||
6d72cf1f82 | |||
d199dcf9f7 | |||
0f28c80a15 |
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",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Nsec.app",
|
||||
"short_name": "Nsec.app - Nostr key management tool",
|
||||
"name": "Nsec.app - Nostr key management tool",
|
||||
"short_name": "Nsec.app",
|
||||
"start_url": ".",
|
||||
"icons": [
|
||||
{
|
||||
|
27
src/App.tsx
27
src/App.tsx
@ -1,6 +1,6 @@
|
||||
import { DbKey, dbi } from './modules/db'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { swicOnRender } from './modules/swic'
|
||||
import { swicOnReload, swicOnRender } from './modules/swic'
|
||||
import { useAppDispatch } from './store/hooks/redux'
|
||||
import { setApps, setKeys, setPending, setPerms } from './store/reducers/content.slice'
|
||||
import AppRoutes from './routes/AppRoutes'
|
||||
@ -9,16 +9,20 @@ 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 { useSessionStorage } from 'usehooks-ts'
|
||||
import { RELOAD_STORAGE_KEY } from './utils/consts'
|
||||
|
||||
function App() {
|
||||
const [render, setRender] = useState(0)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// eslint-disable-next-line
|
||||
const [_, setNeedReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const keys: DbKey[] = await dbi.listKeys()
|
||||
// console.log(keys, 'keys')
|
||||
|
||||
dispatch(setKeys({ keys }))
|
||||
const loadProfiles = async () => {
|
||||
@ -55,7 +59,6 @@ function App() {
|
||||
// rerender
|
||||
// setRender((r) => r + 1)
|
||||
|
||||
// if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||
// eslint-disable-next-line
|
||||
}, [dispatch])
|
||||
|
||||
@ -77,6 +80,24 @@ function App() {
|
||||
setRender((r) => r + 1)
|
||||
})
|
||||
|
||||
// subscribe to service worker updates
|
||||
swicOnReload(() => {
|
||||
console.log('reload')
|
||||
setNeedReload(true)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = () => {
|
||||
setNeedReload(false)
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppRoutes />
|
||||
|
@ -68,7 +68,6 @@ export const ModalAppDetails = () => {
|
||||
if (isEmptyString(url)) return
|
||||
|
||||
try {
|
||||
|
||||
const u = new URL(url)
|
||||
|
||||
if (isEmptyString(name)) setDetails((prev) => ({ ...prev, name: u.hostname }))
|
||||
|
@ -73,11 +73,11 @@ export const ModalConfirmConnect = () => {
|
||||
useEffect(() => {
|
||||
if (isModalOpened) {
|
||||
if (isPopup) {
|
||||
console.log("waiting for sw")
|
||||
console.log('waiting for sw')
|
||||
// wait for SW to start
|
||||
swicWaitStarted().then(() => {
|
||||
// give it some time to load the pending reqs etc
|
||||
console.log("waiting for sw done")
|
||||
console.log('waiting for sw done')
|
||||
setTimeout(() => setIsLoaded(true), 500)
|
||||
})
|
||||
} else {
|
||||
@ -95,8 +95,10 @@ 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
|
||||
}
|
||||
}
|
||||
@ -123,7 +125,6 @@ export const ModalConfirmConnect = () => {
|
||||
const options = { perms, appUrl }
|
||||
await confirmPending(pendingReqId, true, true, options)
|
||||
} else {
|
||||
|
||||
try {
|
||||
await askNotificationPermission()
|
||||
const result = await swicCall('enablePush')
|
||||
@ -170,7 +171,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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
174
src/components/Modal/ModalEditName/ModalEditName.tsx
Normal file
174
src/components/Modal/ModalEditName/ModalEditName.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { selectKeys } from '@/store'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||
import { Stack, Typography, useTheme } from '@mui/material'
|
||||
import { ChangeEvent, Fragment, useCallback, useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { StyledSettingContainer } from './styled'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
|
||||
export const ModalEditName = () => {
|
||||
const keys = useAppSelector(selectKeys)
|
||||
const notify = useEnqueueSnackbar()
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
|
||||
const key = keys.find((k) => k.npub === npub)
|
||||
const name = key?.name || ''
|
||||
|
||||
const { palette } = useTheme()
|
||||
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||
|
||||
const [enteredName, setEnteredName] = useState('')
|
||||
const [debouncedName] = useDebounce(enteredName, 300)
|
||||
const isNameEqual = debouncedName === name
|
||||
|
||||
const [receiverNpub, setReceiverNpub] = useState('')
|
||||
|
||||
const [isAvailable, setIsAvailable] = useState(true)
|
||||
const [isChecking, setIsChecking] = useState(false)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isTransferLoading, setIsTransferLoading] = useState(false)
|
||||
|
||||
const checkIsUsernameAvailable = useCallback(async () => {
|
||||
if (!debouncedName.trim().length) return undefined
|
||||
try {
|
||||
setIsChecking(true)
|
||||
const npubNip05 = await fetchNip05(`${debouncedName}@${DOMAIN}`)
|
||||
setIsAvailable(!npubNip05 || npubNip05 === npub)
|
||||
setIsChecking(false)
|
||||
} catch (error) {
|
||||
setIsAvailable(true)
|
||||
setIsChecking(false)
|
||||
}
|
||||
}, [debouncedName, npub])
|
||||
|
||||
useEffect(() => {
|
||||
checkIsUsernameAvailable()
|
||||
}, [checkIsUsernameAvailable])
|
||||
|
||||
useEffect(() => {
|
||||
setEnteredName(name)
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
setEnteredName('')
|
||||
setReceiverNpub('')
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isModalOpened])
|
||||
|
||||
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => setEnteredName(e.target.value)
|
||||
|
||||
const handleReceiverNpubChange = (e: ChangeEvent<HTMLInputElement>) => setReceiverNpub(e.target.value)
|
||||
|
||||
const getInputHelperText = () => {
|
||||
if (!debouncedName.trim().length || isNameEqual) return ''
|
||||
if (isChecking) return 'Loading...'
|
||||
if (!isAvailable) return 'Already taken'
|
||||
return (
|
||||
<Fragment>
|
||||
<CheckmarkIcon /> Available
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
const inputHelperText = getInputHelperText()
|
||||
|
||||
const getHelperTextColor = useCallback(() => {
|
||||
if (!debouncedName || isChecking || isNameEqual) return palette.textSecondaryDecorate.main
|
||||
return isAvailable ? palette.success.main : palette.error.main
|
||||
// deps
|
||||
}, [debouncedName, isAvailable, isChecking, isNameEqual, palette])
|
||||
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
if (isModalOpened && !isNpubExists) {
|
||||
handleCloseModal()
|
||||
return null
|
||||
}
|
||||
|
||||
const isEditButtonDisabled = isNameEqual || !isAvailable || isChecking || isLoading || !enteredName.trim().length
|
||||
const isTransferButtonDisabled = !name.length || !receiverNpub.trim().length || isTransferLoading
|
||||
|
||||
const handleEditName = async () => {
|
||||
if (isEditButtonDisabled) return
|
||||
try {
|
||||
setIsLoading(true)
|
||||
await swicCall('editName', npub, enteredName)
|
||||
notify('Username updated!', 'success')
|
||||
setIsLoading(false)
|
||||
} catch (error: any) {
|
||||
setIsLoading(false)
|
||||
notify(error?.message || 'Failed to edit username!', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTransferName = async () => {
|
||||
if (isTransferButtonDisabled) return
|
||||
try {
|
||||
setIsTransferLoading(true)
|
||||
await swicCall('transferName', npub, enteredName, receiverNpub)
|
||||
notify('Username transferred!', 'success')
|
||||
setIsTransferLoading(false)
|
||||
setEnteredName('')
|
||||
} catch (error: any) {
|
||||
setIsTransferLoading(false)
|
||||
notify(error?.message || 'Failed to transfer username!', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} title="Username Settings" onClose={handleCloseModal}>
|
||||
<Stack gap={'1rem'}>
|
||||
<StyledSettingContainer>
|
||||
<SectionTitle>Change name</SectionTitle>
|
||||
<Input
|
||||
label="User name"
|
||||
fullWidth
|
||||
placeholder="Enter a Username"
|
||||
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||
helperText={inputHelperText}
|
||||
onChange={handleNameChange}
|
||||
value={enteredName}
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
'&.helper_text': {
|
||||
color: getHelperTextColor(),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button fullWidth disabled={isEditButtonDisabled} onClick={handleEditName}>
|
||||
Save name {isLoading && <LoadingSpinner />}
|
||||
</Button>
|
||||
</StyledSettingContainer>
|
||||
<StyledSettingContainer>
|
||||
<SectionTitle>Transfer name</SectionTitle>
|
||||
<Input
|
||||
label="Receiver npub"
|
||||
fullWidth
|
||||
placeholder="npub1..."
|
||||
onChange={handleReceiverNpubChange}
|
||||
value={receiverNpub}
|
||||
/>
|
||||
<Button fullWidth onClick={handleTransferName} disabled={isTransferButtonDisabled}>
|
||||
Transfer name
|
||||
</Button>
|
||||
</StyledSettingContainer>
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
}
|
10
src/components/Modal/ModalEditName/styled.tsx
Normal file
10
src/components/Modal/ModalEditName/styled.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Stack, StackProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledSettingContainer = styled((props: StackProps) => (
|
||||
<Stack gap={'0.75rem'} component={'form'} {...props} />
|
||||
))(({ theme }) => ({
|
||||
padding: '1rem',
|
||||
borderRadius: '1rem',
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
}))
|
@ -60,7 +60,8 @@ export const ModalSignUp = () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const k: any = await swicCall('generateKey', name)
|
||||
notify(`Account created for "${name}"`, 'success')
|
||||
if (k.name) notify(`Account created for "${k.name}"`, 'success')
|
||||
else notify(`Failed to assign name "${name}", try again`, 'error')
|
||||
setIsLoading(false)
|
||||
setTimeout(() => {
|
||||
// give frontend time to read the new key first
|
||||
|
35
src/components/ReloadBadge/ReloadBadge.tsx
Normal file
35
src/components/ReloadBadge/ReloadBadge.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { FC, memo, useCallback } from 'react'
|
||||
import { Collapse, Stack, Typography } from '@mui/material'
|
||||
import { StyledAlert, StyledReloadButton } from './styled'
|
||||
import { useSessionStorage } from 'usehooks-ts'
|
||||
import { RELOAD_STORAGE_KEY } from '@/utils/consts'
|
||||
|
||||
type ReloadBadgeContentProps = {
|
||||
onReload: () => void
|
||||
}
|
||||
|
||||
const ReloadBadgeContent: FC<ReloadBadgeContentProps> = memo(({ onReload }) => {
|
||||
return (
|
||||
<Collapse in>
|
||||
<StyledAlert>
|
||||
<Stack direction={'row'} className="content">
|
||||
<Typography flex={1} className="title">
|
||||
New version available!
|
||||
</Typography>
|
||||
<StyledReloadButton onClick={onReload}>Reload</StyledReloadButton>
|
||||
</Stack>
|
||||
</StyledAlert>
|
||||
</Collapse>
|
||||
)
|
||||
})
|
||||
|
||||
export const ReloadBadge = () => {
|
||||
const [needReload, setNeedReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||
|
||||
const handleReload = useCallback(() => {
|
||||
setNeedReload(false)
|
||||
window.location.reload()
|
||||
}, [setNeedReload])
|
||||
|
||||
return <>{needReload && <ReloadBadgeContent onReload={handleReload} />}</>
|
||||
}
|
50
src/components/ReloadBadge/styled.tsx
Normal file
50
src/components/ReloadBadge/styled.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||
import { Alert, AlertProps, styled } from '@mui/material'
|
||||
import RefreshIcon from '@mui/icons-material/Refresh'
|
||||
|
||||
export const StyledAlert = styled((props: AlertProps) => (
|
||||
<Alert {...props} variant="outlined" severity="info" classes={{ message: 'message' }} />
|
||||
))(() => {
|
||||
return {
|
||||
height: 'auto',
|
||||
marginTop: '0.5rem',
|
||||
alignItems: 'center',
|
||||
'& .message': {
|
||||
flex: 1,
|
||||
overflow: 'initial',
|
||||
},
|
||||
'& .content': {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
},
|
||||
'& .title': {
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
'@media screen and (max-width: 320px)': {
|
||||
'& .title': {
|
||||
fontSize: '14px',
|
||||
WebkitLineClamp: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export const StyledReloadButton = styled((props: AppButtonProps) => <Button startIcon={<RefreshIcon />} {...props} />)(
|
||||
({ theme }) => {
|
||||
const isDarkTheme = theme.palette.mode === 'dark'
|
||||
return {
|
||||
padding: '0.25rem 1rem',
|
||||
'&.button:is(:hover, :active, &)': {
|
||||
background: isDarkTheme ? '#b8e7fb' : '#014361',
|
||||
},
|
||||
'@media screen and (max-width: 320px)': {
|
||||
padding: '0.25rem 0.5rem',
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
@ -13,14 +13,8 @@ export const Warning: FC<WarningProps> = ({ hint, message, icon, ...restProps })
|
||||
<StyledContainer {...restProps}>
|
||||
{icon && <IconContainer>{icon}</IconContainer>}
|
||||
<Stack flex={1} direction={'column'} gap={'0.2rem'}>
|
||||
<Typography noWrap>
|
||||
{message}
|
||||
</Typography>
|
||||
{hint && (
|
||||
<Typography>
|
||||
{hint}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography noWrap>{message}</Typography>
|
||||
{hint && <Typography>{hint}</Typography>}
|
||||
</Stack>
|
||||
</StyledContainer>
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Avatar, Stack, Toolbar, Typography } from '@mui/material'
|
||||
import { Avatar, Stack, Toolbar, Typography, Divider, DividerProps, styled } from '@mui/material'
|
||||
import { StyledAppBar, StyledAppLogo, StyledAppName, StyledProfileContainer, StyledThemeButton } from './styled'
|
||||
import { Menu } from './components/Menu'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
@ -8,11 +8,14 @@ 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'
|
||||
|
||||
export const Header = () => {
|
||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useAppDispatch()
|
||||
const [needReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||
@ -30,7 +33,7 @@ export const Header = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledAppBar position="fixed">
|
||||
<StyledAppBar position={needReload ? 'relative' : 'fixed'}>
|
||||
<Toolbar sx={{ padding: '12px' }}>
|
||||
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}>
|
||||
{showProfile && (
|
||||
@ -56,6 +59,15 @@ export const Header = () => {
|
||||
{showProfile ? <ProfileMenu /> : <Menu />}
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
<StyledDivider />
|
||||
</StyledAppBar>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
left: 0,
|
||||
height: '2px',
|
||||
})
|
||||
|
@ -4,28 +4,21 @@ import { Avatar, ListItemIcon, MenuItem, Typography } from '@mui/material'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
type ListItemProfileProps = {
|
||||
onClickItem: () => void
|
||||
onClickItem: () => void
|
||||
} & DbKey
|
||||
|
||||
export const ListItemProfile: FC<ListItemProfileProps> = ({
|
||||
onClickItem,
|
||||
npub,
|
||||
}) => {
|
||||
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||
return (
|
||||
<MenuItem sx={{ gap: '0.5rem' }} onClick={onClickItem}>
|
||||
<ListItemIcon>
|
||||
<Avatar
|
||||
src={userAvatar}
|
||||
alt={userName}
|
||||
sx={{ width: 36, height: 36 }}
|
||||
>
|
||||
{avatarTitle}
|
||||
</Avatar>
|
||||
</ListItemIcon>
|
||||
<Typography variant='body2' noWrap>
|
||||
{userName}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
)
|
||||
export const ListItemProfile: FC<ListItemProfileProps> = ({ onClickItem, npub }) => {
|
||||
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||
return (
|
||||
<MenuItem sx={{ gap: '0.5rem' }} onClick={onClickItem}>
|
||||
<ListItemIcon>
|
||||
<Avatar src={userAvatar} alt={userName} sx={{ width: 36, height: 36 }}>
|
||||
{avatarTitle}
|
||||
</Avatar>
|
||||
</ListItemIcon>
|
||||
<Typography variant="body2" noWrap>
|
||||
{userName}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { FC } from 'react'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { Header } from './Header/Header'
|
||||
import { Container, ContainerProps, Divider, DividerProps, styled } from '@mui/material'
|
||||
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 [needReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||
const containerClassName = needReload ? 'reload' : ''
|
||||
|
||||
return (
|
||||
<StyledContainer maxWidth="md">
|
||||
<StyledContainer maxWidth="md" className={containerClassName}>
|
||||
<ReloadBadge />
|
||||
<Header />
|
||||
<StyledDivider />
|
||||
<main>
|
||||
<Outlet />
|
||||
</main>
|
||||
@ -21,17 +27,11 @@ 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)',
|
||||
},
|
||||
})
|
||||
|
||||
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
|
||||
position: 'absolute',
|
||||
top: '66px',
|
||||
width: '100%',
|
||||
left: 0,
|
||||
height: '2px',
|
||||
})
|
||||
|
@ -28,6 +28,7 @@ enum DECISION {
|
||||
export interface KeyInfo {
|
||||
npub: string
|
||||
nip05?: string
|
||||
name?: string
|
||||
locked: boolean
|
||||
}
|
||||
|
||||
@ -47,7 +48,7 @@ interface Pending {
|
||||
}
|
||||
|
||||
interface IAllowCallbackParams {
|
||||
backend: NDKNip46Backend
|
||||
backend: Nip46Backend
|
||||
npub: string
|
||||
id: string
|
||||
method: string
|
||||
@ -192,7 +193,7 @@ class Nip46Backend extends NDKNip46Backend {
|
||||
// }
|
||||
// }
|
||||
|
||||
// FIXME why do we need it? Just to print
|
||||
// FIXME why do we need it? Just to print
|
||||
// class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
|
||||
// readonly backend: NDKNip46Backend
|
||||
// readonly method: string
|
||||
@ -245,13 +246,8 @@ export class NoauthBackend {
|
||||
|
||||
const self = this
|
||||
swg.addEventListener('activate', (event) => {
|
||||
console.log('activate')
|
||||
// swg.addEventListener('activate', event => event.waitUntil(swg.clients.claim()));
|
||||
})
|
||||
|
||||
swg.addEventListener('install', (event) => {
|
||||
console.log('install')
|
||||
// swg.addEventListener('install', event => event.waitUntil(swg.skipWaiting()));
|
||||
console.log('activate new sw worker')
|
||||
this.reloadUI()
|
||||
})
|
||||
|
||||
swg.addEventListener('push', (event) => {
|
||||
@ -366,7 +362,7 @@ export class NoauthBackend {
|
||||
if (r.status !== 200 && r.status !== 201) {
|
||||
console.log('Fetch error', url, method, r.status)
|
||||
const body = await r.json()
|
||||
throw new Error('Failed to fetch ' + url, { cause: body })
|
||||
throw new Error('Failed to fetch ' + url, { cause: { body, status: r.status } })
|
||||
}
|
||||
|
||||
return await r.json()
|
||||
@ -501,13 +497,48 @@ export class NoauthBackend {
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.log('error', e.cause)
|
||||
if (e.cause && e.cause.minPow > pow) pow = e.cause.minPow
|
||||
if (e.cause && e.cause.body && e.cause.body.minPow > pow) pow = e.cause.body.minPow
|
||||
else throw e
|
||||
}
|
||||
}
|
||||
throw new Error('Too many requests, retry later')
|
||||
}
|
||||
|
||||
private async sendDeleteNameToServer(npub: string, name: string) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
name,
|
||||
})
|
||||
|
||||
const method = 'DELETE'
|
||||
const url = `${NOAUTHD_URL}/name`
|
||||
|
||||
return this.sendPostAuthd({
|
||||
npub,
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
private async sendTransferNameToServer(npub: string, name: string, newNpub: string) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
name,
|
||||
newNpub,
|
||||
})
|
||||
|
||||
const method = 'PUT'
|
||||
const url = `${NOAUTHD_URL}/name`
|
||||
|
||||
return this.sendPostAuthd({
|
||||
npub,
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
private async sendTokenToServer(npub: string, token: string) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
@ -602,6 +633,7 @@ export class NoauthBackend {
|
||||
return {
|
||||
npub: k.npub,
|
||||
nip05: k.nip05,
|
||||
name: k.name,
|
||||
locked: this.isLocked(k.npub),
|
||||
}
|
||||
}
|
||||
@ -642,11 +674,16 @@ export class NoauthBackend {
|
||||
await this.startKey({ npub, sk })
|
||||
|
||||
// assign nip05 before adding the key
|
||||
// FIXME set name to db and if this call to 'send' fails
|
||||
// then retry later
|
||||
if (!existingName && name && !name.includes('@')) {
|
||||
console.log('adding key', npub, name)
|
||||
await this.sendNameToServer(npub, name)
|
||||
try {
|
||||
await this.sendNameToServer(npub, name)
|
||||
} catch (e) {
|
||||
console.log('create name failed', e)
|
||||
// clear it
|
||||
await dbi.editName(npub, '')
|
||||
dbKey.name = ''
|
||||
}
|
||||
}
|
||||
|
||||
const sub = await this.swg.registration.pushManager.getSubscription()
|
||||
@ -655,7 +692,9 @@ export class NoauthBackend {
|
||||
return this.keyInfo(dbKey)
|
||||
}
|
||||
|
||||
private getDecision(req: DbPending): DECISION {
|
||||
private getDecision(backend: Nip46Backend, req: DbPending): DECISION {
|
||||
if (!(req.method in backend.handlers)) return DECISION.IGNORE
|
||||
|
||||
const reqPerm = getReqPerm(req)
|
||||
const appPerms = this.perms.filter((p) => p.npub === req.npub && p.appNpub === req.appNpub)
|
||||
|
||||
@ -766,7 +805,7 @@ export class NoauthBackend {
|
||||
return // noop
|
||||
case DECISION.ALLOW:
|
||||
case DECISION.DISALLOW:
|
||||
// fall through
|
||||
// fall through
|
||||
}
|
||||
|
||||
const allow = decision === DECISION.ALLOW
|
||||
@ -840,7 +879,7 @@ export class NoauthBackend {
|
||||
const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub)
|
||||
console.log('updated perms', this.perms, 'otherReqs', otherReqs, 'connected', connected)
|
||||
for (const r of otherReqs) {
|
||||
const dec = this.getDecision(r.req)
|
||||
const dec = this.getDecision(backend, r.req)
|
||||
if (dec !== DECISION.ASK) {
|
||||
r.cb(dec, false)
|
||||
}
|
||||
@ -849,7 +888,7 @@ export class NoauthBackend {
|
||||
}
|
||||
|
||||
// check perms
|
||||
const dec = this.getDecision(req)
|
||||
const dec = this.getDecision(backend, req)
|
||||
console.log(Date.now(), 'decision', req.id, dec)
|
||||
|
||||
// have perm?
|
||||
@ -874,12 +913,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()
|
||||
@ -902,6 +951,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 })
|
||||
@ -1116,6 +1167,36 @@ export class NoauthBackend {
|
||||
this.updateUI()
|
||||
}
|
||||
|
||||
private async editName(npub: string, name: string) {
|
||||
const key = this.enckeys.find((k) => k.npub === npub)
|
||||
if (!key) throw new Error('Npub not found')
|
||||
if (key.name) {
|
||||
try {
|
||||
await this.sendDeleteNameToServer(npub, key.name)
|
||||
} catch (e: any) {
|
||||
if (e.cause && e.cause.status !== 404) throw e
|
||||
console.log("Deleted name didn't exist")
|
||||
}
|
||||
}
|
||||
if (name) {
|
||||
await this.sendNameToServer(npub, name)
|
||||
}
|
||||
await dbi.editName(npub, name)
|
||||
key.name = name
|
||||
this.updateUI()
|
||||
}
|
||||
|
||||
private async transferName(npub: string, name: string, newNpub: string) {
|
||||
const key = this.enckeys.find((k) => k.npub === npub)
|
||||
if (!key) throw new Error('Npub not found')
|
||||
if (!name) throw new Error('Empty name')
|
||||
if (key.name !== name) throw new Error('Name changed, please reload')
|
||||
await this.sendTransferNameToServer(npub, key.name, newNpub)
|
||||
await dbi.editName(npub, '')
|
||||
key.name = ''
|
||||
this.updateUI()
|
||||
}
|
||||
|
||||
private async enablePush(): Promise<boolean> {
|
||||
const options = {
|
||||
userVisibleOnly: true,
|
||||
@ -1162,6 +1243,10 @@ export class NoauthBackend {
|
||||
result = await this.deleteApp(args[0])
|
||||
} else if (method === 'deletePerm') {
|
||||
result = await this.deletePerm(args[0])
|
||||
} else if (method === 'editName') {
|
||||
result = await this.editName(args[0], args[1])
|
||||
} else if (method === 'transferName') {
|
||||
result = await this.transferName(args[0], args[1], args[2])
|
||||
} else if (method === 'enablePush') {
|
||||
result = await this.enablePush()
|
||||
} else if (method === 'fetchPendingRequests') {
|
||||
@ -1192,10 +1277,20 @@ export class NoauthBackend {
|
||||
}
|
||||
}
|
||||
|
||||
private async reloadUI() {
|
||||
const clients = await this.swg.clients.matchAll({
|
||||
includeUncontrolled: true,
|
||||
})
|
||||
console.log('reloadUI clients', clients.length)
|
||||
for (const client of clients) {
|
||||
client.postMessage({ result: 'reload' })
|
||||
}
|
||||
}
|
||||
|
||||
public async onPush(event: any) {
|
||||
console.log('push', { data: event.data })
|
||||
// noop - we just need browser to launch this worker
|
||||
// FIXME use event.waitUntil and and unblock after we
|
||||
// show a notification
|
||||
// show a notification to avoid annoying the browser
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,16 @@ export const dbi = {
|
||||
return []
|
||||
}
|
||||
},
|
||||
editName: async (npub: string, name: string): Promise<void> => {
|
||||
try {
|
||||
await db.keys.where({ npub }).modify({
|
||||
name,
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(`db editName error: ${error}`)
|
||||
return
|
||||
}
|
||||
},
|
||||
getApp: async (appNpub: string) => {
|
||||
try {
|
||||
return await db.apps.get(appNpub)
|
||||
|
@ -43,7 +43,7 @@ export function isWeakPassphase(passphrase: string): boolean {
|
||||
const compl = Math.pow(base, passphrase.length)
|
||||
const thresh = Math.pow(11, 14)
|
||||
// console.log({ big, small, number, punct, base, compl, thresh });
|
||||
return compl < thresh;
|
||||
return compl < thresh
|
||||
}
|
||||
|
||||
export class Keys {
|
||||
|
@ -1,10 +1,12 @@
|
||||
// service-worker client interface
|
||||
// service-worker client interface,
|
||||
// works on the frontend, not sw
|
||||
import * as serviceWorkerRegistration from '../serviceWorkerRegistration'
|
||||
|
||||
export let swr: ServiceWorkerRegistration | null = null
|
||||
const reqs = new Map<number, { ok: (r: any) => void; rej: (r: any) => void }>()
|
||||
let nextReqId = 1
|
||||
let onRender: (() => void) | null = null
|
||||
let onReload: (() => void) | null = null
|
||||
const queue: (() => Promise<void> | void)[] = []
|
||||
|
||||
export async function swicRegister() {
|
||||
@ -14,7 +16,11 @@ export async function swicRegister() {
|
||||
swr = registration
|
||||
},
|
||||
onError(e) {
|
||||
console.log(`error ${e}`)
|
||||
console.log('sw error', e)
|
||||
},
|
||||
onUpdate() {
|
||||
// tell new SW that it should activate immediately
|
||||
swr?.waiting?.postMessage({ type: 'SKIP_WAITING' })
|
||||
},
|
||||
})
|
||||
|
||||
@ -27,8 +33,7 @@ export async function swicRegister() {
|
||||
console.log('This page is not currently controlled by a service worker.')
|
||||
}
|
||||
|
||||
while (queue.length)
|
||||
await (queue.shift()!)()
|
||||
while (queue.length) await queue.shift()!()
|
||||
})
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
@ -37,18 +42,22 @@ export async function swicRegister() {
|
||||
}
|
||||
|
||||
export function swicWaitStarted() {
|
||||
return new Promise<void>(ok => {
|
||||
return new Promise<void>((ok) => {
|
||||
if (swr && swr.active) ok()
|
||||
else queue.push(ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onMessage(data: any) {
|
||||
const { id, result, error } = data
|
||||
console.log('SW message', id, result, error)
|
||||
|
||||
if (!id) {
|
||||
if (onRender) onRender()
|
||||
if (result === 'reload') {
|
||||
if (onReload) onReload()
|
||||
} else {
|
||||
if (onRender) onRender()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -68,7 +77,6 @@ export async function swicCall(method: string, ...args: any[]) {
|
||||
nextReqId++
|
||||
|
||||
return new Promise((ok, rej) => {
|
||||
|
||||
const call = async () => {
|
||||
if (!swr || !swr.active) {
|
||||
rej(new Error('No active service worker'))
|
||||
@ -93,3 +101,7 @@ export async function swicCall(method: string, ...args: any[]) {
|
||||
export function swicOnRender(cb: () => void) {
|
||||
onRender = cb
|
||||
}
|
||||
|
||||
export function swicOnReload(cb: () => void) {
|
||||
onReload = cb
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useAppSelector } from '../../store/hooks/redux'
|
||||
import { Navigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { Stack } from '@mui/material'
|
||||
import { Box, IconButton, Stack } from '@mui/material'
|
||||
import { StyledIconButton } from './styled'
|
||||
import { SettingsIcon, ShareIcon } from '@/assets'
|
||||
import { Apps } from './components/Apps'
|
||||
@ -18,7 +19,9 @@ import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { checkNpubSyncQuerier } from './utils'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||
import MoreHorizRoundedIcon from '@mui/icons-material/MoreHorizRounded'
|
||||
import { ModalEditName } from '@/components/Modal/ModalEditName/ModalEditName'
|
||||
|
||||
const KeyPage = () => {
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
@ -60,6 +63,7 @@ const KeyPage = () => {
|
||||
|
||||
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
||||
const handleOpenEditNameModal = () => handleOpen(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -70,13 +74,20 @@ const KeyPage = () => {
|
||||
<UserValueSection
|
||||
title="Your login"
|
||||
value={username}
|
||||
copyValue={username}
|
||||
endAdornment={
|
||||
<Box display={'flex'} alignItems={'center'} gap={'0.25rem'}>
|
||||
<IconButton onClick={handleOpenEditNameModal} color={username ? 'default' : 'error'}>
|
||||
<MoreHorizRoundedIcon />
|
||||
</IconButton>
|
||||
<InputCopyButton value={username} />
|
||||
</Box>
|
||||
}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
|
||||
/>
|
||||
<UserValueSection
|
||||
title="Your NPUB"
|
||||
value={npub}
|
||||
copyValue={npub}
|
||||
endAdornment={<InputCopyButton value={npub} />}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||
/>
|
||||
|
||||
@ -98,11 +109,13 @@ const KeyPage = () => {
|
||||
|
||||
<Apps apps={filteredApps} npub={npub} />
|
||||
</Stack>
|
||||
|
||||
<ModalConnectApp />
|
||||
<ModalSettings isSynced={isSynced} />
|
||||
<ModalExplanation />
|
||||
<ModalConfirmConnect />
|
||||
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
|
||||
<ModalEditName />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -16,12 +16,7 @@ export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({ is
|
||||
Enable background service {isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
|
||||
</Stack>
|
||||
}
|
||||
hint={
|
||||
<Typography variant='body2'>
|
||||
Please allow notifications
|
||||
for background operation.
|
||||
</Typography>
|
||||
}
|
||||
hint={<Typography variant="body2">Please allow notifications for background operation.</Typography>}
|
||||
icon={<AutoModeOutlinedIcon htmlColor="white" />}
|
||||
onClick={isEnabling ? undefined : onEnableBackSigning}
|
||||
/>
|
||||
|
@ -3,7 +3,6 @@ import { Box, Stack } from '@mui/material'
|
||||
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { AppLink } from '@/shared/AppLink/AppLink'
|
||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||
import { StyledInput } from '../styled'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
|
||||
@ -11,10 +10,10 @@ type UserValueSectionProps = {
|
||||
title: string
|
||||
value: string
|
||||
explanationType: EXPLANATION_MODAL_KEYS
|
||||
copyValue: string
|
||||
endAdornment?: React.ReactNode
|
||||
}
|
||||
|
||||
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, copyValue }) => {
|
||||
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, endAdornment }) => {
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
|
||||
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
|
||||
@ -30,7 +29,7 @@ const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanation
|
||||
<SectionTitle>{title}</SectionTitle>
|
||||
<AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
|
||||
</Stack>
|
||||
<StyledInput value={value} readOnly endAdornment={<InputCopyButton value={copyValue} />} />
|
||||
<StyledInput value={value} readOnly endAdornment={endAdornment} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ const AppRoutes = () => {
|
||||
<Routes>
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route path="/" element={<Navigate to={'/home'} />} />
|
||||
{/* <Route path='/welcome' element={<WelcomePage />} /> */}
|
||||
<Route path="/home" element={<HomePage />} />
|
||||
<Route path="/key/:npub" element={<KeyPage />} />
|
||||
<Route path="/key/:npub/app/:appNpub" element={<AppPage />} />
|
||||
|
@ -39,9 +39,9 @@ const StyledButton = styled(
|
||||
background: theme.palette.primary.main,
|
||||
},
|
||||
color: theme.palette.text.secondary,
|
||||
'&.disabled': {
|
||||
'&.button.disabled': {
|
||||
color: theme.palette.text.secondary,
|
||||
background: `${theme.palette.primary.main}50`,
|
||||
background: `${theme.palette.primary.main}75`,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
}
|
||||
|
@ -26,7 +26,12 @@ export const Input = forwardRef<HTMLInputElement, AppInputProps>(
|
||||
{label}
|
||||
</FormLabel>
|
||||
) : null}
|
||||
<InputBase autoComplete="off" className="input" {...props} classes={{ error: 'error' }} ref={ref} />
|
||||
<InputBase
|
||||
autoComplete="off"
|
||||
{...props}
|
||||
classes={{ error: 'error', root: 'input_root', input: 'input', disabled: 'disabled' }}
|
||||
ref={ref}
|
||||
/>
|
||||
{helperText ? (
|
||||
<FormHelperText {...helperTextProps} className="helper_text">
|
||||
{helperText}
|
||||
@ -41,20 +46,20 @@ const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({
|
||||
const isDark = theme.palette.mode === 'dark'
|
||||
return {
|
||||
width: '100%',
|
||||
'& > .input': {
|
||||
'& > .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',
|
||||
'& input::placeholder': {
|
||||
color: '#fff',
|
||||
},
|
||||
'&.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,
|
||||
|
@ -2,16 +2,7 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'
|
||||
import { contentSlice } from './reducers/content.slice'
|
||||
import { uiSlice } from './reducers/ui.slice'
|
||||
|
||||
import {
|
||||
persistStore,
|
||||
persistReducer,
|
||||
// FLUSH,
|
||||
// REGISTER,
|
||||
// REHYDRATE,
|
||||
// PAUSE,
|
||||
// PERSIST,
|
||||
// PURGE,
|
||||
} from 'redux-persist'
|
||||
import { persistStore, persistReducer } from 'redux-persist'
|
||||
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web
|
||||
import memoizeOne from 'memoize-one'
|
||||
import isDeepEqual from 'lodash.isequal'
|
||||
|
@ -10,6 +10,7 @@ export enum MODAL_PARAMS_KEYS {
|
||||
CONFIRM_EVENT = 'confirm-event',
|
||||
ACTIVITY = 'activity',
|
||||
APP_DETAILS = 'app-details',
|
||||
EDIT_NAME = 'edit-name',
|
||||
}
|
||||
|
||||
export enum EXPLANATION_MODAL_KEYS {
|
||||
|
@ -9,6 +9,8 @@ export const MAX_POW = 19
|
||||
|
||||
export const KIND_RPC = 24133
|
||||
|
||||
export const RELOAD_STORAGE_KEY = 'reload'
|
||||
|
||||
export enum ACTION_TYPE {
|
||||
BASIC = 'basic',
|
||||
ADVANCED = 'advanced',
|
||||
@ -16,7 +18,7 @@ export enum ACTION_TYPE {
|
||||
}
|
||||
|
||||
export const ACTIONS: { [type: string]: string } = {
|
||||
basic: 'Basic permissions',
|
||||
basic: 'Basic permissions',
|
||||
get_public_key: 'Get public key',
|
||||
sign_event: 'Sign event',
|
||||
connect: 'Connect',
|
||||
|
@ -3,7 +3,7 @@ import { format } from 'date-fns'
|
||||
export const formatTimestampDate = (timestamp: number) => {
|
||||
try {
|
||||
const date = new Date(timestamp)
|
||||
const formattedDate = format(date, "dd-MM-yyyy HH:mm")
|
||||
const formattedDate = format(date, 'dd-MM-yyyy HH:mm')
|
||||
return formattedDate
|
||||
} catch (error) {
|
||||
return ''
|
||||
|
Reference in New Issue
Block a user