Merge branch 'develop' of https://github.com/nostrband/noauth into develop

This commit is contained in:
artur 2024-01-30 11:19:35 +03:00
commit cddf0b7805
34 changed files with 519 additions and 96 deletions

32
package-lock.json generated
View File

@ -23,7 +23,9 @@
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^18.2.17",
"crypto": "^1.0.1",
"date-fns": "^3.3.1",
"dexie": "^3.2.4",
"dexie-react-hooks": "^1.1.7",
"lodash.isequal": "^4.5.0",
"memoize-one": "^6.0.0",
"nostr-tools": "^1.17.0",
@ -7245,6 +7247,15 @@
"node": ">=10"
}
},
"node_modules/date-fns": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz",
"integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -7465,6 +7476,16 @@
"node": ">=6.0"
}
},
"node_modules/dexie-react-hooks": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.7.tgz",
"integrity": "sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==",
"peerDependencies": {
"@types/react": ">=16",
"dexie": "^3.2 || ^4.0.1-alpha",
"react": ">=16"
}
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -23335,6 +23356,11 @@
"whatwg-url": "^8.0.0"
}
},
"date-fns": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz",
"integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -23496,6 +23522,12 @@
"resolved": "https://registry.npmjs.org/dexie/-/dexie-3.2.4.tgz",
"integrity": "sha512-VKoTQRSv7+RnffpOJ3Dh6ozknBqzWw/F3iqMdsZg958R0AS8AnY9x9d1lbwENr0gzeGJHXKcGhAMRaqys6SxqA=="
},
"dexie-react-hooks": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.7.tgz",
"integrity": "sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==",
"requires": {}
},
"didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",

View File

@ -18,7 +18,9 @@
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^18.2.17",
"crypto": "^1.0.1",
"date-fns": "^3.3.1",
"dexie": "^3.2.4",
"dexie-react-hooks": "^1.1.7",
"lodash.isequal": "^4.5.0",
"memoize-one": "^6.0.0",
"nostr-tools": "^1.17.0",

View File

@ -1,7 +1,7 @@
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { call, getShortenNpub } from '@/utils/helpers'
import { call, getShortenNpub } from '@/utils/helpers/helpers'
import { Avatar, Box, Stack, Typography } from '@mui/material'
import { useParams, useSearchParams } from 'react-router-dom'
import { useAppSelector } from '@/store/hooks/redux'

View File

@ -1,7 +1,7 @@
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { call, getShortenNpub, getSignReqKind } from '@/utils/helpers'
import { call, getShortenNpub, getSignReqKind } from '@/utils/helpers/helpers'
import {
Avatar,
Box,
@ -47,6 +47,7 @@ type ModalConfirmEventProps = {
export const ACTIONS: { [type: string]: string } = {
get_public_key: 'Get public key',
sign_event: 'Sign event',
connect: 'Connect',
nip04_encrypt: 'Encrypt message',
nip04_decrypt: 'Decrypt message',
}
@ -134,8 +135,7 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
const action = ACTIONS[req.method]
if (req.method === 'sign_event') {
const kind = getSignReqKind(req)
if (kind !== undefined)
return `${action} of kind ${kind}`
if (kind !== undefined) return `${action} of kind ${kind}`
}
return action
}

View File

@ -6,7 +6,7 @@ import { Input } from '@/shared/Input/Input'
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { getBunkerLink } from '@/utils/helpers'
import { getBunkerLink } from '@/utils/helpers/helpers'
import { Stack, Typography } from '@mui/material'
import { useRef } from 'react'
import { useParams } from 'react-router-dom'

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Button } from '@/shared/Button/Button'
import { Modal } from '@/shared/Modal/Modal'
@ -18,9 +18,17 @@ export const ModalInitial = () => {
setShowAdvancedContent(true)
}
useEffect(() => {
return () => {
if (isModalOpened) {
setShowAdvancedContent(false)
}
}
}, [isModalOpened])
return (
<Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack paddingTop={'1rem'} gap={'1rem'}>
<Stack paddingTop={'2.5rem'} gap={'1rem'}>
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}>
Sign up
</Button>

View File

@ -11,6 +11,7 @@ import { Input } from '@/shared/Input/Input'
import { Button } from '@/shared/Button/Button'
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
import { useNavigate } from 'react-router-dom'
export const ModalLogin = () => {
const { getModalOpened, handleClose } = useModalSearchParams()
@ -19,6 +20,8 @@ export const ModalLogin = () => {
const notify = useEnqueueSnackbar()
const navigate = useNavigate()
const [enteredUsername, setEnteredUsername] = useState('')
const [enteredPassword, setEnteredPassword] = useState('')
const [isPasswordShown, setIsPasswordShown] = useState(false)
@ -37,9 +40,9 @@ export const ModalLogin = () => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
const user = enteredUsername.split('@')[0]
const [username, domain] = enteredUsername.split('@')
const response = await fetch(
'https://domain.com/.well-known/nostr.json?name=' + user,
`https://${domain}/.well-known/nostr.json?name=${username}`,
)
const getNpub: {
names: {
@ -47,13 +50,13 @@ export const ModalLogin = () => {
}
} = await response.json()
const pubkey = getNpub.names[user]
const pubkey = getNpub.names[username]
const npub = nip19.npubEncode(pubkey)
const passphrase = enteredPassword
console.log('fetch', npub, passphrase)
const k: any = await swicCall('fetchKey', npub, passphrase)
notify(`Fetched ${k.npub}`, 'success')
navigate(`/key/${k.npub}`)
} catch (error: any) {
notify(error.message, 'error')
}

View File

@ -15,9 +15,15 @@ import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
import { ChangeEvent, useState } from 'react'
import { Checkbox } from '@/shared/Checkbox/Checkbox'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { swicCall } from '@/modules/swic'
import { useParams } from 'react-router-dom'
export const ModalSettings = () => {
const { getModalOpened, handleClose } = useModalSearchParams()
const { npub = '' } = useParams<{ npub: string }>()
const notify = useEnqueueSnackbar()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS)
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.SETTINGS)
@ -27,22 +33,16 @@ export const ModalSettings = () => {
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
const [isPasswordSynched, setIsPasswordSynched] = useState(false)
const [isChecked, setIsChecked] = useState(false)
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
setIsPasswordInvalid(false)
setEnteredPassword(e.target.value)
}
const handlePasswordTypeChange = () =>
setIsPasswordShown((prevState) => !prevState)
const handleSync = () => {
setIsPasswordInvalid(false)
if (enteredPassword.trim().length < 6) {
return setIsPasswordInvalid(true)
}
setIsPasswordSynched(true)
}
const onClose = () => {
handleCloseModal()
setEnteredPassword('')
@ -50,10 +50,33 @@ export const ModalSettings = () => {
setIsPasswordSynched(false)
}
const handleChangeCheckbox = (e: unknown, checked: boolean) => {
setIsChecked(checked)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsPasswordInvalid(false)
setIsPasswordSynched(false)
if (enteredPassword.trim().length < 6) {
return setIsPasswordInvalid(true)
}
try {
await swicCall('saveKey', npub, enteredPassword)
notify('Key saved', 'success')
setIsPasswordInvalid(false)
setIsPasswordSynched(true)
} catch (error) {
setIsPasswordInvalid(false)
setIsPasswordSynched(false)
}
}
return (
<Modal open={isModalOpened} onClose={onClose} title='Settings'>
<Stack gap={'1rem'}>
<StyledSettingContainer>
<StyledSettingContainer onSubmit={handleSubmit}>
<Stack direction={'row'} justifyContent={'space-between'}>
<SectionTitle>Cloud sync</SectionTitle>
{isPasswordSynched && (
@ -63,7 +86,10 @@ export const ModalSettings = () => {
)}
</Stack>
<Box>
<Checkbox />
<Checkbox
onChange={handleChangeCheckbox}
checked={isChecked}
/>
<Typography variant='caption'>
Use this login on multiple devices
</Typography>
@ -82,7 +108,7 @@ export const ModalSettings = () => {
)}
</IconButton>
}
type={isPasswordShown ? 'password' : 'text'}
type={isPasswordShown ? 'text' : 'password'}
onChange={handlePasswordChange}
value={enteredPassword}
helperText={isPasswordInvalid ? 'Invalid password' : ''}
@ -94,8 +120,9 @@ export const ModalSettings = () => {
},
},
}}
disabled={!isChecked}
/>
<StyledButton type='button' fullWidth onClick={handleSync}>
<StyledButton type='submit' fullWidth disabled={!isChecked}>
Sync
</StyledButton>
</StyledSettingContainer>

View File

@ -8,7 +8,7 @@ import {
} from '@mui/material'
export const StyledSettingContainer = styled((props: StackProps) => (
<Stack {...props} gap={'1rem'} />
<Stack gap={'1rem'} component={'form'} {...props} />
))(({ theme }) => ({
padding: '0.75rem',
borderRadius: '1rem',

View File

@ -8,16 +8,18 @@ import { StyledAppLogo } from './styled'
import { Input } from '@/shared/Input/Input'
import { Button } from '@/shared/Button/Button'
import { CheckmarkIcon } from '@/assets'
import { swicCall } from '@/modules/swic'
import { useNavigate } from 'react-router-dom'
export const ModalSignUp = () => {
const { getModalOpened, handleClose } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP)
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.SIGN_UP)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const notify = useEnqueueSnackbar()
const theme = useTheme()
const navigate = useNavigate()
const [enteredValue, setEnteredValue] = useState('')
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
@ -36,6 +38,13 @@ export const ModalSignUp = () => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
const k: any = await swicCall('generateKey')
notify(`New key ${k.npub}`, 'success')
navigate(`/key/${k.npub}`)
} catch (error: any) {
notify(error.message, 'error')
}
}
return (
@ -77,7 +86,9 @@ export const ModalSignUp = () => {
},
}}
/>
<Button fullWidth>Sign up</Button>
<Button fullWidth type='submit'>
Sign up
</Button>
</Stack>
</Modal>
)

View File

@ -7,7 +7,7 @@ import { useCallback, useEffect, useState } from 'react'
import { MetaEvent } from '@/types/meta-event'
import { fetchProfile } from '@/modules/nostr'
import { ProfileMenu } from './components/ProfileMenu'
import { getShortenNpub } from '@/utils/helpers'
import { getShortenNpub } from '@/utils/helpers/helpers'
export const Header = () => {
const { npub = '' } = useParams<{ npub: string }>()

View File

@ -1,5 +1,5 @@
import { DbKey } from '@/modules/db'
import { getShortenNpub } from '@/utils/helpers'
import { getShortenNpub } from '@/utils/helpers/helpers'
import {
Avatar,
ListItemIcon,

View File

@ -2,6 +2,7 @@ import { Menu as MuiMenu } from '@mui/material'
import DarkModeIcon from '@mui/icons-material/DarkMode'
import LightModeIcon from '@mui/icons-material/LightMode'
import LoginIcon from '@mui/icons-material/Login'
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
import { setThemeMode } from '@/store/reducers/ui.slice'
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
@ -10,13 +11,16 @@ import { MenuButton } from './styled'
import { useOpenMenu } from '@/hooks/useOpenMenu'
import { MenuItem } from './MenuItem'
import MenuRoundedIcon from '@mui/icons-material/MenuRounded'
import { selectKeys } from '@/store'
export const Menu = () => {
const themeMode = useAppSelector((state) => state.ui.themeMode)
const keys = useAppSelector(selectKeys)
const dispatch = useAppDispatch()
const { handleOpen } = useModalSearchParams()
const isDarkMode = themeMode === 'dark'
const isNoKeys = !keys || keys.length === 0
const {
anchorEl,
@ -53,9 +57,11 @@ export const Menu = () => {
}}
>
<MenuItem
Icon={<LoginIcon />}
Icon={
isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />
}
onClick={handleNavigateToAuth}
title='Sign up'
title={isNoKeys ? 'Sign up' : 'Add account'}
/>
<MenuItem
Icon={themeIcon}

View File

@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom'
import LoginIcon from '@mui/icons-material/Login'
import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded'
import HomeRoundedIcon from '@mui/icons-material/HomeRounded'
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
import { selectKeys } from '@/store'
import { setThemeMode } from '@/store/reducers/ui.slice'
@ -26,6 +27,7 @@ export const ProfileMenu = () => {
const { handleOpen } = useModalSearchParams()
const keys = useAppSelector(selectKeys)
const isNoKeys = !keys || keys.length === 0
const themeMode = useAppSelector((state) => state.ui.themeMode)
const isDarkMode = themeMode === 'dark'
@ -84,9 +86,11 @@ export const ProfileMenu = () => {
title='Home'
/>
<MenuItem
Icon={<LoginIcon />}
Icon={
isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />
}
onClick={handleNavigateToAuth}
title='Sign up'
title={isNoKeys ? 'Sign up' : 'Add account'}
/>
<MenuItem
Icon={themeIcon}

View File

@ -10,7 +10,7 @@ import NDK, {
} from '@nostr-dev-kit/ndk'
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS } from '../utils/consts'
import { Nip04 } from './nip04'
import { getReqPerm, isPackagePerm } from '@/utils/helpers'
import { getReqPerm, isPackagePerm } from '@/utils/helpers/helpers'
//import { PrivateKeySigner } from './signer'
//const PERF_TEST = false
@ -139,7 +139,6 @@ class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
}
}
export class NoauthBackend {
readonly swg: ServiceWorkerGlobalScope
private keysModule: Keys
@ -443,18 +442,15 @@ export class NoauthBackend {
private getPerm(req: DbPending): string {
const reqPerm = getReqPerm(req)
const appPerms = this.perms.filter(
(p) =>
p.npub === req.npub &&
p.appNpub === req.appNpub
(p) => p.npub === req.npub && p.appNpub === req.appNpub,
)
// exact match first
let perm = appPerms.find((p) => p.perm === reqPerm)
// non-exact next
if (!perm)
perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm))
if (!perm) perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm))
console.log("req", req, "perm", reqPerm, "value", perm, appPerms);
console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms)
return perm?.value || ''
}
@ -489,7 +485,7 @@ export class NoauthBackend {
manual: boolean,
allow: boolean,
remember: boolean,
options?: any
options?: any,
) => {
// confirm
console.log(
@ -535,10 +531,8 @@ export class NoauthBackend {
if (index >= 0) self.confirmBuffer.splice(index, 1)
if (remember) {
let perm = getReqPerm(req)
if (allow && options && options.perm)
perm = options.perm
if (allow && options && options.perm) perm = options.perm
await dbi.addPerm({
id: req.id,
@ -554,10 +548,15 @@ export class NoauthBackend {
const otherReqs = self.confirmBuffer.filter(
(r) => r.req.appNpub === req.appNpub,
)
console.log("updated perms", this.perms, "otherReqs", otherReqs)
console.log(
'updated perms',
this.perms,
'otherReqs',
otherReqs,
)
for (const r of otherReqs) {
const perm = this.getPerm(r.req);
// if (r.req.method === req.method) {
const perm = this.getPerm(r.req)
// if (r.req.method === req.method) {
if (perm) {
r.cb(perm === '1', false)
}
@ -590,7 +589,8 @@ export class NoauthBackend {
// put to a list of pending requests
this.confirmBuffer.push({
req,
cb: (allow, remember, options) => onAllow(true, allow, remember, options),
cb: (allow, remember, options) =>
onAllow(true, allow, remember, options),
})
// show notifs
@ -753,7 +753,12 @@ export class NoauthBackend {
return k
}
private async confirm(id: string, allow: boolean, remember: boolean, options?: any) {
private async confirm(
id: string,
allow: boolean,
remember: boolean,
options?: any,
) {
const req = this.confirmBuffer.find((r) => r.req.id === id)
if (!req) {
console.log('req ', id, 'not found')

View File

@ -1,7 +0,0 @@
import React from 'react'
const AppPage = () => {
return <div>AppPage</div>
}
export default AppPage

View File

@ -0,0 +1,95 @@
import { useLiveQuery } from 'dexie-react-hooks'
import { DbHistory, db } from '@/modules/db'
import { useParams } from 'react-router'
import { useAppSelector } from '@/store/hooks/redux'
import { selectAppByAppNpub, selectPermsByNpubAndAppNpub } from '@/store'
import { Navigate } from 'react-router-dom'
import { formatTimestampDate } from '@/utils/helpers/date'
import { Avatar, Box, Stack, Typography } from '@mui/material'
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
import { getShortenNpub } from '@/utils/helpers/helpers'
import { PermissionMenuButton } from './styled'
import { PermissionsMenu } from './components/PermissionsMenu'
import { useOpenMenu } from '@/hooks/useOpenMenu'
import { ActivityList } from './components/ActivityList'
const getAppHistoryQuery = (appNpub: string) =>
db.history.where('appNpub').equals(appNpub).toArray()
const AppPage = () => {
const { appNpub = '', npub = '' } = useParams()
const perms = useAppSelector((state) =>
selectPermsByNpubAndAppNpub(state, npub, appNpub),
)
const currentApp = useAppSelector((state) =>
selectAppByAppNpub(state, appNpub),
)
const history = useLiveQuery(
() => {
if (!appNpub.trim().length) return []
return getAppHistoryQuery(appNpub)
},
[],
[] as DbHistory[],
)
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
const connectPerm = perms.find((perm) => perm.perm === 'connect')
if (!currentApp) {
return <Navigate to={`/key/${npub}`} />
}
const { icon = '', name = '' } = currentApp || {}
const appName = name || getShortenNpub(appNpub)
const { timestamp } = connectPerm || {}
const connectedOn =
connectPerm && timestamp
? `Connected at ${formatTimestampDate(timestamp)}`
: 'Not connected'
return (
<Stack maxHeight={'100%'} overflow={'auto'}>
<Stack
marginBottom={'1rem'}
direction={'row'}
gap={'1rem'}
width={'100%'}
>
<Avatar
src={icon}
sx={{
width: 70,
height: 70,
}}
variant='rounded'
/>
<Box flex={'1'} overflow={'hidden'}>
<Typography variant='h4' noWrap>
{appName}
</Typography>
<Typography variant='body2' noWrap>
{connectedOn}
</Typography>
</Box>
</Stack>
<Box marginBottom={'1rem'}>
<SectionTitle marginBottom={'0.5rem'}>Permissions</SectionTitle>
<PermissionMenuButton onClick={handleOpen}>
Basic/Advanced/Custom {perms.length}
</PermissionMenuButton>
<PermissionsMenu
open={open}
anchorEl={anchorEl}
perms={perms}
onClose={handleClose}
/>
</Box>
<ActivityList history={history} />
</Stack>
)
}
export default AppPage

View File

@ -0,0 +1,53 @@
import React, { FC } from 'react'
import { DbHistory } from '@/modules/db'
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
import { Box, IconButton, Stack, Typography } from '@mui/material'
import MoreIcon from '@mui/icons-material/MoreVert'
import { ACTIONS } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent'
import { formatTimestampDate } from '@/utils/helpers/date'
import { StyledButton } from './styled'
type ActivityListProps = {
history: DbHistory[]
}
export const ActivityList: FC<ActivityListProps> = ({ history = [] }) => {
return (
<>
<SectionTitle marginBottom={'0.5rem'}>Activity</SectionTitle>
<Box
flex={1}
overflow={'auto'}
display={'flex'}
flexDirection={'column'}
gap={'0.5rem'}
>
{history.map((h) => {
return (
<Stack>
<Box
width={'100%'}
display={'flex'}
gap={'0.5rem'}
alignItems={'center'}
>
<Typography flex={1} fontWeight={700}>
{ACTIONS[h.method] || h.method}
</Typography>
<StyledButton>
{h.allowed ? 'allow' : 'disallow'}
</StyledButton>
<IconButton>
<MoreIcon />
</IconButton>
</Box>
<Typography variant='caption'>
{formatTimestampDate(h.timestamp)}
</Typography>
</Stack>
)
})}
</Box>
</>
)
}

View File

@ -0,0 +1,37 @@
import { FC } from 'react'
import { Box, Typography } from '@mui/material'
import { DbPerm } from '@/modules/db'
import { ACTIONS } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent'
import { StyledMenuItem } from './styled'
import { formatTimestampDate } from '@/utils/helpers/date'
type ItemPermissionMenuProps = {
permission: DbPerm
}
export const ItemPermissionMenu: FC<ItemPermissionMenuProps> = ({
permission,
}) => {
const { perm, value, timestamp } = permission || {}
return (
<StyledMenuItem>
<Box
width={'100%'}
display={'flex'}
gap={'0.5rem'}
alignItems={'center'}
>
<Typography flex={1} fontWeight={700}>
{ACTIONS[perm] || perm}
</Typography>
<Typography textTransform={'capitalize'} variant='body2'>
{value === '1' ? 'allow' : 'disallow'}
</Typography>
</Box>
<Typography variant='body2'>
{formatTimestampDate(timestamp)}
</Typography>
</StyledMenuItem>
)
}

View File

@ -0,0 +1,26 @@
import { DbPerm } from '@/modules/db'
import { Menu, MenuItem, MenuProps } from '@mui/material'
import { FC } from 'react'
import { ItemPermissionMenu } from './ItemPermissionMenu'
type PermissionsMenuProps = {
perms: DbPerm[]
} & MenuProps
export const PermissionsMenu: FC<PermissionsMenuProps> = ({
perms,
open,
anchorEl,
onClose,
}) => {
const isNoPerms = perms.length === 0
return (
<Menu open={open} anchorEl={anchorEl} onClose={onClose}>
{isNoPerms && <MenuItem>No permissions</MenuItem>}
{!isNoPerms &&
perms.map((perm) => (
<ItemPermissionMenu permission={perm} key={perm.id} />
))}
</Menu>
)
}

View File

@ -0,0 +1,18 @@
import { Button } from '@/shared/Button/Button'
import { MenuItem, MenuItemProps, styled } from '@mui/material'
export const StyledMenuItem = styled((props: MenuItemProps) => (
<MenuItem {...props} />
))(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: '0.5rem',
'&:not(:first-of-type)': {
borderTop: '1px solid ' + theme.palette.secondary.main,
},
}))
export const StyledButton = styled(Button)({
textTransform: 'capitalize',
})

View File

@ -0,0 +1,6 @@
import { AppButtonProps, Button } from '@/shared/Button/Button'
import { styled } from '@mui/material'
export const PermissionMenuButton = styled((props: AppButtonProps) => (
<Button {...props} variant='outlined' fullWidth />
))(() => ({}))

View File

@ -1,19 +1,43 @@
import { useAppSelector } from '../../store/hooks/redux'
import { selectKeys } from '../../store'
import { Fragment } from 'react'
import { ItemKey } from './components/ItemKey'
import { Stack } from '@mui/material'
import { SectionTitle } from '../../shared/SectionTitle/SectionTitle'
import { Box, Stack, Typography } from '@mui/material'
import { AddAccountButton } from './styled'
import { useAppSelector } from '@/store/hooks/redux'
import { selectKeys } from '@/store'
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
const HomePage = () => {
const keys = useAppSelector(selectKeys)
const isNoKeys = !keys || keys.length === 0
const { handleOpen } = useModalSearchParams()
const handleClickAddAccount = () => handleOpen(MODAL_PARAMS_KEYS.INITIAL)
return (
<Stack>
<SectionTitle marginBottom={'0.5rem'}>Keys:</SectionTitle>
<Stack gap={'0.5rem'}>
{keys.map((key) => (
<ItemKey {...key} key={key.npub} />
))}
<Stack maxHeight={'100%'} overflow={'auto'}>
<SectionTitle marginBottom={'0.5rem'}>
{isNoKeys ? 'Welcome!' : 'Keys:'}
</SectionTitle>
<Stack gap={'0.5rem'} overflow={'auto'}>
{isNoKeys && (
<Typography textAlign={'center'} variant='h5'>
Hello, this is a key storage app for Nostr
</Typography>
)}
{!isNoKeys && (
<Fragment>
<Box flex={1} overflow={'auto'} borderRadius={'8px'}>
{keys.map((key) => (
<ItemKey {...key} key={key.npub} />
))}
</Box>
<AddAccountButton onClick={handleClickAddAccount}>
Add account
</AddAccountButton>
</Fragment>
)}
</Stack>
</Stack>
)

View File

@ -1,4 +1,4 @@
import { FC, useRef } from 'react'
import { FC } from 'react'
import { DbKey } from '../../../modules/db'
import {
Avatar,
@ -8,8 +8,7 @@ import {
TypographyProps,
styled,
} from '@mui/material'
import { call, getShortenNpub, log } from '../../../utils/helpers'
import { swicCall } from '../../../modules/swic'
import { getShortenNpub } from '../../../utils/helpers/helpers'
import { useNavigate } from 'react-router-dom'
type ItemKeyProps = DbKey
@ -18,17 +17,6 @@ export const ItemKey: FC<ItemKeyProps> = (props) => {
const { npub, profile } = props
const navigate = useNavigate()
const passPhraseInputRef = useRef<HTMLInputElement | null>(null)
// eslint-disable-next-line
async function saveKey(npub: string) {
call(async () => {
const passphrase = passPhraseInputRef.current?.value
await swicCall('saveKey', npub, passphrase)
log('Key saved')
})
}
const handleNavigate = () => {
navigate('/key/' + npub)
}

View File

@ -0,0 +1,10 @@
import { AppButtonProps, Button } from '@/shared/Button/Button'
import { styled } from '@mui/material'
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
export const AddAccountButton = styled((props: AppButtonProps) => (
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
))(() => ({
alignSelf: 'center',
padding: '0.35rem 1rem',
}))

View File

@ -1,10 +1,12 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { SectionTitle } from '../../shared/SectionTitle/SectionTitle'
import { useAppSelector } from '../../store/hooks/redux'
import { askNotificationPermission, getShortenNpub } from '../../utils/helpers'
import {
askNotificationPermission,
getShortenNpub,
} from '../../utils/helpers/helpers'
import { useParams } from 'react-router-dom'
import { fetchProfile } from '../../modules/nostr'
import { nip19 } from 'nostr-tools'
import { Badge, Box, CircularProgress, Stack } from '@mui/material'
import { StyledIconButton } from './styled'
import { SettingsIcon, ShareIcon } from '@/assets'
@ -53,7 +55,10 @@ const KeyPage = () => {
const notify = useEnqueueSnackbar()
const [profile, setProfile] = useState<MetaEvent | null>(null)
const userName = profile?.info?.name || profile?.info?.display_name || getShortenNpub(npub)
const userName =
profile?.info?.name ||
profile?.info?.display_name ||
getShortenNpub(npub)
const userNameWithPrefix = userName + '@nsec.app'
const [showWarning, setShowWarning] = useState(false)

View File

@ -5,7 +5,7 @@ import { Box, Stack, Typography } from '@mui/material'
import { FC } from 'react'
import { StyledEmptyAppsBox } from '../styled'
import { Button } from '@/shared/Button/Button'
import { call } from '@/utils/helpers'
import { call } from '@/utils/helpers/helpers'
import { swicCall } from '@/modules/swic'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { ItemApp } from './ItemApp'

View File

@ -3,7 +3,7 @@ import { Avatar, Stack, Typography } from '@mui/material'
import { FC } from 'react'
import { Link } from 'react-router-dom'
// import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'
import { getShortenNpub } from '@/utils/helpers'
import { getShortenNpub } from '@/utils/helpers/helpers'
import { StyledItemAppContainer } from './styled'
type ItemAppProps = DbApp

View File

@ -7,7 +7,7 @@ import { CircularProgress, Stack } from '@mui/material'
const KeyPage = lazy(() => import('../pages/KeyPage/Key.Page'))
const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
const AppPage = lazy(() => import('../pages/App.Page'))
const AppPage = lazy(() => import('../pages/AppPage/App.Page'))
const LoadingSpinner = () => (
<Stack height={'100%'} justifyContent={'center'} alignItems={'center'}>

View File

@ -1,9 +1,17 @@
import { DialogProps, Slide } from '@mui/material'
import { DialogProps, IconButton, Slide } from '@mui/material'
import { TransitionProps } from '@mui/material/transitions'
import { FC, forwardRef } from 'react'
import { StyledDialog, StyledDialogContent, StyledDialogTitle } from './styled'
import {
StyledCloseButtonWrapper,
StyledDialog,
StyledDialogContent,
StyledDialogTitle,
} from './styled'
import CloseRoundedIcon from '@mui/icons-material/CloseRounded'
type ModalProps = DialogProps
type ModalProps = DialogProps & {
withCloseButton?: boolean
}
const Transition = forwardRef(function Transition(
props: TransitionProps & {
@ -14,9 +22,29 @@ const Transition = forwardRef(function Transition(
return <Slide direction='up' ref={ref} {...props} />
})
export const Modal: FC<ModalProps> = ({ children, title, ...props }) => {
export const Modal: FC<ModalProps> = ({
children,
title,
onClose,
withCloseButton = true,
...props
}) => {
return (
<StyledDialog {...props} TransitionComponent={Transition}>
<StyledDialog
{...props}
onClose={onClose}
TransitionComponent={Transition}
>
{withCloseButton && (
<StyledCloseButtonWrapper>
<IconButton
onClick={() => onClose && onClose({}, 'backdropClick')}
className='close_btn'
>
<CloseRoundedIcon />
</IconButton>
</StyledCloseButtonWrapper>
)}
{title && <StyledDialogTitle>{title}</StyledDialogTitle>}
<StyledDialogContent>{children}</StyledDialogContent>
</StyledDialog>

View File

@ -1,4 +1,6 @@
import {
Box,
BoxProps,
Dialog,
DialogContent,
DialogContentProps,
@ -56,3 +58,17 @@ export const StyledDialogContent = styled((props: DialogContentProps) => (
padding: '0 1rem 1rem',
}
})
export const StyledCloseButtonWrapper = styled((props: BoxProps) => (
<Box {...props} />
))(() => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: '0.5rem 1rem',
position: 'relative',
'& > .close_btn': {
position: 'absolute',
transform: 'translateY(50%)',
},
}))

View File

@ -55,9 +55,25 @@ export const selectPermsByNpub = memoizeOne(
isDeepEqual,
)
export const selectPermsByNpubAndAppNpub = memoizeOne(
(state: RootState, npub: string, appNpub: string) => {
return state.content.perms.filter(
(perm) => perm.npub === npub && perm.appNpub === appNpub,
)
},
isDeepEqual,
)
export const selectPendingsByNpub = memoizeOne(
(state: RootState, npub: string) => {
return state.content.pending.filter((pending) => pending.npub === npub)
},
isDeepEqual,
)
export const selectAppByAppNpub = memoizeOne(
(state: RootState, appNpub: string) => {
return state.content.apps.find((app) => app.appNpub === appNpub)
},
isDeepEqual,
)

11
src/utils/helpers/date.ts Normal file
View File

@ -0,0 +1,11 @@
import { format } from 'date-fns'
export const formatTimestampDate = (timestamp: number) => {
try {
const date = new Date(timestamp)
const formattedDate = format(date, "HH:mm',' dd-MM-yyyy")
return formattedDate
} catch (error) {
return ''
}
}

View File

@ -1,5 +1,5 @@
import { nip19 } from 'nostr-tools'
import { ACTION_TYPE, NIP46_RELAYS } from './consts'
import { ACTION_TYPE, NIP46_RELAYS } from '../consts'
import { DbPending } from '@/modules/db'
export async function log(s: string) {
@ -52,8 +52,7 @@ export function getSignReqKind(req: DbPending): number | undefined {
export function getReqPerm(req: DbPending): string {
if (req.method === 'sign_event') {
const kind = getSignReqKind(req)
if (kind !== undefined)
return `${req.method}:${kind}`
if (kind !== undefined) return `${req.method}:${kind}`
}
return req.method
}