Compare commits

...

31 Commits

Author SHA1 Message Date
Bekbolsun
59c03d16eb add reload badge on sw update 2024-02-19 21:41:10 +06:00
Bekbolsun
adbc7d455d format code 2024-02-19 19:35:12 +06:00
artur
13f9bb13fd Fix ignore unknown methods 2024-02-19 14:54:00 +03:00
Nostr.Band
b1dd6a5424 Merge pull request #98 from nostrband/feature/sw-update
Feature/sw update
2024-02-19 14:14:37 +03:00
artur
e3feb8b5a2 Merge w/ develop 2024-02-19 14:12:51 +03:00
artur
06fa8ffbd7 Leave name empty if failed to assign at addKey 2024-02-19 14:10:15 +03:00
Nostr.Band
22753a8d89 Merge pull request #90 from nostrband/feature/hints
add hints
2024-02-19 11:03:22 +03:00
artur
6d4a8b4f64 Remove logos from signup modals, move signup hints to the top of modals, fix signup hints 2024-02-19 11:01:34 +03:00
Nostr.Band
425f7277fc Merge pull request #89 from nostrband/feature/edit-name
Feature/edit name
2024-02-19 10:38:45 +03:00
artur
ba3775e6c6 Make username settings button red if name empty, add sections to username settings, UI fixes to username settings, remove qs params from username settings 2024-02-19 10:37:06 +03:00
artur
8c77b10b60 Add sw activation logic 2024-02-19 09:56:46 +03:00
Bekbolsun
b98339e177 add hints 2024-02-16 20:09:33 +06:00
Bekbolsun
4ad66c8711 add transfer name field 2024-02-16 19:47:25 +06:00
artur
a60fcd65b5 Show app npub if app only has url 2024-02-16 15:29:06 +03:00
Bekbolsun
6a04c3ec4b Merge branch 'develop' of https://github.com/nostrband/noauth into feature/edit-name 2024-02-16 18:14:59 +06:00
Nostr.Band
93f6135baf Merge pull request #86 from nostrband/fix/enable-push-signup-error
Don't stop signup if enable-push failed
2024-02-16 15:08:20 +03:00
Bekbolsun
6d72cf1f82 implement edit username logic in edit modal 2024-02-16 17:59:59 +06:00
Nostr.Band
2e522b79ad Merge pull request #84 from nostrband/main
Merge w/ main
2024-02-16 14:48:48 +03:00
Nostr.Band
453a16690f Merge pull request #83 from nostrband/feature/ignore
Add ignore logic to stop interfering with replies from other instances
2024-02-16 14:48:16 +03:00
Nostr.Band
8ef8157c38 Merge pull request #81 from nostrband/feature/watcher
Feature/watcher
2024-02-16 13:34:29 +03:00
Nostr.Band
4f00a014d0 Merge pull request #80 from nostrband/feature/watcher
Feature/watcher
2024-02-16 13:33:50 +03:00
Nostr.Band
04373e7991 Merge pull request #79 from nostrband/develop
Show app npubs
2024-02-16 12:02:24 +03:00
Bekbolsun
d199dcf9f7 Merge branch 'feature/edit-name' of https://github.com/nostrband/noauth into feature/edit-name 2024-02-16 14:22:30 +06:00
artur
0f28c80a15 Add editName and transferName to backend 2024-02-16 09:47:46 +03:00
Nostr.Band
34b516a1e3 Merge pull request #71 from nostrband/develop
Many minor fixes in UI, spinners etc.
2024-02-15 09:28:45 +03:00
Nostr.Band
40f4a9922a Merge pull request #69 from nostrband/develop
Fix redirect to confirm connect w/ popup=true after login
2024-02-15 09:00:24 +03:00
Nostr.Band
4b1f7564e7 Merge pull request #68 from nostrband/develop
Add logic to confirm after login
2024-02-15 08:42:14 +03:00
Nostr.Band
83d5c013cf Merge pull request #65 from nostrband/develop
Show kind in sign-event in activity history, show import key without …
2024-02-14 11:40:45 +03:00
Nostr.Band
e96edf90fe Merge pull request #64 from nostrband/develop
Fix - close confirm event popup after confirmed
2024-02-14 10:51:12 +03:00
Nostr.Band
56e71219a5 Merge pull request #63 from nostrband/develop
Readme
2024-02-14 10:17:22 +03:00
Nostr.Band
67b6a3bfcf Merge pull request #62 from nostrband/develop
Develop
2024-02-14 09:58:06 +03:00
31 changed files with 529 additions and 142 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "Nsec.app", "name": "Nsec.app - Nostr key management tool",
"short_name": "Nsec.app - Nostr key management tool", "short_name": "Nsec.app",
"start_url": ".", "start_url": ".",
"icons": [ "icons": [
{ {

View File

@@ -1,6 +1,6 @@
import { DbKey, dbi } from './modules/db' import { DbKey, dbi } from './modules/db'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { swicOnRender } from './modules/swic' import { swicOnReload, swicOnRender } from './modules/swic'
import { useAppDispatch } from './store/hooks/redux' import { useAppDispatch } from './store/hooks/redux'
import { setApps, setKeys, setPending, setPerms } from './store/reducers/content.slice' import { setApps, setKeys, setPending, setPerms } from './store/reducers/content.slice'
import AppRoutes from './routes/AppRoutes' import AppRoutes from './routes/AppRoutes'
@@ -9,16 +9,17 @@ import { ModalInitial } from './components/Modal/ModalInitial/ModalInitial'
import { ModalImportKeys } from './components/Modal/ModalImportKeys/ModalImportKeys' import { ModalImportKeys } from './components/Modal/ModalImportKeys/ModalImportKeys'
import { ModalSignUp } from './components/Modal/ModalSignUp/ModalSignUp' import { ModalSignUp } from './components/Modal/ModalSignUp/ModalSignUp'
import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin' import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin'
import { useSearchParams } from 'react-router-dom'
function App() { function App() {
const [render, setRender] = useState(0) const [render, setRender] = useState(0)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const [searchParams, setSearchParams] = useSearchParams()
const [isConnected, setIsConnected] = useState(false) const [isConnected, setIsConnected] = useState(false)
const load = useCallback(async () => { const load = useCallback(async () => {
const keys: DbKey[] = await dbi.listKeys() const keys: DbKey[] = await dbi.listKeys()
// console.log(keys, 'keys')
dispatch(setKeys({ keys })) dispatch(setKeys({ keys }))
const loadProfiles = async () => { const loadProfiles = async () => {
@@ -55,7 +56,6 @@ function App() {
// rerender // rerender
// setRender((r) => r + 1) // setRender((r) => r + 1)
// if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
// eslint-disable-next-line // eslint-disable-next-line
}, [dispatch]) }, [dispatch])
@@ -77,9 +77,17 @@ function App() {
setRender((r) => r + 1) setRender((r) => r + 1)
}) })
// subscribe to service worker updates
swicOnReload(() => {
console.log('reload')
searchParams.set('reload', 'true')
setSearchParams(searchParams)
})
return ( return (
<> <>
<AppRoutes /> <AppRoutes />
<ModalInitial /> <ModalInitial />
<ModalImportKeys /> <ModalImportKeys />
<ModalSignUp /> <ModalSignUp />

View File

@@ -68,7 +68,6 @@ export const ModalAppDetails = () => {
if (isEmptyString(url)) return if (isEmptyString(url)) return
try { try {
const u = new URL(url) const u = new URL(url)
if (isEmptyString(name)) setDetails((prev) => ({ ...prev, name: u.hostname })) if (isEmptyString(name)) setDetails((prev) => ({ ...prev, name: u.hostname }))

View File

@@ -73,11 +73,11 @@ export const ModalConfirmConnect = () => {
useEffect(() => { useEffect(() => {
if (isModalOpened) { if (isModalOpened) {
if (isPopup) { if (isPopup) {
console.log("waiting for sw") console.log('waiting for sw')
// wait for SW to start // wait for SW to start
swicWaitStarted().then(() => { swicWaitStarted().then(() => {
// give it some time to load the pending reqs etc // 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) setTimeout(() => setIsLoaded(true), 500)
}) })
} else { } else {
@@ -123,7 +123,6 @@ export const ModalConfirmConnect = () => {
const options = { perms, appUrl } const options = { perms, appUrl }
await confirmPending(pendingReqId, true, true, options) await confirmPending(pendingReqId, true, true, options)
} else { } else {
try { try {
await askNotificationPermission() await askNotificationPermission()
const result = await swicCall('enablePush') const result = await swicCall('enablePush')

View 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>
)
}

View 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,
}))

View File

@@ -6,7 +6,6 @@ import { Input } from '@/shared/Input/Input'
import { Modal } from '@/shared/Modal/Modal' import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { Stack, Typography, useTheme } from '@mui/material' import { Stack, Typography, useTheme } from '@mui/material'
import { StyledAppLogo } from './styled'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { FormInputType, schema } from './const' import { FormInputType, schema } from './const'
@@ -149,13 +148,15 @@ export const ModalImportKeys = () => {
const nsecHelperText = getNsecHelperText() const nsecHelperText = getNsecHelperText()
return ( return (
<Modal open={isModalOpened} onClose={handleCloseModal}> <Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}> <Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}> <Stack gap={'0.2rem'} padding={'0 1rem'} alignSelf={'flex-start'}>
<StyledAppLogo />
<Typography fontWeight={600} variant="h5"> <Typography fontWeight={600} variant="h5">
Import key Import key
</Typography> </Typography>
<Typography noWrap variant="body2" color={'GrayText'}>
Bring your existing Nostr keys to Nsec.app
</Typography>
</Stack> </Stack>
<Input <Input
label="Choose a username" label="Choose a username"

View File

@@ -5,7 +5,6 @@ import { swicCall } from '@/modules/swic'
import { Modal } from '@/shared/Modal/Modal' import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { Stack, Typography } from '@mui/material' import { Stack, Typography } from '@mui/material'
import { StyledAppLogo } from './styled'
import { Input } from '@/shared/Input/Input' import { Input } from '@/shared/Input/Input'
import { Button } from '@/shared/Button/Button' import { Button } from '@/shared/Button/Button'
import { useNavigate, useSearchParams } from 'react-router-dom' import { useNavigate, useSearchParams } from 'react-router-dom'
@@ -100,7 +99,7 @@ export const ModalLogin = () => {
if (isPopup && isModalOpened) { if (isPopup && isModalOpened) {
swicCall('fetchPendingRequests', npub, appNpub) swicCall('fetchPendingRequests', npub, appNpub)
fetchNpubNames(npub).then(names => { fetchNpubNames(npub).then((names) => {
if (names.length) { if (names.length) {
setValue('username', `${names[0]}@${DOMAIN}`) setValue('username', `${names[0]}@${DOMAIN}`)
} }
@@ -119,13 +118,15 @@ export const ModalLogin = () => {
}, [isModalOpened, cleanUpStates]) }, [isModalOpened, cleanUpStates])
return ( return (
<Modal open={isModalOpened} onClose={handleCloseModal}> <Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}> <Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}> <Stack gap={'0.2rem'} padding={'0 1rem'} alignSelf={'flex-start'}>
<StyledAppLogo />
<Typography fontWeight={600} variant="h5"> <Typography fontWeight={600} variant="h5">
Login Login
</Typography> </Typography>
<Typography noWrap variant="body2" color={'GrayText'}>
Sync keys from the cloud to this device
</Typography>
</Stack> </Stack>
<Input <Input
label="Username or nip05 or npub" label="Username or nip05 or npub"
@@ -141,10 +142,14 @@ export const ModalLogin = () => {
{...register('password')} {...register('password')}
{...inputProps} {...inputProps}
error={!!errors.password} error={!!errors.password}
helperText={'Password you set in Cloud Sync settings'}
/> />
<Button type="submit" fullWidth disabled={isLoading}>
Add account {isLoading && <LoadingSpinner />} <Stack gap={'0.5rem'}>
</Button> <Button type="submit" fullWidth disabled={isLoading}>
Add account {isLoading && <LoadingSpinner />}
</Button>
</Stack>
</Stack> </Stack>
</Modal> </Modal>
) )

View File

@@ -4,7 +4,6 @@ import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { Stack, Typography, useTheme } from '@mui/material' import { Stack, Typography, useTheme } from '@mui/material'
import React, { ChangeEvent, useEffect, useState } from 'react' import React, { ChangeEvent, useEffect, useState } from 'react'
import { StyledAppLogo } from './styled'
import { Input } from '@/shared/Input/Input' import { Input } from '@/shared/Input/Input'
import { Button } from '@/shared/Button/Button' import { Button } from '@/shared/Button/Button'
import { CheckmarkIcon } from '@/assets' import { CheckmarkIcon } from '@/assets'
@@ -61,7 +60,8 @@ export const ModalSignUp = () => {
try { try {
setIsLoading(true) setIsLoading(true)
const k: any = await swicCall('generateKey', name) 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) setIsLoading(false)
setTimeout(() => { setTimeout(() => {
// give frontend time to read the new key first // give frontend time to read the new key first
@@ -84,13 +84,15 @@ export const ModalSignUp = () => {
}, [isModalOpened]) }, [isModalOpened])
return ( return (
<Modal open={isModalOpened} onClose={handleCloseModal}> <Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}> <Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}> <Stack gap={'0.2rem'} padding={'0 1rem'} alignSelf={'flex-start'}>
<StyledAppLogo />
<Typography fontWeight={600} variant="h5"> <Typography fontWeight={600} variant="h5">
Sign up Sign up
</Typography> </Typography>
<Typography noWrap variant="body2" color={'GrayText'}>
Generate new Nostr keys
</Typography>
</Stack> </Stack>
<Input <Input
label="Username" label="Username"
@@ -113,9 +115,11 @@ export const ModalSignUp = () => {
}, },
}} }}
/> />
<Button fullWidth type="submit" disabled={isLoading}> <Stack gap={'0.5rem'}>
Create account {isLoading && <LoadingSpinner />} <Button fullWidth type="submit" disabled={isLoading}>
</Button> Create account {isLoading && <LoadingSpinner />}
</Button>
</Stack>
</Stack> </Stack>
</Modal> </Modal>
) )

View File

@@ -0,0 +1,33 @@
import { FC } from 'react'
import { Collapse, Stack, Typography } from '@mui/material'
import { useSearchParams } from 'react-router-dom'
import { StyledAlert, StyledReloadButton } from './styled'
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>
)
}
export const ReloadBadge = () => {
const [searchParams] = useSearchParams()
const open = searchParams.get('reload') === 'true'
return <>{open && <ReloadBadgeContent />}</>
}

View 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',
},
}
}
)

View File

@@ -13,14 +13,8 @@ export const Warning: FC<WarningProps> = ({ hint, message, icon, ...restProps })
<StyledContainer {...restProps}> <StyledContainer {...restProps}>
{icon && <IconContainer>{icon}</IconContainer>} {icon && <IconContainer>{icon}</IconContainer>}
<Stack flex={1} direction={'column'} gap={'0.2rem'}> <Stack flex={1} direction={'column'} gap={'0.2rem'}>
<Typography noWrap> <Typography noWrap>{message}</Typography>
{message} {hint && <Typography>{hint}</Typography>}
</Typography>
{hint && (
<Typography>
{hint}
</Typography>
)}
</Stack> </Stack>
</StyledContainer> </StyledContainer>
) )

View File

@@ -1,7 +1,7 @@
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 { StyledAppBar, StyledAppLogo, StyledAppName, StyledProfileContainer, StyledThemeButton } from './styled'
import { Menu } from './components/Menu' import { Menu } from './components/Menu'
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { ProfileMenu } from './components/ProfileMenu' import { ProfileMenu } from './components/ProfileMenu'
import { useProfile } from '@/hooks/useProfile' import { useProfile } from '@/hooks/useProfile'
import DarkModeIcon from '@mui/icons-material/DarkMode' import DarkModeIcon from '@mui/icons-material/DarkMode'
@@ -13,6 +13,8 @@ export const Header = () => {
const themeMode = useAppSelector((state) => state.ui.themeMode) const themeMode = useAppSelector((state) => state.ui.themeMode)
const navigate = useNavigate() const navigate = useNavigate()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const [searchParams] = useSearchParams()
const needReload = searchParams.get('reload') === 'true'
const { npub = '' } = useParams<{ npub: string }>() const { npub = '' } = useParams<{ npub: string }>()
const { userName, userAvatar, avatarTitle } = useProfile(npub) const { userName, userAvatar, avatarTitle } = useProfile(npub)
@@ -30,7 +32,7 @@ export const Header = () => {
} }
return ( return (
<StyledAppBar position="fixed"> <StyledAppBar position={needReload ? 'relative' : 'fixed'}>
<Toolbar sx={{ padding: '12px' }}> <Toolbar sx={{ padding: '12px' }}>
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}> <Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}>
{showProfile && ( {showProfile && (
@@ -56,6 +58,15 @@ export const Header = () => {
{showProfile ? <ProfileMenu /> : <Menu />} {showProfile ? <ProfileMenu /> : <Menu />}
</Stack> </Stack>
</Toolbar> </Toolbar>
<StyledDivider />
</StyledAppBar> </StyledAppBar>
) )
} }
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
position: 'absolute',
bottom: 0,
width: '100%',
left: 0,
height: '2px',
})

View File

@@ -4,28 +4,21 @@ import { Avatar, ListItemIcon, MenuItem, Typography } from '@mui/material'
import React, { FC } from 'react' import React, { FC } from 'react'
type ListItemProfileProps = { type ListItemProfileProps = {
onClickItem: () => void onClickItem: () => void
} & DbKey } & DbKey
export const ListItemProfile: FC<ListItemProfileProps> = ({ export const ListItemProfile: FC<ListItemProfileProps> = ({ onClickItem, npub }) => {
onClickItem, const { userName, userAvatar, avatarTitle } = useProfile(npub)
npub, return (
}) => { <MenuItem sx={{ gap: '0.5rem' }} onClick={onClickItem}>
const { userName, userAvatar, avatarTitle } = useProfile(npub) <ListItemIcon>
return ( <Avatar src={userAvatar} alt={userName} sx={{ width: 36, height: 36 }}>
<MenuItem sx={{ gap: '0.5rem' }} onClick={onClickItem}> {avatarTitle}
<ListItemIcon> </Avatar>
<Avatar </ListItemIcon>
src={userAvatar} <Typography variant="body2" noWrap>
alt={userName} {userName}
sx={{ width: 36, height: 36 }} </Typography>
> </MenuItem>
{avatarTitle} )
</Avatar>
</ListItemIcon>
<Typography variant='body2' noWrap>
{userName}
</Typography>
</MenuItem>
)
} }

View File

@@ -1,13 +1,17 @@
import { FC } from 'react' import { FC } from 'react'
import { Outlet } from 'react-router-dom' import { Outlet, useSearchParams } from 'react-router-dom'
import { Header } from './Header/Header' 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'
export const Layout: FC = () => { export const Layout: FC = () => {
const [searchParams] = useSearchParams()
const needReload = searchParams.get('reload') === 'true'
return ( return (
<StyledContainer maxWidth="md"> <StyledContainer maxWidth="md" className={needReload ? 'reload' : ''}>
<ReloadBadge />
<Header /> <Header />
<StyledDivider />
<main> <main>
<Outlet /> <Outlet />
</main> </main>
@@ -21,17 +25,11 @@ const StyledContainer = styled((props: ContainerProps) => <Container maxWidth="s
flexDirection: 'column', flexDirection: 'column',
paddingBottom: '1rem', paddingBottom: '1rem',
position: 'relative', position: 'relative',
'& > main': { '&': {
flex: 1, flex: 1,
maxHeight: '100%', maxHeight: '100%',
},
'&:not(.reload) > main': {
paddingTop: 'calc(66px + 1rem)', paddingTop: 'calc(66px + 1rem)',
}, },
}) })
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
position: 'absolute',
top: '66px',
width: '100%',
left: 0,
height: '2px',
})

View File

@@ -28,6 +28,7 @@ enum DECISION {
export interface KeyInfo { export interface KeyInfo {
npub: string npub: string
nip05?: string nip05?: string
name?: string
locked: boolean locked: boolean
} }
@@ -47,7 +48,7 @@ interface Pending {
} }
interface IAllowCallbackParams { interface IAllowCallbackParams {
backend: NDKNip46Backend backend: Nip46Backend
npub: string npub: string
id: string id: string
method: string method: string
@@ -245,13 +246,8 @@ export class NoauthBackend {
const self = this const self = this
swg.addEventListener('activate', (event) => { swg.addEventListener('activate', (event) => {
console.log('activate') console.log('activate new sw worker')
// swg.addEventListener('activate', event => event.waitUntil(swg.clients.claim())); this.reloadUI()
})
swg.addEventListener('install', (event) => {
console.log('install')
// swg.addEventListener('install', event => event.waitUntil(swg.skipWaiting()));
}) })
swg.addEventListener('push', (event) => { swg.addEventListener('push', (event) => {
@@ -366,7 +362,7 @@ export class NoauthBackend {
if (r.status !== 200 && r.status !== 201) { if (r.status !== 200 && r.status !== 201) {
console.log('Fetch error', url, method, r.status) console.log('Fetch error', url, method, r.status)
const body = await r.json() 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() return await r.json()
@@ -501,13 +497,48 @@ export class NoauthBackend {
}) })
} catch (e: any) { } catch (e: any) {
console.log('error', e.cause) 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 else throw e
} }
} }
throw new Error('Too many requests, retry later') 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) { private async sendTokenToServer(npub: string, token: string) {
const body = JSON.stringify({ const body = JSON.stringify({
npub, npub,
@@ -602,6 +633,7 @@ export class NoauthBackend {
return { return {
npub: k.npub, npub: k.npub,
nip05: k.nip05, nip05: k.nip05,
name: k.name,
locked: this.isLocked(k.npub), locked: this.isLocked(k.npub),
} }
} }
@@ -642,11 +674,16 @@ export class NoauthBackend {
await this.startKey({ npub, sk }) await this.startKey({ npub, sk })
// assign nip05 before adding the key // 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('@')) { if (!existingName && name && !name.includes('@')) {
console.log('adding key', npub, name) 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() const sub = await this.swg.registration.pushManager.getSubscription()
@@ -655,7 +692,9 @@ export class NoauthBackend {
return this.keyInfo(dbKey) 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 reqPerm = getReqPerm(req)
const appPerms = this.perms.filter((p) => p.npub === req.npub && p.appNpub === req.appNpub) const appPerms = this.perms.filter((p) => p.npub === req.npub && p.appNpub === req.appNpub)
@@ -766,7 +805,7 @@ export class NoauthBackend {
return // noop return // noop
case DECISION.ALLOW: case DECISION.ALLOW:
case DECISION.DISALLOW: case DECISION.DISALLOW:
// fall through // fall through
} }
const allow = decision === DECISION.ALLOW const allow = decision === DECISION.ALLOW
@@ -840,7 +879,7 @@ export class NoauthBackend {
const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub) const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub)
console.log('updated perms', this.perms, 'otherReqs', otherReqs, 'connected', connected) console.log('updated perms', this.perms, 'otherReqs', otherReqs, 'connected', connected)
for (const r of otherReqs) { for (const r of otherReqs) {
const dec = this.getDecision(r.req) const dec = this.getDecision(backend, r.req)
if (dec !== DECISION.ASK) { if (dec !== DECISION.ASK) {
r.cb(dec, false) r.cb(dec, false)
} }
@@ -849,7 +888,7 @@ export class NoauthBackend {
} }
// check perms // check perms
const dec = this.getDecision(req) const dec = this.getDecision(backend, req)
console.log(Date.now(), 'decision', req.id, dec) console.log(Date.now(), 'decision', req.id, dec)
// have perm? // have perm?
@@ -1116,6 +1155,36 @@ export class NoauthBackend {
this.updateUI() 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> { private async enablePush(): Promise<boolean> {
const options = { const options = {
userVisibleOnly: true, userVisibleOnly: true,
@@ -1162,6 +1231,10 @@ export class NoauthBackend {
result = await this.deleteApp(args[0]) result = await this.deleteApp(args[0])
} else if (method === 'deletePerm') { } else if (method === 'deletePerm') {
result = await this.deletePerm(args[0]) 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') { } else if (method === 'enablePush') {
result = await this.enablePush() result = await this.enablePush()
} else if (method === 'fetchPendingRequests') { } else if (method === 'fetchPendingRequests') {
@@ -1192,10 +1265,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) { public async onPush(event: any) {
console.log('push', { data: event.data }) console.log('push', { data: event.data })
// noop - we just need browser to launch this worker // noop - we just need browser to launch this worker
// FIXME use event.waitUntil and and unblock after we // FIXME use event.waitUntil and and unblock after we
// show a notification // show a notification to avoid annoying the browser
} }
} }

View File

@@ -89,6 +89,16 @@ export const dbi = {
return [] 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) => { getApp: async (appNpub: string) => {
try { try {
return await db.apps.get(appNpub) return await db.apps.get(appNpub)

View File

@@ -43,7 +43,7 @@ export function isWeakPassphase(passphrase: string): boolean {
const compl = Math.pow(base, passphrase.length) const compl = Math.pow(base, passphrase.length)
const thresh = Math.pow(11, 14) const thresh = Math.pow(11, 14)
// console.log({ big, small, number, punct, base, compl, thresh }); // console.log({ big, small, number, punct, base, compl, thresh });
return compl < thresh; return compl < thresh
} }
export class Keys { export class Keys {

View File

@@ -1,10 +1,12 @@
// service-worker client interface // service-worker client interface,
// works on the frontend, not sw
import * as serviceWorkerRegistration from '../serviceWorkerRegistration' import * as serviceWorkerRegistration from '../serviceWorkerRegistration'
export let swr: ServiceWorkerRegistration | null = null export let swr: ServiceWorkerRegistration | null = null
const reqs = new Map<number, { ok: (r: any) => void; rej: (r: any) => void }>() const reqs = new Map<number, { ok: (r: any) => void; rej: (r: any) => void }>()
let nextReqId = 1 let nextReqId = 1
let onRender: (() => void) | null = null let onRender: (() => void) | null = null
let onReload: (() => void) | null = null
const queue: (() => Promise<void> | void)[] = [] const queue: (() => Promise<void> | void)[] = []
export async function swicRegister() { export async function swicRegister() {
@@ -14,7 +16,11 @@ export async function swicRegister() {
swr = registration swr = registration
}, },
onError(e) { 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.') console.log('This page is not currently controlled by a service worker.')
} }
while (queue.length) while (queue.length) await queue.shift()!()
await (queue.shift()!)()
}) })
navigator.serviceWorker.addEventListener('message', (event) => { navigator.serviceWorker.addEventListener('message', (event) => {
@@ -37,7 +42,7 @@ export async function swicRegister() {
} }
export function swicWaitStarted() { export function swicWaitStarted() {
return new Promise<void>(ok => { return new Promise<void>((ok) => {
if (swr && swr.active) ok() if (swr && swr.active) ok()
else queue.push(ok) else queue.push(ok)
}) })
@@ -48,7 +53,11 @@ function onMessage(data: any) {
console.log('SW message', id, result, error) console.log('SW message', id, result, error)
if (!id) { if (!id) {
if (onRender) onRender() if (result === 'reload') {
if (onReload) onReload()
} else {
if (onRender) onRender()
}
return return
} }
@@ -68,7 +77,6 @@ export async function swicCall(method: string, ...args: any[]) {
nextReqId++ nextReqId++
return new Promise((ok, rej) => { return new Promise((ok, rej) => {
const call = async () => { const call = async () => {
if (!swr || !swr.active) { if (!swr || !swr.active) {
rej(new Error('No active service worker')) rej(new Error('No active service worker'))
@@ -93,3 +101,7 @@ export async function swicCall(method: string, ...args: any[]) {
export function swicOnRender(cb: () => void) { export function swicOnRender(cb: () => void) {
onRender = cb onRender = cb
} }
export function swicOnReload(cb: () => void) {
onReload = cb
}

View File

@@ -46,7 +46,7 @@ const AppPage = () => {
const shortAppNpub = getShortenNpub(appNpub) const shortAppNpub = getShortenNpub(appNpub)
const appName = name || appDomain || shortAppNpub const appName = name || appDomain || shortAppNpub
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub) const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
const isAppNameExists = !!name const isAppNameExists = !!name || !!appDomain
const { timestamp } = connectPerm || {} const { timestamp } = connectPerm || {}
const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected' const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected'

View File

@@ -1,6 +1,7 @@
import { useCallback, useState } from 'react'
import { useAppSelector } from '../../store/hooks/redux' import { useAppSelector } from '../../store/hooks/redux'
import { Navigate, useParams, useSearchParams } from 'react-router-dom' 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 { StyledIconButton } from './styled'
import { SettingsIcon, ShareIcon } from '@/assets' import { SettingsIcon, ShareIcon } from '@/assets'
import { Apps } from './components/Apps' import { Apps } from './components/Apps'
@@ -18,7 +19,9 @@ import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
import { checkNpubSyncQuerier } from './utils' import { checkNpubSyncQuerier } from './utils'
import { DOMAIN } from '@/utils/consts' 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 KeyPage = () => {
const { npub = '' } = useParams<{ npub: string }>() const { npub = '' } = useParams<{ npub: string }>()
@@ -60,6 +63,7 @@ const KeyPage = () => {
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP) const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS) const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
const handleOpenEditNameModal = () => handleOpen(MODAL_PARAMS_KEYS.EDIT_NAME)
return ( return (
<> <>
@@ -70,13 +74,20 @@ const KeyPage = () => {
<UserValueSection <UserValueSection
title="Your login" title="Your login"
value={username} 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} explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
/> />
<UserValueSection <UserValueSection
title="Your NPUB" title="Your NPUB"
value={npub} value={npub}
copyValue={npub} endAdornment={<InputCopyButton value={npub} />}
explanationType={EXPLANATION_MODAL_KEYS.NPUB} explanationType={EXPLANATION_MODAL_KEYS.NPUB}
/> />
@@ -98,11 +109,13 @@ const KeyPage = () => {
<Apps apps={filteredApps} npub={npub} /> <Apps apps={filteredApps} npub={npub} />
</Stack> </Stack>
<ModalConnectApp /> <ModalConnectApp />
<ModalSettings isSynced={isSynced} /> <ModalSettings isSynced={isSynced} />
<ModalExplanation /> <ModalExplanation />
<ModalConfirmConnect /> <ModalConfirmConnect />
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} /> <ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
<ModalEditName />
</> </>
) )
} }

View File

@@ -16,12 +16,7 @@ export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({ is
Enable background service {isEnabling ? <CircularProgress size={'1.5rem'} /> : null} Enable background service {isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
</Stack> </Stack>
} }
hint={ hint={<Typography variant="body2">Please allow notifications for background operation.</Typography>}
<Typography variant='body2'>
Please allow notifications
for background operation.
</Typography>
}
icon={<AutoModeOutlinedIcon htmlColor="white" />} icon={<AutoModeOutlinedIcon htmlColor="white" />}
onClick={isEnabling ? undefined : onEnableBackSigning} onClick={isEnabling ? undefined : onEnableBackSigning}
/> />

View File

@@ -13,7 +13,7 @@ export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) =>
const appName = name || appDomain || shortAppNpub const appName = name || appDomain || shortAppNpub
const appIcon = icon || `https://${appDomain}/favicon.ico` const appIcon = icon || `https://${appDomain}/favicon.ico`
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub) const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
const isAppNameExists = !!name const isAppNameExists = !!name || !!appDomain
return ( return (
<StyledItemAppContainer <StyledItemAppContainer

View File

@@ -3,7 +3,6 @@ import { Box, Stack } from '@mui/material'
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal' import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle' import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
import { AppLink } from '@/shared/AppLink/AppLink' import { AppLink } from '@/shared/AppLink/AppLink'
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
import { StyledInput } from '../styled' import { StyledInput } from '../styled'
import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { useModalSearchParams } from '@/hooks/useModalSearchParams'
@@ -11,10 +10,10 @@ type UserValueSectionProps = {
title: string title: string
value: string value: string
explanationType: EXPLANATION_MODAL_KEYS 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 { handleOpen } = useModalSearchParams()
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => { const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
@@ -30,7 +29,7 @@ const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanation
<SectionTitle>{title}</SectionTitle> <SectionTitle>{title}</SectionTitle>
<AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} /> <AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
</Stack> </Stack>
<StyledInput value={value} readOnly endAdornment={<InputCopyButton value={copyValue} />} /> <StyledInput value={value} readOnly endAdornment={endAdornment} />
</Box> </Box>
) )
} }

View File

@@ -21,7 +21,6 @@ const AppRoutes = () => {
<Routes> <Routes>
<Route path="/" element={<Layout />}> <Route path="/" element={<Layout />}>
<Route path="/" element={<Navigate to={'/home'} />} /> <Route path="/" element={<Navigate to={'/home'} />} />
{/* <Route path='/welcome' element={<WelcomePage />} /> */}
<Route path="/home" element={<HomePage />} /> <Route path="/home" element={<HomePage />} />
<Route path="/key/:npub" element={<KeyPage />} /> <Route path="/key/:npub" element={<KeyPage />} />
<Route path="/key/:npub/app/:appNpub" element={<AppPage />} /> <Route path="/key/:npub/app/:appNpub" element={<AppPage />} />

View File

@@ -39,9 +39,9 @@ const StyledButton = styled(
background: theme.palette.primary.main, background: theme.palette.primary.main,
}, },
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
'&.disabled': { '&.button.disabled': {
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
background: `${theme.palette.primary.main}50`, background: `${theme.palette.primary.main}75`,
cursor: 'not-allowed', cursor: 'not-allowed',
}, },
} }

View File

@@ -26,7 +26,12 @@ export const Input = forwardRef<HTMLInputElement, AppInputProps>(
{label} {label}
</FormLabel> </FormLabel>
) : null} ) : 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 ? ( {helperText ? (
<FormHelperText {...helperTextProps} className="helper_text"> <FormHelperText {...helperTextProps} className="helper_text">
{helperText} {helperText}
@@ -41,22 +46,22 @@ const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({
const isDark = theme.palette.mode === 'dark' const isDark = theme.palette.mode === 'dark'
return { return {
width: '100%', width: '100%',
'& > .input': { '& > .input_root': {
background: isDark ? '#000000A8' : '#000', background: isDark ? '#000000A8' : '#000',
color: theme.palette.common.white, color: theme.palette.common.white,
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
borderRadius: '1rem', borderRadius: '1rem',
border: '0.3px solid #FFFFFF54', border: '0.3px solid #FFFFFF54',
fontSize: '0.875rem', fontSize: '0.875rem',
'& input::placeholder': {
color: '#fff',
},
'&.error': { '&.error': {
border: '0.3px solid ' + theme.palette.error.main, border: '0.3px solid ' + theme.palette.error.main,
}, },
}, },
'& .input:is(.disabled, &)': {
WebkitTextFillColor: '#ffffff80',
},
'& > .helper_text': { '& > .helper_text': {
margin: '0.5rem 1rem 0', margin: '0.5rem 0.5rem 0',
color: theme.palette.text.primary, color: theme.palette.text.primary,
}, },
'& > .label': { '& > .label': {

View File

@@ -2,16 +2,7 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { contentSlice } from './reducers/content.slice' import { contentSlice } from './reducers/content.slice'
import { uiSlice } from './reducers/ui.slice' import { uiSlice } from './reducers/ui.slice'
import { import { persistStore, persistReducer } from 'redux-persist'
persistStore,
persistReducer,
// FLUSH,
// REGISTER,
// REHYDRATE,
// PAUSE,
// PERSIST,
// PURGE,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web import storage from 'redux-persist/lib/storage' // defaults to localStorage for web
import memoizeOne from 'memoize-one' import memoizeOne from 'memoize-one'
import isDeepEqual from 'lodash.isequal' import isDeepEqual from 'lodash.isequal'

View File

@@ -10,6 +10,7 @@ export enum MODAL_PARAMS_KEYS {
CONFIRM_EVENT = 'confirm-event', CONFIRM_EVENT = 'confirm-event',
ACTIVITY = 'activity', ACTIVITY = 'activity',
APP_DETAILS = 'app-details', APP_DETAILS = 'app-details',
EDIT_NAME = 'edit-name',
} }
export enum EXPLANATION_MODAL_KEYS { export enum EXPLANATION_MODAL_KEYS {

View File

@@ -16,7 +16,7 @@ export enum ACTION_TYPE {
} }
export const ACTIONS: { [type: string]: string } = { export const ACTIONS: { [type: string]: string } = {
basic: 'Basic permissions', basic: 'Basic permissions',
get_public_key: 'Get public key', get_public_key: 'Get public key',
sign_event: 'Sign event', sign_event: 'Sign event',
connect: 'Connect', connect: 'Connect',

View File

@@ -3,7 +3,7 @@ import { format } from 'date-fns'
export const formatTimestampDate = (timestamp: number) => { export const formatTimestampDate = (timestamp: number) => {
try { try {
const date = new Date(timestamp) 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 return formattedDate
} catch (error) { } catch (error) {
return '' return ''